1package com.peachpasswords.sync23import kotlinx.coroutines.CoroutineScope4import kotlinx.coroutines.Dispatchers5import kotlinx.coroutines.SupervisorJob6import kotlinx.coroutines.launch7import kotlinx.coroutines.runBlocking8import okhttp3.OkHttpClient9import okhttp3.Request10import okhttp3.Response11import okhttp3.WebSocket12import okhttp3.WebSocketListener13import org.junit.Assert.assertEquals14import org.junit.Assert.assertTrue15import org.junit.Test16import java.util.concurrent.CompletableFuture17import java.util.concurrent.TimeUnit181920212223242526class CodePairingServerTest {2728 private val identity = LocalChirpIdentity(29 deviceId = "test-device",30 deviceName = "Test Phone",31 deviceType = DeviceType.ANDROID,32 longTermPublicKeyHex = "00".repeat(32),33 signingPublicKeyHex = "00".repeat(32),34 )3536 37 private class FakeRegistry : PairedDeviceRegistry {38 val devices = mutableListOf<PairedDevice>()39 override fun getAll(): List<PairedDevice> = devices40 override fun get(id: String): PairedDevice? = devices.firstOrNull { it.deviceId == id }41 override fun upsert(device: PairedDevice) { devices.add(device) }42 }4344 private fun server() = CodePairingServer(FakeRegistry())4546 private class Client {47 val opened = CompletableFuture<Boolean>()48 val closed = CompletableFuture<Boolean>()49 val frames = java.util.concurrent.LinkedBlockingQueue<String>()50 lateinit var ws: WebSocket5152 fun connect(port: Int) {53 ws = OkHttpClient.Builder().readTimeout(10, TimeUnit.SECONDS).build().newWebSocket(54 Request.Builder().url("ws://127.0.0.1:$port").build(),55 object : WebSocketListener() {56 override fun onOpen(webSocket: WebSocket, response: Response) { opened.complete(true) }57 override fun onMessage(webSocket: WebSocket, text: String) { frames.add(text) }58 override fun onFailure(webSocket: WebSocket, t: Throwable, r: Response?) { closed.complete(true) }59 override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { closed.complete(true) }60 },61 )62 opened.get(5, TimeUnit.SECONDS)63 }64 }6566 6768697071 private class Harness(val scope: CoroutineScope) {72 val events = java.util.concurrent.LinkedBlockingQueue<PairingEvent>()73 inline fun <reified T : PairingEvent> await(timeoutMs: Long = 10_000): T {74 val deadline = System.currentTimeMillis() + timeoutMs75 val seen = mutableListOf<PairingEvent>()76 while (System.currentTimeMillis() < deadline) {77 val e = events.poll(200, TimeUnit.MILLISECONDS) ?: continue78 seen.add(e)79 if (e is T) return e80 }81 throw AssertionError("never saw ${T::class.simpleName}; saw $seen")82 }83 }8485 private fun CoroutineScope.run(srv: CodePairingServer, windowMs: Long = CodePairingServer.PAIRING_WINDOW_MS): Harness {86 val h = Harness(this)87 launch { srv.start(this@run, ByteArray(32), ByteArray(32), identity, windowMs).collect { h.events.add(it) } }88 return h89 }9091 @Test fun `a wrong token gets no reply and the socket closes`() = runBlocking {92 val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)93 val srv = server()94 try {95 val listening = scope.run(srv).await<PairingEvent.Listening>()9697 val c = Client()98 c.connect(listening.port)99 c.ws.send("""{"type":"PEACH_HELLO","token":"00000000"}""")100101 assertTrue("socket should be closed on a bad token", c.closed.get(5, TimeUnit.SECONDS))102 assertEquals("nothing should be sent back — silence is the signal", 0, c.frames.size)103 } finally {104 srv.stop()105 }106 }107108 @Test fun `the pairing window expires and stops accepting`() = runBlocking {109 val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)110 val srv = server()111 try {112 113 114 val h = scope.run(srv, windowMs = 300)115 val listening = h.await<PairingEvent.Listening>()116 h.await<PairingEvent.Expired>(timeoutMs = 5_000)117118 var refused = false119 runCatching { java.net.Socket("127.0.0.1", listening.port).close() }120 .onFailure { refused = true }121 assertTrue("the port should be closed after the window", refused)122 } finally {123 srv.stop()124 }125 }126127 @Test fun `the advertised address is never a tunnel interface`() {128 129 130 val ip = server().localIpv4()131 if (ip != null) {132 assertTrue("expected a site-local address, got $ip", java.net.InetAddress.getByName(ip).isSiteLocalAddress)133 }134 }135}