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:
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
|
||||
|
||||
Reference in new issue
Block a user