107 lines
6.2 KiB
JavaScript
107 lines
6.2 KiB
JavaScript
import { spawnSync } from 'node:child_process';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
|
|
const image = process.argv[2] ?? 'dsh-web:0.2.0-rc.2-1';
|
|
let interrupted = false;
|
|
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => { interrupted = true; });
|
|
function docker(args, input) {
|
|
const result = spawnSync('docker', args, { input, encoding: 'utf8', timeout: 20000, maxBuffer: 16 * 1024 * 1024 });
|
|
if (result.error || result.status !== 0) throw new Error(`Docker ${args[0]} failed (output suppressed)`);
|
|
return result.stdout + (args[0] === 'logs' ? result.stderr : '');
|
|
}
|
|
function check(condition, message) {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
async function probe(publicURL) {
|
|
const name = `dsh-runtime-probe-${randomUUID()}`;
|
|
let created = false;
|
|
try {
|
|
const id = docker(['image', 'inspect', '--format', '{{.Id}}', image]).trim();
|
|
docker(['create', '--name', name, '--pull', 'never', '--network', 'none',
|
|
'--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
|
|
'--user', '1000:1000', '--memory', '2g', '--cpus', '2', '--pids-limit', '256',
|
|
'--tmpfs', '/tmp:rw,exec,nosuid,nodev,size=256m',
|
|
'--tmpfs', '/data/dsh-home:rw,nosuid,nodev,uid=1000,gid=1000,size=512m',
|
|
'--tmpfs', '/workspace:rw,nosuid,nodev,uid=1000,gid=1000,size=64m', id,
|
|
'web', '--no-open', '--port', '3080',
|
|
...(publicURL ? ['--public-url', publicURL, '--trusted-host', new URL(publicURL).host] : [])]);
|
|
created = true;
|
|
docker(['start', name]);
|
|
let startupURL;
|
|
for (let attempt = 0; attempt < 60; attempt++) {
|
|
check(!interrupted, 'Interrupted');
|
|
const logs = docker(['logs', name]);
|
|
const match = logs.match(/dsh web:\s*(https?:\/\/[^\s\x1b]+[?&]token=[^\s\x1b]+)/);
|
|
if (match) { startupURL = new URL(match[1]); break; }
|
|
if (docker(['inspect', '--format', '{{.State.Running}}', name]).trim() !== 'true') {
|
|
if (logs.includes("unknown option '--public-url'")) console.log(JSON.stringify({mode: 'public-url', supported: false, reason: "unknown option '--public-url'"}));
|
|
throw new Error('Runtime exited before readiness');
|
|
}
|
|
await delay(1000);
|
|
}
|
|
check(startupURL?.searchParams.get('token'), 'No startup token found (logs suppressed)');
|
|
if (publicURL) check(startupURL.origin + startupURL.pathname === publicURL, 'Public startup URL differs');
|
|
const input = JSON.stringify({ token: startupURL.searchParams.get('token'), publicURL });
|
|
const source = String.raw`
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { token, publicURL } = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
const headers = publicURL ? {host: new URL(publicURL).host, 'x-forwarded-proto': 'https'} : {};
|
|
const request = (suffix, extra = {}) => fetch('http://127.0.0.1:8080' + suffix, {headers: {...headers, ...extra}, redirect: 'manual', signal: AbortSignal.timeout(10000)});
|
|
(async () => {
|
|
const anonymous = await request('/');
|
|
const exchange = await request('/?token=' + encodeURIComponent(token));
|
|
const cookies = exchange.headers.getSetCookie();
|
|
const cookie = cookies.map(value => value.split(';')[0]).join('; ');
|
|
const home = await request('/', {cookie});
|
|
const html = await home.text();
|
|
const scripts = [...html.matchAll(/<script[^>]*src=["']([^"']+)/g)].map(match => match[1]);
|
|
const evidence = [];
|
|
function scan(dir) {
|
|
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
|
const file = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) scan(file);
|
|
else if (/\.(js|mjs|html)$/.test(file) && fs.statSync(file).size < 20000000) {
|
|
const text = fs.readFileSync(file, 'utf8');
|
|
for (const needle of ['new WebSocket(', 'function remoteStreamUrl', 'REMOTE_STREAM_MUX_PATH =']) {
|
|
let index = text.indexOf(needle);
|
|
if (index >= 0) evidence.push({file, needle, line: text.slice(0,index).split('\n').length, context: text.slice(Math.max(0,index-180),index+260)});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
scan('/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-api-gateway/lib');
|
|
const result = {anonymousStatus: anonymous.status, exchangeStatus: exchange.status,
|
|
redirect: exchange.headers.get('location'), cookies: cookies.map(value => ({name: value.split('=')[0], attributes: value.split(';').slice(1).map(v => v.trim())})),
|
|
authenticatedStatus: home.status, htmlConfig: [...html.matchAll(/.{0,120}(?:WebSocket|websocket|endpoint|base href|wsPath).{0,240}/g)].map(m => m[0]), scripts, evidence};
|
|
// Never serialize raw credentials, even if an upstream response reflects them.
|
|
let output = JSON.stringify(result);
|
|
for (const secret of [token, ...cookies.map(value => value.split(';')[0].slice(value.indexOf('=')+1))]) if (secret) output = output.split(secret).join('[REDACTED]');
|
|
console.log(output);
|
|
})().catch(() => {console.error('In-container probe failed (details suppressed)'); process.exit(1);});
|
|
`;
|
|
const result = JSON.parse(docker(['exec', '-i', name, 'node', '-e', source], input));
|
|
console.log(JSON.stringify({mode: publicURL ? 'public-url' : 'default', startupURL: startupURL.origin + startupURL.pathname + '?token=[REDACTED]', ...result}, null, 2));
|
|
check(result.anonymousStatus === 401, 'Expected anonymous HTTP 401');
|
|
check(result.cookies.length > 0, 'Token exchange did not set cookie');
|
|
check(result.authenticatedStatus === 200, 'Authenticated homepage was not HTTP 200');
|
|
check(result.evidence.some(item => item.needle === 'REMOTE_STREAM_MUX_PATH ='), 'WebSocket path constant not found');
|
|
} finally {
|
|
if (created) {
|
|
docker(['rm', '-f', name]);
|
|
console.log(JSON.stringify({cleanup: 'removed', container: name}));
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
await probe();
|
|
// Optional capability check: current npm rc.2 intentionally fails this mode.
|
|
if (process.argv.includes('--public-url')) await probe('https://probe.invalid/dsh/probe/');
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
}
|