From 9db73b9e86488a58d0907b7bb3b2443daa2f8fb3 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 21 Feb 2026 23:24:39 +0000 Subject: [PATCH 1/5] [speculative/not to land] Add pcap logging for decorated sockets Adds a RecordingSocketFactory that decorates a TCP socket and captures events as Pcap or a sealed event trait. A potential debugging tool... - after the fact flaky tests - visibility into HTTP/2 frames - confirming behaviour of mock sockets The last point is why I'm exploring this. See whether we can run some category of tests without real I/O and flakiness. Run in near instant time. --- gradle/libs.versions.toml | 2 + mocksocket/build.gradle.kts | 32 +++ .../mockwebserver/socket/NetLogRecorder.kt | 117 +++++++++ .../mockwebserver/socket/PcapRecorder.kt | 220 ++++++++++++++++ .../mockwebserver/socket/RecordingSocket.kt | 218 ++++++++++++++++ .../socket/RecordingSocketFactory.kt | 46 ++++ .../mockwebserver/socket/SocketDecorator.kt | 89 +++++++ .../mockwebserver/socket/SocketEvent.kt | 160 ++++++++++++ .../socket/SocketEventListener.kt | 37 +++ .../java/mockwebserver/socket/CaptureTest.kt | 234 ++++++++++++++++++ settings.gradle.kts | 1 + 11 files changed, 1156 insertions(+) create mode 100644 mocksocket/build.gradle.kts create mode 100644 mocksocket/src/main/kotlin/mockwebserver/socket/NetLogRecorder.kt create mode 100644 mocksocket/src/main/kotlin/mockwebserver/socket/PcapRecorder.kt create mode 100644 mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocket.kt create mode 100644 mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocketFactory.kt create mode 100644 mocksocket/src/main/kotlin/mockwebserver/socket/SocketDecorator.kt create mode 100644 mocksocket/src/main/kotlin/mockwebserver/socket/SocketEvent.kt create mode 100644 mocksocket/src/main/kotlin/mockwebserver/socket/SocketEventListener.kt create mode 100644 mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65398c5425ca..92fb8e892c6e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,6 +49,7 @@ org-bouncycastle = "1.83" org-conscrypt = "2.5.2" org-junit-jupiter = "5.13.4" playservices-safetynet = "18.1.0" +pkts-core = "3.0.3" robolectric = "4.16.1" robolectric-android = "16-robolectric-13921718" serialization = "1.10.0" @@ -141,6 +142,7 @@ square-okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" } testcontainers-junit5 = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" } square-zstd-kmp-okio = { module = "com.squareup.zstd:zstd-kmp-okio", version.ref = "zstd-kmp-okio" } +pkts-core = { module = "io.pkts:pkts-core", version.ref = "pkts-core" } # Build Logic Dependencies gradlePlugin-android = { module = "com.android.tools.build:gradle", version.ref = "agp" } diff --git a/mocksocket/build.gradle.kts b/mocksocket/build.gradle.kts new file mode 100644 index 000000000000..2041345128eb --- /dev/null +++ b/mocksocket/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + kotlin("jvm") + id("okhttp.publish-conventions") + id("okhttp.jvm-conventions") + id("okhttp.quality-conventions") + id("okhttp.testing-conventions") +} + +project.applyJavaModules("mocksocket") + +dependencies { + api(libs.square.okio) + api(libs.kotlinx.coroutines.core) + implementation(libs.pkts.core) + + testImplementation(libs.assertk) + testImplementation(libs.junit.jupiter.api) + testImplementation(projects.okhttp) + testRuntimeOnly(libs.junit.jupiter.engine) +} + +tasks.withType { + jvmArgs( + "--add-opens=java.base/sun.security.ssl=ALL-UNNAMED", + "--add-opens=java.base/sun.security.util=ALL-UNNAMED", + "--add-opens=java.base/sun.security.provider=ALL-UNNAMED", + ) +} + +kotlin { + explicitApi() +} diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/NetLogRecorder.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/NetLogRecorder.kt new file mode 100644 index 000000000000..1f5e97284f4e --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/NetLogRecorder.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2026 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(ExperimentalTime::class) + +package mockwebserver.socket + +import java.io.Closeable +import kotlin.time.ExperimentalTime +import okio.BufferedSink +import okio.FileSystem +import okio.Path +import okio.buffer + +public class NetLogRecorder(file: Path, fileSystem: FileSystem = FileSystem.SYSTEM) : SocketEventListener, Closeable { + private val writer = fileSystem.sink(file).buffer() + private var isFirstEvent = true + private var closed = false + + init { + writer.println("{") + writer.println(" \"constants\": {},") + writer.println(" \"events\": [") + writer.flush() + } + + override fun onEvent(event: SocketEvent) { + val time = event.timestamp.toEpochMilliseconds() + + val jsonEvent = when (event) { + is SocketEvent.Connect -> { + """ + { + "phase": 1, + "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, + "time": "$time", + "type": 67, + "params": { + "address": "${event.host}:${event.port}" + } + } + """.trimIndent() + } + is SocketEvent.ReadSuccess -> { + // Not recording actual base64 payload to save memory, just counts + """ + { + "phase": 0, + "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, + "time": "$time", + "type": 113, + "params": { "byte_count": ${event.byteCount} } + } + """.trimIndent() + } + is SocketEvent.WriteSuccess -> { + """ + { + "phase": 0, + "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, + "time": "$time", + "type": 114, + "params": { "byte_count": ${event.byteCount} } + } + """.trimIndent() + } + is SocketEvent.Close -> { + """ + { + "phase": 2, + "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, + "time": "$time", + "type": 67 + } + """.trimIndent() + } + else -> null + } + + if (jsonEvent != null) { + synchronized(this) { + if (!isFirstEvent) { + writer.println(",") + } + isFirstEvent = false + writer.writeUtf8(jsonEvent.replace("\n", "\n ")) + writer.flush() + } + } + } + + override fun close() { + if (closed) return + closed = true + if (!isFirstEvent) writer.writeUtf8("\n") + writer.println(" ]") + writer.println("}") + writer.close() + } +} + +private fun BufferedSink.println(string: String) { + writeUtf8(string) + writeUtf8("\n") +} diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/PcapRecorder.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/PcapRecorder.kt new file mode 100644 index 000000000000..d5ce2ce2fcd4 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/PcapRecorder.kt @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2026 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(ExperimentalTime::class) + +package mockwebserver.socket + +import io.pkts.PcapOutputStream +import io.pkts.buffer.Buffers +import io.pkts.frame.PcapGlobalHeader +import io.pkts.frame.PcapRecordHeader +import io.pkts.packet.impl.PCapPacketImpl +import java.io.Closeable +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +import okio.Buffer +import okio.FileSystem +import okio.Path +import okio.buffer + +public class PcapRecorder(file: Path, fileSystem: FileSystem = FileSystem.SYSTEM) : SocketEventListener, Closeable { + + private val globalHeader = PcapGlobalHeader.createDefaultHeader() + private val out = PcapOutputStream.create(globalHeader, fileSystem.sink(file).buffer().outputStream()) + private var closed = false + + // Track synthetic sequence numbers per socket to map TCP window flow + private val sequenceNumbers = mutableMapOf() + private val ackNumbers = mutableMapOf() + + public val simulateTcp: Boolean = false + + override fun onEvent(event: SocketEvent) { + synchronized(this) { + if (closed) return + + var seq = sequenceNumbers.getOrDefault(event.socketName, 1000L) + var ack = ackNumbers.getOrDefault(event.socketName, 1000L) + + when (event) { + is SocketEvent.Connect -> { + if (simulateTcp) { + // SYN + writePacket( + out, + event.timestamp, + event.connection, + seq, + ack, + syn = true, + ackFlag = false, + payload = null + ) + seq++ + } + } + + is SocketEvent.WriteSuccess -> { + // PSH, ACK + val payloadBytes = event.payload?.readByteArray() + writePacket( + out, + event.timestamp, + event.connection, + seq, + ack, + syn = false, + ackFlag = true, + psh = true, + payload = payloadBytes + ) + if (payloadBytes != null) seq += payloadBytes.size + } + + is SocketEvent.ReadSuccess -> { + // For reads, we write from the perspective of the server sending to the client + val payloadBytes = event.payload?.readByteArray() + writePacket( + out, + event.timestamp, + event.connection, + ack, + seq, + syn = false, + ackFlag = true, + psh = true, + payload = payloadBytes, + clientSide = false, + ) + if (payloadBytes != null) ack += payloadBytes.size + } + + is SocketEvent.Close -> { + // FIN, ACK + writePacket( + out, + event.timestamp, + event.connection, + seq, + ack, + syn = false, + ackFlag = true, + fin = true, + payload = null + ) + seq++ + } + + else -> {} + } + + sequenceNumbers[event.socketName] = seq + ackNumbers[event.socketName] = ack + } + } + + override fun close() { + synchronized(this) { + if (closed) return + closed = true + out.close() + } + } + + private fun writePacket( + out: PcapOutputStream, + timestamp: Instant, + socketConnection: SocketEvent.SocketConnection, + seq: Long, + ack: Long, + clientSide: Boolean = true, + syn: Boolean = false, + ackFlag: Boolean = false, + fin: Boolean = false, + psh: Boolean = false, + payload: ByteArray? = null + ) { + // Because pkts.io is built around reading packets rather than forging them from scratch natively as a builder + // we manually construct a raw Ethernet + IPv4 + TCP packet byte string for the writer, using standard standard header lengths. + + val tcpLen = 20 + (payload?.size ?: 0) + val ipv4Len = 20 + tcpLen + val totalLen = 14 + ipv4Len + + val pkt = Buffer() + + // Ethernet (14 bytes) + pkt.write(ByteArray(6) { 0x00 }) // Dst MAC + pkt.write(ByteArray(6) { 0x00 }) // Src MAC + pkt.writeShort(0x0800) // Type IPv4 + + // IPv4 (20 bytes) + pkt.writeByte(0x45) // Version 4, IHL 5 + pkt.writeByte(0x00) // DSCP + pkt.writeShort(ipv4Len) // Total Length + pkt.writeShort(0x0000) // Identification + pkt.writeShort(0x4000) // Flags + Fragment offset + pkt.writeByte(0x40) // TTL 64 + pkt.writeByte(0x06) // Protocol TCP (6) + pkt.writeShort(0x0000) // Checksum (ignored by most readers if missing) + + if (clientSide) { + pkt.write(socketConnection.local.address.address) + pkt.write(socketConnection.peer.address.address) + pkt.writeShort(socketConnection.local.port) + pkt.writeShort(socketConnection.peer.port) + } else { + pkt.write(socketConnection.peer.address.address) + pkt.write(socketConnection.local.address.address) + pkt.writeShort(socketConnection.peer.port) + pkt.writeShort(socketConnection.local.port) + } + + // TCP (20 bytes) + pkt.writeInt(seq.toInt()) // Sequence Number + pkt.writeInt(ack.toInt()) // Ack Number + + val dataOffset = (5 shl 4).toByte() + pkt.writeByte(dataOffset.toInt()) + + var flags = 0 + if (fin) flags = flags or 0x01 + if (syn) flags = flags or 0x02 + if (psh) flags = flags or 0x08 + if (ackFlag) flags = flags or 0x10 + pkt.writeByte(flags) + + pkt.writeShort(65535) // Window size + pkt.writeShort(0x0000) // Checksum + pkt.writeShort(0x0000) // Urgent pointer + + // Payload + if (payload != null) { + pkt.write(payload) + } + + val rawPkt = pkt.readByteArray() + val recordHeader = PcapRecordHeader.createDefaultHeader(timestamp.toEpochMilliseconds()) + recordHeader.capturedLength = rawPkt.size.toLong() + recordHeader.totalLength = rawPkt.size.toLong() + val frame = PCapPacketImpl( + globalHeader, + recordHeader, + Buffers.wrap(rawPkt) + ) + out.write(frame) + } +} diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocket.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocket.kt new file mode 100644 index 000000000000..151e61a1bb75 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocket.kt @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2026 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(ExperimentalTime::class) + +package mockwebserver.socket + +import java.io.InputStream +import java.io.OutputStream +import java.net.InetSocketAddress +import java.net.Socket +import java.net.SocketAddress +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +import okio.Buffer +import okio.ForwardingSink +import okio.ForwardingSource +import okio.Sink +import okio.Source +import okio.buffer +import okio.sink +import okio.source + +/** A [Socket] implementation that delegates to another [Socket] and records events. */ +public open class RecordingSocket( + delegate: Socket, + private val socketEventListener: SocketEventListener, + public val socketName: String = "Socket" +) : SocketDecorator(delegate) { + init { + if (delegate.isConnected) { + recordSocketConnection() + recordConnect(socketConnection.peer) + } + } + + private val clock = Clock.System + private val lock = ReentrantLock() + + private lateinit var socketConnection: SocketEvent.SocketConnection + + override fun connect(endpoint: SocketAddress?) { + super.connect(endpoint) + recordSocketConnection() + recordConnect(endpoint) + } + + override fun connect(endpoint: SocketAddress?, timeout: Int) { + super.connect(endpoint, timeout) + recordSocketConnection() + recordConnect(endpoint) + } + + private fun recordSocketConnection() { + this.socketConnection = SocketEvent.SocketConnection( + delegate.localSocketAddress as InetSocketAddress, + delegate.remoteSocketAddress as InetSocketAddress + ) + } + + private val mySource: Source by lazy { + object : ForwardingSource(delegate.source()) { + override fun read(sink: Buffer, byteCount: Long): Long { + val startSize = sink.size + val readCount = super.read(sink, byteCount) + + val payloadSize = sink.size - startSize + val payload = if (payloadSize > 0) { + val clone = Buffer() + sink.copyTo(clone, startSize, payloadSize) + clone + } else null + + val event = if (readCount == -1L) { + SocketEvent.ReadEof( + clock.now(), Thread.currentThread().name, socketName, + socketConnection, + ) + } else { + SocketEvent.ReadSuccess( + clock.now(), + Thread.currentThread().name, + socketName, + socketConnection, + readCount, + payload, + ) + } + socketEventListener.onEvent(event) + return readCount + } + + override fun close() { + super.close() + socketEventListener.onEvent( + SocketEvent.ShutdownInput( + clock.now(), + Thread.currentThread().name, + socketName, + socketConnection, + ) + ) + } + } + } + + private val mySink: Sink by lazy { + object : ForwardingSink(delegate.sink()) { + override fun write(source: Buffer, byteCount: Long) { + val payload = if (byteCount > 0) { + val clone = Buffer() + source.copyTo(clone, 0, byteCount) + clone + } else null + + super.write(source, byteCount) + + socketEventListener.onEvent( + SocketEvent.WriteSuccess( + clock.now(), + Thread.currentThread().name, + socketName, + socketConnection, + byteCount, + clock.now(), + payload + ) + ) + } + + override fun close() { + super.close() + socketEventListener.onEvent( + SocketEvent.ShutdownOutput( + clock.now(), + Thread.currentThread().name, + socketName, + socketConnection, + ) + ) + } + } + } + + private val myInputStream by lazy { mySource.buffer().inputStream() } + private val myOutputStream by lazy { mySink.buffer().outputStream() } + + private fun recordConnect(endpoint: SocketAddress?) { + val address = endpoint as? java.net.InetSocketAddress + socketEventListener.onEvent( + SocketEvent.Connect( + clock.now(), + Thread.currentThread().name, + socketName, + socketConnection, + address?.hostName, + address?.port ?: 0 + ) + ) + } + + override fun getInputStream(): InputStream = myInputStream + + override fun getOutputStream(): OutputStream = myOutputStream + + override fun close() { + delegate.close() + if (this::socketConnection.isInitialized) { + socketEventListener.onEvent( + SocketEvent.Close( + clock.now(), Thread.currentThread().name, socketName, + socketConnection, + ) + ) + } + } + + override fun shutdownInput() { + delegate.shutdownInput() + socketEventListener.onEvent( + SocketEvent.ShutdownInput( + clock.now(), + Thread.currentThread().name, + socketName, + socketConnection, + ) + ) + } + + override fun shutdownOutput() { + delegate.shutdownOutput() + lock.withLock { + socketEventListener.onEvent( + SocketEvent.ShutdownOutput( + clock.now(), + Thread.currentThread().name, + socketName, + socketConnection, + ) + ) + } + } +} + diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocketFactory.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocketFactory.kt new file mode 100644 index 000000000000..b5d0c4cc73f8 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocketFactory.kt @@ -0,0 +1,46 @@ +package mockwebserver.socket + +import java.net.InetAddress +import java.net.Socket +import javax.net.SocketFactory + +public class RecordingSocketFactory( + private val socketEventListener: SocketEventListener, + private val delegate: SocketFactory = getDefault(), +) : SocketFactory() { + override fun createSocket(): Socket { + return RecordingSocket(delegate.createSocket(), socketEventListener) + } + + override fun createSocket(host: String?, port: Int): Socket { + return RecordingSocket(delegate.createSocket(host, port), socketEventListener) + } + + override fun createSocket( + host: String?, + port: Int, + localHost: InetAddress?, + localPort: Int + ): Socket { + return RecordingSocket( + delegate.createSocket(host, port, localHost, localPort), + socketEventListener + ) + } + + override fun createSocket(host: InetAddress?, port: Int): Socket { + return RecordingSocket(delegate.createSocket(host, port), socketEventListener) + } + + override fun createSocket( + address: InetAddress?, + port: Int, + localAddress: InetAddress?, + localPort: Int + ): Socket { + return RecordingSocket( + delegate.createSocket(address, port, localAddress, localPort), + socketEventListener + ) + } +} diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/SocketDecorator.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketDecorator.kt new file mode 100644 index 000000000000..5af0e1362bb6 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketDecorator.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(ExperimentalTime::class) + +package mockwebserver.socket + +import java.io.InputStream +import java.io.OutputStream +import java.net.InetAddress +import java.net.Socket +import java.net.SocketAddress +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +import okio.Buffer +import okio.ForwardingSink +import okio.ForwardingSource +import okio.Sink +import okio.Source +import okio.Timeout +import okio.buffer +import okio.sink +import okio.source + +/** + * Wraps a standard java.net.Socket with Okio sources and sinks that + * emit SocketEvents to a provided listener. This Allows intercepting OkHttp's actual calls. + */ +public open class SocketDecorator( + public val delegate: Socket, +) : Socket() { + + override fun connect(endpoint: SocketAddress?) { delegate.connect(endpoint) } + override fun connect(endpoint: SocketAddress?, timeout: Int) { delegate.connect(endpoint, timeout) } + override fun bind(bindpoint: SocketAddress?) { delegate.bind(bindpoint) } + override fun getInetAddress(): InetAddress? = delegate.inetAddress + override fun getLocalAddress(): InetAddress? = delegate.localAddress + override fun getPort(): Int = delegate.port + override fun getLocalPort(): Int = delegate.localPort + override fun getRemoteSocketAddress(): SocketAddress? = delegate.remoteSocketAddress + override fun getLocalSocketAddress(): SocketAddress? = delegate.localSocketAddress + override fun getChannel(): java.nio.channels.SocketChannel? = delegate.channel + override fun getInputStream(): InputStream = delegate.getInputStream() + override fun getOutputStream(): OutputStream = delegate.getOutputStream() + + override fun close() { + delegate.close() + } + + override fun setTcpNoDelay(on: Boolean) { delegate.tcpNoDelay = on } + override fun getTcpNoDelay(): Boolean = delegate.tcpNoDelay + override fun setSoLinger(on: Boolean, linger: Int) { delegate.setSoLinger(on, linger) } + override fun getSoLinger(): Int = delegate.soLinger + override fun sendUrgentData(data: Int) { delegate.sendUrgentData(data) } + override fun setOOBInline(on: Boolean) { delegate.oobInline = on } + override fun getOOBInline(): Boolean = delegate.oobInline + override fun setSoTimeout(timeout: Int) { delegate.soTimeout = timeout } + override fun getSoTimeout(): Int = delegate.soTimeout + override fun setSendBufferSize(size: Int) { delegate.sendBufferSize = size } + override fun getSendBufferSize(): Int = delegate.sendBufferSize + override fun setReceiveBufferSize(size: Int) { delegate.receiveBufferSize = size } + override fun getReceiveBufferSize(): Int = delegate.receiveBufferSize + override fun setKeepAlive(on: Boolean) { delegate.keepAlive = on } + override fun getKeepAlive(): Boolean = delegate.keepAlive + override fun setTrafficClass(tc: Int) { delegate.trafficClass = tc } + override fun getTrafficClass(): Int = delegate.trafficClass + override fun setReuseAddress(on: Boolean) { delegate.reuseAddress = on } + override fun getReuseAddress(): Boolean = delegate.reuseAddress + override fun shutdownInput() { delegate.shutdownInput() } + override fun shutdownOutput() { delegate.shutdownOutput() } + override fun toString(): String = delegate.toString() + override fun isConnected(): Boolean = delegate.isConnected + override fun isBound(): Boolean = delegate.isBound + override fun isClosed(): Boolean = delegate.isClosed + override fun isInputShutdown(): Boolean = delegate.isInputShutdown + override fun isOutputShutdown(): Boolean = delegate.isOutputShutdown +} diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEvent.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEvent.kt new file mode 100644 index 000000000000..170207259fe5 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEvent.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2026 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(ExperimentalTime::class) + +package mockwebserver.socket + +import java.net.InetSocketAddress +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +public sealed class SocketEvent { + public data class SocketConnection(val local: InetSocketAddress, val peer: InetSocketAddress) + + public abstract val timestamp: Instant + public abstract val threadName: String + public abstract val socketName: String + public abstract val connection: SocketConnection + + public data class ReadSuccess( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val byteCount: Long, + val payload: okio.Buffer? = null, + ) : SocketEvent() + + public data class ReadFailed( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val reason: String + ) : SocketEvent() + + public data class ReadWait( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val waitNanos: Long + ) : SocketEvent() + + public data class ReadEof( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + ) : SocketEvent() + + public data class ReadTimeout( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + public val timeoutMs: Int + ) : SocketEvent() + + public data class TimeoutReached( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + public val message: String + ) : SocketEvent() + + public data class WriteSuccess( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val byteCount: Long, + val arrivalTime: Instant, + val payload: okio.Buffer? = null + ) : SocketEvent() + + public data class WriteFailed( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val reason: String + ) : SocketEvent() + + public data class WriteWaitBufferFull( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val bufferSize: Long + ) : SocketEvent() + + public data class Close( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + ) : SocketEvent() + + public data class ShutdownInput( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + ) : SocketEvent() + + public data class ShutdownOutput( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + ) : SocketEvent() + + public data class Connect( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val host: String?, + val port: Int + ) : SocketEvent() + + public data class AcceptStarting( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + ) : SocketEvent() + + public data class AcceptReturning( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val peerSocketName: String + ) : SocketEvent() + + public data class DataArrival( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val byteCount: Long, + val arrivalTime: Instant, + val payload: okio.Buffer? = null + ) : SocketEvent() +} diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEventListener.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEventListener.kt new file mode 100644 index 000000000000..11e279537d27 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEventListener.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package mockwebserver.socket + +public interface SocketEventListener { + public fun onEvent(event: SocketEvent) + + public companion object { + public val Noop: SocketEventListener = object : SocketEventListener { + override fun onEvent(event: SocketEvent) {} + } + } +} + +public class MemorySocketEventListener( + private val _events: MutableList = mutableListOf() +) : SocketEventListener { + public val events: List + get() = _events.toList() + + override fun onEvent(event: SocketEvent) { + _events.add(event) + } +} diff --git a/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt b/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt new file mode 100644 index 000000000000..71a95c7444d8 --- /dev/null +++ b/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt @@ -0,0 +1,234 @@ +@file:OptIn(ExperimentalStdlibApi::class) + +package mockwebserver.socket + +import assertk.assertThat +import assertk.assertions.isGreaterThan +import assertk.assertions.isNotNull +import assertk.assertions.isTrue +import java.lang.reflect.Field +import java.util.logging.Handler +import java.util.logging.Level +import java.util.logging.LogRecord +import java.util.logging.Logger +import javax.crypto.SecretKey +import javax.net.ssl.ExtendedSSLSession +import javax.net.ssl.SSLSocket +import kotlinx.coroutines.runBlocking +import okhttp3.Call +import okhttp3.CipherSuite +import okhttp3.Connection +import okhttp3.ConnectionSpec +import okhttp3.EventListener +import okhttp3.Handshake +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.TlsVersion +import okio.BufferedSink +import okio.FileSystem +import okio.Path.Companion.toPath +import okio.buffer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class CaptureTest { + private lateinit var logger: Logger + private lateinit var keyLogOut: BufferedSink + private var random: String? = null + private val fileNetLog = "build/reports/test-netlog.json".toPath() + private var filePcap = "build/reports/test-capture-1.pcap".toPath() + private val keyLog = "build/reports/keylog.txt".toPath() + val fileSystem = FileSystem.SYSTEM + + private val loggerHandler = + object : Handler() { + + override fun publish(record: LogRecord) { + // https://timothybasanov.com/2016/05/26/java-pre-master-secret.html + // https://security.stackexchange.com/questions/35639/decrypting-tls-in-wireshark-when-using-dhe-rsa-ciphersuites + // https://stackoverflow.com/questions/36240279/how-do-i-extract-the-pre-master-secret-using-an-openssl-based-client + + // TLSv1.2 Events + // Produced ClientHello handshake message + // Consuming ServerHello handshake message + // Consuming server Certificate handshake message + // Consuming server CertificateStatus handshake message + // Found trusted certificate + // Consuming ECDH ServerKeyExchange handshake message + // Consuming ServerHelloDone handshake message + // Produced ECDHE ClientKeyExchange handshake message + // Produced client Finished handshake message + // Consuming server Finished handshake message + // Produced ClientHello handshake message + // + // Raw write + // Raw read + // Plaintext before ENCRYPTION + // Plaintext after DECRYPTION + val message = record.message + val parameters = record.parameters + + if (parameters != null && !message.startsWith("Raw") && !message.startsWith("Plaintext")) { + // JSSE logs additional messages as parameters that are not referenced in the log message. + val parameter = parameters[0] as String + + if (message == "Produced ClientHello handshake message") { + random = readClientRandom(parameter) + } + } + } + + override fun flush() {} + + override fun close() {} + } + + @BeforeEach + fun setUp() { + // Enable JUL logging for SSL events, must be activated early or via -D option. + System.setProperty("javax.net.debug", "") + logger = + Logger + .getLogger("javax.net.ssl") + .apply { + level = Level.FINEST + useParentHandlers = false + } + logger.addHandler(loggerHandler) + + fileSystem.createDirectory(keyLog.parent!!) + var i = 1 + while (fileSystem.exists(filePcap)) { + i++ + filePcap = "build/reports/test-capture-$i.pcap".toPath() + } + + keyLogOut = fileSystem.appendingSink(keyLog).buffer() + + // Enable JUL logging for SSL events, must be activated early or via -D option. + logger = + Logger + .getLogger("javax.net.ssl") + .apply { + level = Level.FINEST + useParentHandlers = false + } + } + + @AfterEach + fun tearDown() { + // Leave files for manual inspection if needed, or delete. + } + + @Test + fun exportPcapAndNetlog(): Unit = runBlocking { + // Compose multiple listeners so both pcap and netlog can be generated in a single pass. + val netLogRecorder = NetLogRecorder(file = fileNetLog) + val pcapRecorder = PcapRecorder(file = filePcap) + val multiListener = object : SocketEventListener { + override fun onEvent(event: SocketEvent) { + netLogRecorder.onEvent(event) + pcapRecorder.onEvent(event) + } + } + + try { + val connectionSpec = + ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) +// .cipherSuites( +// CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, +// CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384, +// CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, +// CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, +// CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA, +// ) + .tlsVersions(TlsVersion.TLS_1_2) + .build() + + val client = OkHttpClient.Builder() + .socketFactory(RecordingSocketFactory(socketEventListener = multiListener)) + .connectionSpecs(listOf(connectionSpec)) + .eventListener(object : EventListener() { + + override fun secureConnectEnd(call: Call, handshake: Handshake?) { + super.secureConnectEnd(call, handshake) + println(handshake) + } + + override fun connectionAcquired(call: Call, connection: Connection) { + if (connection.handshake() != null) { + val sslSocket = connection.socket() as SSLSocket + val session = sslSocket.session as ExtendedSSLSession + logKey(session) + } + } + }) + .build() + + client.newCall(Request("https://google.com/robots.txt".toHttpUrl())).execute().use { + it.body.string() + } + + client.newCall(Request("https://github.com/robots.txt".toHttpUrl())).execute().use { + it.body.string() + } + } finally { + netLogRecorder.close() + pcapRecorder.close() + } + + // Verify traces got written + assertThat(fileSystem.exists(fileNetLog)).isTrue() + assertThat(fileSystem.metadata(fileNetLog).size).isNotNull().isGreaterThan(100L) + + assertThat(fileSystem.exists(filePcap)).isTrue() + assertThat(fileSystem.metadata(filePcap).size).isNotNull().isGreaterThan(100L) + } + + private val masterSecretField: Field = run { + val clazz = Class.forName("sun.security.ssl.SSLSessionImpl") + val field = clazz.getDeclaredField("masterSecret") + field.isAccessible = true + field + } + + private fun logKey(session: ExtendedSSLSession) { + val masterSecret = masterSecretField.get(session) as SecretKey? + + if (masterSecret != null) { + val masterSecretHex = masterSecret.encoded.toHexString(HexFormat.Default) + + if (random != null) { + keyLogOut + .writeUtf8("CLIENT_RANDOM $random $masterSecretHex\n") + .flush() + } + + val id = session.id + keyLogOut + .writeUtf8("RSA Session-ID:${id.toHexString(HexFormat.Default)} Master-Key:$masterSecretHex\n") + .flush() + } + } + + val randomRegex = "\"random\"\\s+:\\s+\"([^\"]+)\"".toRegex() + + private fun readClientRandom(param: String): String? { + val matchResult = randomRegex.find(param) + + return if (matchResult != null) { + matchResult.groupValues[1].replace(" ", "") + } else { + null + } + } + + companion object { + init { + System.setProperty("javax.net.debug", "") + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 53d4dfdba76e..85bf48b7b7df 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,6 +30,7 @@ include(":mockwebserver-junit4") project(":mockwebserver-junit4").name = "mockwebserver3-junit4" include(":mockwebserver-junit5") project(":mockwebserver-junit5").name = "mockwebserver3-junit5" +include(":mocksocket") val androidBuild: String by settings val graalBuild: String by settings From 3d786f0f59d28f83e82fc3847eec7bd6b665103a Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 22 Feb 2026 12:19:45 +0000 Subject: [PATCH 2/5] Disable compression to make debug easier --- mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt b/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt index 71a95c7444d8..82b6964a0b6b 100644 --- a/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt +++ b/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt @@ -151,6 +151,9 @@ class CaptureTest { val client = OkHttpClient.Builder() .socketFactory(RecordingSocketFactory(socketEventListener = multiListener)) .connectionSpecs(listOf(connectionSpec)) + .addInterceptor { it.proceed(it.request().newBuilder() + .header("Accept-Encoding", "identity") + .build()) } .eventListener(object : EventListener() { override fun secureConnectEnd(call: Call, handshake: Handshake?) { From 234b8186bd7f7cbc8b05c0c0e18661baf2ddc2c1 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 22 Feb 2026 12:49:05 +0000 Subject: [PATCH 3/5] Fix JDK 8 tests --- mocksocket/build.gradle.kts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/mocksocket/build.gradle.kts b/mocksocket/build.gradle.kts index 2041345128eb..929475678d2e 100644 --- a/mocksocket/build.gradle.kts +++ b/mocksocket/build.gradle.kts @@ -1,3 +1,5 @@ +import okhttp3.buildsupport.testJavaVersion + plugins { kotlin("jvm") id("okhttp.publish-conventions") @@ -19,12 +21,16 @@ dependencies { testRuntimeOnly(libs.junit.jupiter.engine) } +val testJavaVersion = project.testJavaVersion + +if (testJavaVersion >= 11) { tasks.withType { - jvmArgs( - "--add-opens=java.base/sun.security.ssl=ALL-UNNAMED", - "--add-opens=java.base/sun.security.util=ALL-UNNAMED", - "--add-opens=java.base/sun.security.provider=ALL-UNNAMED", - ) + jvmArgs( + "--add-opens=java.base/sun.security.ssl=ALL-UNNAMED", + "--add-opens=java.base/sun.security.util=ALL-UNNAMED", + "--add-opens=java.base/sun.security.provider=ALL-UNNAMED", + ) + } } kotlin { From 465bdf51afd28ade5334b5ffcc83642f350d8d40 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 22 Feb 2026 13:08:40 +0000 Subject: [PATCH 4/5] api file --- mocksocket/api/mocksocket.api | 429 ++++++++++++++++++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 mocksocket/api/mocksocket.api diff --git a/mocksocket/api/mocksocket.api b/mocksocket/api/mocksocket.api new file mode 100644 index 000000000000..d67fae85f7bd --- /dev/null +++ b/mocksocket/api/mocksocket.api @@ -0,0 +1,429 @@ +public final class mockwebserver/socket/MemorySocketEventListener : mockwebserver/socket/SocketEventListener { + public fun ()V + public fun (Ljava/util/List;)V + public synthetic fun (Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getEvents ()Ljava/util/List; + public fun onEvent (Lmockwebserver/socket/SocketEvent;)V +} + +public final class mockwebserver/socket/NetLogRecorder : java/io/Closeable, mockwebserver/socket/SocketEventListener { + public fun (Lokio/Path;Lokio/FileSystem;)V + public synthetic fun (Lokio/Path;Lokio/FileSystem;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close ()V + public fun onEvent (Lmockwebserver/socket/SocketEvent;)V +} + +public final class mockwebserver/socket/PcapRecorder : java/io/Closeable, mockwebserver/socket/SocketEventListener { + public fun (Lokio/Path;Lokio/FileSystem;)V + public synthetic fun (Lokio/Path;Lokio/FileSystem;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close ()V + public final fun getSimulateTcp ()Z + public fun onEvent (Lmockwebserver/socket/SocketEvent;)V +} + +public class mockwebserver/socket/RecordingSocket : mockwebserver/socket/SocketDecorator { + public fun (Ljava/net/Socket;Lmockwebserver/socket/SocketEventListener;Ljava/lang/String;)V + public synthetic fun (Ljava/net/Socket;Lmockwebserver/socket/SocketEventListener;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close ()V + public fun connect (Ljava/net/SocketAddress;)V + public fun connect (Ljava/net/SocketAddress;I)V + public fun getInputStream ()Ljava/io/InputStream; + public fun getOutputStream ()Ljava/io/OutputStream; + public final fun getSocketName ()Ljava/lang/String; + public fun shutdownInput ()V + public fun shutdownOutput ()V +} + +public final class mockwebserver/socket/RecordingSocketFactory : javax/net/SocketFactory { + public fun (Lmockwebserver/socket/SocketEventListener;Ljavax/net/SocketFactory;)V + public synthetic fun (Lmockwebserver/socket/SocketEventListener;Ljavax/net/SocketFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun createSocket ()Ljava/net/Socket; + public fun createSocket (Ljava/lang/String;I)Ljava/net/Socket; + public fun createSocket (Ljava/lang/String;ILjava/net/InetAddress;I)Ljava/net/Socket; + public fun createSocket (Ljava/net/InetAddress;I)Ljava/net/Socket; + public fun createSocket (Ljava/net/InetAddress;ILjava/net/InetAddress;I)Ljava/net/Socket; +} + +public class mockwebserver/socket/SocketDecorator : java/net/Socket { + public fun (Ljava/net/Socket;)V + public fun bind (Ljava/net/SocketAddress;)V + public fun close ()V + public fun connect (Ljava/net/SocketAddress;)V + public fun connect (Ljava/net/SocketAddress;I)V + public fun getChannel ()Ljava/nio/channels/SocketChannel; + public final fun getDelegate ()Ljava/net/Socket; + public fun getInetAddress ()Ljava/net/InetAddress; + public fun getInputStream ()Ljava/io/InputStream; + public fun getKeepAlive ()Z + public fun getLocalAddress ()Ljava/net/InetAddress; + public fun getLocalPort ()I + public fun getLocalSocketAddress ()Ljava/net/SocketAddress; + public fun getOOBInline ()Z + public fun getOutputStream ()Ljava/io/OutputStream; + public fun getPort ()I + public fun getReceiveBufferSize ()I + public fun getRemoteSocketAddress ()Ljava/net/SocketAddress; + public fun getReuseAddress ()Z + public fun getSendBufferSize ()I + public fun getSoLinger ()I + public fun getSoTimeout ()I + public fun getTcpNoDelay ()Z + public fun getTrafficClass ()I + public fun isBound ()Z + public fun isClosed ()Z + public fun isConnected ()Z + public fun isInputShutdown ()Z + public fun isOutputShutdown ()Z + public fun sendUrgentData (I)V + public fun setKeepAlive (Z)V + public fun setOOBInline (Z)V + public fun setReceiveBufferSize (I)V + public fun setReuseAddress (Z)V + public fun setSendBufferSize (I)V + public fun setSoLinger (ZI)V + public fun setSoTimeout (I)V + public fun setTcpNoDelay (Z)V + public fun setTrafficClass (I)V + public fun shutdownInput ()V + public fun shutdownOutput ()V + public fun toString ()Ljava/lang/String; +} + +public abstract class mockwebserver/socket/SocketEvent { + public abstract fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public abstract fun getSocketName ()Ljava/lang/String; + public abstract fun getThreadName ()Ljava/lang/String; + public abstract fun getTimestamp ()Lkotlin/time/Instant; +} + +public final class mockwebserver/socket/SocketEvent$AcceptReturning : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;)Lmockwebserver/socket/SocketEvent$AcceptReturning; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$AcceptReturning;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$AcceptReturning; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun getPeerSocketName ()Ljava/lang/String; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$AcceptStarting : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;)Lmockwebserver/socket/SocketEvent$AcceptStarting; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$AcceptStarting;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$AcceptStarting; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$Close : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;)Lmockwebserver/socket/SocketEvent$Close; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$Close;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$Close; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$Connect : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;I)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()I + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;I)Lmockwebserver/socket/SocketEvent$Connect; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$Connect;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;IILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$Connect; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun getHost ()Ljava/lang/String; + public final fun getPort ()I + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$DataArrival : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLkotlin/time/Instant;Lokio/Buffer;)V + public synthetic fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLkotlin/time/Instant;Lokio/Buffer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun component5 ()J + public final fun component6 ()Lkotlin/time/Instant; + public final fun component7 ()Lokio/Buffer; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLkotlin/time/Instant;Lokio/Buffer;)Lmockwebserver/socket/SocketEvent$DataArrival; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$DataArrival;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLkotlin/time/Instant;Lokio/Buffer;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$DataArrival; + public fun equals (Ljava/lang/Object;)Z + public final fun getArrivalTime ()Lkotlin/time/Instant; + public final fun getByteCount ()J + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun getPayload ()Lokio/Buffer; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$ReadEof : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;)Lmockwebserver/socket/SocketEvent$ReadEof; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$ReadEof;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$ReadEof; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$ReadFailed : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;)Lmockwebserver/socket/SocketEvent$ReadFailed; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$ReadFailed;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$ReadFailed; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun getReason ()Ljava/lang/String; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$ReadSuccess : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLokio/Buffer;)V + public synthetic fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLokio/Buffer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun component5 ()J + public final fun component6 ()Lokio/Buffer; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLokio/Buffer;)Lmockwebserver/socket/SocketEvent$ReadSuccess; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$ReadSuccess;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLokio/Buffer;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$ReadSuccess; + public fun equals (Ljava/lang/Object;)Z + public final fun getByteCount ()J + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun getPayload ()Lokio/Buffer; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$ReadTimeout : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;I)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun component5 ()I + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;I)Lmockwebserver/socket/SocketEvent$ReadTimeout; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$ReadTimeout;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;IILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$ReadTimeout; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public final fun getTimeoutMs ()I + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$ReadWait : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;J)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun component5 ()J + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;J)Lmockwebserver/socket/SocketEvent$ReadWait; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$ReadWait;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$ReadWait; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public final fun getWaitNanos ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$ShutdownInput : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;)Lmockwebserver/socket/SocketEvent$ShutdownInput; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$ShutdownInput;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$ShutdownInput; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$ShutdownOutput : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;)Lmockwebserver/socket/SocketEvent$ShutdownOutput; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$ShutdownOutput;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$ShutdownOutput; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$SocketConnection { + public fun (Ljava/net/InetSocketAddress;Ljava/net/InetSocketAddress;)V + public final fun component1 ()Ljava/net/InetSocketAddress; + public final fun component2 ()Ljava/net/InetSocketAddress; + public final fun copy (Ljava/net/InetSocketAddress;Ljava/net/InetSocketAddress;)Lmockwebserver/socket/SocketEvent$SocketConnection; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/net/InetSocketAddress;Ljava/net/InetSocketAddress;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$SocketConnection; + public fun equals (Ljava/lang/Object;)Z + public final fun getLocal ()Ljava/net/InetSocketAddress; + public final fun getPeer ()Ljava/net/InetSocketAddress; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$TimeoutReached : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;)Lmockwebserver/socket/SocketEvent$TimeoutReached; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$TimeoutReached;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$TimeoutReached; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun getMessage ()Ljava/lang/String; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$WriteFailed : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;)Lmockwebserver/socket/SocketEvent$WriteFailed; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$WriteFailed;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;Ljava/lang/String;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$WriteFailed; + public fun equals (Ljava/lang/Object;)Z + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun getReason ()Ljava/lang/String; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$WriteSuccess : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLkotlin/time/Instant;Lokio/Buffer;)V + public synthetic fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLkotlin/time/Instant;Lokio/Buffer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun component5 ()J + public final fun component6 ()Lkotlin/time/Instant; + public final fun component7 ()Lokio/Buffer; + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLkotlin/time/Instant;Lokio/Buffer;)Lmockwebserver/socket/SocketEvent$WriteSuccess; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$WriteSuccess;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JLkotlin/time/Instant;Lokio/Buffer;ILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$WriteSuccess; + public fun equals (Ljava/lang/Object;)Z + public final fun getArrivalTime ()Lkotlin/time/Instant; + public final fun getByteCount ()J + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun getPayload ()Lokio/Buffer; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class mockwebserver/socket/SocketEvent$WriteWaitBufferFull : mockwebserver/socket/SocketEvent { + public fun (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;J)V + public final fun component1 ()Lkotlin/time/Instant; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public final fun component5 ()J + public final fun copy (Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;J)Lmockwebserver/socket/SocketEvent$WriteWaitBufferFull; + public static synthetic fun copy$default (Lmockwebserver/socket/SocketEvent$WriteWaitBufferFull;Lkotlin/time/Instant;Ljava/lang/String;Ljava/lang/String;Lmockwebserver/socket/SocketEvent$SocketConnection;JILjava/lang/Object;)Lmockwebserver/socket/SocketEvent$WriteWaitBufferFull; + public fun equals (Ljava/lang/Object;)Z + public final fun getBufferSize ()J + public fun getConnection ()Lmockwebserver/socket/SocketEvent$SocketConnection; + public fun getSocketName ()Ljava/lang/String; + public fun getThreadName ()Ljava/lang/String; + public fun getTimestamp ()Lkotlin/time/Instant; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class mockwebserver/socket/SocketEventListener { + public static final field Companion Lmockwebserver/socket/SocketEventListener$Companion; + public abstract fun onEvent (Lmockwebserver/socket/SocketEvent;)V +} + +public final class mockwebserver/socket/SocketEventListener$Companion { + public final fun getNoop ()Lmockwebserver/socket/SocketEventListener; +} + From 5afa19213326bf4d3c519998a3212f221a446da7 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 22 Feb 2026 16:33:04 +0000 Subject: [PATCH 5/5] clenaup --- mocksocket/build.gradle.kts | 2 +- .../mockwebserver/socket/NetLogRecorder.kt | 163 ++++++----- .../mockwebserver/socket/PcapRecorder.kt | 54 ++-- .../mockwebserver/socket/RecordingSocket.kt | 110 ++++--- .../socket/RecordingSocketFactory.kt | 40 ++- .../mockwebserver/socket/SocketDecorator.kt | 160 +++++++--- .../mockwebserver/socket/SocketEvent.kt | 273 +++++++++--------- .../socket/SocketEventListener.kt | 21 +- .../java/mockwebserver/socket/CaptureTest.kt | 138 +++++---- 9 files changed, 543 insertions(+), 418 deletions(-) diff --git a/mocksocket/build.gradle.kts b/mocksocket/build.gradle.kts index 929475678d2e..1c33146548b5 100644 --- a/mocksocket/build.gradle.kts +++ b/mocksocket/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { val testJavaVersion = project.testJavaVersion if (testJavaVersion >= 11) { -tasks.withType { + tasks.withType { jvmArgs( "--add-opens=java.base/sun.security.ssl=ALL-UNNAMED", "--add-opens=java.base/sun.security.util=ALL-UNNAMED", diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/NetLogRecorder.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/NetLogRecorder.kt index 1f5e97284f4e..a28b55d4970f 100644 --- a/mocksocket/src/main/kotlin/mockwebserver/socket/NetLogRecorder.kt +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/NetLogRecorder.kt @@ -24,91 +24,102 @@ import okio.FileSystem import okio.Path import okio.buffer -public class NetLogRecorder(file: Path, fileSystem: FileSystem = FileSystem.SYSTEM) : SocketEventListener, Closeable { - private val writer = fileSystem.sink(file).buffer() - private var isFirstEvent = true - private var closed = false +public class NetLogRecorder( + file: Path, + fileSystem: FileSystem = FileSystem.SYSTEM, +) : SocketEventListener, + Closeable { + private val writer = fileSystem.sink(file).buffer() + private var isFirstEvent = true + private var closed = false - init { - writer.println("{") - writer.println(" \"constants\": {},") - writer.println(" \"events\": [") - writer.flush() - } + init { + writer.println("{") + writer.println(" \"constants\": {},") + writer.println(" \"events\": [") + writer.flush() + } - override fun onEvent(event: SocketEvent) { - val time = event.timestamp.toEpochMilliseconds() + override fun onEvent(event: SocketEvent) { + val time = event.timestamp.toEpochMilliseconds() - val jsonEvent = when (event) { - is SocketEvent.Connect -> { - """ - { - "phase": 1, - "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, - "time": "$time", - "type": 67, - "params": { - "address": "${event.host}:${event.port}" - } - } - """.trimIndent() - } - is SocketEvent.ReadSuccess -> { - // Not recording actual base64 payload to save memory, just counts - """ - { - "phase": 0, - "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, - "time": "$time", - "type": 113, - "params": { "byte_count": ${event.byteCount} } - } - """.trimIndent() - } - is SocketEvent.WriteSuccess -> { - """ - { - "phase": 0, - "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, - "time": "$time", - "type": 114, - "params": { "byte_count": ${event.byteCount} } - } - """.trimIndent() + val jsonEvent = + when (event) { + is SocketEvent.Connect -> { + """ + { + "phase": 1, + "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, + "time": "$time", + "type": 67, + "params": { + "address": "${event.host}:${event.port}" } - is SocketEvent.Close -> { - """ - { - "phase": 2, - "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, - "time": "$time", - "type": 67 - } - """.trimIndent() - } - else -> null + } + """.trimIndent() } - if (jsonEvent != null) { - synchronized(this) { - if (!isFirstEvent) { - writer.println(",") - } - isFirstEvent = false - writer.writeUtf8(jsonEvent.replace("\n", "\n ")) - writer.flush() - } + is SocketEvent.ReadSuccess -> { + // Not recording actual base64 payload to save memory, just counts + """ + { + "phase": 0, + "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, + "time": "$time", + "type": 113, + "params": { "byte_count": ${event.byteCount} } + } + """.trimIndent() } - } - override fun close() { - if (closed) return - closed = true - if (!isFirstEvent) writer.writeUtf8("\n") - writer.println(" ]") - writer.println("}") - writer.close() + is SocketEvent.WriteSuccess -> { + """ + { + "phase": 0, + "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, + "time": "$time", + "type": 114, + "params": { "byte_count": ${event.byteCount} } + } + """.trimIndent() + } + + is SocketEvent.Close -> { + """ + { + "phase": 2, + "source": { "id": ${event.socketName.hashCode()}, "type": 10 }, + "time": "$time", + "type": 67 + } + """.trimIndent() + } + + else -> { + null + } + } + + if (jsonEvent != null) { + synchronized(this) { + if (!isFirstEvent) { + writer.println(",") + } + isFirstEvent = false + writer.writeUtf8(jsonEvent.replace("\n", "\n ")) + writer.flush() + } } + } + + override fun close() { + if (closed) return + closed = true + if (!isFirstEvent) writer.writeUtf8("\n") + writer.println(" ]") + writer.println("}") + writer.close() + } } private fun BufferedSink.println(string: String) { diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/PcapRecorder.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/PcapRecorder.kt index d5ce2ce2fcd4..ba554da90fd3 100644 --- a/mocksocket/src/main/kotlin/mockwebserver/socket/PcapRecorder.kt +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/PcapRecorder.kt @@ -30,8 +30,11 @@ import okio.FileSystem import okio.Path import okio.buffer -public class PcapRecorder(file: Path, fileSystem: FileSystem = FileSystem.SYSTEM) : SocketEventListener, Closeable { - +public class PcapRecorder( + file: Path, + fileSystem: FileSystem = FileSystem.SYSTEM, +) : SocketEventListener, + Closeable { private val globalHeader = PcapGlobalHeader.createDefaultHeader() private val out = PcapOutputStream.create(globalHeader, fileSystem.sink(file).buffer().outputStream()) private var closed = false @@ -61,27 +64,27 @@ public class PcapRecorder(file: Path, fileSystem: FileSystem = FileSystem.SYSTEM ack, syn = true, ackFlag = false, - payload = null + payload = null, ) seq++ } } is SocketEvent.WriteSuccess -> { - // PSH, ACK - val payloadBytes = event.payload?.readByteArray() - writePacket( - out, - event.timestamp, - event.connection, - seq, - ack, - syn = false, - ackFlag = true, - psh = true, - payload = payloadBytes - ) - if (payloadBytes != null) seq += payloadBytes.size + // PSH, ACK + val payloadBytes = event.payload?.readByteArray() + writePacket( + out, + event.timestamp, + event.connection, + seq, + ack, + syn = false, + ackFlag = true, + psh = true, + payload = payloadBytes, + ) + if (payloadBytes != null) seq += payloadBytes.size } is SocketEvent.ReadSuccess -> { @@ -113,7 +116,7 @@ public class PcapRecorder(file: Path, fileSystem: FileSystem = FileSystem.SYSTEM syn = false, ackFlag = true, fin = true, - payload = null + payload = null, ) seq++ } @@ -145,7 +148,7 @@ public class PcapRecorder(file: Path, fileSystem: FileSystem = FileSystem.SYSTEM ackFlag: Boolean = false, fin: Boolean = false, psh: Boolean = false, - payload: ByteArray? = null + payload: ByteArray? = null, ) { // Because pkts.io is built around reading packets rather than forging them from scratch natively as a builder // we manually construct a raw Ethernet + IPv4 + TCP packet byte string for the writer, using standard standard header lengths. @@ -159,7 +162,7 @@ public class PcapRecorder(file: Path, fileSystem: FileSystem = FileSystem.SYSTEM // Ethernet (14 bytes) pkt.write(ByteArray(6) { 0x00 }) // Dst MAC pkt.write(ByteArray(6) { 0x00 }) // Src MAC - pkt.writeShort(0x0800) // Type IPv4 + pkt.writeShort(0x0800) // Type IPv4 // IPv4 (20 bytes) pkt.writeByte(0x45) // Version 4, IHL 5 @@ -210,11 +213,12 @@ public class PcapRecorder(file: Path, fileSystem: FileSystem = FileSystem.SYSTEM val recordHeader = PcapRecordHeader.createDefaultHeader(timestamp.toEpochMilliseconds()) recordHeader.capturedLength = rawPkt.size.toLong() recordHeader.totalLength = rawPkt.size.toLong() - val frame = PCapPacketImpl( - globalHeader, - recordHeader, - Buffers.wrap(rawPkt) - ) + val frame = + PCapPacketImpl( + globalHeader, + recordHeader, + Buffers.wrap(rawPkt), + ) out.write(frame) } } diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocket.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocket.kt index 151e61a1bb75..efc7f2281d12 100644 --- a/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocket.kt +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocket.kt @@ -39,7 +39,7 @@ import okio.source public open class RecordingSocket( delegate: Socket, private val socketEventListener: SocketEventListener, - public val socketName: String = "Socket" + public val socketName: String = "Socket", ) : SocketDecorator(delegate) { init { if (delegate.isConnected) { @@ -59,47 +59,60 @@ public open class RecordingSocket( recordConnect(endpoint) } - override fun connect(endpoint: SocketAddress?, timeout: Int) { + override fun connect( + endpoint: SocketAddress?, + timeout: Int, + ) { super.connect(endpoint, timeout) recordSocketConnection() recordConnect(endpoint) } private fun recordSocketConnection() { - this.socketConnection = SocketEvent.SocketConnection( - delegate.localSocketAddress as InetSocketAddress, - delegate.remoteSocketAddress as InetSocketAddress - ) + this.socketConnection = + SocketEvent.SocketConnection( + delegate.localSocketAddress as InetSocketAddress, + delegate.remoteSocketAddress as InetSocketAddress, + ) } private val mySource: Source by lazy { object : ForwardingSource(delegate.source()) { - override fun read(sink: Buffer, byteCount: Long): Long { + override fun read( + sink: Buffer, + byteCount: Long, + ): Long { val startSize = sink.size val readCount = super.read(sink, byteCount) val payloadSize = sink.size - startSize - val payload = if (payloadSize > 0) { - val clone = Buffer() - sink.copyTo(clone, startSize, payloadSize) - clone - } else null - - val event = if (readCount == -1L) { - SocketEvent.ReadEof( - clock.now(), Thread.currentThread().name, socketName, - socketConnection, - ) - } else { - SocketEvent.ReadSuccess( - clock.now(), - Thread.currentThread().name, - socketName, - socketConnection, - readCount, - payload, - ) - } + val payload = + if (payloadSize > 0) { + val clone = Buffer() + sink.copyTo(clone, startSize, payloadSize) + clone + } else { + null + } + + val event = + if (readCount == -1L) { + SocketEvent.ReadEof( + clock.now(), + Thread.currentThread().name, + socketName, + socketConnection, + ) + } else { + SocketEvent.ReadSuccess( + clock.now(), + Thread.currentThread().name, + socketName, + socketConnection, + readCount, + payload, + ) + } socketEventListener.onEvent(event) return readCount } @@ -112,7 +125,7 @@ public open class RecordingSocket( Thread.currentThread().name, socketName, socketConnection, - ) + ), ) } } @@ -120,12 +133,18 @@ public open class RecordingSocket( private val mySink: Sink by lazy { object : ForwardingSink(delegate.sink()) { - override fun write(source: Buffer, byteCount: Long) { - val payload = if (byteCount > 0) { - val clone = Buffer() - source.copyTo(clone, 0, byteCount) - clone - } else null + override fun write( + source: Buffer, + byteCount: Long, + ) { + val payload = + if (byteCount > 0) { + val clone = Buffer() + source.copyTo(clone, 0, byteCount) + clone + } else { + null + } super.write(source, byteCount) @@ -137,8 +156,8 @@ public open class RecordingSocket( socketConnection, byteCount, clock.now(), - payload - ) + payload, + ), ) } @@ -150,7 +169,7 @@ public open class RecordingSocket( Thread.currentThread().name, socketName, socketConnection, - ) + ), ) } } @@ -168,8 +187,8 @@ public open class RecordingSocket( socketName, socketConnection, address?.hostName, - address?.port ?: 0 - ) + address?.port ?: 0, + ), ) } @@ -182,9 +201,11 @@ public open class RecordingSocket( if (this::socketConnection.isInitialized) { socketEventListener.onEvent( SocketEvent.Close( - clock.now(), Thread.currentThread().name, socketName, + clock.now(), + Thread.currentThread().name, + socketName, socketConnection, - ) + ), ) } } @@ -197,7 +218,7 @@ public open class RecordingSocket( Thread.currentThread().name, socketName, socketConnection, - ) + ), ) } @@ -210,9 +231,8 @@ public open class RecordingSocket( Thread.currentThread().name, socketName, socketConnection, - ) + ), ) } } } - diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocketFactory.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocketFactory.kt index b5d0c4cc73f8..7ced110dca28 100644 --- a/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocketFactory.kt +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocketFactory.kt @@ -5,42 +5,40 @@ import java.net.Socket import javax.net.SocketFactory public class RecordingSocketFactory( - private val socketEventListener: SocketEventListener, - private val delegate: SocketFactory = getDefault(), + private val socketEventListener: SocketEventListener, + private val delegate: SocketFactory = getDefault(), ) : SocketFactory() { - override fun createSocket(): Socket { - return RecordingSocket(delegate.createSocket(), socketEventListener) - } + override fun createSocket(): Socket = RecordingSocket(delegate.createSocket(), socketEventListener) - override fun createSocket(host: String?, port: Int): Socket { - return RecordingSocket(delegate.createSocket(host, port), socketEventListener) - } + override fun createSocket( + host: String?, + port: Int, + ): Socket = RecordingSocket(delegate.createSocket(host, port), socketEventListener) override fun createSocket( host: String?, port: Int, localHost: InetAddress?, - localPort: Int - ): Socket { - return RecordingSocket( + localPort: Int, + ): Socket = + RecordingSocket( delegate.createSocket(host, port, localHost, localPort), - socketEventListener + socketEventListener, ) - } - override fun createSocket(host: InetAddress?, port: Int): Socket { - return RecordingSocket(delegate.createSocket(host, port), socketEventListener) - } + override fun createSocket( + host: InetAddress?, + port: Int, + ): Socket = RecordingSocket(delegate.createSocket(host, port), socketEventListener) override fun createSocket( address: InetAddress?, port: Int, localAddress: InetAddress?, - localPort: Int - ): Socket { - return RecordingSocket( + localPort: Int, + ): Socket = + RecordingSocket( delegate.createSocket(address, port, localAddress, localPort), - socketEventListener + socketEventListener, ) - } } diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/SocketDecorator.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketDecorator.kt index 5af0e1362bb6..c905aaa9a7a3 100644 --- a/mocksocket/src/main/kotlin/mockwebserver/socket/SocketDecorator.kt +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketDecorator.kt @@ -41,49 +41,121 @@ import okio.source public open class SocketDecorator( public val delegate: Socket, ) : Socket() { + override fun connect(endpoint: SocketAddress?) { + delegate.connect(endpoint) + } - override fun connect(endpoint: SocketAddress?) { delegate.connect(endpoint) } - override fun connect(endpoint: SocketAddress?, timeout: Int) { delegate.connect(endpoint, timeout) } - override fun bind(bindpoint: SocketAddress?) { delegate.bind(bindpoint) } - override fun getInetAddress(): InetAddress? = delegate.inetAddress - override fun getLocalAddress(): InetAddress? = delegate.localAddress - override fun getPort(): Int = delegate.port - override fun getLocalPort(): Int = delegate.localPort - override fun getRemoteSocketAddress(): SocketAddress? = delegate.remoteSocketAddress - override fun getLocalSocketAddress(): SocketAddress? = delegate.localSocketAddress - override fun getChannel(): java.nio.channels.SocketChannel? = delegate.channel - override fun getInputStream(): InputStream = delegate.getInputStream() - override fun getOutputStream(): OutputStream = delegate.getOutputStream() - - override fun close() { - delegate.close() - } - - override fun setTcpNoDelay(on: Boolean) { delegate.tcpNoDelay = on } - override fun getTcpNoDelay(): Boolean = delegate.tcpNoDelay - override fun setSoLinger(on: Boolean, linger: Int) { delegate.setSoLinger(on, linger) } - override fun getSoLinger(): Int = delegate.soLinger - override fun sendUrgentData(data: Int) { delegate.sendUrgentData(data) } - override fun setOOBInline(on: Boolean) { delegate.oobInline = on } - override fun getOOBInline(): Boolean = delegate.oobInline - override fun setSoTimeout(timeout: Int) { delegate.soTimeout = timeout } - override fun getSoTimeout(): Int = delegate.soTimeout - override fun setSendBufferSize(size: Int) { delegate.sendBufferSize = size } - override fun getSendBufferSize(): Int = delegate.sendBufferSize - override fun setReceiveBufferSize(size: Int) { delegate.receiveBufferSize = size } - override fun getReceiveBufferSize(): Int = delegate.receiveBufferSize - override fun setKeepAlive(on: Boolean) { delegate.keepAlive = on } - override fun getKeepAlive(): Boolean = delegate.keepAlive - override fun setTrafficClass(tc: Int) { delegate.trafficClass = tc } - override fun getTrafficClass(): Int = delegate.trafficClass - override fun setReuseAddress(on: Boolean) { delegate.reuseAddress = on } - override fun getReuseAddress(): Boolean = delegate.reuseAddress - override fun shutdownInput() { delegate.shutdownInput() } - override fun shutdownOutput() { delegate.shutdownOutput() } - override fun toString(): String = delegate.toString() - override fun isConnected(): Boolean = delegate.isConnected - override fun isBound(): Boolean = delegate.isBound - override fun isClosed(): Boolean = delegate.isClosed - override fun isInputShutdown(): Boolean = delegate.isInputShutdown - override fun isOutputShutdown(): Boolean = delegate.isOutputShutdown + override fun connect( + endpoint: SocketAddress?, + timeout: Int, + ) { + delegate.connect(endpoint, timeout) + } + + override fun bind(bindpoint: SocketAddress?) { + delegate.bind(bindpoint) + } + + override fun getInetAddress(): InetAddress? = delegate.inetAddress + + override fun getLocalAddress(): InetAddress? = delegate.localAddress + + override fun getPort(): Int = delegate.port + + override fun getLocalPort(): Int = delegate.localPort + + override fun getRemoteSocketAddress(): SocketAddress? = delegate.remoteSocketAddress + + override fun getLocalSocketAddress(): SocketAddress? = delegate.localSocketAddress + + override fun getChannel(): java.nio.channels.SocketChannel? = delegate.channel + + override fun getInputStream(): InputStream = delegate.getInputStream() + + override fun getOutputStream(): OutputStream = delegate.getOutputStream() + + override fun close() { + delegate.close() + } + + override fun setTcpNoDelay(on: Boolean) { + delegate.tcpNoDelay = on + } + + override fun getTcpNoDelay(): Boolean = delegate.tcpNoDelay + + override fun setSoLinger( + on: Boolean, + linger: Int, + ) { + delegate.setSoLinger(on, linger) + } + + override fun getSoLinger(): Int = delegate.soLinger + + override fun sendUrgentData(data: Int) { + delegate.sendUrgentData(data) + } + + override fun setOOBInline(on: Boolean) { + delegate.oobInline = on + } + + override fun getOOBInline(): Boolean = delegate.oobInline + + override fun setSoTimeout(timeout: Int) { + delegate.soTimeout = timeout + } + + override fun getSoTimeout(): Int = delegate.soTimeout + + override fun setSendBufferSize(size: Int) { + delegate.sendBufferSize = size + } + + override fun getSendBufferSize(): Int = delegate.sendBufferSize + + override fun setReceiveBufferSize(size: Int) { + delegate.receiveBufferSize = size + } + + override fun getReceiveBufferSize(): Int = delegate.receiveBufferSize + + override fun setKeepAlive(on: Boolean) { + delegate.keepAlive = on + } + + override fun getKeepAlive(): Boolean = delegate.keepAlive + + override fun setTrafficClass(tc: Int) { + delegate.trafficClass = tc + } + + override fun getTrafficClass(): Int = delegate.trafficClass + + override fun setReuseAddress(on: Boolean) { + delegate.reuseAddress = on + } + + override fun getReuseAddress(): Boolean = delegate.reuseAddress + + override fun shutdownInput() { + delegate.shutdownInput() + } + + override fun shutdownOutput() { + delegate.shutdownOutput() + } + + override fun toString(): String = delegate.toString() + + override fun isConnected(): Boolean = delegate.isConnected + + override fun isBound(): Boolean = delegate.isBound + + override fun isClosed(): Boolean = delegate.isClosed + + override fun isInputShutdown(): Boolean = delegate.isInputShutdown + + override fun isOutputShutdown(): Boolean = delegate.isOutputShutdown } diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEvent.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEvent.kt index 170207259fe5..efda64b35957 100644 --- a/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEvent.kt +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEvent.kt @@ -22,139 +22,142 @@ import kotlin.time.ExperimentalTime import kotlin.time.Instant public sealed class SocketEvent { - public data class SocketConnection(val local: InetSocketAddress, val peer: InetSocketAddress) - - public abstract val timestamp: Instant - public abstract val threadName: String - public abstract val socketName: String - public abstract val connection: SocketConnection - - public data class ReadSuccess( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - val byteCount: Long, - val payload: okio.Buffer? = null, - ) : SocketEvent() - - public data class ReadFailed( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - val reason: String - ) : SocketEvent() - - public data class ReadWait( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - val waitNanos: Long - ) : SocketEvent() - - public data class ReadEof( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - ) : SocketEvent() - - public data class ReadTimeout( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - public val timeoutMs: Int - ) : SocketEvent() - - public data class TimeoutReached( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - public val message: String - ) : SocketEvent() - - public data class WriteSuccess( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - val byteCount: Long, - val arrivalTime: Instant, - val payload: okio.Buffer? = null - ) : SocketEvent() - - public data class WriteFailed( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - val reason: String - ) : SocketEvent() - - public data class WriteWaitBufferFull( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - val bufferSize: Long - ) : SocketEvent() - - public data class Close( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - ) : SocketEvent() - - public data class ShutdownInput( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - ) : SocketEvent() - - public data class ShutdownOutput( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - ) : SocketEvent() - - public data class Connect( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - val host: String?, - val port: Int - ) : SocketEvent() - - public data class AcceptStarting( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - ) : SocketEvent() - - public data class AcceptReturning( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - val peerSocketName: String - ) : SocketEvent() - - public data class DataArrival( - override val timestamp: Instant, - override val threadName: String, - override val socketName: String, - override val connection: SocketConnection, - val byteCount: Long, - val arrivalTime: Instant, - val payload: okio.Buffer? = null - ) : SocketEvent() + public data class SocketConnection( + val local: InetSocketAddress, + val peer: InetSocketAddress, + ) + + public abstract val timestamp: Instant + public abstract val threadName: String + public abstract val socketName: String + public abstract val connection: SocketConnection + + public data class ReadSuccess( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val byteCount: Long, + val payload: okio.Buffer? = null, + ) : SocketEvent() + + public data class ReadFailed( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val reason: String, + ) : SocketEvent() + + public data class ReadWait( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val waitNanos: Long, + ) : SocketEvent() + + public data class ReadEof( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + ) : SocketEvent() + + public data class ReadTimeout( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + public val timeoutMs: Int, + ) : SocketEvent() + + public data class TimeoutReached( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + public val message: String, + ) : SocketEvent() + + public data class WriteSuccess( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val byteCount: Long, + val arrivalTime: Instant, + val payload: okio.Buffer? = null, + ) : SocketEvent() + + public data class WriteFailed( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val reason: String, + ) : SocketEvent() + + public data class WriteWaitBufferFull( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val bufferSize: Long, + ) : SocketEvent() + + public data class Close( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + ) : SocketEvent() + + public data class ShutdownInput( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + ) : SocketEvent() + + public data class ShutdownOutput( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + ) : SocketEvent() + + public data class Connect( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val host: String?, + val port: Int, + ) : SocketEvent() + + public data class AcceptStarting( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + ) : SocketEvent() + + public data class AcceptReturning( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val peerSocketName: String, + ) : SocketEvent() + + public data class DataArrival( + override val timestamp: Instant, + override val threadName: String, + override val socketName: String, + override val connection: SocketConnection, + val byteCount: Long, + val arrivalTime: Instant, + val payload: okio.Buffer? = null, + ) : SocketEvent() } diff --git a/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEventListener.kt b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEventListener.kt index 11e279537d27..a059373fe6b4 100644 --- a/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEventListener.kt +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEventListener.kt @@ -16,22 +16,23 @@ package mockwebserver.socket public interface SocketEventListener { - public fun onEvent(event: SocketEvent) + public fun onEvent(event: SocketEvent) - public companion object { - public val Noop: SocketEventListener = object : SocketEventListener { + public companion object { + public val Noop: SocketEventListener = + object : SocketEventListener { override fun onEvent(event: SocketEvent) {} } - } + } } public class MemorySocketEventListener( - private val _events: MutableList = mutableListOf() + private val _events: MutableList = mutableListOf(), ) : SocketEventListener { - public val events: List - get() = _events.toList() + public val events: List + get() = _events.toList() - override fun onEvent(event: SocketEvent) { - _events.add(event) - } + override fun onEvent(event: SocketEvent) { + _events.add(event) + } } diff --git a/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt b/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt index 82b6964a0b6b..46948b94aa9d 100644 --- a/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt +++ b/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt @@ -45,7 +45,6 @@ class CaptureTest { private val loggerHandler = object : Handler() { - override fun publish(record: LogRecord) { // https://timothybasanov.com/2016/05/26/java-pre-master-secret.html // https://security.stackexchange.com/questions/35639/decrypting-tls-in-wireshark-when-using-dhe-rsa-ciphersuites @@ -124,20 +123,23 @@ class CaptureTest { } @Test - fun exportPcapAndNetlog(): Unit = runBlocking { - // Compose multiple listeners so both pcap and netlog can be generated in a single pass. - val netLogRecorder = NetLogRecorder(file = fileNetLog) - val pcapRecorder = PcapRecorder(file = filePcap) - val multiListener = object : SocketEventListener { - override fun onEvent(event: SocketEvent) { - netLogRecorder.onEvent(event) - pcapRecorder.onEvent(event) - } - } + fun exportPcapAndNetlog(): Unit = + runBlocking { + // Compose multiple listeners so both pcap and netlog can be generated in a single pass. + val netLogRecorder = NetLogRecorder(file = fileNetLog) + val pcapRecorder = PcapRecorder(file = filePcap) + val multiListener = + object : SocketEventListener { + override fun onEvent(event: SocketEvent) { + netLogRecorder.onEvent(event) + pcapRecorder.onEvent(event) + } + } - try { - val connectionSpec = - ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + try { + val connectionSpec = + ConnectionSpec + .Builder(ConnectionSpec.MODERN_TLS) // .cipherSuites( // CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, // CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384, @@ -145,58 +147,72 @@ class CaptureTest { // CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, // CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // ) - .tlsVersions(TlsVersion.TLS_1_2) - .build() - - val client = OkHttpClient.Builder() - .socketFactory(RecordingSocketFactory(socketEventListener = multiListener)) - .connectionSpecs(listOf(connectionSpec)) - .addInterceptor { it.proceed(it.request().newBuilder() - .header("Accept-Encoding", "identity") - .build()) } - .eventListener(object : EventListener() { - - override fun secureConnectEnd(call: Call, handshake: Handshake?) { - super.secureConnectEnd(call, handshake) - println(handshake) - } - - override fun connectionAcquired(call: Call, connection: Connection) { - if (connection.handshake() != null) { - val sslSocket = connection.socket() as SSLSocket - val session = sslSocket.session as ExtendedSSLSession - logKey(session) - } - } - }) - .build() - - client.newCall(Request("https://google.com/robots.txt".toHttpUrl())).execute().use { - it.body.string() - } + .tlsVersions(TlsVersion.TLS_1_2) + .build() + + val client = + OkHttpClient + .Builder() + .socketFactory(RecordingSocketFactory(socketEventListener = multiListener)) + .connectionSpecs(listOf(connectionSpec)) + .addInterceptor { + it.proceed( + it + .request() + .newBuilder() + .header("Accept-Encoding", "identity") + .build(), + ) + }.eventListener( + object : EventListener() { + override fun secureConnectEnd( + call: Call, + handshake: Handshake?, + ) { + super.secureConnectEnd(call, handshake) + println(handshake) + } + + override fun connectionAcquired( + call: Call, + connection: Connection, + ) { + if (connection.handshake() != null) { + val sslSocket = connection.socket() as SSLSocket + val session = sslSocket.session as ExtendedSSLSession + logKey(session) + } + } + }, + ).build() + + client.newCall(Request("https://google.com/robots.txt".toHttpUrl())).execute().use { + it.body.string() + } - client.newCall(Request("https://github.com/robots.txt".toHttpUrl())).execute().use { - it.body.string() + client.newCall(Request("https://github.com/robots.txt".toHttpUrl())).execute().use { + it.body.string() + } + } finally { + netLogRecorder.close() + pcapRecorder.close() } - } finally { - netLogRecorder.close() - pcapRecorder.close() - } - // Verify traces got written - assertThat(fileSystem.exists(fileNetLog)).isTrue() - assertThat(fileSystem.metadata(fileNetLog).size).isNotNull().isGreaterThan(100L) + // Verify traces got written + assertThat(fileSystem.exists(fileNetLog)).isTrue() + assertThat(fileSystem.metadata(fileNetLog).size).isNotNull().isGreaterThan(100L) - assertThat(fileSystem.exists(filePcap)).isTrue() - assertThat(fileSystem.metadata(filePcap).size).isNotNull().isGreaterThan(100L) - } + assertThat(fileSystem.exists(filePcap)).isTrue() + assertThat(fileSystem.metadata(filePcap).size).isNotNull().isGreaterThan(100L) + } - private val masterSecretField: Field = run { - val clazz = Class.forName("sun.security.ssl.SSLSessionImpl") - val field = clazz.getDeclaredField("masterSecret") - field.isAccessible = true - field - } + private val masterSecretField: Field = + run { + val clazz = Class.forName("sun.security.ssl.SSLSessionImpl") + val field = clazz.getDeclaredField("masterSecret") + field.isAccessible = true + field + } private fun logKey(session: ExtendedSSLSession) { val masterSecret = masterSecretField.get(session) as SecretKey?