import { createHash, generateKeyPairSync, sign } from 'crypto' import { createServer, type Server } from 'http' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { LanDeviceInfo } from '../../packages/server/src/services/lan-discovery' const keyPair = generateKeyPairSync('ed25519', { publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, }) const deviceId = `hwui_${createHash('sha256').update(keyPair.publicKey).digest('base64url').slice(0, 32)}` const device: LanDeviceInfo = { id: deviceId, device_id: deviceId, device_public_key: keyPair.publicKey, computer_name: 'paired-device', endpoint_kind: 'web', ip: '192.168.1.20', http_port: 8648, url: 'http://192.168.1.20:8648', os: { type: 'Linux', platform: 'linux', release: '1', arch: 'x64', }, hermes_agent_version: 'v1', hermes_web_ui_version: '1', response_ms: 12, last_seen_at: new Date().toISOString(), } describe('devices controller', () => { let db: any = null beforeEach(async () => { vi.resetModules() const { DatabaseSync } = await import('node:sqlite') db = new DatabaseSync(':memory:') vi.doMock('../../packages/server/src/db/index', () => ({ getDb: () => db, getStoragePath: () => ':memory:', })) const { initAllHermesTables } = await import('../../packages/server/src/db/hermes/schemas') initAllHermesTables() }) afterEach(() => { db?.close() db = null vi.unstubAllGlobals() vi.doUnmock('../../packages/server/src/db/index') vi.doUnmock('../../packages/server/src/services/login-limiter') vi.resetModules() }) it('returns the inbound pairing status for a signed device status request', async () => { const { requestInboundDeviceLink, updateInboundStatus } = await import('../../packages/server/src/db/hermes/devices-store') requestInboundDeviceLink(device) updateInboundStatus(device.id, 'approved') const timestamp = Date.now() const nonce = 'status-nonce-1' const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url') const ctx: any = { request: { body: { device_id: device.id, device_public_key: device.device_public_key, timestamp, nonce, signature, }, }, } const { requestDeviceLinkStatusController } = await import('../../packages/server/src/controllers/devices') await requestDeviceLinkStatusController(ctx) expect(ctx.status).toBeUndefined() expect(ctx.body).toEqual({ status: 'approved' }) }) it('keeps LAN pairing requests code-free and derives the device URL from the request source', async () => { const timestamp = Date.now() const nonce = 'lan-request-nonce-1' const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url') const ctx: any = { ip: '192.168.1.99', request: { ip: '192.168.1.99', body: { ...device, http_port: 8648, url: 'javascript:alert(1)', timestamp, nonce, signature, }, }, } const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store') const { requestDeviceLinkController } = await import('../../packages/server/src/controllers/devices') await requestDeviceLinkController(ctx) expect(ctx.status).toBeUndefined() expect(ctx.body).toEqual({ status: 'pending' }) expect(getDeviceRelation(device.id)?.url).toBe('http://192.168.1.99:8648') }) it('requires the startup pairing code before public requests enter the approval list', async () => { const timestamp = Date.now() const nonce = 'public-request-nonce-1' const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url') const ctx: any = { ip: '8.8.8.8', request: { ip: '8.8.8.8', body: { ...device, http_port: 8648, timestamp, nonce, signature, }, }, } const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store') const { requestDeviceLinkController } = await import('../../packages/server/src/controllers/devices') await requestDeviceLinkController(ctx) expect(ctx.status).toBe(403) expect(ctx.body).toEqual({ error: 'Invalid pairing code' }) expect(getDeviceRelation(device.id)).toBeNull() }) it('does not trust forged forwarded private IPs for public pairing requests', async () => { const recordPairingFailure = vi.fn() vi.doMock('../../packages/server/src/services/login-limiter', () => ({ checkPairing: (ip: string) => { expect(ip).toBe('8.8.8.8') return { allowed: true } }, recordPairingFailure, })) const timestamp = Date.now() const nonce = 'forged-forwarded-private-request-nonce-1' const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url') const ctx: any = { ip: '8.8.8.8', get: (name: string) => name.toLowerCase() === 'x-forwarded-for' ? '192.168.1.44' : '', request: { ip: '8.8.8.8', body: { ...device, http_port: 8648, timestamp, nonce, signature, }, }, } const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store') const { requestDeviceLinkController } = await import('../../packages/server/src/controllers/devices') await requestDeviceLinkController(ctx) expect(ctx.status).toBe(403) expect(ctx.body).toEqual({ error: 'Invalid pairing code' }) expect(recordPairingFailure).toHaveBeenCalledWith('8.8.8.8') expect(getDeviceRelation(device.id)).toBeNull() }) it('requires the startup pairing code for public clients forwarded through loopback proxies', async () => { const timestamp = Date.now() const nonce = 'forwarded-public-request-nonce-1' const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url') const ctx: any = { ip: '127.0.0.1', get: (name: string) => name.toLowerCase() === 'x-forwarded-for' ? '203.0.113.10' : '', request: { ip: '127.0.0.1', body: { ...device, http_port: 8648, timestamp, nonce, signature, }, }, } const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store') const { requestDeviceLinkController } = await import('../../packages/server/src/controllers/devices') await requestDeviceLinkController(ctx) expect(ctx.status).toBe(403) expect(ctx.body).toEqual({ error: 'Invalid pairing code' }) expect(getDeviceRelation(device.id)).toBeNull() }) it('stores public pairing requests as pending when the startup pairing code is valid', async () => { vi.doMock('../../packages/server/src/services/device-pairing-code', () => ({ getDevicePairingCode: () => 'pair-secret', verifyDevicePairingCode: (value: unknown) => value === 'pair-secret', })) const timestamp = Date.now() const nonce = 'public-request-nonce-2' const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url') const ctx: any = { ip: '8.8.4.4', request: { ip: '8.8.4.4', body: { ...device, http_port: 8648, pairing_code: 'pair-secret', timestamp, nonce, signature, }, }, } const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store') const { requestDeviceLinkController } = await import('../../packages/server/src/controllers/devices') await requestDeviceLinkController(ctx) expect(ctx.status).toBeUndefined() expect(ctx.body).toEqual({ status: 'pending' }) expect(getDeviceRelation(device.id)?.inbound_status).toBe('pending') expect(getDeviceRelation(device.id)?.url).toBe('http://8.8.4.4:8648') }) it('prefers public request hosts when building copyable pairing links', async () => { vi.doMock('../../packages/server/src/services/device-pairing-code', () => ({ getDevicePairingCode: () => 'pair-secret', verifyDevicePairingCode: () => false, })) const { getDevicePairingLink } = await import('../../packages/server/src/controllers/devices') const ctx: any = { protocol: 'http', host: 'studio.example.com', get: (name: string) => name.toLowerCase() === 'x-forwarded-proto' ? 'https' : '', } await getDevicePairingLink(ctx) expect(ctx.body).toEqual({ code: 'pair-secret', link: 'https://studio.example.com/#/hermes/devices?pairing_code=pair-secret', }) }) it('falls back from localhost to LAN addresses when building copyable pairing links', async () => { vi.doMock('os', async () => { const actual = await vi.importActual('os') return { ...actual, networkInterfaces: () => ({ lo0: [{ family: 'IPv4', internal: true, address: '127.0.0.1' }], en0: [{ family: 'IPv4', internal: false, address: '192.168.1.88' }], }), } }) vi.doMock('../../packages/server/src/services/device-pairing-code', () => ({ getDevicePairingCode: () => 'pair-secret', verifyDevicePairingCode: () => false, })) const { getDevicePairingLink } = await import('../../packages/server/src/controllers/devices') const ctx: any = { protocol: 'http', host: 'localhost:8648', get: () => '', } await getDevicePairingLink(ctx) expect(ctx.body).toEqual({ code: 'pair-secret', link: 'http://192.168.1.88:8648/#/hermes/devices?pairing_code=pair-secret', }) }) it('rejects peer socket connections until outbound pairing is approved locally', async () => { vi.doMock('../../packages/server/src/services/lan-discovery', async () => { const actual = await vi.importActual( '../../packages/server/src/services/lan-discovery', ) return { ...actual, getLanDiscoveryCache: () => ({ scanning: false, last_scanned_at: new Date().toISOString(), devices: [device], }), } }) const { connectPeerDevice } = await import('../../packages/server/src/controllers/devices') const ctx: any = { params: { id: device.id }, request: { body: {} }, } await connectPeerDevice(ctx) expect(ctx.status).toBe(403) expect(ctx.body).toEqual({ error: 'Device pairing has not been approved' }) }) it('records outbound status when requesting pairing from a device with inbound history', async () => { vi.doMock('../../packages/server/src/services/lan-discovery', async () => { const actual = await vi.importActual( '../../packages/server/src/services/lan-discovery', ) return { ...actual, getLanDiscoveryCache: () => ({ scanning: false, last_scanned_at: new Date().toISOString(), devices: [device], }), } }) vi.doMock('../../packages/server/src/services/system-info', async () => { const actual = await vi.importActual( '../../packages/server/src/services/system-info', ) return { ...actual, getPublicSystemInfo: async () => ({ device_id: 'hwui_local', device_public_key: keyPair.publicKey, computer_name: 'local', os: { type: 'TestOS', platform: 'linux', release: '1', arch: 'x64' }, hermes_agent_version: 'v1', hermes_web_ui_version: '1', }), createDeviceSignature: async () => 'signature', } }) const fetchMock = vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ status: 'pending' }), })) vi.stubGlobal('fetch', fetchMock) const { requestInboundDeviceLink, updateInboundStatus, getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store') requestInboundDeviceLink(device) updateInboundStatus(device.id, 'approved') const { requestDevicePairing } = await import('../../packages/server/src/controllers/devices') const ctx: any = { params: { id: device.id }, request: { body: {} }, } await requestDevicePairing(ctx) const relation = getDeviceRelation(device.id) expect(ctx.status).toBeUndefined() expect(relation?.inbound_status).toBe('approved') expect(relation?.outbound_status).toBe('pending') expect(relation?.outbound_requested_at).toBeGreaterThan(0) expect(fetchMock).toHaveBeenCalledWith( 'http://192.168.1.20:8648/api/devices/link-request', expect.objectContaining({ method: 'POST' }), ) }) it('falls back to the native HTTP client when fetch cannot reach a LAN peer', async () => { let receivedRequest: any = null let server: Server | null = null await new Promise((resolve) => { server = createServer((req, res) => { const chunks: Buffer[] = [] req.on('data', chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))) req.on('end', () => { if (req.url === '/api/devices/link-request') { receivedRequest = JSON.parse(Buffer.concat(chunks).toString('utf-8')) } res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ status: 'pending' })) }) }) server.listen(0, '127.0.0.1', resolve) }) const address = server!.address() const port = typeof address === 'object' && address ? address.port : 0 const fallbackDevice: LanDeviceInfo = { ...device, ip: '127.0.0.1', http_port: port, url: `http://127.0.0.1:${port}`, } try { vi.doMock('../../packages/server/src/services/lan-discovery', async () => { const actual = await vi.importActual( '../../packages/server/src/services/lan-discovery', ) return { ...actual, getLanDiscoveryCache: () => ({ scanning: false, last_scanned_at: new Date().toISOString(), devices: [fallbackDevice], }), } }) vi.doMock('../../packages/server/src/services/system-info', async () => { const actual = await vi.importActual( '../../packages/server/src/services/system-info', ) return { ...actual, getPublicSystemInfo: async () => ({ device_id: 'hwui_local', device_public_key: keyPair.publicKey, computer_name: 'local', os: { type: 'TestOS', platform: 'darwin', release: '1', arch: 'arm64' }, hermes_agent_version: 'v1', hermes_web_ui_version: '1', }), createDeviceSignature: async () => 'signature', } }) const fetchMock = vi.fn(async () => { throw Object.assign(new Error(`connect EHOSTUNREACH ${fallbackDevice.ip}:${fallbackDevice.http_port}`), { code: 'EHOSTUNREACH', syscall: 'connect', address: fallbackDevice.ip, port: fallbackDevice.http_port, }) }) vi.stubGlobal('fetch', fetchMock) const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store') const { requestDevicePairing } = await import('../../packages/server/src/controllers/devices') const ctx: any = { params: { id: fallbackDevice.id }, request: { body: {} }, } await requestDevicePairing(ctx) const relation = getDeviceRelation(fallbackDevice.id) expect(ctx.status).toBeUndefined() expect(relation?.outbound_status).toBe('pending') expect(receivedRequest).toEqual(expect.objectContaining({ device_id: 'hwui_local', signature: 'signature', })) expect(fetchMock).toHaveBeenCalledWith( `http://127.0.0.1:${port}/api/devices/link-request`, expect.objectContaining({ method: 'POST' }), ) } finally { await new Promise((resolve, reject) => { server!.close(err => err ? reject(err) : resolve()) }) } }) it('requests pairing from a manually entered remote URL', async () => { vi.doMock('../../packages/server/src/services/lan-discovery', async () => { const actual = await vi.importActual( '../../packages/server/src/services/lan-discovery', ) return { ...actual, getLanDiscoveryCache: () => ({ scanning: false, last_scanned_at: new Date().toISOString(), devices: [], }), } }) vi.doMock('../../packages/server/src/services/system-info', async () => { const actual = await vi.importActual( '../../packages/server/src/services/system-info', ) return { ...actual, getPublicSystemInfo: async () => ({ device_id: 'hwui_local', device_public_key: keyPair.publicKey, computer_name: 'local', os: { type: 'TestOS', platform: 'linux', release: '1', arch: 'x64' }, hermes_agent_version: 'v1', hermes_web_ui_version: '1', }), createDeviceSignature: async () => 'signature', } }) const fetchMock = vi.fn(async (url: string, options?: RequestInit) => { if (url === 'https://remote.example.com/api/devices/link-info') { return { ok: true, status: 200, json: async () => ({ device_id: device.id, device_public_key: device.device_public_key, computer_name: 'remote-device', endpoint_kind: 'web', http_port: 443, os: { type: 'Linux', platform: 'linux', release: '1', arch: 'x64' }, hermes_agent_version: 'v1', hermes_web_ui_version: '1', }), } } if (url === 'https://remote.example.com/api/devices/link-request' && options?.method === 'POST') { expect(JSON.parse(String(options.body))).toEqual(expect.objectContaining({ pairing_code: 'pair-secret', })) return { ok: true, status: 200, json: async () => ({ status: 'pending' }), } } if (url === 'https://remote.example.com/api/devices/link-status' && options?.method === 'POST') { return { ok: true, status: 200, json: async () => ({ status: 'pending' }), } } throw new Error(`unexpected fetch: ${url}`) }) vi.stubGlobal('fetch', fetchMock) const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store') const { requestManualDevicePairing } = await import('../../packages/server/src/controllers/devices') const ctx: any = { request: { body: { url: 'https://remote.example.com/#/hermes/devices?pairing_code=pair-secret', }, }, } await requestManualDevicePairing(ctx) const relation = getDeviceRelation(device.id) expect(ctx.status).toBeUndefined() expect(relation?.outbound_status).toBe('pending') expect(relation?.url).toBe('https://remote.example.com') expect(ctx.body.devices).toEqual([ expect.objectContaining({ id: device.id, computer_name: 'remote-device', online: true, outbound_status: 'pending', url: 'https://remote.example.com', }), ]) expect(fetchMock).toHaveBeenCalledWith( 'https://remote.example.com/api/devices/link-info', expect.objectContaining({ method: 'GET' }), ) expect(fetchMock).toHaveBeenCalledWith( 'https://remote.example.com/api/devices/link-request', expect.objectContaining({ method: 'POST' }), ) }) it('keeps a manually paired remote device in the list when it is offline', async () => { vi.doMock('../../packages/server/src/services/lan-discovery', async () => { const actual = await vi.importActual( '../../packages/server/src/services/lan-discovery', ) return { ...actual, getLanDiscoveryCache: () => ({ scanning: false, last_scanned_at: new Date().toISOString(), devices: [], }), } }) vi.doMock('../../packages/server/src/services/system-info', async () => { const actual = await vi.importActual( '../../packages/server/src/services/system-info', ) return { ...actual, getPublicSystemInfo: async () => ({ device_id: 'hwui_local', device_public_key: keyPair.publicKey, computer_name: 'local', os: { type: 'TestOS', platform: 'linux', release: '1', arch: 'x64' }, hermes_agent_version: 'v1', hermes_web_ui_version: '1', }), createDeviceSignature: async () => 'signature', } }) const fetchMock = vi.fn(async () => { throw new Error('offline') }) vi.stubGlobal('fetch', fetchMock) const { updateOutboundStatus } = await import('../../packages/server/src/db/hermes/devices-store') const { listDevices } = await import('../../packages/server/src/controllers/devices') updateOutboundStatus(device.id, 'approved', { ...device, ip: 'remote.example.com', http_port: 443, url: 'https://remote.example.com', }) const ctx: any = {} await listDevices(ctx) expect(ctx.body.devices).toEqual([ expect.objectContaining({ id: device.id, online: false, outbound_status: 'approved', url: 'https://remote.example.com', }), ]) expect(fetchMock).toHaveBeenCalledWith( 'https://remote.example.com/api/devices/link-status', expect.objectContaining({ method: 'POST' }), ) }) })