feat(aiturk): phone link works from any network through an end-to-end encrypted tunnel (1.0.0-beta.16)

The phone link no longer depends on the local network. The IDE dials out to
turkservis.online and bridges tunnel streams to its own TLS proxy on
loopback; the phone runs TLS to the pinned IDE certificate inside the
tunnel, so the relay only carries ciphertext. The proxy binds to 127.0.0.1
by default and enabling the feature never asks for a firewall rule; direct
same-network access is an explicit option. The window shows the real relay,
listener and firewall state, and pairing no longer needs local addresses.

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:22:51 +03:00
1 parent 03d3803dfc
commit 2ed9a6b2de
11 files changed
+2905 -130

No files matched your search

+426 -31
View File
@@ -8,6 +8,7 @@
*/
import crypto from 'node:crypto'
import { EventEmitter } from 'node:events'
import http from 'node:http'
import https from 'node:https'
import net from 'node:net'
@@ -22,16 +23,21 @@ import {
createMobileLink,
createWindowsFirewall,
firewallCommand,
firewallQueryCommand,
formatPairCode,
generatePairCode,
isPhoneForbidden,
isPrivateIpv4,
MOBILE_LAN_DIRECT_HOST,
MOBILE_LOOPBACK_HOST,
MobileLinkError,
type MobileLinkEvent,
normalizePairCode,
PAIR_CODE_ALPHABET,
pairLookup,
parseFirewallRuleOutput,
parseMobileLinkStored,
publishPairOffer,
selectLanAddresses
} from './mobile-lan-proxy'
@@ -191,9 +197,9 @@ function wsOpen(url: string, ca: string, extra: Record<string, unknown> = {}) {
})
}
const connectOutcome = (port: number) =>
const connectOutcome = (port: number, host = '127.0.0.1') =>
new Promise<string>(resolve => {
const socket = net.connect(port, '127.0.0.1')
const socket = net.connect(port, host)
socket.once('connect', () => {
socket.destroy()
@@ -698,15 +704,57 @@ describe('LAN proxy: TLS, listening and logs', () => {
})
// ── controller ──────────────────────────────────────────────────────────────
// Stand-in for the LAN bind (production: 0.0.0.0). Still loopback, so no test socket leaves this machine,
// but it is a DIFFERENT address from 127.0.0.1, which is what "listening on the LAN or not" needs.
const LAN_HOST = '127.0.0.2'
const RELAY_IDS = ['3f2b8c1e-5d47-4a90-8e21-6b0c9d7a1f55', '9a1d0c2b-7e63-4f18-b5a4-2c8e6f013d77', '5c7e9a13-2b46-4d80-9f3e-1a2b3c4d5e6f']
const HOST_TOKENS = RELAY_IDS.map((_id, index) => `tsmr_${String.fromCharCode(65 + index).repeat(43)}`)
/** Host socket that opens at once and never carries traffic; enough for the controller tests. */
class FakeHostSocket extends EventEmitter {
sent: (Buffer | string)[] = []
closed: null | number = null
bufferedAmount = 0
constructor(
public url: string,
public headers: Record<string, string>
) {
super()
setTimeout(() => this.emit('open'), 0)
}
send(data: Buffer | string) {
this.sent.push(data)
}
close(code = 1000) {
if (this.closed === null) {
this.closed = code
setTimeout(() => this.emit('close', code), 0)
}
}
}
function linkHarness(over: Record<string, any> = {}) {
const vault = { value: { accountToken: 'eski-alan' } as Record<string, any> }
const store = { value: null as any, saves: 0 }
const store = { value: (over.stored ?? null) as any, saves: 0 }
const firewallCalls: string[] = []
const firewallResults: any[] = over.firewallResults ?? []
const fetchCalls: { url: string; init: any }[] = []
const events: MobileLinkEvent[] = []
const logs: string[] = []
const state = { remote: false, offerStatus: over.offerStatus ?? 201 }
const hostSockets: FakeHostSocket[] = []
const state = {
remote: false,
offerStatus: over.offerStatus ?? 201,
/** false = `POST /sessions` fails (no internet). */
sessions: over.sessions ?? true,
sessionCount: 0,
/** What `netsh … show rule` would answer. */
rule: (over.rule ?? { state: 'absent' }) as any
}
const deps = {
vault: {
@@ -728,17 +776,39 @@ function linkHarness(over: Record<string, any> = {}) {
addRule: async (port: number) => {
firewallCalls.push(`add:${port}`)
return firewallResults.shift() ?? { ok: true }
const result = firewallResults.shift() ?? { ok: true }
if (result.ok) {
state.rule = { state: 'present', ports: [port] }
}
return result
},
removeRule: async () => {
firewallCalls.push('remove')
state.rule = { state: 'absent' }
return { ok: true }
},
queryRule: async () => {
firewallCalls.push('query')
return structuredClone(state.rule)
}
},
fetch: async (url: string, init: any) => {
fetchCalls.push({ url, init })
if (url.endsWith('/mobile-relay/sessions')) {
if (!state.sessions) {
throw new Error('network down')
}
const index = state.sessionCount++
return { status: 201, json: async () => ({ relayId: RELAY_IDS[index], hostToken: HOST_TOKENS[index] }) }
}
if (state.offerStatus === 'throw') {
throw new Error('network down')
}
@@ -752,11 +822,18 @@ function linkHarness(over: Record<string, any> = {}) {
return { status: 200, json: async () => ({ cancelled: true }) }
},
relayConnect: (url: string, headers: Record<string, string>) => {
const socket = new FakeHostSocket(url, headers)
hostSockets.push(socket)
return socket
},
addresses: async () => over.addresses ?? ['192.168.1.20', '10.0.0.5'],
version: '1.0.0-test',
pcName: 'TEST-PC',
platform: 'win32',
listenHost: '127.0.0.1',
listenHost: LAN_HOST,
ports: [0],
log: (line: string) => logs.push(line)
}
@@ -769,7 +846,11 @@ function linkHarness(over: Record<string, any> = {}) {
return link
}
return { link: make(), make, vault, store, firewallCalls, fetchCalls, events, logs, state }
/** Firewall calls that change something (the read-only query is asked often). */
const firewallWrites = () => firewallCalls.filter(entry => entry !== 'query')
const offers = () => fetchCalls.filter(entry => entry.url.endsWith('/offers'))
return { link: make(), make, vault, store, firewallCalls, firewallWrites, fetchCalls, offers, events, logs, state, hostSockets }
}
describe('mobile link controller', () => {
@@ -781,8 +862,9 @@ describe('mobile link controller', () => {
const enabled = await h.link.enable()
const port = enabled.port as number
expect(enabled).toMatchObject({ enabled: true, listening: false, firewall: 'ok', remote: false })
expect(h.firewallCalls).toEqual([`add:${port}`])
expect(enabled).toMatchObject({ enabled: true, listening: false, firewall: 'needed', remote: false })
// Turning the feature on never touches the firewall (no UAC prompt): only the read-only query runs.
expect(h.firewallWrites()).toEqual([])
expect(await connectOutcome(port)).toBe('ECONNREFUSED')
const pairing = await h.link.pairBegin()
@@ -794,9 +876,8 @@ describe('mobile link controller', () => {
expect(cancelled).toMatchObject({ listening: false, pairing: null })
expect(await connectOutcome(port)).toBe('ECONNREFUSED')
// The firewall is asked once per port, not on every enable.
await h.link.enable()
expect(h.firewallCalls).toEqual([`add:${port}`])
expect(h.firewallWrites()).toEqual([])
await h.link.stop()
})
@@ -811,13 +892,15 @@ describe('mobile link controller', () => {
const fp = h.vault.value.lanTls.fingerprint
expect(pairing.code).toMatch(new RegExp(`^[${PAIR_CODE_ALPHABET}]{8}$`))
expect(pairing.qr).toBe(`aiturk://pair?v=1&u=192.168.1.20:${port}&u=10.0.0.5:${port}&fp=${fp}&c=${pairing.code}`)
// Default (direct LAN access off): no `u`, only the fingerprint, the code and the relay id.
expect(pairing.qr).toBe(`aiturk://pair?v=1&fp=${fp}&c=${pairing.code}&r=${RELAY_IDS[0]}`)
expect(pairing.securityCode).toBe(fp.slice(0, 8))
expect(fp).toMatch(/^[A-Za-z0-9_-]{43}$/)
expect(pairing.codePath).toBe('pending')
await until(() => h.events.some(event => event.state.pairing?.codePath === 'ready'))
const offer = h.fetchCalls[0]
const offer = h.offers()[0]
const body = JSON.parse(offer.init.body)
expect(offer.url).toBe('https://turkservis.online/v1/aiturk/mobile-pair/offers')
@@ -826,8 +909,9 @@ describe('mobile link controller', () => {
expect(offer.init.signal).toBeInstanceOf(AbortSignal)
expect(body).toEqual({
lookup: crypto.createHash('sha256').update(`aiturk-mobile-pair-v1:${pairing.code}`).digest('hex'),
urls: [`https://192.168.1.20:${port}`, `https://10.0.0.5:${port}`],
fp
urls: [],
fp,
relay: RELAY_IDS[0]
})
expect(offer.init.body).not.toContain(pairing.code)
expect(JSON.stringify(offer.init.headers)).not.toContain(pairing.code)
@@ -922,7 +1006,7 @@ describe('mobile link controller', () => {
expect(off).toMatchObject({ enabled: false, listening: false })
expect(off.devices).toHaveLength(1)
expect(await connectOutcome(port)).toBe('ECONNREFUSED')
expect(h.firewallCalls).toEqual([`add:${port}`]) // disabling never removes the rule
expect(h.firewallWrites()).toEqual([]) // neither enabling nor disabling touches the rule
await expect(h.link.pairBegin()).rejects.toThrow('aiturk-mobile:not_enabled')
// Disabled at app start: no socket.
@@ -938,7 +1022,7 @@ describe('mobile link controller', () => {
const restarted = h.make()
const resumed = await restarted.start()
expect(resumed).toMatchObject({ enabled: true, listening: true, port, firewall: 'ok' })
expect(resumed).toMatchObject({ enabled: true, listening: true, port, lan: { direct: false, listening: false } })
expect((await call(port, 'GET', '/api/sessions', { ca, token: claim.json.token })).status).toBe(200)
await restarted.stop()
@@ -952,7 +1036,7 @@ describe('mobile link controller', () => {
await expect(h.link.enable()).rejects.toBeInstanceOf(MobileLinkError)
await expect(h.link.enable()).rejects.toThrow('aiturk-mobile:remote_mode')
expect(await h.link.state()).toMatchObject({ enabled: false, listening: false, remote: true })
expect(h.firewallCalls).toEqual([])
expect(h.firewallWrites()).toEqual([])
h.state.remote = false
await h.link.enable()
@@ -962,26 +1046,126 @@ describe('mobile link controller', () => {
await h.link.stop()
})
it('a refused UAC prompt leaves a clear state and "Yeniden dene" adds the rule', async () => {
it('"İzin ver" is the only thing that adds the firewall rule; a refused UAC prompt leaves direct access off', async () => {
const h = linkHarness({ firewallResults: [{ ok: false, reason: 'denied' }, { ok: false, reason: 'failed' }] })
const state = await h.link.enable()
expect(state).toMatchObject({ enabled: true, firewall: 'denied' })
expect((await h.link.firewallRetry()).firewall).toBe('failed')
expect((await h.link.firewallRetry()).firewall).toBe('ok')
expect(h.firewallCalls).toHaveLength(3)
expect((await h.link.firewallRetry()).firewall).toBe('ok')
expect(h.firewallCalls).toHaveLength(3)
expect((await h.link.firewallRemove()).firewall).toBe('needed')
expect(h.firewallCalls.at(-1)).toBe('remove')
expect(state).toMatchObject({ enabled: true, firewall: 'needed', lan: { direct: false, listening: false, firewall: 'missing' } })
expect(h.firewallWrites()).toEqual([])
expect(await h.link.lanAllow()).toMatchObject({ firewall: 'denied', lan: { direct: false, firewall: 'missing' } })
expect(await h.link.firewallRetry()).toMatchObject({ firewall: 'failed', lan: { direct: false } })
expect(await h.link.firewallAllow()).toMatchObject({ firewall: 'ok', lan: { direct: true, firewall: 'allowed' } })
expect(h.firewallWrites()).toHaveLength(3)
// The rule is there (asked from Windows, not remembered): no second prompt.
expect((await h.link.lanAllow()).firewall).toBe('ok')
expect(h.firewallWrites()).toHaveLength(3)
expect(await h.link.firewallRemove()).toMatchObject({ firewall: 'needed', lan: { direct: false, firewall: 'missing' } })
expect(h.firewallWrites().at(-1)).toBe('remove')
await expect(linkHarness().link.lanAllow()).rejects.toThrow('aiturk-mobile:not_enabled')
await h.link.stop()
})
it('without a usable LAN address pairing is refused; a failing vault is reported by code only', async () => {
const h = linkHarness({ addresses: ['8.8.8.8', '169.254.3.4'] })
it('default bind is 127.0.0.1 only; direct LAN access moves the socket to the LAN bind and puts `u` into the QR and the offer', async () => {
expect(MOBILE_LOOPBACK_HOST).toBe('127.0.0.1')
expect(MOBILE_LAN_DIRECT_HOST).toBe('0.0.0.0')
const h = linkHarness()
await h.link.enable()
const first = await h.link.pairBegin()
const port = first.port as number
// Listening, but only on loopback: the stand-in LAN address refuses the connection.
expect(first).toMatchObject({ listening: true, lan: { direct: false, listening: false, port, firewall: 'missing' } })
expect(await connectOutcome(port, '127.0.0.1')).toBe('open')
expect(await connectOutcome(port, LAN_HOST)).toBe('ECONNREFUSED')
expect(first.pairing!.qr).not.toContain('u=')
await until(() => h.offers().length === 1)
expect(JSON.parse(h.offers()[0].init.body)).toMatchObject({ urls: [], relay: RELAY_IDS[0] })
// The user allows direct access: rule added now, pairing on screen is cancelled, socket moves.
const allowed = await h.link.lanAllow()
expect(h.firewallWrites()).toEqual([`add:${port}`])
expect(allowed).toMatchObject({ pairing: null, listening: false, lan: { direct: true, listening: false, firewall: 'allowed' } })
expect(h.store.value.lanDirect).toBe(true)
const second = await h.link.pairBegin()
const fp = h.vault.value.lanTls.fingerprint
expect(second).toMatchObject({ listening: true, lan: { direct: true, listening: true, port } })
expect(await connectOutcome(port, LAN_HOST)).toBe('open')
expect(second.pairing!.qr).toBe(
`aiturk://pair?v=1&u=192.168.1.20:${port}&u=10.0.0.5:${port}&fp=${fp}&c=${second.pairing!.code}&r=${RELAY_IDS[0]}`
)
await until(() => h.offers().length === 2)
expect(JSON.parse(h.offers()[1].init.body)).toMatchObject({
urls: [`https://192.168.1.20:${port}`, `https://10.0.0.5:${port}`],
relay: RELAY_IDS[0]
})
// The rule disappears outside the IDE: the next state query notices and goes back to loopback.
h.state.rule = { state: 'absent' }
const lost = await h.link.state()
expect(lost).toMatchObject({ listening: true, lan: { direct: true, listening: false, firewall: 'missing' } })
expect(await connectOutcome(port, LAN_HOST)).toBe('ECONNREFUSED')
expect(await connectOutcome(port, '127.0.0.1')).toBe('open')
// A rule for another port is not permission for this one.
h.state.rule = { state: 'present', ports: [port === 47321 ? 47322 : 47321] }
expect((await h.link.state()).lan.firewall).toBe('missing')
h.state.rule = { state: 'unknown' }
expect((await h.link.state()).lan.firewall).toBe('unknown')
const off = await h.link.lanDisallow()
expect(off).toMatchObject({ pairing: null, lan: { direct: false, listening: false } })
expect(h.firewallWrites()).toEqual([`add:${port}`]) // turning it off needs no prompt
await h.link.stop()
})
it('a record written by beta.15 (enabled, firewallPort set, no lanDirect) stays on loopback and raises no prompt', async () => {
const id = '0f8fad5b-d9cb-469f-a165-70867728950e'
const h = linkHarness({
rule: { state: 'present', ports: [47321] },
stored: { v: 1, enabled: true, port: null, firewallPort: 47321, fingerprint: null, devices: [{ id, name: 'Pixel', tokenHash: 'a'.repeat(64), createdAt: 5, lastSeenAt: 5 }] }
})
const resumed = await h.link.start()
expect(resumed).toMatchObject({ enabled: true, listening: true, lan: { direct: false, listening: false } })
expect(await connectOutcome(resumed.port as number, LAN_HOST)).toBe('ECONNREFUSED')
expect(h.firewallWrites()).toEqual([])
// The relay follows the same condition as the listener: a paired phone is enough.
await until(() => h.events.some(event => event.state.relay.status === 'connected'))
expect(h.hostSockets).toHaveLength(1)
await h.link.stop()
expect(h.hostSockets[0].closed).toBe(1000)
expect((await h.link.state()).relay.status).toBe('off')
})
it('without a relay registration "Telefon ekle" starts nothing unless direct LAN access is on', async () => {
const h = linkHarness({ sessions: false, addresses: ['8.8.8.8', '169.254.3.4'] })
await h.link.enable()
await expect(h.link.pairBegin()).rejects.toThrow(/^aiturk-mobile:relay_unavailable$/)
expect(await h.link.state()).toMatchObject({ pairing: null, listening: false, relay: { ready: false, status: 'off' } })
expect(h.offers()).toEqual([])
// Direct access on, but no usable LAN address either.
await h.link.lanAllow()
await expect(h.link.pairBegin()).rejects.toThrow('aiturk-mobile:no_lan_address')
// Internet back: the relay alone is enough, even with no LAN address at all.
h.state.sessions = true
const state = await h.link.pairBegin()
expect(state.pairing!.qr).toMatch(new RegExp(`^aiturk://pair\\?v=1&fp=[A-Za-z0-9_-]{43}&c=[A-Z2-9]{8}&r=${RELAY_IDS[0]}$`))
await h.link.stop()
const broken = createMobileLink({
@@ -1007,6 +1191,71 @@ describe('mobile link controller', () => {
await expect(broken.enable()).rejects.toThrow(/^aiturk-mobile:vault_unavailable$/)
})
it('relay: same on/off condition as the listener, token only in the vault and the Authorization header, renewal drops the phones', async () => {
const h = linkHarness()
expect((await h.link.enable()).relay).toEqual({ status: 'off', code: null, ready: false, reissued: false })
expect(h.hostSockets).toHaveLength(0)
const state = await h.link.pairBegin()
const port = state.port as number
const ca = h.vault.value.lanTls.cert
await until(() => h.events.some(event => event.state.relay.status === 'connected'))
expect(h.hostSockets[0].url).toBe(`wss://turkservis.online/v1/aiturk/mobile-relay/host/${RELAY_IDS[0]}`)
expect(h.hostSockets[0].headers).toEqual({ Authorization: `Bearer ${HOST_TOKENS[0]}` })
const sessions = h.fetchCalls.find(entry => entry.url.endsWith('/sessions'))!
expect(sessions.url).toBe('https://turkservis.online/v1/aiturk/mobile-relay/sessions')
expect(sessions.init).toMatchObject({ method: 'POST', body: '{}', redirect: 'error' })
expect(sessions.init.signal).toBeInstanceOf(AbortSignal)
// relayId on disk, hostToken only in the vault.
expect(h.store.value.relayId).toBe(RELAY_IDS[0])
expect(h.vault.value.relay).toEqual({ relayId: RELAY_IDS[0], hostToken: HOST_TOKENS[0] })
expect(h.vault.value.accountToken).toBe('eski-alan')
const claim = await call(port, 'POST', '/aiturk/pair/claim', { ca, body: { code: state.pairing!.code, device_name: 'Pixel' } })
expect(claim.status).toBe(200)
await until(() => h.events.some(event => event.type === 'paired'))
expect((await h.link.state()).relay).toEqual({ status: 'connected', code: null, ready: true, reissued: false })
// One registration for the whole life of the pairing: the phone's stored id must not change.
expect(h.fetchCalls.filter(entry => entry.url.endsWith('/sessions'))).toHaveLength(1)
// "Röle kimliğini yenile".
const renewed = await h.link.relayRenew()
expect(renewed).toMatchObject({ devices: [], listening: false, relay: { status: 'off', ready: true, reissued: true } })
expect(h.store.value.relayId).toBe(RELAY_IDS[1])
expect(h.vault.value.relay.hostToken).toBe(HOST_TOKENS[1])
expect((await call(port, 'GET', '/api/sessions', { ca, token: claim.json.token }).catch(() => ({ status: 0 }))).status).not.toBe(200)
await until(() => h.hostSockets[0].closed !== null)
const again = await h.link.pairBegin()
expect(again.pairing!.qr).toContain(`&r=${RELAY_IDS[1]}`)
await until(() => h.events.at(-1)?.state.relay.status === 'connected')
// Every socket after the renewal carries the new token; the old one is never sent again.
expect(h.hostSockets.length).toBeGreaterThan(1)
expect(h.hostSockets.slice(1).every(socket => socket.headers.Authorization === `Bearer ${HOST_TOKENS[1]}`)).toBe(true)
expect(h.hostSockets.at(-1)!.url).toBe(`wss://turkservis.online/v1/aiturk/mobile-relay/host/${RELAY_IDS[1]}`)
// Renewal that cannot reach the gateway changes nothing.
h.state.sessions = false
await expect(h.link.relayRenew()).rejects.toThrow('aiturk-mobile:relay_unavailable')
expect(h.store.value.relayId).toBe(RELAY_IDS[1])
// The host token never reaches a log line, an event, a state answer or the plain store.
const exposed = [h.logs.join('\n'), JSON.stringify(h.events), JSON.stringify(await h.link.state()), JSON.stringify(h.store.value)].join('\n')
expect(exposed).not.toContain('tsmr_')
expect(h.logs.filter(line => line.startsWith('relay '))).toEqual(expect.arrayContaining(['relay connecting', 'relay connected', 'relay off']))
expect(h.logs.filter(line => line.startsWith('relay ')).every(line => /^relay (?:off|connecting|connected|failed:[a-z_]+)$/.test(line))).toBe(true)
await h.link.stop()
})
it('a regenerated certificate drops phones paired against the old one', async () => {
const h = linkHarness()
@@ -1142,7 +1391,153 @@ describe('pure helpers', () => {
port: 99999,
devices: [{ id, name: 'Pixel', tokenHash: hash, createdAt: 5, token: 'amd_x' }, { id: 'x', name: 'bad', tokenHash: hash, createdAt: 1 }, null]
})
).toEqual({ enabled: false, port: null, firewallPort: null, fingerprint: null, devices: [{ id, name: 'Pixel', tokenHash: hash, createdAt: 5, lastSeenAt: 5 }] })
expect(parseMobileLinkStored(null)).toEqual({ enabled: false, port: null, firewallPort: null, fingerprint: null, devices: [] })
).toEqual({
enabled: false,
port: null,
firewallPort: null,
fingerprint: null,
devices: [{ id, name: 'Pixel', tokenHash: hash, createdAt: 5, lastSeenAt: 5 }],
lanDirect: false,
relayId: null,
relayReissued: false
})
expect(parseMobileLinkStored(null)).toEqual({
enabled: false,
port: null,
firewallPort: null,
fingerprint: null,
devices: [],
lanDirect: false,
relayId: null,
relayReissued: false
})
// Only an explicit `true` turns direct LAN access on; a relay id must be a uuid.
expect(parseMobileLinkStored({ lanDirect: 'yes', relayId: 'https://evil.example', relayReissued: 1 })).toMatchObject({ lanDirect: false, relayId: null, relayReissued: false })
expect(parseMobileLinkStored({ lanDirect: true, relayId: RELAY_IDS[0].toUpperCase(), relayReissued: true })).toMatchObject({
lanDirect: true,
relayId: RELAY_IDS[0],
relayReissued: true
})
})
it('QR payload and rendezvous body: `r` / `relay` is a plain lower-case uuid, `u` / `urls` optional only next to it', async () => {
const fp = 'f'.repeat(43)
const relay = RELAY_IDS[0]
expect(buildPairQrPayload({ addresses: [], port: 47321, fingerprint: fp, code: 'ABCDEFGH', relay })).toBe(`aiturk://pair?v=1&fp=${fp}&c=ABCDEFGH&r=${relay}`)
expect(buildPairQrPayload({ addresses: ['192.168.1.20'], port: 47321, fingerprint: fp, code: 'ABCDEFGH', relay })).toBe(
`aiturk://pair?v=1&u=192.168.1.20:47321&fp=${fp}&c=ABCDEFGH&r=${relay}`
)
expect(buildPairQrPayload({ addresses: ['192.168.1.20'], port: 47321, fingerprint: fp, code: 'ABCDEFGH' })).toBe(`aiturk://pair?v=1&u=192.168.1.20:47321&fp=${fp}&c=ABCDEFGH`)
// Anything that is not exactly a uuid is dropped, never echoed into the QR.
for (const bad of [`https://turkservis.online/v1/aiturk/mobile-relay/r/${relay}`, relay.toUpperCase(), `${relay}&u=1.2.3.4:1`, '', 7 as any]) {
expect(buildPairQrPayload({ addresses: ['10.0.0.5'], port: 47321, fingerprint: fp, code: 'ABCDEFGH', relay: bad })).not.toContain('r=')
expect(() => buildPairQrPayload({ addresses: [], port: 47321, fingerprint: fp, code: 'ABCDEFGH', relay: bad })).toThrow('aiturk-mobile:no_lan_address')
}
const bodies: any[] = []
const fetcher = async (_url: string, init: any) => {
bodies.push(JSON.parse(init.body))
return { status: 201, json: async () => ({ offerId: '0f8fad5b-d9cb-469f-a165-70867728950e', cancelToken: `tsmp_${'c'.repeat(43)}` }) }
}
expect(await publishPairOffer(fetcher, { code: 'ABCDEFGH', addresses: [], port: 47321, fingerprint: fp, relay })).not.toBeNull()
expect(await publishPairOffer(fetcher, { code: 'ABCDEFGH', addresses: ['10.0.0.5'], port: 47321, fingerprint: fp })).not.toBeNull()
// Neither a LAN url nor a relay: nothing is published.
expect(await publishPairOffer(fetcher, { code: 'ABCDEFGH', addresses: [], port: 47321, fingerprint: fp, relay: 'nope' })).toBeNull()
expect(bodies).toEqual([
{ lookup: pairLookup('ABCDEFGH'), urls: [], fp, relay },
{ lookup: pairLookup('ABCDEFGH'), urls: ['https://10.0.0.5:47321'], fp }
])
})
it('firewall rule query: fixed netsh arguments; English and Turkish answers; present / absent / other port / disabled / unreadable', async () => {
const query = firewallQueryCommand('C:\\Windows')
expect(query).toEqual({ file: 'C:\\Windows\\System32\\netsh.exe', args: ['advfirewall', 'firewall', 'show', 'rule', 'name=AITURK IDE Telefon Baglantisi'] })
expect(firewallQueryCommand("C:\\x'; calc; '").file).toBe('C:\\Windows\\System32\\netsh.exe')
const english = (port: string, enabled = 'Yes') =>
[
'',
'Rule Name: AITURK IDE Telefon Baglantisi',
'----------------------------------------------------------------------',
`Enabled: ${enabled}`,
'Direction: In',
'Profiles: Private,Public',
'Grouping: ',
'LocalIP: Any',
'RemoteIP: Any',
'Protocol: TCP',
`LocalPort: ${port}`,
'RemotePort: Any',
'Edge traversal: No',
'Action: Allow',
'Ok.',
''
].join('\r\n')
// Turkish Windows, as the bytes arrive when the console code page is read as latin1 (labels garbled on purpose).
const turkish = (port: string, enabled = 'Evet') =>
[
'',
'Kural Adý: AITURK IDE Telefon Baglantisi',
'----------------------------------------------------------------------',
`Etkin: ${enabled}`,
'Yön: Gelen',
'Profiller: Özel,Ortak',
'Gruplandýrma: ',
'YerelIP: Herhangi',
'UzakIP: Herhangi',
'Ýletiþim Kuralý: TCP',
`YerelBağlantıNoktası: ${port}`,
'UzakBağlantıNoktası: Herhangi',
'Kenar geçiþi: Hayýr',
'Eylem: Ýzin Ver',
'Tamam.',
''
].join('\r\n')
expect(parseFirewallRuleOutput(english('47321'), 0)).toEqual({ state: 'present', ports: [47321] })
expect(parseFirewallRuleOutput(turkish('47321'), 0)).toEqual({ state: 'present', ports: [47321] })
expect(parseFirewallRuleOutput(english('47330'), 0)).toEqual({ state: 'present', ports: [47330] })
expect(parseFirewallRuleOutput(turkish('47321,47325-47326'), 0)).toEqual({ state: 'present', ports: [47321, 47325, 47326] })
// Two rules under the same name (the port changed once): both ports count.
expect(parseFirewallRuleOutput(english('47321') + turkish('47322'), 0)).toEqual({ state: 'present', ports: [47321, 47322] })
// A disabled rule allows nothing.
expect(parseFirewallRuleOutput(english('47321', 'No'), 0)).toEqual({ state: 'present', ports: [] })
expect(parseFirewallRuleOutput(turkish('47321', 'Hayýr'), 0)).toEqual({ state: 'present', ports: [] })
expect(parseFirewallRuleOutput('\r\nNo rules match the specified criteria.\r\n\r\n', 1)).toEqual({ state: 'absent' })
expect(parseFirewallRuleOutput('\r\nBelirtilen ölçütlerle eþleen kural yok.\r\n\r\n', 1)).toEqual({ state: 'absent' })
// No answer at all (netsh missing, timeout) is not "no rule".
expect(parseFirewallRuleOutput('', null)).toEqual({ state: 'unknown' })
expect(parseFirewallRuleOutput('', 1)).toEqual({ state: 'unknown' })
expect(parseFirewallRuleOutput('something unexpected', 0)).toEqual({ state: 'unknown' })
const ran: any[] = []
const firewall = createWindowsFirewall({
platform: 'win32',
env: { SystemRoot: 'C:\\Windows' },
spawn: () => {
throw new Error('the query must not use the elevated path')
},
run: async (file, args) => {
ran.push({ file, args })
return { code: 0, stdout: english('47321') }
}
})
expect(await firewall.queryRule!()).toEqual({ state: 'present', ports: [47321] })
expect(ran).toEqual([query])
const failing = createWindowsFirewall({ platform: 'win32', env: {}, spawn: () => ({ on: () => undefined }), run: async () => Promise.reject(new Error('x')) })
expect(await failing.queryRule!()).toEqual({ state: 'unknown' })
expect(await createWindowsFirewall({ platform: 'linux', env: {}, spawn: () => ({ on: () => undefined }) }).queryRule!()).toEqual({ state: 'unknown' })
})
})