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/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; +} + diff --git a/mocksocket/build.gradle.kts b/mocksocket/build.gradle.kts new file mode 100644 index 000000000000..1c33146548b5 --- /dev/null +++ b/mocksocket/build.gradle.kts @@ -0,0 +1,38 @@ +import okhttp3.buildsupport.testJavaVersion + +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) +} + +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", + ) + } +} + +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..a28b55d4970f --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/NetLogRecorder.kt @@ -0,0 +1,128 @@ +/* + * 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..ba554da90fd3 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/PcapRecorder.kt @@ -0,0 +1,224 @@ +/* + * 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..efc7f2281d12 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocket.kt @@ -0,0 +1,238 @@ +/* + * 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..7ced110dca28 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/RecordingSocketFactory.kt @@ -0,0 +1,44 @@ +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 = RecordingSocket(delegate.createSocket(), 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 = + RecordingSocket( + delegate.createSocket(host, port, localHost, localPort), + 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 = + 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..c905aaa9a7a3 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketDecorator.kt @@ -0,0 +1,161 @@ +/* + * 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..efda64b35957 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEvent.kt @@ -0,0 +1,163 @@ +/* + * 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..a059373fe6b4 --- /dev/null +++ b/mocksocket/src/main/kotlin/mockwebserver/socket/SocketEventListener.kt @@ -0,0 +1,38 @@ +/* + * 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..46948b94aa9d --- /dev/null +++ b/mocksocket/src/test/java/mockwebserver/socket/CaptureTest.kt @@ -0,0 +1,253 @@ +@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)) + .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() + } + } 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