diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/manager/NetworkClientManager.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/manager/NetworkClientManager.kt index c3f87931..42ecaf67 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/manager/NetworkClientManager.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/manager/NetworkClientManager.kt @@ -185,6 +185,7 @@ class NetworkClientManager : CallDataStore { val d = CallData( id = uuidString(), subprojectId = subprojectId, + sslConfig = environment?.sslConfig ?: SslConfig(), response = UserResponse( id = uuidString(), requestExampleId = requestExampleId, diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/model/ConnectionSecurity.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/model/ConnectionSecurity.kt new file mode 100644 index 00000000..f8129b07 --- /dev/null +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/model/ConnectionSecurity.kt @@ -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 +} diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/model/UserResponse.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/model/UserResponse.kt index 5143f13b..bfadd624 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/model/UserResponse.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/model/UserResponse.kt @@ -29,6 +29,7 @@ data class UserResponse( var errorMessage: String? = null, @Transient var postFlightErrorMessage: String? = null, var headers: List>? = null, + var connectionSecurity: ConnectionSecurity? = null, var rawExchange: RawExchange = RawExchange(exchanges = Collections.synchronizedList(mutableListOf())), var payloadExchanges: MutableList? = // null = not support streaming; empty list = streaming without data if (application in setOf(ProtocolApplication.WebSocket, ProtocolApplication.Graphql)) diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/AbstractTransportClient.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/AbstractTransportClient.kt index 5e2e86d6..daed28a7 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/AbstractTransportClient.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/AbstractTransportClient.kt @@ -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() val incomingBytesFlow = MutableSharedFlow() val optionalResponseSize = AtomicInteger() @@ -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) diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/ApacheHttpTransportClient.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/ApacheHttpTransportClient.kt index 100ac661..8c90e295 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/ApacheHttpTransportClient.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/ApacheHttpTransportClient.kt @@ -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 @@ -72,6 +73,7 @@ class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : Ab private fun buildHttpClient( callId: String, + callData: CallData, httpConfig: HttpConfig, sslConfig: SslConfig, outgoingBytesFlow: MutableSharedFlow, @@ -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") } @@ -124,6 +127,12 @@ class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : Ab peerPrincipal: Principal?, peerCertificates: Array? ) { + 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'" @@ -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, diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/GraphqlSubscriptionTransportClient.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/GraphqlSubscriptionTransportClient.kt index 6b9cfdab..e5f0337f 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/GraphqlSubscriptionTransportClient.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/GraphqlSubscriptionTransportClient.kt @@ -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() diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/GrpcTransportClient.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/GrpcTransportClient.kt index c1a3b9a7..67ccd88f 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/GrpcTransportClient.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/GrpcTransportClient.kt @@ -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 @@ -80,7 +81,7 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract callId: String, uri: URI, sslConfig: SslConfig, outgoingBytesFlow: MutableSharedFlow?, incomingBytesFlow: MutableSharedFlow?, - out: UserResponse? + callData: CallData?, out: UserResponse? ): ManagedChannel { val uri0 = uri.let { val uriString = it.toString() @@ -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.") } @@ -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'" @@ -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 @@ -441,6 +454,7 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract sslConfig = sslConfig, outgoingBytesFlow = call.outgoingBytes as MutableSharedFlow, incomingBytesFlow = call.incomingBytes as MutableSharedFlow, + callData = call, out = out, ) // blocking call val channel = ClientInterceptors.intercept(channel0, object : ClientInterceptor { @@ -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 { diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/OkHttpTransportClient.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/OkHttpTransportClient.kt index 9afbac84..d3868ee2 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/OkHttpTransportClient.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/OkHttpTransportClient.kt @@ -57,6 +57,7 @@ private val inputStreamSourceConstructor = Class.forName("okio.InputStreamSource isAccessible = true } +@Deprecated("Use ApacheHttpTransportClient") class OkHttpTransportClient(networkClientManager: NetworkClientManager) : AbstractTransportClient(networkClientManager) { fun buildHttpClient( @@ -231,6 +232,7 @@ class OkHttpTransportClient(networkClientManager: NetworkClientManager) : Abstra requestExampleId = requestExampleId, requestId = requestId, subprojectId = subprojectId, + sslConfig = sslConfig, ) val callId = data.id diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/TransportClient.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/TransportClient.kt index afc8091f..28b0eedb 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/TransportClient.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/TransportClient.kt @@ -34,6 +34,8 @@ class CallData( var isPrepared: Boolean = false, var status: ConnectionStatus = ConnectionStatus.PREPARING, + val sslConfig: SslConfig, + val events: SharedFlow, val eventsStateFlow: StateFlow, val outgoingBytes: SharedFlow, diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/WebSocketTransportClient.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/WebSocketTransportClient.kt index 16ecd5b0..4c54f21e 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/WebSocketTransportClient.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/WebSocketTransportClient.kt @@ -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() diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/util/CallDataUserResponseUtil.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/util/CallDataUserResponseUtil.kt new file mode 100644 index 00000000..3426a51d --- /dev/null +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/util/CallDataUserResponseUtil.kt @@ -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?, + peerCertificates: Array?, + ) { + 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), + ) +} diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/util/InspectedWebSocketClient.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/util/InspectedWebSocketClient.kt index 2d7c0a81..7a6eb98b 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/util/InspectedWebSocketClient.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/util/InspectedWebSocketClient.kt @@ -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 @@ -67,6 +69,7 @@ abstract class InspectedWebSocketClient( } override fun onConnected(address: InetSocketAddress) { + CallDataUserResponseUtil.onConnected(out) emitEvent(callId, "Connected to $address") } @@ -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" + diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/AppTooltipArea.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/AppTooltipArea.kt index 9bddf20f..914713c5 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/AppTooltipArea.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/AppTooltipArea.kt @@ -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 @@ -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 @@ -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() + } } } }, diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/ResponseViewerView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/ResponseViewerView.kt index d61d03d8..9e8e08da 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/ResponseViewerView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/ResponseViewerView.kt @@ -7,7 +7,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -15,6 +17,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -39,6 +42,8 @@ import com.jayway.jsonpath.JsonPath import com.sunnychung.application.multiplatform.hellohttp.AppContext import com.sunnychung.application.multiplatform.hellohttp.network.ConnectionStatus import com.sunnychung.application.multiplatform.hellohttp.manager.Prettifier +import com.sunnychung.application.multiplatform.hellohttp.model.Certificate +import com.sunnychung.application.multiplatform.hellohttp.model.ConnectionSecurityType import com.sunnychung.application.multiplatform.hellohttp.model.PayloadMessage import com.sunnychung.application.multiplatform.hellohttp.model.ProtocolApplication import com.sunnychung.application.multiplatform.hellohttp.model.RawExchange @@ -70,10 +75,64 @@ fun ResponseViewerView(response: UserResponse, connectionStatus: ConnectionStatu val updateTime by responseViewModel.subscribe() Column { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { - StatusLabel(response = response, connectionStatus = connectionStatus) - DurationLabel(response = response, updateTime = updateTime, connectionStatus = connectionStatus) - ResponseSizeLabel(response = response) + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.height(IntrinsicSize.Max)) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp).weight(1f)) { + StatusLabel(response = response, connectionStatus = connectionStatus) + DurationLabel(response = response, updateTime = updateTime, connectionStatus = connectionStatus) + ResponseSizeLabel(response = response) + } + response.connectionSecurity?.let { + AppTooltipArea( + tooltipText = "", + tooltipContent = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.widthIn(max = 360.dp)) { + AppText(text = when (it.security) { + ConnectionSecurityType.Unencrypted -> "Not encrypted" + ConnectionSecurityType.InsecureEncrypted -> "Unverified TLS" + ConnectionSecurityType.VerifiedEncrypted -> "One-way TLS" + ConnectionSecurityType.MutuallyVerifiedEncrypted -> "mTLS" + }) + CertificateView(title = "Client Certificate", it.clientCertificatePrincipal) + CertificateView(title = "Server Certificate", it.peerCertificatePrincipal) + } + }, + modifier = Modifier.fillMaxHeight() + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxHeight()) { + val modifier = Modifier //.align(Alignment.Center) + when (it.security) { + ConnectionSecurityType.Unencrypted -> AppImage( + resource = "insecure.svg", + color = colors.placeholder, + size = 24.dp, + modifier = modifier, + ) + + ConnectionSecurityType.InsecureEncrypted -> AppImage( + resource = "questionable-secure.svg", + size = 24.dp, + modifier = modifier, + ) + + ConnectionSecurityType.VerifiedEncrypted -> AppImage( + resource = "secure.svg", + color = colors.successful, + size = 24.dp, + modifier = modifier, + ) + + ConnectionSecurityType.MutuallyVerifiedEncrypted -> AppText( + text = "mTLS", + isFitContent = true, + maxLines = 1, + color = colors.successful, + modifier = modifier.width(32.dp), + ) + } + } + } + Spacer(Modifier.width(4.dp)) + } } val tabs = if ( @@ -295,6 +354,41 @@ fun ResponseSizeLabel(modifier: Modifier = Modifier, response: UserResponse) { DataLabel(modifier = modifier, text = text) } +@Composable +fun CertificateView(title: String, cert: Certificate?) { + val headerColumnWidth = 136.dp + val indentWidth = 16.dp + Column { + Row { + AppText(text = title, modifier = Modifier.width(headerColumnWidth)) + if (cert == null) { + AppText("N/A") + } + } + if (cert != null) { + Column(modifier = Modifier.padding(start = indentWidth)) { + val columnWidth = headerColumnWidth - indentWidth + Row { + AppText(text = "Principal", modifier = Modifier.width(columnWidth)) + AppText(text = cert.principal) + } + Row { + AppText(text = "Issuer", modifier = Modifier.width(columnWidth)) + AppText(text = cert.issuerPrincipal) + } + Row { + AppText(text = "Expiry at", modifier = Modifier.width(columnWidth)) + AppText(text = cert.notAfter.atZoneOffset(KZoneOffset.local()).format(KDateTimeFormat.ISO8601_DATETIME.pattern)) + } + Row { + AppText(text = "Issued at", modifier = Modifier.width(columnWidth)) + AppText(text = cert.notBefore.atZoneOffset(KZoneOffset.local()).format(KDateTimeFormat.ISO8601_DATETIME.pattern)) + } + } + } + } +} + @Composable fun ResponseEmptyView(modifier: Modifier = Modifier, type: String, isCommunicating: Boolean) { val colours = LocalColor.current diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/local/AppColor.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/local/AppColor.kt index 243cf223..4884924f 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/local/AppColor.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/local/AppColor.kt @@ -19,6 +19,7 @@ data class AppColor( val primary: Color, val bright: Color, + val successful: Color, val text: Color = primary, val image: Color = primary, val line: Color, @@ -95,6 +96,7 @@ fun darkColorScheme(): AppColor = AppColor( primary = Color(red = 0.8f, green = 0.8f, blue = 1.0f), bright = Color.White, + successful = Color(red = 0.1f, green = 0.8f, blue = 0.1f), line = Color(red = 0.6f, green = 0.6f, blue = 0.6f), placeholder = Color(red = 0.5f, green = 0.5f, blue = 0.5f), @@ -163,6 +165,7 @@ fun lightColorScheme(): AppColor = AppColor( primary = Color(red = 0.2f, green = 0.2f, blue = 0.3f), bright = Color.Black, + successful = Color(red = 0.1f, green = 0.6f, blue = 0.1f), line = Color(red = 0.4f, green = 0.4f, blue = 0.4f), placeholder = Color(red = 0.5f, green = 0.5f, blue = 0.5f), diff --git a/src/jvmMain/resources/image/insecure.svg b/src/jvmMain/resources/image/insecure.svg new file mode 100644 index 00000000..09f5d3ee --- /dev/null +++ b/src/jvmMain/resources/image/insecure.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/jvmMain/resources/image/questionable-secure.svg b/src/jvmMain/resources/image/questionable-secure.svg new file mode 100644 index 00000000..8f1034d0 --- /dev/null +++ b/src/jvmMain/resources/image/questionable-secure.svg @@ -0,0 +1,6 @@ + + + unlock-line + + + \ No newline at end of file diff --git a/src/jvmMain/resources/image/secure.svg b/src/jvmMain/resources/image/secure.svg new file mode 100644 index 00000000..64ae5683 --- /dev/null +++ b/src/jvmMain/resources/image/secure.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file