'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const http = require('node:http'); const os = require('node:os'); const path = require('node:path'); const { execFile } = require('node:child_process'); const { promisify } = require('node:util'); const { createRemotePlayService, createWorkerRequestEnvelope, parseWorkerResponseEnvelope, searchPacket, wakePacket, parseDiscoveryResponse, WORKER_API_PREFIX, } = require('../../src/services/remote-play-service'); test('Chiaki PS5 discovery and wake packets match the upstream wire format', () => { assert.equal(searchPacket().toString(), 'SRCH * HTTP/1.1\ndevice-discovery-protocol-version:00030010\n'); const wake = wakePacket('A1B2C3D4').toString(); assert.match(wake, /^WAKEUP \* HTTP\/1\.1/m); assert.match(wake, /^client-type:vr$/m); assert.match(wake, /^auth-type:R$/m); assert.match(wake, /^user-credential:2712847316$/m); assert.match(wake, /^device-discovery-protocol-version:00030010$/m); assert.doesNotMatch(wake, /A1B2C3D4/); assert.throws(() => wakePacket('not-hex'), /hextekens/); assert.equal(parseDiscoveryResponse(Buffer.from('HTTP/1.1 620 Server Standby\nhost-name:PS5\n')).state, 'standby'); const ready = parseDiscoveryResponse(Buffer.from('HTTP/1.1 200 OK\nhost-name:PS5-313\nhost-request-port:997\nsystem-version:09600004\n')); assert.equal(ready.state, 'ready'); assert.equal(ready.console.name, 'PS5-313'); assert.equal(ready.console.requestPort, 997); }); test('Remote Play profile never returns the encrypted registration key', () => { const values = new Map(); const settings = { get: key => key === 'ftp.host' ? '192.168.1.20' : values.get(key), set: (key, value) => values.set(key, value) }; const secrets = { has: key => values.has(`secret:${key}`), get: key => values.get(`secret:${key}`) || '', set: (key, value) => values.set(`secret:${key}`, value) }; const service = createRemotePlayService({ settings, secrets }); const status = service.saveProfile({ nickname: 'Living Room', registrationKey: '1234ABCD' }); assert.equal(status.registrationKeyConfigured, true); assert.equal(status.nickname, 'Living Room'); assert.equal(Object.values(status).includes('1234ABCD'), false); assert.equal(secrets.get('remotePlay.registrationKey'), '1234ABCD'); const companion = service.companion(); assert.equal(companion.streaming.embedded, false); assert.equal(companion.streaming.clientSide, true); assert.ok(companion.clients.some(client => /SHA-256/.test(client.command))); const exported = service.exportProfile(); assert.match(exported, /streetpea\/chiaki-ng/); assert.doesNotMatch(exported, /1234ABCD/); assert.match(exported, /intentionally not exported/); }); test('Remote Play wake waits until the PS5 reports ready and never returns the key', async () => { let clock = 1_000; const values = new Map([['ftp.host', '192.168.1.105'], ['secret:remotePlay.registrationKey', '1234ABCD']]); const settings = { get: (key, fallback) => values.has(key) ? values.get(key) : fallback, set: (key, value) => values.set(key, value) }; const secrets = { has: key => values.has(`secret:${key}`), get: key => values.get(`secret:${key}`) || '', set: (key, value) => values.set(`secret:${key}`, value) }; const sent = []; let searches = 0; const service = createRemotePlayService({ settings, secrets, now: () => clock, sleep: async ms => { clock += ms; }, udpExchangeImpl: async (_host, payload, options) => { sent.push(payload.toString('ascii')); if (!options.waitForResponse) return { sent: true }; searches += 1; return searches === 1 ? { status: 620, state: 'standby', headers: {}, console: { name: 'PS5-313' }, latencyMs: 2 } : { status: 200, state: 'ready', headers: {}, console: { name: 'PS5-313' }, latencyMs: 2 }; }, }); const result = await service.wake(); assert.equal(result.ready, true); assert.equal(result.before.state, 'standby'); assert.equal(result.after.state, 'ready'); assert.ok(sent.some(packet => packet.startsWith('WAKEUP * HTTP/1.1'))); assert.doesNotMatch(JSON.stringify(result), /1234ABCD/); }); test('Remote Play worker enrollment, signed polling and fixed jobs are replay-safe', () => { let clock = 10_000; const values = new Map([['ftp.host', '192.168.1.105'], ['remotePlay.clientNickname', 'PS5-313']]); const secretValues = new Map(); const settings = { get: (key, fallback) => values.has(key) ? values.get(key) : fallback, set: (key, value) => values.set(key, value) }; const secrets = { has: key => secretValues.has(key), get: key => secretValues.get(key) || '', set: (key, value) => value ? secretValues.set(key, value) : secretValues.delete(key), }; const service = createRemotePlayService({ settings, secrets, now: () => clock, publicOrigin: 'https://chimera.example.test' }); const superseded = service.pairWorker({ label: 'Gaming PC' }); const pairing = service.pairWorker({ label: 'Gaming PC' }); assert.equal(service.status().workers.length, 1); assert.equal(service.status().workers[0].id, pairing.worker.id); assert.throws(() => service.handleWorkerRequest(`${WORKER_API_PREFIX}/enroll`, { workerId: superseded.worker.id, enrollmentToken: superseded.config.enrollmentToken }), /Ongeldig of verlopen/); assert.equal(pairing.permanentKeyReturned, false); assert.match(pairing.config.enrollmentToken, /^[a-f0-9]{64}$/); assert.doesNotMatch(JSON.stringify(pairing), /pairingKey/); const enrollment = service.handleWorkerRequest(`${WORKER_API_PREFIX}/enroll`, { workerId: pairing.worker.id, enrollmentToken: pairing.config.enrollmentToken }); assert.match(enrollment.pairingKey, /^[a-f0-9]{64}$/); assert.throws(() => service.handleWorkerRequest(`${WORKER_API_PREFIX}/enroll`, { workerId: pairing.worker.id, enrollmentToken: pairing.config.enrollmentToken }), /Ongeldig of verlopen/); const first = createWorkerRequestEnvelope({ workerId: pairing.worker.id, key: enrollment.pairingKey, path: `${WORKER_API_PREFIX}/poll`, timestamp: clock, nonce: '11'.repeat(16), payload: { status: { version: '1.1.0', platform: 'win32/x64', hostname: 'gaming-pc', client: { installed: true, version: '1.10.0', executable: 'chiaki.exe', managed: true }, registeredHosts: ['PS5-313'], session: { active: false }, streamHealth: { checkedAt: clock, resolution: '1920x1080', rttMs: 5.6, decoder: 'vulkan', packetErrors: 0, duplicateUnits: 0, decoderDrops: 0, status: 'healthy' } } }, }); const firstResponse = service.handleWorkerRequest(`${WORKER_API_PREFIX}/poll`, first); const firstPayload = parseWorkerResponseEnvelope(firstResponse, { workerId: pairing.worker.id, key: enrollment.pairingKey, requestNonce: first.nonce, now: clock }); assert.equal(firstPayload.job, null); const publicStatus = service.status(); assert.equal(publicStatus.workers[0].online, true); assert.equal(publicStatus.workers[0].client.version, '1.10.0'); assert.equal(publicStatus.workers[0].client.managed, true); assert.equal(publicStatus.workers[0].streamHealth.status, 'healthy'); assert.equal(publicStatus.workers[0].registeredHosts[0], 'PS5-313'); assert.doesNotMatch(JSON.stringify(publicStatus), new RegExp(enrollment.pairingKey, 'i')); assert.throws(() => service.handleWorkerRequest(`${WORKER_API_PREFIX}/poll`, first), /al verwerkt/); const queued = service.workerAction({ workerId: pairing.worker.id, action: 'diagnose' }); clock += 1_000; const second = createWorkerRequestEnvelope({ workerId: pairing.worker.id, key: enrollment.pairingKey, path: `${WORKER_API_PREFIX}/poll`, timestamp: clock, nonce: '22'.repeat(16), payload: { status: { version: '1.0.0', client: { installed: true }, registeredHosts: ['PS5-313'], session: { active: false } } } }); const secondPayload = parseWorkerResponseEnvelope(service.handleWorkerRequest(`${WORKER_API_PREFIX}/poll`, second), { workerId: pairing.worker.id, key: enrollment.pairingKey, requestNonce: second.nonce, now: clock }); assert.equal(secondPayload.job.id, queued.id); assert.equal(secondPayload.job.action, 'diagnose'); assert.deepEqual(Object.keys(secondPayload.job.payload), ['host']); clock += 1_000; const third = createWorkerRequestEnvelope({ workerId: pairing.worker.id, key: enrollment.pairingKey, path: `${WORKER_API_PREFIX}/poll`, timestamp: clock, nonce: '33'.repeat(16), payload: { status: { version: '1.0.0', client: { installed: true }, registeredHosts: ['PS5-313'], session: { active: false } }, result: { jobId: queued.id, success: true, message: 'Diagnose voltooid.', registeredHosts: ['PS5-313'] } } }); service.handleWorkerRequest(`${WORKER_API_PREFIX}/poll`, third); assert.equal(service.status().jobs.find(job => job.id === queued.id).status, 'completed'); values.delete('ftp.host'); const install = service.workerAction({ workerId: pairing.worker.id, action: 'install-client', confirmation: 'INSTALL CHIAKI' }); clock += 1_000; const fourth = createWorkerRequestEnvelope({ workerId: pairing.worker.id, key: enrollment.pairingKey, path: `${WORKER_API_PREFIX}/poll`, timestamp: clock, nonce: '44'.repeat(16), payload: { status: { version: '1.0.0', client: { installed: true }, registeredHosts: ['PS5-313'], session: { active: false } } } }); const fourthPayload = parseWorkerResponseEnvelope(service.handleWorkerRequest(`${WORKER_API_PREFIX}/poll`, fourth), { workerId: pairing.worker.id, key: enrollment.pairingKey, requestNonce: fourth.nonce, now: clock }); assert.equal(fourthPayload.job.id, install.id); assert.deepEqual(fourthPayload.job.payload, {}); }); test('bundled Remote Play worker enrolls and completes a signed poll end to end', async t => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-remote-worker-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const values = new Map([['ftp.host', '192.168.1.105']]); const secretValues = new Map(); const settings = { get: (key, fallback) => values.has(key) ? values.get(key) : fallback, set: (key, value) => values.set(key, value) }; const secrets = { has: key => secretValues.has(key), get: key => secretValues.get(key) || '', set: (key, value) => value ? secretValues.set(key, value) : secretValues.delete(key) }; const service = createRemotePlayService({ settings, secrets }); const server = http.createServer((req, res) => { const chunks = []; req.on('data', chunk => chunks.push(chunk)); req.on('end', () => { try { const body = JSON.parse(Buffer.concat(chunks).toString('utf8')); const result = service.handleWorkerRequest(req.url, body); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result)); } catch (error) { res.writeHead(error.statusCode || 400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } }); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); t.after(() => new Promise(resolve => server.close(resolve))); const port = server.address().port; const pairing = service.pairWorker({ label: 'Integration worker', serverOrigin: `http://127.0.0.1:${port}` }); const configPath = path.join(root, 'pairing.json'); fs.writeFileSync(configPath, JSON.stringify(pairing.config)); const isolatedEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => ![ 'localappdata', 'appdata', 'programfiles', 'programfiles(x86)', 'path', ].includes(key.toLowerCase()))); const workerEnv = { ...isolatedEnv, LOCALAPPDATA: path.join(root, 'local-app-data'), APPDATA: path.join(root, 'roaming-app-data'), ProgramFiles: path.join(root, 'program-files'), 'ProgramFiles(x86)': path.join(root, 'program-files-x86'), PATH: path.dirname(process.execPath), }; try { await promisify(execFile)(process.execPath, [path.resolve(__dirname, '../../remote-play-worker/worker.js'), '--config', configPath, '--once'], { timeout: 30_000, env: workerEnv, }); } catch (error) { if (error.code !== 'EPERM') throw error; const previousEnv = Object.fromEntries(Object.keys(workerEnv).map(key => [key, process.env[key]])); Object.assign(process.env, workerEnv); try { await require('../../remote-play-worker/worker').runOnce(configPath); } finally { for (const [key, value] of Object.entries(previousEnv)) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } } } const saved = JSON.parse(fs.readFileSync(configPath, 'utf8')); assert.match(saved.pairingKey, /^[a-f0-9]{64}$/); assert.equal(saved.enrollmentToken, undefined); const worker = service.status().workers[0]; assert.equal(worker.online, true); assert.equal(worker.version, '1.1.0'); assert.equal(typeof worker.client.installed, 'boolean'); }); test('Docker build context retains every Remote Play worker package template', () => { const root = path.resolve(__dirname, '../..'); const ignoreLines = fs.readFileSync(path.join(root, '.dockerignore'), 'utf8').split(/\r?\n/).map(line => line.trim()); const blanketPs1Ignore = ignoreLines.lastIndexOf('**/*.ps1'); assert.ok(blanketPs1Ignore >= 0); for (const filename of ['install.ps1', 'uninstall.ps1']) { const relative = `remote-play-worker/${filename}`; assert.ok(ignoreLines.lastIndexOf(`!${relative}`) > blanketPs1Ignore, `${relative} moet na de brede PS1-ignore opnieuw worden toegelaten`); assert.ok(fs.statSync(path.join(root, relative)).size > 0); } const dockerfile = fs.readFileSync(path.join(root, 'Dockerfile'), 'utf8'); assert.match(dockerfile, /COPY --chown=chimera:chimera remote-play-worker \.\/remote-play-worker/); });