Files
ghaymah-dns-net-sec-tests/scripts/remote_exec.js
T

136 lines
4.0 KiB
JavaScript

const WSS_URL = 'wss://client-terminal-0f1fe89209a8.hosted.ghaymah.systems/ws';
class RemoteTerminal {
constructor() {
this.ws = null;
this.ready = false;
}
async connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(WSS_URL, ['tty']);
this.ws.onopen = () => {
this.ws.send(JSON.stringify({ AuthToken: '' }));
this.ws.send('1' + JSON.stringify({ columns: 200, rows: 50 }));
setTimeout(() => {
this.ready = true;
resolve();
}, 800);
};
this.ws.onerror = (e) => reject(new Error('WS error: ' + e.message));
});
}
async exec(command, timeoutMs = 10000) {
if (!this.ready) await this.connect();
return new Promise((resolve, reject) => {
const markerId = Math.random().toString(36).substring(2, 8);
let rawOutput = '';
let timer = null;
const messageHandler = async (event) => {
let text = '';
if (event.data instanceof Blob) {
text = await event.data.text();
} else if (typeof event.data === 'string') {
text = event.data;
} else {
text = Buffer.from(event.data).toString('utf8');
}
if (text.startsWith('0')) text = text.substring(1);
rawOutput += text;
// Match \n__RET__<code|marker>__END__ when printed by echo, NOT during command typing
const regex = new RegExp(`\r?\n__RET__(\\d+)_${markerId}__END__`);
const match = rawOutput.match(regex);
if (match) {
cleanup();
const clean = rawOutput
.replace(/[\x1b\x9b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '')
.replace(/\r/g, '');
const exitCode = parseInt(match[1]);
const lines = clean.split('\n');
// Find the line where the command was echoed
const cmdIdx = lines.findIndex(l => l.includes(markerId) && l.includes('echo'));
// Find the line where the result marker is
const retIdx = lines.findIndex(l => l.startsWith(`__RET__${exitCode}_${markerId}__END__`));
let outLines = [];
if (cmdIdx !== -1 && retIdx !== -1 && retIdx > cmdIdx) {
outLines = lines.slice(cmdIdx + 1, retIdx);
} else if (retIdx !== -1) {
outLines = lines.slice(0, retIdx);
} else {
outLines = lines;
}
// Filter out prompt lines
const filtered = outLines.filter(l => !l.endsWith('~#') && !l.endsWith('$') && !l.includes('bash ('));
resolve({
command,
output: filtered.join('\n').trim(),
exitCode,
raw: rawOutput
});
}
};
const cleanup = () => {
clearTimeout(timer);
this.ws.removeEventListener('message', messageHandler);
};
timer = setTimeout(() => {
cleanup();
resolve({
command,
output: rawOutput.replace(/[\x1b\x9b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '').trim(),
exitCode: -1,
timedOut: true
});
}, timeoutMs);
this.ws.addEventListener('message', messageHandler);
// Execute command and output return marker
const fullCmd = `${command}; echo __RET__$?_${markerId}__END__\n`;
this.ws.send('0' + fullCmd);
});
}
close() {
if (this.ws) {
try { this.ws.close(); } catch (e) {}
}
}
}
module.exports = { RemoteTerminal };
if (require.main === module) {
(async () => {
const term = new RemoteTerminal();
await term.connect();
console.log('Connected! Testing uname -a...');
const res1 = await term.exec('uname -a');
console.log('--- OUTPUT 1 ---');
console.log(res1.output);
console.log('Exit code:', res1.exitCode);
console.log('\nTesting cat /etc/resolv.conf...');
const res2 = await term.exec('cat /etc/resolv.conf');
console.log('--- OUTPUT 2 ---');
console.log(res2.output);
console.log('Exit code:', res2.exitCode);
term.close();
})().catch(console.error);
}