Skip to content

Instantly share code, notes, and snippets.

@moeiscool
Last active April 3, 2025 07:00
Show Gist options
  • Save moeiscool/6204d56cb4ea09241a8b2028db3b64f1 to your computer and use it in GitHub Desktop.
Save moeiscool/6204d56cb4ea09241a8b2028db3b64f1 to your computer and use it in GitHub Desktop.

Revisions

  1. moeiscool revised this gist Apr 3, 2025. 1 changed file with 40 additions and 14 deletions.
    54 changes: 40 additions & 14 deletions server.js
    Original file line number Diff line number Diff line change
    @@ -8,7 +8,7 @@ const [HOST, PORT] = WS_HOST.split(':');
    const START_PORT = 9022;

    // Client tracking
    const clients = new Map(); // identifier -> { ws, port, lastActive }
    const clients = new Map(); // identifier -> { ws, port, server, socket, lastActive }
    const portAllocation = new Map(); // port -> identifier
    const PORT_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours

    @@ -56,39 +56,65 @@ wss.on('connection', (ws) => {
    for (const [identifier, client] of clients.entries()) {
    if (client.ws === ws) {
    console.log(`Client ${identifier} disconnected`);
    if (client.socket) client.socket.end();
    // Keep port reserved until timeout
    // Don't close the server, just mark WS as disconnected and close socket
    client.ws = null;
    if (client.socket) {
    client.socket.end();
    client.socket = null;
    }
    }
    }
    });
    });

    function registerClient(ws, identifier) {
    // Find available port
    // Check if this identifier already has a port assigned
    const existingClient = clients.get(identifier);

    if (existingClient) {
    // Reuse the existing port
    existingClient.ws = ws;
    existingClient.lastActive = Date.now();
    console.log(`[${identifier}] Reconnected, using existing port ${existingClient.port}`);

    ws.send(JSON.stringify({ type: 'registered', port: existingClient.port }));
    return;
    }

    // Find available port for new client
    let port = START_PORT;
    while (portAllocation.has(port)) port++;

    const server = net.createServer((socket) => {
    clients.get(identifier).socket = socket;
    console.log(`[${identifier}] New connection on port ${port}`);
    const client = clients.get(identifier);
    if (client) {
    client.socket = socket;
    console.log(`[${identifier}] New connection on port ${port}`);
    }

    socket.on('data', (data) => {
    ws.send(JSON.stringify({
    type: 'data',
    identifier,
    data: data.toString('base64')
    }));
    const client = clients.get(identifier);
    if (client && client.ws) {
    client.ws.send(JSON.stringify({
    type: 'data',
    identifier,
    data: data.toString('base64')
    }));
    }
    });

    socket.on('close', () => {
    clients.get(identifier).socket = null;
    const client = clients.get(identifier);
    if (client) {
    client.socket = null;
    }
    });
    });

    server.listen(port, '0.0.0.0', () => {
    clients.set(identifier, { ws, port, server, lastActive: Date.now() });
    clients.set(identifier, { ws, port, server, socket: null, lastActive: Date.now() });
    portAllocation.set(port, identifier);
    console.log(`[${identifier}] Assigned port ${port}`);
    ws.send(JSON.stringify({ type: 'registered', port }));
    });
    }
    }
  2. moeiscool created this gist Apr 3, 2025.
    28 changes: 28 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,28 @@
    ## Simple Remote SSH over Websocket Protocol with Node.js (Alpha)
    ### by Shinobi Systems
    #### Based on the Simple TCP Proxy by kfox at https://gist.github.com/kfox/2313683

    With this you spawn a websocket server on a server to allow SSH to a remote machine.
    When a client is connected it will be available on 9022 of the server. Every new client connected will be on port proceeding 9022.
    So if a server is already on 9022 the next one will be on 9023 and the next on 9024 and so on. Ports are released 24 hours after disconnection to be used again later.


    Server machine run :
    ```
    node server.js BIND_ADDRESS
    ```

    Example :
    ```
    node server.js 0.0.0.0:8090
    ```


    Client machine run :
    ```
    node client.js HOST IDENTIFIER
    ```
    Example :
    ```
    node client.js 127.0.0.1:8090 someserverid
    ```
    111 changes: 111 additions & 0 deletions client.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,111 @@
    const WebSocket = require('ws');
    const net = require('net');
    const process = require('process');

    // Configuration
    if (process.argv.length < 4) {
    console.log('Usage: node client.js <host:port> <identifier>');
    process.exit(1);
    }

    const WS_SERVER = process.argv[2];
    const IDENTIFIER = process.argv[3];
    const SSH_PORT = 22;
    const RECONNECT_INTERVAL = 10000;

    let ws = null;
    let sshSocket = null;
    let assignedPort = null;
    let reconnectTimer = null;

    function connectWebSocket() {
    if (ws) {
    ws.removeAllListeners();
    if (ws.readyState === ws.OPEN) ws.close();
    }

    console.log(`Connecting to ${WS_SERVER} as ${IDENTIFIER}...`);
    ws = new WebSocket(`ws://${WS_SERVER}`);

    ws.on('open', () => {
    console.log('Connected, registering...');
    ws.send(JSON.stringify({
    type: 'register',
    identifier: IDENTIFIER
    }));
    });

    ws.on('message', (message) => {
    const msg = JSON.parse(message);
    if (msg.type === 'registered') {
    assignedPort = msg.port;
    console.log(`Registered! Forwarding port ${assignedPort} to SSH`);
    }
    else if (msg.type === 'data') {
    if (!sshSocket) connectSSH();
    sshSocket.write(Buffer.from(msg.data, 'base64'));
    }
    });

    ws.on('close', () => {
    console.log('Disconnected from server');
    if (sshSocket) sshSocket.end();
    scheduleReconnect();
    });

    ws.on('error', (err) => {
    console.error('WebSocket error:', err);
    scheduleReconnect();
    });
    }

    function connectSSH() {
    if (sshSocket) sshSocket.end();

    sshSocket = new net.Socket();
    sshSocket.connect(SSH_PORT, '127.0.0.1', () => {
    console.log('Connected to SSH server');
    });

    sshSocket.on('data', (data) => {
    if (ws && ws.readyState === ws.OPEN) {
    ws.send(JSON.stringify({
    type: 'data',
    identifier: IDENTIFIER,
    data: data.toString('base64')
    }));
    }
    });

    sshSocket.on('error', (err) => {
    console.error('SSH error:', err);
    sshSocket = null;
    });

    sshSocket.on('close', () => {
    sshSocket = null;
    });
    }

    function scheduleReconnect() {
    if (!reconnectTimer) {
    console.log(`Reconnecting in ${RECONNECT_INTERVAL/1000} seconds...`);
    reconnectTimer = setTimeout(() => {
    reconnectTimer = null;
    connectWebSocket();
    }, RECONNECT_INTERVAL);
    }
    }

    // Cleanup
    function cleanup() {
    if (reconnectTimer) clearTimeout(reconnectTimer);
    if (ws) ws.close();
    if (sshSocket) sshSocket.end();
    }

    process.on('SIGINT', cleanup);
    process.on('SIGTERM', cleanup);

    // Start connection
    connectWebSocket();
    94 changes: 94 additions & 0 deletions server.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,94 @@
    const WebSocket = require('ws');
    const net = require('net');
    const process = require('process');

    // Configuration
    const WS_HOST = process.argv[2] || '127.0.0.1:8090';
    const [HOST, PORT] = WS_HOST.split(':');
    const START_PORT = 9022;

    // Client tracking
    const clients = new Map(); // identifier -> { ws, port, lastActive }
    const portAllocation = new Map(); // port -> identifier
    const PORT_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours

    const wss = new WebSocket.Server({ host: HOST, port: PORT });
    console.log(`WebSocket server running on ws://${HOST}:${PORT}`);

    // Cleanup old ports
    setInterval(() => {
    const now = Date.now();
    for (const [identifier, client] of clients.entries()) {
    if (now - client.lastActive > PORT_TIMEOUT) {
    console.log(`Releasing port ${client.port} from ${identifier} (inactive 24h)`);
    client.server.close();
    portAllocation.delete(client.port);
    clients.delete(identifier);
    }
    }
    }, 60 * 60 * 1000); // Check hourly

    wss.on('connection', (ws) => {
    ws.on('message', (message) => {
    try {
    const msg = JSON.parse(message);

    // Handle registration
    if (msg.type === 'register' && msg.identifier) {
    registerClient(ws, msg.identifier);
    return;
    }

    // Handle data forwarding
    const client = clients.get(msg.identifier);
    if (client && client.ws === ws) {
    client.lastActive = Date.now();
    if (msg.type === 'data' && client.socket) {
    client.socket.write(Buffer.from(msg.data, 'base64'));
    }
    }
    } catch (err) {
    console.error('Message processing error:', err);
    }
    });

    ws.on('close', () => {
    for (const [identifier, client] of clients.entries()) {
    if (client.ws === ws) {
    console.log(`Client ${identifier} disconnected`);
    if (client.socket) client.socket.end();
    // Keep port reserved until timeout
    }
    }
    });
    });

    function registerClient(ws, identifier) {
    // Find available port
    let port = START_PORT;
    while (portAllocation.has(port)) port++;

    const server = net.createServer((socket) => {
    clients.get(identifier).socket = socket;
    console.log(`[${identifier}] New connection on port ${port}`);

    socket.on('data', (data) => {
    ws.send(JSON.stringify({
    type: 'data',
    identifier,
    data: data.toString('base64')
    }));
    });

    socket.on('close', () => {
    clients.get(identifier).socket = null;
    });
    });

    server.listen(port, '0.0.0.0', () => {
    clients.set(identifier, { ws, port, server, lastActive: Date.now() });
    portAllocation.set(port, identifier);
    console.log(`[${identifier}] Assigned port ${port}`);
    ws.send(JSON.stringify({ type: 'registered', port }));
    });
    }