fix(mobile): route phone APIs to selected profile and unblock runtime readiness
This commit is contained in:
1 parent
b1db2870c1
commit
61d765e2df
15 files changed
+970
-176
No files matched your search
@@ -46,7 +46,10 @@ import {
|
||||
|
||||
const SESSION = crypto.randomBytes(32).toString('base64url')
|
||||
const LOOPBACK = new Set(['localhost', '127.0.0.1', '::1'])
|
||||
const hostOnly = (value: unknown) => String(value || '').replace(/:\d+$/, '').toLowerCase()
|
||||
const hostOnly = (value: unknown) =>
|
||||
String(value || '')
|
||||
.replace(/:\d+$/, '')
|
||||
.toLowerCase()
|
||||
const seen: { path: string; host: string; raw: string; method: string }[] = []
|
||||
const wrongCode = (code: string) => (code === 'ZZZZZZZZ' ? 'YYYYYYYY' : 'ZZZZZZZZ')
|
||||
|
||||
@@ -90,7 +93,12 @@ beforeAll(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
seen.push({ path: req.url ?? '', host: String(req.headers.host), method: String(req.method), raw: JSON.stringify(req.headers) + req.url })
|
||||
seen.push({
|
||||
path: req.url ?? '',
|
||||
host: String(req.headers.host),
|
||||
method: String(req.method),
|
||||
raw: JSON.stringify(req.headers) + req.url
|
||||
})
|
||||
send(200, JSON.stringify({ ok: true, path: url.pathname }))
|
||||
})
|
||||
|
||||
@@ -143,48 +151,50 @@ interface CallOptions {
|
||||
}
|
||||
|
||||
function call(port: number, method: string, path: string, options: CallOptions = {}) {
|
||||
return new Promise<{ status: number; text: string; headers: http.IncomingHttpHeaders; json: any }>((resolve, reject) => {
|
||||
const payload = options.rawBody ?? (options.body === undefined ? undefined : JSON.stringify(options.body))
|
||||
return new Promise<{ status: number; text: string; headers: http.IncomingHttpHeaders; json: any }>(
|
||||
(resolve, reject) => {
|
||||
const payload = options.rawBody ?? (options.body === undefined ? undefined : JSON.stringify(options.body))
|
||||
|
||||
const req = https.request(
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
method,
|
||||
path,
|
||||
agent: false,
|
||||
...(options.ca ? { ca: options.ca } : {}),
|
||||
checkServerIdentity: () => undefined,
|
||||
headers: {
|
||||
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
||||
...(options.headerToken ? { 'x-hermes-session-token': options.headerToken } : {}),
|
||||
...(payload ? { 'content-type': 'application/json' } : {}),
|
||||
origin: 'http://evil.example',
|
||||
cookie: 'session=evil',
|
||||
...options.headers
|
||||
}
|
||||
},
|
||||
res => {
|
||||
let text = ''
|
||||
|
||||
res.on('data', chunk => (text += chunk))
|
||||
res.on('end', () => {
|
||||
let json: any = null
|
||||
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch {
|
||||
json = null
|
||||
const req = https.request(
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
method,
|
||||
path,
|
||||
agent: false,
|
||||
...(options.ca ? { ca: options.ca } : {}),
|
||||
checkServerIdentity: () => undefined,
|
||||
headers: {
|
||||
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
||||
...(options.headerToken ? { 'x-hermes-session-token': options.headerToken } : {}),
|
||||
...(payload ? { 'content-type': 'application/json' } : {}),
|
||||
origin: 'http://evil.example',
|
||||
cookie: 'session=evil',
|
||||
...options.headers
|
||||
}
|
||||
},
|
||||
res => {
|
||||
let text = ''
|
||||
|
||||
resolve({ status: res.statusCode ?? 0, text, headers: res.headers, json })
|
||||
})
|
||||
}
|
||||
)
|
||||
res.on('data', chunk => (text += chunk))
|
||||
res.on('end', () => {
|
||||
let json: any = null
|
||||
|
||||
req.on('error', reject)
|
||||
req.end(payload)
|
||||
})
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch {
|
||||
json = null
|
||||
}
|
||||
|
||||
resolve({ status: res.statusCode ?? 0, text, headers: res.headers, json })
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
req.on('error', reject)
|
||||
req.end(payload)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function wsOpen(url: string, ca: string, extra: Record<string, unknown> = {}) {
|
||||
@@ -277,7 +287,10 @@ describe('LAN proxy: pairing (hello / claim)', () => {
|
||||
|
||||
it('claim is 410 pairing_closed while pairing is not open', async () => {
|
||||
const t = await startProxy()
|
||||
const r = await call(t.port, 'POST', '/aiturk/pair/claim', { ca: t.ca, body: { code: 'AAAAAAAA', device_name: 'x' } })
|
||||
const r = await call(t.port, 'POST', '/aiturk/pair/claim', {
|
||||
ca: t.ca,
|
||||
body: { code: 'AAAAAAAA', device_name: 'x' }
|
||||
})
|
||||
|
||||
expect([r.status, r.json]).toEqual([410, { error: 'pairing_closed' }])
|
||||
await t.proxy.stop()
|
||||
@@ -288,13 +301,19 @@ describe('LAN proxy: pairing (hello / claim)', () => {
|
||||
const { code } = t.proxy.beginPairing()
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const r = await call(t.port, 'POST', '/aiturk/pair/claim', { ca: t.ca, body: { code: wrongCode(code), device_name: 'x' } })
|
||||
const r = await call(t.port, 'POST', '/aiturk/pair/claim', {
|
||||
ca: t.ca,
|
||||
body: { code: wrongCode(code), device_name: 'x' }
|
||||
})
|
||||
|
||||
expect([r.status, r.json]).toEqual([401, { error: 'wrong_code' }])
|
||||
}
|
||||
|
||||
const typed = `${code.slice(0, 4).toLowerCase()}-${code.slice(4).toLowerCase()} `
|
||||
const ok = await call(t.port, 'POST', '/aiturk/pair/claim', { ca: t.ca, body: { code: typed, device_name: ' Pixel\u0000 <b>9</b> ' } })
|
||||
const ok = await call(t.port, 'POST', '/aiturk/pair/claim', {
|
||||
ca: t.ca,
|
||||
body: { code: typed, device_name: ' Pixel\u0000 <b>9</b> ' }
|
||||
})
|
||||
|
||||
expect(ok.status).toBe(200)
|
||||
expect(Object.keys(ok.json).sort()).toEqual(['device_id', 'pc_name', 'token', 'version'])
|
||||
@@ -326,10 +345,20 @@ describe('LAN proxy: pairing (hello / claim)', () => {
|
||||
const { code } = t.proxy.beginPairing()
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect((await call(t.port, 'POST', '/aiturk/pair/claim', { ca: t.ca, body: { code: wrongCode(code), device_name: 'x' } })).status).toBe(401)
|
||||
expect(
|
||||
(
|
||||
await call(t.port, 'POST', '/aiturk/pair/claim', {
|
||||
ca: t.ca,
|
||||
body: { code: wrongCode(code), device_name: 'x' }
|
||||
})
|
||||
).status
|
||||
).toBe(401)
|
||||
}
|
||||
|
||||
const fifth = await call(t.port, 'POST', '/aiturk/pair/claim', { ca: t.ca, body: { code: wrongCode(code), device_name: 'x' } })
|
||||
const fifth = await call(t.port, 'POST', '/aiturk/pair/claim', {
|
||||
ca: t.ca,
|
||||
body: { code: wrongCode(code), device_name: 'x' }
|
||||
})
|
||||
|
||||
expect([fifth.status, fifth.json]).toEqual([410, { error: 'pairing_locked' }])
|
||||
|
||||
@@ -358,16 +387,28 @@ describe('LAN proxy: pairing (hello / claim)', () => {
|
||||
const t = await startProxy()
|
||||
const { code } = t.proxy.beginPairing()
|
||||
|
||||
for (const rawBody of ['{not json', '[]', '{"code":12345678,"device_name":"x"}', `{"code":"${code}"}`, `{"code":"${code}","device_name":""}`, JSON.stringify({ code, device_name: 'n'.repeat(61) })]) {
|
||||
for (const rawBody of [
|
||||
'{not json',
|
||||
'[]',
|
||||
'{"code":12345678,"device_name":"x"}',
|
||||
`{"code":"${code}"}`,
|
||||
`{"code":"${code}","device_name":""}`,
|
||||
JSON.stringify({ code, device_name: 'n'.repeat(61) })
|
||||
]) {
|
||||
const r = await call(t.port, 'POST', '/aiturk/pair/claim', { ca: t.ca, rawBody })
|
||||
|
||||
expect([r.status, r.json]).toEqual([400, { error: 'invalid_request' }])
|
||||
}
|
||||
|
||||
const big = await call(t.port, 'POST', '/aiturk/pair/claim', { ca: t.ca, rawBody: JSON.stringify({ code, device_name: 'x', pad: 'p'.repeat(5000) }) })
|
||||
const big = await call(t.port, 'POST', '/aiturk/pair/claim', {
|
||||
ca: t.ca,
|
||||
rawBody: JSON.stringify({ code, device_name: 'x', pad: 'p'.repeat(5000) })
|
||||
})
|
||||
|
||||
expect(big.status).toBe(413)
|
||||
expect((await call(t.port, 'POST', '/aiturk/pair/claim', { ca: t.ca, body: { code, device_name: 'x' } })).status).toBe(200)
|
||||
expect(
|
||||
(await call(t.port, 'POST', '/aiturk/pair/claim', { ca: t.ca, body: { code, device_name: 'x' } })).status
|
||||
).toBe(200)
|
||||
await t.proxy.stop()
|
||||
})
|
||||
})
|
||||
@@ -421,7 +462,11 @@ describe('LAN proxy: REST', () => {
|
||||
|
||||
for (const options of [{ token }, { headerToken: token }]) {
|
||||
seen.length = 0
|
||||
const r = await call(t.port, 'GET', '/api/sessions?limit=20', { ca: t.ca, ...options, headers: { 'x-forwarded-for': '10.0.0.9' } })
|
||||
const r = await call(t.port, 'GET', '/api/sessions?limit=20', {
|
||||
ca: t.ca,
|
||||
...options,
|
||||
headers: { 'x-forwarded-for': '10.0.0.9' }
|
||||
})
|
||||
|
||||
expect(r.status).toBe(200)
|
||||
expect(r.json).toEqual({ ok: true, path: '/api/sessions' })
|
||||
@@ -498,7 +543,11 @@ describe('LAN proxy: REST', () => {
|
||||
resolveCalls = 0
|
||||
|
||||
for (const [method, path] of forbidden) {
|
||||
const r = await call(t.port, method, path, { ca: t.ca, token, ...(method === 'GET' || method === 'DELETE' ? {} : { body: {} }) })
|
||||
const r = await call(t.port, method, path, {
|
||||
ca: t.ca,
|
||||
token,
|
||||
...(method === 'GET' || method === 'DELETE' ? {} : { body: {} })
|
||||
})
|
||||
|
||||
expect([method, path, r.status, r.json]).toEqual([method, path, 403, { error: 'forbidden' }])
|
||||
}
|
||||
@@ -527,6 +576,50 @@ describe('LAN proxy: REST', () => {
|
||||
await t.proxy.stop()
|
||||
})
|
||||
|
||||
it('routes REST and WebSocket by the phone profile, never the desktop selection', async () => {
|
||||
const profiles: string[] = []
|
||||
const t = await startProxy({
|
||||
resolveBackend: async (profile: string) => {
|
||||
profiles.push(profile)
|
||||
return resolveBackend()
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { token } = await t.pair()
|
||||
for (const query of ['', '?profile=kodlama', '?profile=all']) {
|
||||
expect((await call(t.port, 'GET', `/api/cron/jobs${query}`, { ca: t.ca, token })).status).toBe(200)
|
||||
}
|
||||
const opened = await wsOpen(`wss://127.0.0.1:${t.port}/api/ws?token=${token}&profile=seo`, t.ca)
|
||||
expect(opened.open).toBe(true)
|
||||
opened.ws?.close()
|
||||
expect(profiles).toEqual(['default', 'kodlama', 'default', 'seo'])
|
||||
expect(seen.some(row => row.path === '/api/cron/jobs?profile=all')).toBe(true)
|
||||
} finally {
|
||||
await t.proxy.stop()
|
||||
}
|
||||
})
|
||||
|
||||
it('bounds a pending setup/backend resolution for REST and WebSocket and never forwards the late result', async () => {
|
||||
let finish: (value: { baseUrl: string; token: string }) => void = () => {}
|
||||
const pending = new Promise<{ baseUrl: string; token: string }>(resolve => {
|
||||
finish = resolve
|
||||
})
|
||||
const t = await startProxy({ resolveBackend: () => pending, backendResolveTimeoutMs: 40 })
|
||||
try {
|
||||
const { token } = await t.pair()
|
||||
const before = seen.length
|
||||
const rest = await call(t.port, 'GET', '/api/cron/jobs?profile=default', { ca: t.ca, token })
|
||||
expect([rest.status, rest.json]).toEqual([503, { error: 'backend_unavailable' }])
|
||||
expect((await wsOpen(`wss://127.0.0.1:${t.port}/api/ws?token=${token}`, t.ca)).status).toBe(503)
|
||||
finish({ baseUrl: `http://127.0.0.1:${backendPort}`, token: SESSION })
|
||||
await new Promise(resolve => setTimeout(resolve, 30))
|
||||
expect(seen.length).toBe(before)
|
||||
expect((await call(t.port, 'GET', '/api/cron/jobs?profile=default', { ca: t.ca, token })).status).toBe(200)
|
||||
} finally {
|
||||
await t.proxy.stop()
|
||||
}
|
||||
})
|
||||
|
||||
it('refuses when the backend cannot be resolved locally (remote mode / not loopback) without leaking why', async () => {
|
||||
const remote = await startProxy({
|
||||
resolveBackend: async () => {
|
||||
@@ -541,7 +634,9 @@ describe('LAN proxy: REST', () => {
|
||||
expect((await wsOpen(`wss://127.0.0.1:${remote.port}/api/ws?token=${a.token}`, remote.ca)).status).toBe(503)
|
||||
await remote.proxy.stop()
|
||||
|
||||
const elsewhere = await startProxy({ resolveBackend: async () => ({ baseUrl: 'https://gateway.example', token: SESSION }) })
|
||||
const elsewhere = await startProxy({
|
||||
resolveBackend: async () => ({ baseUrl: 'https://gateway.example', token: SESSION })
|
||||
})
|
||||
const b = await elsewhere.pair()
|
||||
|
||||
expect((await call(elsewhere.port, 'GET', '/api/sessions', { ca: elsewhere.ca, token: b.token })).status).toBe(503)
|
||||
@@ -628,10 +723,18 @@ describe('LAN proxy: TLS, listening and logs', () => {
|
||||
const t = await startProxy()
|
||||
|
||||
const peer = await new Promise<string>((resolve, reject) => {
|
||||
const req = https.request({ host: '127.0.0.1', port: t.port, path: '/aiturk/pair/hello', rejectUnauthorized: false, agent: false }, res => {
|
||||
resolve(crypto.createHash('sha256').update((res.socket as any).getPeerCertificate(true).raw).digest('base64url'))
|
||||
res.resume()
|
||||
})
|
||||
const req = https.request(
|
||||
{ host: '127.0.0.1', port: t.port, path: '/aiturk/pair/hello', rejectUnauthorized: false, agent: false },
|
||||
res => {
|
||||
resolve(
|
||||
crypto
|
||||
.createHash('sha256')
|
||||
.update((res.socket as any).getPeerCertificate(true).raw)
|
||||
.digest('base64url')
|
||||
)
|
||||
res.resume()
|
||||
}
|
||||
)
|
||||
|
||||
req.on('error', reject)
|
||||
req.end()
|
||||
@@ -641,10 +744,14 @@ describe('LAN proxy: TLS, listening and logs', () => {
|
||||
expect(peer).not.toBe(createSelfSignedCertificate().fingerprint)
|
||||
|
||||
// A phone pinned to ANOTHER certificate must refuse this server.
|
||||
await expect(call(t.port, 'GET', '/aiturk/pair/hello', { ca: createSelfSignedCertificate().cert })).rejects.toThrow()
|
||||
await expect(
|
||||
call(t.port, 'GET', '/aiturk/pair/hello', { ca: createSelfSignedCertificate().cert })
|
||||
).rejects.toThrow()
|
||||
|
||||
const systemCa = await new Promise<string>(resolve => {
|
||||
const req = https.request({ host: '127.0.0.1', port: t.port, path: '/aiturk/pair/hello', agent: false }, () => resolve('accepted'))
|
||||
const req = https.request({ host: '127.0.0.1', port: t.port, path: '/aiturk/pair/hello', agent: false }, () =>
|
||||
resolve('accepted')
|
||||
)
|
||||
|
||||
req.on('error', (error: NodeJS.ErrnoException) => resolve(String(error.code)))
|
||||
req.end()
|
||||
@@ -653,7 +760,9 @@ describe('LAN proxy: TLS, listening and logs', () => {
|
||||
expect(systemCa).toMatch(/SELF_SIGNED|UNABLE_TO_VERIFY|DEPTH_ZERO/)
|
||||
|
||||
const plain = await new Promise<string>(resolve => {
|
||||
const req = http.request({ host: '127.0.0.1', port: t.port, path: '/api/sessions' }, res => resolve(String(res.statusCode)))
|
||||
const req = http.request({ host: '127.0.0.1', port: t.port, path: '/api/sessions' }, res =>
|
||||
resolve(String(res.statusCode))
|
||||
)
|
||||
|
||||
req.on('error', (error: NodeJS.ErrnoException) => resolve(String(error.code || 'error')))
|
||||
req.end()
|
||||
@@ -710,7 +819,11 @@ describe('LAN proxy: TLS, listening and logs', () => {
|
||||
// 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 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. */
|
||||
@@ -835,7 +948,11 @@ function linkHarness(over: Record<string, any> = {}) {
|
||||
if (isOffer) {
|
||||
return {
|
||||
status: offerOutcome,
|
||||
json: async () => ({ offerId: '0f8fad5b-d9cb-469f-a165-70867728950e', cancelToken: `tsmp_${'c'.repeat(43)}`, expiresIn: 300 })
|
||||
json: async () => ({
|
||||
offerId: '0f8fad5b-d9cb-469f-a165-70867728950e',
|
||||
cancelToken: `tsmp_${'c'.repeat(43)}`,
|
||||
expiresIn: 300
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -886,14 +1003,35 @@ function linkHarness(over: Record<string, any> = {}) {
|
||||
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, retrySleeps, heldRetries }
|
||||
return {
|
||||
link: make(),
|
||||
make,
|
||||
vault,
|
||||
store,
|
||||
firewallCalls,
|
||||
firewallWrites,
|
||||
fetchCalls,
|
||||
offers,
|
||||
events,
|
||||
logs,
|
||||
state,
|
||||
hostSockets,
|
||||
retrySleeps,
|
||||
heldRetries
|
||||
}
|
||||
}
|
||||
|
||||
describe('mobile link controller', () => {
|
||||
it('nothing listens until the feature is on AND pairing runs or a phone is paired', async () => {
|
||||
const h = linkHarness()
|
||||
|
||||
expect(await h.link.state()).toMatchObject({ enabled: false, listening: false, port: null, pairing: null, devices: [] })
|
||||
expect(await h.link.state()).toMatchObject({
|
||||
enabled: false,
|
||||
listening: false,
|
||||
port: null,
|
||||
pairing: null,
|
||||
devices: []
|
||||
})
|
||||
|
||||
const enabled = await h.link.enable()
|
||||
const port = enabled.port as number
|
||||
@@ -953,7 +1091,10 @@ describe('mobile link controller', () => {
|
||||
expect(JSON.stringify(offer.init.headers)).not.toContain(pairing.code)
|
||||
|
||||
const ca = h.vault.value.lanTls.cert
|
||||
const claim = await call(port, 'POST', '/aiturk/pair/claim', { ca, body: { code: pairing.code, device_name: 'Pixel 9' } })
|
||||
const claim = await call(port, 'POST', '/aiturk/pair/claim', {
|
||||
ca,
|
||||
body: { code: pairing.code, device_name: 'Pixel 9' }
|
||||
})
|
||||
|
||||
expect(claim.status).toBe(200)
|
||||
await until(() => h.events.some(event => event.type === 'paired'))
|
||||
@@ -1033,7 +1174,10 @@ describe('mobile link controller', () => {
|
||||
const state = await h.link.pairBegin()
|
||||
const port = state.port as number
|
||||
const ca = h.vault.value.lanTls.cert
|
||||
const claim = await call(port, 'POST', '/aiturk/pair/claim', { ca, body: { code: state.pairing!.code, device_name: 'Pixel' } })
|
||||
const claim = await call(port, 'POST', '/aiturk/pair/claim', {
|
||||
ca,
|
||||
body: { code: state.pairing!.code, device_name: 'Pixel' }
|
||||
})
|
||||
|
||||
await until(() => h.events.some(event => event.type === 'paired'))
|
||||
|
||||
@@ -1083,10 +1227,19 @@ describe('mobile link controller', () => {
|
||||
})
|
||||
|
||||
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 h = linkHarness({
|
||||
firewallResults: [
|
||||
{ ok: false, reason: 'denied' },
|
||||
{ ok: false, reason: 'failed' }
|
||||
]
|
||||
})
|
||||
const state = await h.link.enable()
|
||||
|
||||
expect(state).toMatchObject({ enabled: true, firewall: 'needed', lan: { direct: false, listening: false, firewall: 'missing' } })
|
||||
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 } })
|
||||
@@ -1095,7 +1248,10 @@ describe('mobile link controller', () => {
|
||||
// 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(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()
|
||||
@@ -1113,7 +1269,10 @@ describe('mobile link controller', () => {
|
||||
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(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=')
|
||||
@@ -1124,7 +1283,11 @@ describe('mobile link controller', () => {
|
||||
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(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()
|
||||
@@ -1168,7 +1331,14 @@ describe('mobile link controller', () => {
|
||||
|
||||
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 }] }
|
||||
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()
|
||||
@@ -1189,7 +1359,11 @@ describe('mobile link controller', () => {
|
||||
|
||||
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(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.
|
||||
@@ -1201,7 +1375,9 @@ describe('mobile link controller', () => {
|
||||
|
||||
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]}$`))
|
||||
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({
|
||||
@@ -1252,7 +1428,10 @@ describe('mobile link controller', () => {
|
||||
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' } })
|
||||
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'))
|
||||
@@ -1263,10 +1442,16 @@ describe('mobile link controller', () => {
|
||||
// "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(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)
|
||||
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()
|
||||
@@ -1275,7 +1460,9 @@ describe('mobile link controller', () => {
|
||||
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.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.
|
||||
@@ -1284,11 +1471,22 @@ describe('mobile link controller', () => {
|
||||
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')
|
||||
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)
|
||||
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()
|
||||
})
|
||||
|
||||
@@ -1299,7 +1497,10 @@ describe('mobile link controller', () => {
|
||||
|
||||
const state = await h.link.pairBegin()
|
||||
|
||||
await call(state.port as number, 'POST', '/aiturk/pair/claim', { ca: h.vault.value.lanTls.cert, body: { code: state.pairing!.code, device_name: 'Pixel' } })
|
||||
await call(state.port as number, 'POST', '/aiturk/pair/claim', {
|
||||
ca: h.vault.value.lanTls.cert,
|
||||
body: { code: state.pairing!.code, device_name: 'Pixel' }
|
||||
})
|
||||
await until(() => h.events.some(event => event.type === 'paired'))
|
||||
await h.link.stop()
|
||||
|
||||
@@ -1453,7 +1654,13 @@ describe('mobile link controller: rendezvous record retries', () => {
|
||||
|
||||
const again = (await h.link.pairRepublish()).pairing!
|
||||
|
||||
expect(again).toMatchObject({ code: begun.code, qr: begun.qr, expiresAt: begun.expiresAt, securityCode: begun.securityCode, codePath: 'pending' })
|
||||
expect(again).toMatchObject({
|
||||
code: begun.code,
|
||||
qr: begun.qr,
|
||||
expiresAt: begun.expiresAt,
|
||||
securityCode: begun.securityCode,
|
||||
codePath: 'pending'
|
||||
})
|
||||
await until(() => h.events.at(-1)?.state.pairing?.codePath === 'ready')
|
||||
expect(h.offers()).toHaveLength(2)
|
||||
expect(lookupOf(h, 1)).toBe(lookupOf(h, 0))
|
||||
@@ -1501,7 +1708,13 @@ describe('mobile link controller: rendezvous record retries', () => {
|
||||
})
|
||||
|
||||
describe('publishPairOffer / cancelPairOffer', () => {
|
||||
const input = { code: 'ABCDEFGH', addresses: [] as string[], port: 47321, fingerprint: 'f'.repeat(43), relay: RELAY_IDS[0] }
|
||||
const input = {
|
||||
code: 'ABCDEFGH',
|
||||
addresses: [] as string[],
|
||||
port: 47321,
|
||||
fingerprint: 'f'.repeat(43),
|
||||
relay: RELAY_IDS[0]
|
||||
}
|
||||
const good = { offerId: '0f8fad5b-d9cb-469f-a165-70867728950e', cancelToken: `tsmp_${'c'.repeat(43)}` }
|
||||
|
||||
function plan(outcomes: (number | 'throw')[]) {
|
||||
@@ -1577,7 +1790,10 @@ describe('publishPairOffer / cancelPairOffer', () => {
|
||||
|
||||
const told = plan([409])
|
||||
|
||||
expect(await publishPairOffer(told.fetcher, input, { ...told.options, earlierTryMayHaveLanded: true })).toEqual({ offerId: null, cancelToken: null })
|
||||
expect(await publishPairOffer(told.fetcher, input, { ...told.options, earlierTryMayHaveLanded: true })).toEqual({
|
||||
offerId: null,
|
||||
cancelToken: null
|
||||
})
|
||||
})
|
||||
|
||||
it('stops as soon as the pairing is over, without a log line', async () => {
|
||||
@@ -1624,12 +1840,18 @@ describe('pure helpers', () => {
|
||||
expect(normalizePairCode(' abcd-efgh ')).toBe('ABCDEFGH')
|
||||
expect(normalizePairCode(12345678)).toBe('')
|
||||
expect(formatPairCode('ABCDEFGH')).toBe('ABCD-EFGH')
|
||||
expect(pairLookup('abcd-efgh')).toBe(crypto.createHash('sha256').update('aiturk-mobile-pair-v1:ABCDEFGH').digest('hex'))
|
||||
expect(pairLookup('abcd-efgh')).toBe(
|
||||
crypto.createHash('sha256').update('aiturk-mobile-pair-v1:ABCDEFGH').digest('hex')
|
||||
)
|
||||
})
|
||||
|
||||
it('address candidates: private IPv4 only, default route first, virtual adapters dropped, at most four', () => {
|
||||
expect(['10.1.2.3', '172.16.0.1', '172.31.255.254', '192.168.0.1'].every(isPrivateIpv4)).toBe(true)
|
||||
expect(['172.15.0.1', '172.32.0.1', '8.8.8.8', '169.254.1.1', '127.0.0.1', '192.168.1', '::1', '192.168.1.999'].some(isPrivateIpv4)).toBe(false)
|
||||
expect(
|
||||
['172.15.0.1', '172.32.0.1', '8.8.8.8', '169.254.1.1', '127.0.0.1', '192.168.1', '::1', '192.168.1.999'].some(
|
||||
isPrivateIpv4
|
||||
)
|
||||
).toBe(false)
|
||||
|
||||
const v4 = (address: string, internal = false) => [{ address, family: 'IPv4', internal }]
|
||||
|
||||
@@ -1646,13 +1868,25 @@ describe('pure helpers', () => {
|
||||
'Ethernet 4': v4('192.168.4.2')
|
||||
}
|
||||
|
||||
expect(selectLanAddresses(interfaces, '192.168.1.20')).toEqual(['192.168.1.20', '10.0.0.5', '192.168.2.2', '192.168.3.2'])
|
||||
expect(selectLanAddresses(interfaces, '192.168.1.20')).toEqual([
|
||||
'192.168.1.20',
|
||||
'10.0.0.5',
|
||||
'192.168.2.2',
|
||||
'192.168.3.2'
|
||||
])
|
||||
expect(selectLanAddresses(interfaces, null)[0]).toBe('10.0.0.5')
|
||||
expect(selectLanAddresses({ 'vEthernet (Default Switch)': v4('172.20.0.1') }, null)).toEqual([])
|
||||
expect(() => buildPairQrPayload({ addresses: ['8.8.8.8'], port: 47321, fingerprint: 'f'.repeat(43), code: 'ABCDEFGH' })).toThrow(
|
||||
'aiturk-mobile:no_lan_address'
|
||||
)
|
||||
expect(buildPairQrPayload({ addresses: ['1.1.1.1', '10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4', '10.0.0.5'], port: 47321, fingerprint: 'f'.repeat(43), code: 'ABCDEFGH' })).toBe(
|
||||
expect(() =>
|
||||
buildPairQrPayload({ addresses: ['8.8.8.8'], port: 47321, fingerprint: 'f'.repeat(43), code: 'ABCDEFGH' })
|
||||
).toThrow('aiturk-mobile:no_lan_address')
|
||||
expect(
|
||||
buildPairQrPayload({
|
||||
addresses: ['1.1.1.1', '10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4', '10.0.0.5'],
|
||||
port: 47321,
|
||||
fingerprint: 'f'.repeat(43),
|
||||
code: 'ABCDEFGH'
|
||||
})
|
||||
).toBe(
|
||||
`aiturk://pair?v=1&u=10.0.0.1:47321&u=10.0.0.2:47321&u=10.0.0.3:47321&u=10.0.0.4:47321&fp=${'f'.repeat(43)}&c=ABCDEFGH`
|
||||
)
|
||||
})
|
||||
@@ -1683,7 +1917,11 @@ describe('pure helpers', () => {
|
||||
expect(() => firewallCommand('add', bad)).toThrow('aiturk-mobile:invalid_port')
|
||||
}
|
||||
|
||||
for (const [exit, expected] of [[0, { ok: true }], [77, { ok: false, reason: 'denied' }], [1, { ok: false, reason: 'failed' }]] as const) {
|
||||
for (const [exit, expected] of [
|
||||
[0, { ok: true }],
|
||||
[77, { ok: false, reason: 'denied' }],
|
||||
[1, { ok: false, reason: 'failed' }]
|
||||
] as const) {
|
||||
const spawned: any[] = []
|
||||
|
||||
const firewall = createWindowsFirewall({
|
||||
@@ -1725,7 +1963,11 @@ describe('pure helpers', () => {
|
||||
parseMobileLinkStored({
|
||||
enabled: 'yes',
|
||||
port: 99999,
|
||||
devices: [{ id, name: 'Pixel', tokenHash: hash, createdAt: 5, token: 'amd_x' }, { id: 'x', name: 'bad', tokenHash: hash, createdAt: 1 }, null]
|
||||
devices: [
|
||||
{ id, name: 'Pixel', tokenHash: hash, createdAt: 5, token: 'amd_x' },
|
||||
{ id: 'x', name: 'bad', tokenHash: hash, createdAt: 1 },
|
||||
null
|
||||
]
|
||||
})
|
||||
).toEqual({
|
||||
enabled: false,
|
||||
@@ -1748,8 +1990,12 @@ describe('pure helpers', () => {
|
||||
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({
|
||||
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
|
||||
@@ -1760,16 +2006,30 @@ describe('pure helpers', () => {
|
||||
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: [], 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`
|
||||
)
|
||||
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')
|
||||
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[] = []
|
||||
@@ -1777,13 +2037,22 @@ describe('pure helpers', () => {
|
||||
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)}` }) }
|
||||
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()
|
||||
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(
|
||||
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 }
|
||||
@@ -1793,7 +2062,10 @@ describe('pure helpers', () => {
|
||||
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(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') =>
|
||||
@@ -1840,14 +2112,24 @@ describe('pure helpers', () => {
|
||||
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] })
|
||||
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] })
|
||||
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' })
|
||||
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' })
|
||||
@@ -1871,9 +2153,16 @@ describe('pure helpers', () => {
|
||||
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')) })
|
||||
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' })
|
||||
expect(
|
||||
await createWindowsFirewall({ platform: 'linux', env: {}, spawn: () => ({ on: () => undefined }) }).queryRule!()
|
||||
).toEqual({ state: 'unknown' })
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user