feat(aiturk): accept a relay id in pairing payloads

Contract v3 (tunnel): the QR / deep link may carry r=<lowercase UUIDv4>
and no LAN address at all (the IDE's default). u is now 0..4, but a
payload with neither u nor r is rejected, and a present-but-invalid r
rejects the whole payload instead of falling back to the LAN addresses.

AiturkRelay maps a relay id to the virtual host
<relayId>.relay.aiturk.invalid that addresses a tunnel pairing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8ppceaR5s6cvMnm7uGRnf
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-05 04:08:07 +03:00
1 parent 6f4309c746
commit 9afac6f508
2 files changed
+162 -5

No files matched your search

@@ -16,14 +16,111 @@ class AiturkPairingPayloadTest {
us: List<String> = listOf("192.168.1.20:47321"),
fingerprint: String? = fp,
code: String? = "ABCDEFGH",
relays: List<String> = emptyList(),
): String =
buildList {
v?.let { add("v=$it") }
us.forEach { add("u=$it") }
fingerprint?.let { add("fp=$it") }
code?.let { add("c=$it") }
relays.forEach { add("r=$it") }
}.joinToString("&", prefix = "aiturk://pair?")
private val relayId = "3f2b8c1e-5d4a-4b7c-9e1f-0a1b2c3d4e5f"
// ── Contract v3: relay id ────────────────────────────────────────────
@Test
fun relayOnlyPayloadIsTheDefaultAndNeedsNoAddress() {
// Exactly what the IDE prints by default: aiturk://pair?v=1&fp=…&c=…&r=<relayId>
val result = AiturkPairing.parse("aiturk://pair?v=1&fp=$fp&c=ABCDEFGH&r=$relayId")
val offer = (result as AiturkPayloadResult.Ok).payload.offer
assertEquals(relayId, offer.relay)
assertTrue(offer.endpoints.isEmpty())
assertEquals(fp, offer.fingerprint)
assertEquals("https://$relayId.relay.aiturk.invalid/", AiturkRelay.baseUrl(relayId))
assertEquals("$relayId.relay.aiturk.invalid:443", AiturkRelay.hostPort(relayId))
assertEquals(relayId, AiturkRelay.relayIdOfHost("$relayId.relay.aiturk.invalid"))
}
@Test
fun relayAndLanAddressesCanComeTogether() {
val result = AiturkPairing.parse(link(us = listOf("192.168.1.20:47321", "10.0.0.5:47322"), relays = listOf(relayId)))
val offer = (result as AiturkPayloadResult.Ok).payload.offer
assertEquals(relayId, offer.relay)
assertEquals(2, offer.endpoints.size)
// Without `r` the offer simply has no relay.
assertNull((AiturkPairing.parse(link()) as AiturkPayloadResult.Ok).payload.offer.relay)
}
@Test
fun anythingButALowercaseUuid4RelayIdRejectsThePayload() {
val rejected =
listOf(
// 1. the cancelled v2 form (a URL)
"https%3A%2F%2Fturkservis.online%2Fv1%2Faiturk%2Fmobile-relay%2Fr%2F$relayId",
// 2. upper case
relayId.uppercase(Locale.ROOT),
// 3. not version 4
"3f2b8c1e-5d4a-1b7c-9e1f-0a1b2c3d4e5f",
// 4. wrong variant nibble
"3f2b8c1e-5d4a-4b7c-ce1f-0a1b2c3d4e5f",
// 5. a host name smuggled in
"$relayId.evil.example",
// 6. extra path
"$relayId%2F..%2Fhost",
// 7. surrounding whitespace
"%20$relayId",
// 8. braces / no dashes
"%7B$relayId%7D",
relayId.replace("-", ""),
// 9. too short / empty-ish
relayId.dropLast(1),
"x",
)
rejected.forEach { value ->
// The LAN address is valid: a bad `r` must still reject the whole payload.
assertEquals(AiturkPayloadError.BAD_RELAY, errorOf(link(relays = listOf(value))), value)
assertEquals(AiturkPayloadError.BAD_RELAY, errorOf(link(us = emptyList(), relays = listOf(value))), value)
assertNull(AiturkRelay.parse(java.net.URLDecoder.decode(value, "UTF-8")), value)
}
// 10. `r` given twice
assertEquals(AiturkPayloadError.BAD_RELAY, errorOf(link(relays = listOf(relayId, relayId))))
// An empty `r=` is a parameter without a value.
assertEquals(AiturkPayloadError.MALFORMED, errorOf(link() + "&r"))
assertEquals(AiturkPayloadError.BAD_RELAY, errorOf(link() + "&r="))
}
@Test
fun payloadWithNeitherAddressNorRelayIsRejected() {
assertEquals(AiturkPayloadError.BAD_ADDRESS_COUNT, errorOf(link(us = emptyList())))
val five = (1..5).map { "192.168.1.$it:47321" }
assertEquals(AiturkPayloadError.BAD_ADDRESS_COUNT, errorOf(link(us = five, relays = listOf(relayId))))
// A relay does not excuse a bad LAN address.
assertEquals(AiturkPayloadError.BAD_ADDRESS, errorOf(link(us = listOf("8.8.8.8:443"), relays = listOf(relayId))))
// The fingerprint stays mandatory: it is what makes the tunnel end-to-end.
assertEquals(AiturkPayloadError.BAD_FINGERPRINT, errorOf("aiturk://pair?v=1&c=ABCDEFGH&r=$relayId"))
}
@Test
fun onlyExactVirtualHostsAreRelayHosts() {
assertTrue(AiturkRelay.isVirtualHost("$relayId.relay.aiturk.invalid"))
listOf(
null,
"",
"relay.aiturk.invalid",
".relay.aiturk.invalid",
"abc.relay.aiturk.invalid",
"${relayId.uppercase(Locale.ROOT)}.relay.aiturk.invalid",
"$relayId.relay.aiturk.invalid.evil.example",
"x.$relayId.relay.aiturk.invalid",
"turkservis.online",
"192.168.1.20",
).forEach { assertFalse(AiturkRelay.isVirtualHost(it), it.toString()) }
}
private fun errorOf(raw: String): AiturkPayloadError? = (AiturkPairing.parse(raw) as? AiturkPayloadResult.Invalid)?.error
@Test