arblant
panel-gateway / test / echo-server.mjs
echo-server.mjs62 lines · 3.0 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
/* Minimal WebSocket echo server (node:net) for integration tests.
   Verifies the first message's VLESS UUID == expected, then echoes everything. */
import { createServer } from 'node:net';
import { createHash } from 'node:crypto';

const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; // RFC 6455 §1.3

function sendFrame(socket, opcode, payload) {
  const len = payload.length;
  let head;
  if (len < 126) head = Buffer.from([0x80 | opcode, len]);
  else if (len < 65536) { head = Buffer.alloc(4); head[0] = 0x80 | opcode; head[1] = 126; head.writeUInt16BE(len, 2); }
  else { head = Buffer.alloc(10); head[0] = 0x80 | opcode; head[1] = 127; head.writeBigUInt64BE(BigInt(len), 2); }
  socket.write(Buffer.concat([head, Buffer.from(payload)]));
}

export function startEchoServer(port, expectedUuidBytes, onEvent) {
  const expected = Buffer.from(expectedUuidBytes);
  const server = createServer((socket) => {
    let buf = Buffer.alloc(0), handshaked = false, firstMsg = true;
    socket.on('data', (chunk) => {
      buf = Buffer.concat([buf, chunk]);
      if (!handshaked) {
        const i = buf.indexOf('\r\n\r\n');
        if (i < 0) return;
        const head = buf.slice(0, i).toString('latin1');
        buf = buf.slice(i + 4);
        const m = /sec-websocket-key:\s*(.+)/i.exec(head);
        if (!/upgrade:\s*websocket/i.test(head) || !m) { socket.destroy(); return; }
        const accept = createHash('sha1').update(m[1].trim() + GUID).digest('base64');
        socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ' + accept + '\r\n\r\n');
        handshaked = true;
      }
      for (;;) {
        if (buf.length < 2) return;
        const b0 = buf[0], b1 = buf[1], opcode = b0 & 0x0f, masked = (b1 & 0x80) !== 0;
        let len = b1 & 0x7f, off = 2;
        if (len === 126) { if (buf.length < 4) return; len = buf.readUInt16BE(2); off = 4; }
        else if (len === 127) { if (buf.length < 10) return; len = Number(buf.readBigUInt64BE(2)); off = 10; }
        if (!masked) { socket.destroy(); return; }
        if (buf.length < off + 4 + len) return;
        const mask = buf.subarray(off, off + 4);
        const payload = buf.subarray(off + 4, off + 4 + len);
        buf = buf.slice(off + 4 + len);
        for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i % 4];
        if (opcode === 0x8) { sendFrame(socket, 0x8, payload); socket.end(); return; }
        if (opcode === 0x9) { sendFrame(socket, 0xA, payload); continue; }
        if (opcode === 0x1 || opcode === 0x2) {
          if (firstMsg) {
            firstMsg = false;
            const ok = payload.length >= 17 && Buffer.from(payload.subarray(1, 17)).equals(expected);
            onEvent && onEvent({ type: 'header', ok, len: payload.length });
            if (!ok) { socket.destroy(); return; }
          }
          sendFrame(socket, 0x2, payload);
        }
      }
    });
    socket.on('error', () => {});
  });
  return new Promise((res) => server.listen(port, '127.0.0.1', () => res(server)));
}