Initial curated chimera source from a8ae5b1db07fb49926c94585e2718e5130330974
This commit is contained in:
commit
c737a37f71
531 files changed
+119270
No files matched your search
@@ -0,0 +1,216 @@
|
||||
'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/);
|
||||
});
|
||||
Reference in new issue
Block a user