Skip to content

Commit

Permalink
add connection security indicator to the top-right corner of the Resp…
Browse files Browse the repository at this point in the history
…onse View
  • Loading branch information
sunny-chung committed Dec 16, 2023
1 parent a366276 commit 6053274
Show file tree
Hide file tree
Showing 18 changed files with 270 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ class NetworkClientManager : CallDataStore {
val d = CallData(
id = uuidString(),
subprojectId = subprojectId,
sslConfig = environment?.sslConfig ?: SslConfig(),
response = UserResponse(
id = uuidString(),
requestExampleId = requestExampleId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.sunnychung.application.multiplatform.hellohttp.model

import com.sunnychung.application.multiplatform.hellohttp.annotation.Persisted
import com.sunnychung.lib.multiplatform.kdatetime.KInstant
import kotlinx.serialization.Serializable

@Persisted
@Serializable
data class ConnectionSecurity(
val security: ConnectionSecurityType,
val clientCertificatePrincipal: Certificate?,
val peerCertificatePrincipal: Certificate?,
)

@Persisted
@Serializable
data class Certificate(
val principal: String,
val issuerPrincipal: String,
val notAfter: KInstant,
val notBefore: KInstant,
)

enum class ConnectionSecurityType {
/**
* Cleartext HTTP
*/
Unencrypted,

/**
* TLS without verification
*/
InsecureEncrypted,

/**
* TLS with verification. It could be verified with custom trusted certificates.
*/
VerifiedEncrypted,

/**
* mTLS with verification
*/
MutuallyVerifiedEncrypted
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ data class UserResponse(
var errorMessage: String? = null,
@Transient var postFlightErrorMessage: String? = null,
var headers: List<Pair<String, String>>? = null,
var connectionSecurity: ConnectionSecurity? = null,
var rawExchange: RawExchange = RawExchange(exchanges = Collections.synchronizedList(mutableListOf())),
var payloadExchanges: MutableList<PayloadMessage>? = // null = not support streaming; empty list = streaming without data
if (application in setOf(ProtocolApplication.WebSocket, ProtocolApplication.Graphql))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ abstract class AbstractTransportClient internal constructor(callDataStore: CallD

override fun getCallData(callId: String) = callData[callId]

protected fun createCallData(requestBodySize: Int?, requestExampleId: String, requestId: String, subprojectId: String): CallData {
protected fun createCallData(requestBodySize: Int?, requestExampleId: String, requestId: String, subprojectId: String, sslConfig: SslConfig): CallData {
val outgoingBytesFlow = MutableSharedFlow<RawPayload>()
val incomingBytesFlow = MutableSharedFlow<RawPayload>()
val optionalResponseSize = AtomicInteger()
Expand All @@ -139,6 +139,7 @@ abstract class AbstractTransportClient internal constructor(callDataStore: CallD
val data = CallData(
id = callId,
subprojectId = subprojectId,
sslConfig = sslConfig.copy(),
events = eventSharedFlow.asSharedFlow()
.filter { it.callId == callId }
.flowOn(Dispatchers.IO)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.sunnychung.application.multiplatform.hellohttp.model.SslConfig
import com.sunnychung.application.multiplatform.hellohttp.model.UserResponse
import com.sunnychung.application.multiplatform.hellohttp.network.util.ContentEncodingDecompressProcessor
import com.sunnychung.application.multiplatform.hellohttp.network.apache.Http2FrameSerializer
import com.sunnychung.application.multiplatform.hellohttp.network.util.CallDataUserResponseUtil
import com.sunnychung.application.multiplatform.hellohttp.util.log
import com.sunnychung.lib.multiplatform.kdatetime.KInstant
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -72,6 +73,7 @@ class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : Ab

private fun buildHttpClient(
callId: String,
callData: CallData,
httpConfig: HttpConfig,
sslConfig: SslConfig,
outgoingBytesFlow: MutableSharedFlow<RawPayload>,
Expand Down Expand Up @@ -112,6 +114,7 @@ class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : Ab
}

override fun onConnectedHost(remoteAddress: String, protocolVersion: String) {
CallDataUserResponseUtil.onConnected(callData.response)
emitEvent(callId, "Connected to $remoteAddress with $protocolVersion")
}

Expand All @@ -124,6 +127,12 @@ class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : Ab
peerPrincipal: Principal?,
peerCertificates: Array<Certificate>?
) {
CallDataUserResponseUtil.onTlsUpgraded(
callData = callData,
localCertificates = localCertificates,
peerCertificates = peerCertificates
)

var event = "Established TLS upgrade with protocol '$protocol', cipher suite '$cipherSuite'"
if (applicationProtocol.isNotBlank()) {
event += " and application protocol '$applicationProtocol'"
Expand Down Expand Up @@ -246,11 +255,13 @@ class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : Ab
requestExampleId = requestExampleId,
requestId = requestId,
subprojectId = subprojectId,
sslConfig = sslConfig
)
val callId = data.id

val httpClient = buildHttpClient(
callId = callId,
callData = data,
httpConfig = httpConfig,
sslConfig = sslConfig,
outgoingBytesFlow = data.outgoingBytes as MutableSharedFlow<RawPayload>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class GraphqlSubscriptionTransportClient(networkClientManager: NetworkClientMana
requestExampleId = requestExampleId,
requestId = requestId,
subprojectId = subprojectId,
sslConfig = sslConfig,
)
val callId = data.id
val uri: URI = request.getResolvedUri()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.sunnychung.application.multiplatform.hellohttp.model.ProtocolVersion
import com.sunnychung.application.multiplatform.hellohttp.model.SslConfig
import com.sunnychung.application.multiplatform.hellohttp.model.StringBody
import com.sunnychung.application.multiplatform.hellohttp.model.UserResponse
import com.sunnychung.application.multiplatform.hellohttp.network.util.CallDataUserResponseUtil
import com.sunnychung.application.multiplatform.hellohttp.network.util.flowAndStreamObserver
import com.sunnychung.application.multiplatform.hellohttp.util.emptyToNull
import com.sunnychung.application.multiplatform.hellohttp.util.log
Expand Down Expand Up @@ -80,7 +81,7 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract
callId: String, uri: URI, sslConfig: SslConfig,
outgoingBytesFlow: MutableSharedFlow<RawPayload>?,
incomingBytesFlow: MutableSharedFlow<RawPayload>?,
out: UserResponse?
callData: CallData?, out: UserResponse?
): ManagedChannel {
val uri0 = uri.let {
val uriString = it.toString()
Expand Down Expand Up @@ -374,6 +375,9 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract
}

override fun onChannelActive() {
if (callData != null) {
CallDataUserResponseUtil.onConnected(callData.response)
}
emitEvent(callId, "HTTP/2 channel established.")
}

Expand All @@ -382,6 +386,14 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract
}

override fun onTlsHandshakeComplete(session: SSLSession, applicationProtocol: String?) {
if (callData != null) {
CallDataUserResponseUtil.onTlsUpgraded(
callData = callData,
localCertificates = session.localCertificates,
peerCertificates = session.peerCertificates,
)
}

var event = "Established TLS upgrade with protocol '${session.protocol}', cipher suite '${session.cipherSuite}'"
if (!applicationProtocol.isNullOrBlank()) {
event += " and application protocol '$applicationProtocol'"
Expand Down Expand Up @@ -412,7 +424,8 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract
requestBodySize = null,
requestExampleId = requestExampleId,
requestId = requestId,
subprojectId = subprojectId
subprojectId = subprojectId,
sslConfig = sslConfig,
)

val extra = request.extra as GrpcRequestExtra
Expand Down Expand Up @@ -441,6 +454,7 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract
sslConfig = sslConfig,
outgoingBytesFlow = call.outgoingBytes as MutableSharedFlow<RawPayload>,
incomingBytesFlow = call.incomingBytes as MutableSharedFlow<RawPayload>,
callData = call,
out = out,
) // blocking call
val channel = ClientInterceptors.intercept(channel0, object : ClientInterceptor {
Expand Down Expand Up @@ -714,7 +728,7 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract
sslConfig: SslConfig
): GrpcApiSpec {
val uri = URI.create(url)
val channel = buildChannel("", uri, sslConfig, null, null, null)
val channel = buildChannel("", uri, sslConfig, null, null, null, null)
try {
return fetchServiceSpec("${uri.host}:${uri.port}", channel)
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ private val inputStreamSourceConstructor = Class.forName("okio.InputStreamSource
isAccessible = true
}

@Deprecated("Use ApacheHttpTransportClient")
class OkHttpTransportClient(networkClientManager: NetworkClientManager) : AbstractTransportClient(networkClientManager) {

fun buildHttpClient(
Expand Down Expand Up @@ -231,6 +232,7 @@ class OkHttpTransportClient(networkClientManager: NetworkClientManager) : Abstra
requestExampleId = requestExampleId,
requestId = requestId,
subprojectId = subprojectId,
sslConfig = sslConfig,
)
val callId = data.id

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class CallData(
var isPrepared: Boolean = false,
var status: ConnectionStatus = ConnectionStatus.PREPARING,

val sslConfig: SslConfig,

val events: SharedFlow<NetworkEvent>,
val eventsStateFlow: StateFlow<NetworkEvent?>,
val outgoingBytes: SharedFlow<RawPayload>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ open class WebSocketTransportClient(networkClientManager: NetworkClientManager)
requestExampleId = requestExampleId,
requestId = requestId,
subprojectId = subprojectId,
sslConfig = sslConfig,
)
val callId = data.id
val uri: URI = request.getResolvedUri()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.sunnychung.application.multiplatform.hellohttp.network.util

import com.sunnychung.application.multiplatform.hellohttp.model.ConnectionSecurity
import com.sunnychung.application.multiplatform.hellohttp.model.ConnectionSecurityType
import com.sunnychung.application.multiplatform.hellohttp.model.UserResponse
import com.sunnychung.application.multiplatform.hellohttp.network.CallData
import com.sunnychung.lib.multiplatform.kdatetime.KInstant
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import javax.security.auth.x500.X500Principal

internal object CallDataUserResponseUtil {
internal fun onConnected(out: UserResponse) {
synchronized(out) {
if (out.connectionSecurity == null) {
out.connectionSecurity = ConnectionSecurity(
security = ConnectionSecurityType.Unencrypted,
clientCertificatePrincipal = null,
peerCertificatePrincipal = null
)
}
}
}

internal fun onTlsUpgraded(
callData: CallData,
localCertificates: Array<Certificate>?,
peerCertificates: Array<Certificate>?,
) {
synchronized(callData.response) {
callData.response.connectionSecurity = ConnectionSecurity(
security = when {
callData.sslConfig.isInsecure == true -> ConnectionSecurityType.InsecureEncrypted
!localCertificates.isNullOrEmpty() && !peerCertificates.isNullOrEmpty() -> ConnectionSecurityType.MutuallyVerifiedEncrypted
!peerCertificates.isNullOrEmpty() -> ConnectionSecurityType.VerifiedEncrypted
else -> ConnectionSecurityType.Unencrypted
},
clientCertificatePrincipal = (localCertificates?.firstOrNull() as? X509Certificate)?.toPersistableCertificate(),
peerCertificatePrincipal = (peerCertificates?.firstOrNull() as? X509Certificate)?.toPersistableCertificate(),
)
}
}

private fun X509Certificate.toPersistableCertificate() = com.sunnychung.application.multiplatform.hellohttp.model.Certificate(
principal = subjectX500Principal.getName(X500Principal.RFC1779),
issuerPrincipal = issuerX500Principal.getName(X500Principal.RFC1779),
notAfter = KInstant(notAfter.time),
notBefore = KInstant(notBefore.time),
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.sunnychung.application.multiplatform.hellohttp.network.util

import com.sunnychung.application.multiplatform.hellohttp.model.ConnectionSecurity
import com.sunnychung.application.multiplatform.hellohttp.model.ConnectionSecurityType
import com.sunnychung.application.multiplatform.hellohttp.network.CallData
import com.sunnychung.application.multiplatform.hellohttp.network.ConnectionStatus
import com.sunnychung.application.multiplatform.hellohttp.network.RawPayload
Expand Down Expand Up @@ -67,6 +69,7 @@ abstract class InspectedWebSocketClient(
}

override fun onConnected(address: InetSocketAddress) {
CallDataUserResponseUtil.onConnected(out)
emitEvent(callId, "Connected to $address")
}

Expand All @@ -75,6 +78,11 @@ abstract class InspectedWebSocketClient(
val cipherSuite = event.cipherSuite
val localPrincipal = event.localPrincipal
val peerPrincipal = event.peerPrincipal
CallDataUserResponseUtil.onTlsUpgraded(
callData = data,
localCertificates = event.session.localCertificates,
peerCertificates = event.session.peerCertificates
)
var event = "Established TLS upgrade with protocol '$protocol', cipher suite '$cipherSuite'"
event += ".\n\n" +
"Client principal = $localPrincipal\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.sunnychung.application.multiplatform.hellohttp.ux
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.TooltipArea
import androidx.compose.foundation.TooltipPlacement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
Expand All @@ -20,6 +21,7 @@ fun AppTooltipArea(
isVisible: Boolean = true,
tooltipText: String,
delayMillis: Int = 100,
tooltipContent: @Composable () -> Unit = { AppText(text = tooltipText) },
content: @Composable () -> Unit,
) {
val colours = LocalColor.current
Expand All @@ -31,10 +33,9 @@ fun AppTooltipArea(
color = colours.backgroundTooltip,
shape = RoundedCornerShape(8.dp)
) {
AppText(
text = tooltipText,
modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp)
)
Box(modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp)) {
tooltipContent()
}
}
}
},
Expand Down
Loading

0 comments on commit 6053274

Please sign in to comment.