Hammer1279 d246dda269
Some checks failed
Node.js CI / build (x64, linux) (push) Has been cancelled
Node.js CI / build (x64, win) (push) Has been cancelled
fix missing prompt on debug command
2025-02-27 18:35:43 +01:00

484 lines
19 KiB
JavaScript

// rework to use chunkermaster
const fs = require("fs");
const path = require("path");
const { createConnection } = require('net');
const { createHash, createECDH, createCipheriv, createDecipheriv, randomBytes } = require('crypto');
const { Transform } = require('stream');
// Application settings
const port = process.argv[3] || 23;
const hostname = process.argv[2] || (process.pkg ? "dom.ht-dev.de" : "localhost");
let delayMs = process.argv[4] || 100; // Command delay in milliseconds
const encryptionDelay = 200; // additional delay for encryption
const initializationDelay = 500; // additional delay for initialization
const advancedFeatures = true; // Enable advanced features
const printIO = false; // Print input/output data
class MemoryStream extends Transform {
constructor(options = {}) {
super(options);
}
_transform(chunk, encoding, callback) {
this.push(chunk);
callback();
}
}
/**
* prepare the session and close handler when done
* @param {Buffer} data
* @returns {Buffer[]} chunks
*/
function chunkData(data) {
// Split data into chunks based on IAC
const chunks = [];
let currentChunk = Buffer.alloc(0);
for (let i = 0; i < data.length; i++) {
if (data[i] === IAC[0]) {
if (currentChunk.length > 0) {
chunks.push(currentChunk);
}
// Find the end of the IAC command
let cmdLength = 3; // Default IAC command length
if (data[i + 1] === SB[0]) {
// Find SE to end subnegotiation
const seIndex = data.indexOf(SE[0], i);
cmdLength = seIndex - i + 1;
}
chunks.push(data.subarray(i, i + cmdLength));
i += cmdLength - 1;
currentChunk = Buffer.alloc(0);
} else {
currentChunk = Buffer.concat([currentChunk, Buffer.from([data[i]])]);
}
}
if (currentChunk.length > 0) {
chunks.push(currentChunk);
}
return chunks;
}
const writer = new MemoryStream();
let initialized = false; // Initialization status, do not modify directly, runtime only
let handoffInit = false; // Handoff initialization status to server, do not modify directly, runtime only
let encrypted = false; // Encryption status, do not modify directly, runtime only
let privateKey; // Private key, do not modify directly, runtime only
let keyCurve; // Key curve, do not modify directly, runtime only
let encryptionAlgorithm; // Encryption algorithm, do not modify directly, runtime only
let echdKey; // ECDH key, do not modify directly, runtime only
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const send = async (command) => {
await delay(delayMs);
writer.write(command); // Command
await delay(delayMs);
writer.write("\r\n"); // Line Feed
};
// Numeric Buffer values
const NUL = Buffer.from([0x00]); // Null
const ONE = Buffer.from([0x01]); // One
// Telnet commands
const IAC = Buffer.from([0xff]); // Interpret As Command
const DONT = Buffer.from([0xfe]); // Don't
const DO = Buffer.from([0xfd]); // Do
const WONT = Buffer.from([0xfc]); // Won't
const WILL = Buffer.from([0xfb]); // Will
const SB = Buffer.from([0xfa]); // Subnegotiation Begin
const GA = Buffer.from([0xf9]); // Go Ahead
const EL = Buffer.from([0xf8]); // Erase Line
const EC = Buffer.from([0xf7]); // Erase Character
const AYT = Buffer.from([0xf6]); // Are You There
const AO = Buffer.from([0xf5]); // Abort Output
const IP = Buffer.from([0xf4]); // Interrupt Process
const BRK = Buffer.from([0xf3]); // Break
const DM = Buffer.from([0xf2]); // Data Mark
const NOP = Buffer.from([0xf1]); // No Operation
const SE = Buffer.from([0xf0]); // Subnegotiation End
// Common Telnet options or features
const ECHO = Buffer.from([0x01]); // Echo
const SUPPRESS_GO_AHEAD = Buffer.from([0x03]); // Suppress Go Ahead
const STATUS = Buffer.from([0x05]); // Status
const TIMING_MARK = Buffer.from([0x06]); // Timing Mark
const TERMINAL_TYPE = Buffer.from([0x18]); // Terminal Type
const NAWS = Buffer.from([0x1f]); // Negotiate About Window Size
const TERMINAL_SPEED = Buffer.from([0x20]); // Terminal Speed
const REMOTE_FLOW_CONTROL = Buffer.from([0x21]); // Remote Flow Control
const LINEMODE = Buffer.from([0x22]); // Line Mode
const ENVIRONMENT_VARIABLES = Buffer.from([0x24]); // Environment Variables
// custom codes for own client
const CUSTOM_CLIENT_INIT = Buffer.from([0x80]); // Custom Client Initialization
const KEY_EXCHANGE = Buffer.from([0x81]); // Key Exchange
const ENCRYPTION = Buffer.from([0x82]); // Encryption
const AUTHENTICATION = Buffer.from([0x83]); // Authentication
// 0x84 reserved for future use
const FILE_TRANSFER = Buffer.from([0x85]); // File Transfer
const PAUSE = Buffer.from([0x86]); // Pause
const RESUME = Buffer.from([0x87]); // Resume
const START_CONTENT = Buffer.from([0x88]); // Start Content
const END_CONTENT = Buffer.from([0x89]); // End Content
// ANSI escape codes for colors
const ANSI = {
// Reset
reset: '\x1b[0m',
// Basic foreground colors
black: '\x1b[30m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
// Bright foreground colors
brightBlack: '\x1b[90m',
brightRed: '\x1b[91m',
brightGreen: '\x1b[92m',
brightYellow: '\x1b[93m',
brightBlue: '\x1b[94m',
brightMagenta: '\x1b[95m',
brightCyan: '\x1b[96m',
brightWhite: '\x1b[97m',
// Basic background colors
bgBlack: '\x1b[40m',
bgRed: '\x1b[41m',
bgGreen: '\x1b[42m',
bgYellow: '\x1b[43m',
bgBlue: '\x1b[44m',
bgMagenta: '\x1b[45m',
bgCyan: '\x1b[46m',
bgWhite: '\x1b[47m'
};
// Encrypt
function encrypt(plaintext, key = privateKey, encoding = 'utf8') {
// Generate a random IV (12 bytes recommended for GCM)
const iv = randomBytes(12);
// Create cipher, providing algo, key, and IV
const cipher = createCipheriv(encryptionAlgorithm, key, iv);
// Encrypt data
const encrypted = Buffer.concat([cipher.update(plaintext, encoding), cipher.final()]);
const authTag = cipher.getAuthTag(); // Integrity tag
// Return IV + tag + ciphertext
return Buffer.concat([iv, authTag, encrypted]);
}
// Decrypt
function decrypt(ciphertext, key = privateKey, encoding = 'utf8') {
// Extract IV (first 12 bytes)
const iv = ciphertext.slice(0, 12);
// Extract tag (next 16 bytes)
const tag = ciphertext.slice(12, 28);
// Remainder is the actual encrypted data
const data = ciphertext.slice(28);
// Create decipher, set auth tag
const decipher = createDecipheriv(encryptionAlgorithm, key, iv);
decipher.setAuthTag(tag);
// Decrypt data
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
return decrypted.toString(encoding);
}
// Set the path of the project folder base on whether it is run with nodejs or as an executable
let project_folder;
if (process.pkg) {
// It is run as an executable
project_folder = path.dirname(process.execPath)
} else {
// It is run with nodejs
project_folder = __dirname
}
if (process.pkg) {
console.debug = () => { }; // Disable debug logging when run as an executable
fs.writeFileSync(path.join(project_folder, "README.md"), fs.readFileSync(path.join(__dirname, "README.md"))); // copy newest readme to folder
fs.writeFileSync(path.join(project_folder, "LICENSE"), fs.readFileSync(path.join(__dirname, "LICENSE"))); // copy newest license to folder
}
const socket = createConnection(port, hostname);
writer.pipe(socket);
let hold = false; // Hold input
let holdBuffer = ""; // Buffer data when on hold
process.stdin.setEncoding("ascii");
process.stdin.setRawMode(true);
process.stdin.resume();
// format keys before sending to server
process.stdin.on('data', async (key) => {
if (key === '\u0004') { // Ctrl+D
socket.end(); // End connection
socket.destroy(); // Destroy socket
process.exit(); // Exit process
} else if (key === '\u0003') { // Ctrl+C
send(IP); // Interrupt Process
} else if (key === '\u0018') { // Ctrl+X
process.stdout.write("\r");
console.log("Entered client command mode");
hold = true; // Hold input
process.stdin.setRawMode(false);
// Add any additional functionality for Ctrl+X here
}
// Log Unicode values of input for debugging
// console.debug("Key pressed:", Array.from(key).map(b => '\\u' + b.toString(16).padStart(4, '0')));
if (key == "\r") {
// Enter key
socket.write("\r\n");
} else if (!hold) {
socket.write(key);
} else {
// console.debug("Input on hold");
const command = key.replace(/\r?\n|\r/g, ''); // Remove new line characters
if (["exit", "quit"].includes(command)) {
hold = false; // Release input
process.stdin.setRawMode(true);
console.log("Exited client command mode");
process.stdout.write(holdBuffer);
holdBuffer = "";
process.stdout.write("> ");
} else if (command == "disconnect") {
send(IP); // Interrupt Process
socket.end(); // End connection
} else if (command == "status") {
console.log("Client status:");
console.log("Host:", hostname + ":" + port);
console.log("Advanced Features unlocked:", advancedFeatures);
console.log("Initialized:", initialized);
console.log("Encrypted:", encrypted);
console.log("Batch Delay:", delayMs + "ms");
// console.log("Public Key:", key.getPublicKey() ? key.getPublicKey().toString("hex") : "null");
// console.log("Private Key:", privateKey ? privateKey.toString("hex") : "null");
process.stdout.write("$ ");
} else if (command == "debug") {
socket.write(Buffer.concat([IAC, DO, STATUS])); // Debug request Status
setTimeout(() => {
process.stdout.write("$ ");
}, delayMs);
} else if (command == "keyinfo") {
console.log("Key Information:");
console.log("Key Curve:", keyCurve);
console.log("Encryption Algorithm:", encryptionAlgorithm);
console.log("Public Key:", echdKey.getPublicKey() ? echdKey.getPublicKey().toString("hex") : "null");
console.log("Private Key:", privateKey ? privateKey.toString("hex") : "null");
process.stdout.write("$ ");
} else if (command == "help") {
// Add any additional functionality for client side commands
console.log("Client commands:");
console.log("status - Show client status");
console.log("debug - [DEV] Request server status");
console.log("keyinfo - Show key information");
console.log("disconnect - Disconnect from server");
console.log("exit - Exit client command mode");
process.stdout.write("$ ");
} else {
process.stdout.write("$ ");
}
}
});
socket.on("data", (data) => {
const chunks = chunkData(data);
for (const chunk of chunks) {
if (printIO) {
console.debug("Received chunk:", chunk.toString("hex"));
}
if (!encrypted || !initialized) {
if (chunk.equals(PAUSE)) {
process.stdin.pause();
} else if (chunk.equals(RESUME)) {
process.stdin.resume();
} else if (chunk.equals(IP)) {
process.exit();
} else if (chunk.includes(Buffer.concat([IAC, SB, CUSTOM_CLIENT_INIT, NUL]))) {
const offsetBegin = chunk.indexOf(SB) + 2;
const offsetEnd = chunk.lastIndexOf(SE) - 1;
const algorithmData = chunk.subarray(offsetBegin + 1, offsetEnd);
keyCurve = algorithmData.toString('utf8');
echdKey = createECDH(keyCurve);
console.debug("Using key curve:", keyCurve);
} else if (chunk.equals(Buffer.concat([IAC, DO, CUSTOM_CLIENT_INIT]))) {
// server supports custom client features
socket.write(Buffer.concat([IAC, WILL, KEY_EXCHANGE])); // Start Key Exchange
} else if (chunk.includes(Buffer.concat([IAC, DO, KEY_EXCHANGE]))) {
// generate keys
const publicKey = echdKey.generateKeys();
console.debug("Generated Key: " + publicKey.toString("hex"));
} else if (chunk.equals(Buffer.concat([IAC, SB, KEY_EXCHANGE, ONE /* value required */, IAC, SE]))) {
socket.write(Buffer.concat([IAC, SB, KEY_EXCHANGE, NUL, echdKey.getPublicKey(), IAC, SE]));
// send key to server
} else if (chunk.includes(Buffer.concat([IAC, SB, KEY_EXCHANGE, NUL /* value provided */]))) {
// server sent its key, generate secret
console.debug("Key exchange received");
const offsetBegin = chunk.indexOf(SB) + 2;
const offsetEnd = chunk.lastIndexOf(SE) - 1;
const keyData = chunk.subarray(offsetBegin + 1, offsetEnd); // client public key
console.log("Extracted key:", keyData.toString("hex"));
privateKey = echdKey.computeSecret(keyData);
socket.write(Buffer.concat([IAC, WILL, ENCRYPTION])); // Enable Encryption
} else if (chunk.includes(Buffer.concat([IAC, SB, STATUS, NUL /* value provided */]))) {
// server sent status
const offsetBegin = chunk.indexOf(SB) + 2;
const offsetEnd = chunk.lastIndexOf(SE) - 1;
const statusData = chunk.subarray(offsetBegin + 1, offsetEnd);
console.debug("Server status:", require("util").inspect(JSON.parse(statusData.toString('utf8'))));
} else if (chunk.includes(Buffer.concat([IAC, SB, ENCRYPTION, NUL]))) {
const offsetBegin = chunk.indexOf(SB) + 2;
const offsetEnd = chunk.lastIndexOf(SE) - 1;
const algorithmData = chunk.subarray(offsetBegin + 1, offsetEnd);
encryptionAlgorithm = algorithmData.toString('utf8');
console.debug("Using encryption algorithm:", encryptionAlgorithm);
} else if (chunk.equals(Buffer.concat([IAC, DO, ENCRYPTION]))) {
// enable encryption
encrypted = true;
console.debug("Encryption enabled");
console.debug("Private Key: " + privateKey.toString("hex"));
} else if (chunk.equals(Buffer.concat([IAC, DO, Buffer.from([0x80])]))) {
handoffInit = true;
console.debug("Handoff initialization complete");
} else if (chunk.equals(Buffer.concat([IAC, DO, Buffer.from([0x80])]))) { // Initialize
socket.write(Buffer.concat([IAC, WILL, Buffer.from([0x80])]));
initialized = true;
console.debug("Initialization complete");
} else if (!hold) {
process.stdout.write(chunk);
} else {
// console.debug("Data on hold");
holdBuffer += chunk.toString();
}
} else {
// decrypt data
try {
const decryptedData = decrypt(chunk, privateKey);
process.stdout.write(decryptedData);
} catch (error) {
console.error("Decryption error:", error);
console.error("Data:", chunk.toString("hex"));
console.debug("Algorithm:", encryptionAlgorithm);
console.debug("Private Key:", privateKey.toString("hex"));
console.debug("Public Key:", echdKey.getPublicKey().toString("hex"));
}
}
}
});
socket.on("connect", async () => {
console.log("Connected to server");
process.stdin.pause(); // Pause input
// initialization
const columns = process.stdout.columns || 80;
const rows = process.stdout.rows || 24;
// Create buffer for window size: [columns-high, columns-low, rows-high, rows-low]
const windowSize = Buffer.from([
(columns >> 8) & 0xFF, // high byte of columns
columns & 0xFF, // low byte of columns
(rows >> 8) & 0xFF, // high byte of rows
rows & 0xFF // low byte of rows
]);
socket.write(Buffer.concat([IAC, WILL, NAWS, SB, NAWS, windowSize, SE])); // Negotiate About Window Size
await delay(delayMs); // Wait for server to process
socket.write(Buffer.concat([IAC, WILL, TERMINAL_TYPE, IAC, SB, TERMINAL_TYPE, NUL, Buffer.from("xterm-256color"), IAC, SE])); // Terminal Type
await delay(delayMs);
socket.write(Buffer.concat([IAC, DO, ECHO])); // Tell server to echo input
await delay(delayMs);
if (advancedFeatures) {
socket.write(Buffer.concat([IAC, WILL, CUSTOM_CLIENT_INIT])); // Custom Client Initialization
await delay(delayMs);
}
if (encrypted) {
// increase delay for encryption
delayMs += encryptionDelay;
}
await delay(initializationDelay); // Wait for server to process
await send(""); // Send empty command to initialize connection
if (!handoffInit) {
initialized = true; // Initialization is now handled by server
console.debug("Initialization complete");
}
// socket.write(Buffer.from([0x0d, 0x0a])); // Line Feed
// from here on encryption is enabled, do not use socket.write() directly anymore
// initialization complete
await send("help");
await delay(delayMs);
process.stdout.write("\r" + ANSI.bgRed + ANSI.white + "DEPRECATION NOTICE:" + ANSI.reset + " This is an unsupported version and might not work fully." + "\r\n");
process.stdout.write("\rCtrl+X for client side commands\r\nCtrl+C to exit, Ctrl+D to force close\r\n> ");
process.stdin.resume(); // Resume input
// more commands can be added here
// console.log("Test:", decrypt(Buffer.from("eaffd8d028120ac5b1e8634438c522a91ca5bd9cbda1c5ac88321cd6d743b185b6e43cec1c1d6b1b1ad5fc623012582e666a0d1b83f7656dbdd2a8609e0ec9f1e6123ebb442455316a4bfe883d46", "hex"), privateKey))
if (fs.existsSync("batchrun.txt")) {
const batchCommands = fs.readFileSync("batchrun.txt", "utf-8").split("\n");
for (const command of batchCommands) {
if (command.trim()) {
await send(command.trim());
await delay(delayMs);
}
}
}
});
socket.on("end", () => {
console.log("\nDisconnected from server");
process.exit();
});
process.on('uncaughtException', (err) => {
if (err.code === 'ECONNRESET') {
console.warn("\nDisconnected from server");
process.exit();
} else if (err.code === 'ECONNREFUSED') {
console.warn("Connection refused");
process.exit();
} else if (err.code === 'ETIMEDOUT') {
console.warn("Connection timed out");
process.exit();
} else if (err.code === 'EHOSTUNREACH') {
console.warn("Host is unreachable");
process.exit();
} else if (err.code === 'ENETUNREACH') {
console.warn("Network is unreachable");
process.exit();
} else {
console.error('Unhandled exception:', err);
process.exit(1);
}
});
module.exports = {
delay,
send,
project_folder
};