136 lines
4.0 KiB
JavaScript
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);
|
|
}
|