Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Http server 'upgrade' event does not perform the basic methods of websocket connection #47386

Closed
tghpereira opened this issue Apr 3, 2023 · 1 comment
Labels
duplicate Issues and PRs that are duplicates of other issues or PRs. feature request Issues that request new features to be added to Node.js.

Comments

@tghpereira
Copy link

What is the problem this feature will solve?

Recently I've been modifying a websocket application in nodejs that previously used 'socket.io', such packages have their place in the market and are of great help for small and medium-sized applications.
The big problem is when these applications scale to large projects and we need to refine and have more control over our websockets connections and in these cases
we need to implement all basic websocket methods from handshake to decode and endocode message frames, ping and pong events, etc...

For example as the basic structure of the webscoket followed by an implementation on the http server below written with typescript

Create basic helpers websocket
> touch src/base.ts
import { createHash } from 'node:crypto';

const opcodes = { text: 0x01, close: 0x08 };

const handshake = (key: string): string => {
  return createHash('sha1')
    .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary')
    .digest('base64');
};

const unmask = (payload: Buffer, key: number): Buffer => {
  const result = Buffer.alloc(payload.byteLength);

  for (let i = 0; i < payload.byteLength; ++i) {
    const j = i % 4;
    const maskingKeyByteShift = j === 3 ? 0 : (3 - j) << 3;
    const maskingKeyByte = (maskingKeyByteShift === 0 ? key : key >>> maskingKeyByteShift) & 0b11111111;
    const transformedByte = maskingKeyByte ^ payload.readUInt8(i);
    result.writeUInt8(transformedByte, i);
  }

  return result;
};

const readFrame = (buffer: Buffer): string | null | undefined => {
  const firstByte = buffer.readUInt8(0);
  const opCode = firstByte & 0b00001111; // get last 4 bits of a byte

  if (opCode === opcodes.close) {
    return null;
  } else if (opCode !== opcodes.text) {
    return;
  }

  const secondByte = buffer.readUInt8(1); // start with a payload length

  let offset = 2;
  let payloadLength = secondByte & 0b01111111; // get last 7 bits of a second byte

  if (payloadLength === 126) {
    offset += 2;
  } else if (payloadLength === 127) {
    offset += 8;
  }

  const isMasked = Boolean((secondByte >>> 7) & 0x1); // get first bit of a second byte

  if (isMasked) {
    const maskingKey = buffer.readUInt32BE(offset); // read 4-byte mask
    offset += 4;
    const payload = buffer.subarray(offset);
    const result = unmask(payload, maskingKey);
    return result.toString('utf-8');
  }

  return buffer.subarray(offset).toString('utf-8');
};

const createFrame = (data: string) => {
  const payloadByteLength = Buffer.byteLength(data);
  let payloadBytesOffset = 2;
  let payloadLength = payloadByteLength;

  if (payloadByteLength > 65535) {
    // length value cannot fit in 2 bytes
    payloadBytesOffset += 8;
    payloadLength = 127;
  } else if (payloadByteLength > 125) {
    payloadBytesOffset += 2;
    payloadLength = 126;
  }

  const buffer = Buffer.alloc(payloadBytesOffset + payloadByteLength);

  // first byte
  buffer.writeUInt8(0b10000001, 0); // [FIN (1), RSV1 (0), RSV2 (0), RSV3 (0), Opode (0x01 - text frame)]

  buffer[1] = payloadLength; // second byte - actual payload size (if <= 125 bytes) or 126, or 127

  if (payloadLength === 126) {
    // write actual payload length as a 16-bit unsigned integer
    buffer.writeUInt16BE(payloadByteLength, 2);
  } else if (payloadByteLength === 127) {
    // write actual payload length as a 64-bit unsigned integer
    buffer.writeBigUInt64BE(BigInt(payloadByteLength), 2);
  }

  buffer.write(data, payloadBytesOffset);
  return buffer;
};

export { handshake, readFrame, createFrame };
Create basic api websocket
> touch src/app.ts
import internal from 'node:stream';
import { createServer } from 'node:http';
import { createFrame, handshake, readFrame } from './base';

const clients = new Map<string, internal.Duplex>();

const server = createServer();

server.on('upgrade', (request, socket) => {
  if (request.headers['upgrade']?.toLowerCase() === 'websocket' && request.headers['connection']?.toLowerCase().includes('upgrade')) {
    return socket.destroy();
  }

  const { 'sec-websocket-key': key } = request.headers;
  if (!key) return socket.destroy();

  const acceptedConnection = handshake(key);
  const responseHeaders = ['HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${acceptedConnection}`];

  socket.write(responseHeaders.concat('\r\n').join('\r\n'));
  clients.set(acceptedConnection, socket);

  socket.on('data', chunk => {
    const value = readFrame(chunk);
    if (value) console.log(value);
  });

  socket.on('error', error => {
    console.error(error);
  });

  socket.on('close', () => {
    clients.delete(acceptedConnection);
    console.info('close connection', acceptedConnection, clients.size);
  });
});

server.listen(3333, '0.0.0.0', () => console.log('server running'));

server.on('close', () => {
  for (const socket of clients.values()) {
    socket.destroy();
  }
  clients.clear();
});

function broadcastSender(message: string) {
  if (clients.size > 0) {
    for (const socket of clients.values()) {
      socket.write(createFrame(message));
    }
  }
}

setInterval(() => {
  broadcastSender('hello websocket');
}, 20000);

What is the feature you are proposing to solve the problem?

What is proposed is to natively bring the methods to work with websocket in http
This will help a lot and save time for us developers, it is very exhausting and discouraging to work with websocket in nodejs due to all the work mentioned above. This native implementation for websockets will help those who don't have such in-depth knowledge about networks and protocols and allow less dependency on packages with 'socket.io' and 'ws'

What alternatives have you considered?

There is not so clear nodejs documentation on how to work with websocket connection in http 'upgrade' event

@tghpereira tghpereira added the feature request Issues that request new features to be added to Node.js. label Apr 3, 2023
@mscdex
Copy link
Contributor

mscdex commented Apr 3, 2023

Duplicate of #19308.

@mscdex mscdex closed this as completed Apr 3, 2023
@mscdex mscdex added the duplicate Issues and PRs that are duplicates of other issues or PRs. label Apr 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
duplicate Issues and PRs that are duplicates of other issues or PRs. feature request Issues that request new features to be added to Node.js.
Projects
None yet
Development

No branches or pull requests

2 participants