diff --git a/benchmark/http/websocket-mask.js b/benchmark/http/websocket-mask.js new file mode 100644 index 000000000000..68af6541bbd5 --- /dev/null +++ b/benchmark/http/websocket-mask.js @@ -0,0 +1,56 @@ +'use strict'; + +const common = require('../common.js'); +const { websocketMask, websocketUnmask } = require('node:http'); + +const bench = common.createBenchmark(main, { + type: ['mask', 'unmask'], + // 'http' uses http.websocketMask() / http.websocketUnmask(). 'js' is the + // byte-by-byte loop that userland WebSocket implementations fall back to + // without a native addon, for comparison. + impl: ['http', 'js'], + len: [4, 16, 125, 1024, 16384, 65536], + n: [1e6], +}); + +function jsMask(source, key, output, offset, length) { + for (let i = 0; i < length; i++) { + output[offset + i] = source[i] ^ key[i & 3]; + } +} + +function jsUnmask(buffer, key) { + for (let i = 0; i < buffer.length; i++) { + buffer[i] ^= key[i & 3]; + } +} + +function main({ n, type, impl, len }) { + const key = Buffer.from([0x12, 0x34, 0x56, 0x78]); + const source = Buffer.alloc(len, 'abcdefg'); + // Leave room for a WebSocket frame header before the payload. + const output = Buffer.alloc(len + 14); + + switch (type) { + case 'mask': { + const fn = impl === 'http' ? websocketMask : jsMask; + bench.start(); + for (let i = 0; i < n; i++) { + fn(source, key, output, 14, len); + } + bench.end(n); + break; + } + case 'unmask': { + const fn = impl === 'http' ? websocketUnmask : jsUnmask; + bench.start(); + for (let i = 0; i < n; i++) { + fn(source, key); + } + bench.end(n); + break; + } + default: + throw new Error(`Unexpected type: ${type}`); + } +} diff --git a/doc/api/http.md b/doc/api/http.md index 5351b05eeaa9..88f563a89b34 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -4637,6 +4637,94 @@ requests are made and avoid invoking it in the middle of any requests. See [Built-in Proxy Support][] for details on proxy URL formats and `NO_PROXY` syntax. +## `http.websocketMask(source, mask, output[, offset[, length]])` + + + +* `source` {Buffer|TypedArray|DataView} The data to mask. +* `mask` {Buffer|TypedArray|DataView} The 4-byte masking key. +* `output` {Buffer|TypedArray|DataView} Where to write the masked data. +* `offset` {integer} Byte offset in `output` at which to start writing. + **Default:** `0`. +* `length` {integer} Number of bytes of `source` to mask. + **Default:** `source.byteLength`. + +XORs the first `length` bytes of `source` with `mask`, repeated, and writes the +result to `output` starting at `offset`: byte `i` of `source` is XORed with +byte `i % 4` of `mask`. This is the masking operation that WebSocket clients +apply to every frame payload they send ([RFC 6455, Section 5.3][]), and that +servers undo on every frame they receive. Applying it twice with the same +`mask` restores the original data. `source` is not modified, unless it shares +memory with `output`. + +All arguments are treated as raw bytes, whatever the view type. `source` and +`output` may be the same view or overlapping views over the same memory; the +result is the same as if `source` had been copied first. + +An error is thrown if `mask` is not exactly 4 bytes long, if `length` is +greater than `source.byteLength`, if `offset + length` is greater than +`output.byteLength`, or if `output` is backed by an immutable `ArrayBuffer`. +Nothing is written in that case. `source` and `mask` may be backed by an +immutable `ArrayBuffer`. + +```mjs +import { Buffer } from 'node:buffer'; +import { websocketMask, websocketUnmask } from 'node:http'; + +const key = Buffer.from([0x37, 0xfa, 0x21, 0x3d]); +const payload = Buffer.from('Hello'); + +// Write a masked copy of the payload after a 6-byte frame header. +const frame = Buffer.alloc(6 + payload.length); +websocketMask(payload, key, frame, 6); +console.log(frame.subarray(6)); +// Prints: + +const received = frame.subarray(6); +websocketUnmask(received, key); +console.log(received.toString()); +// Prints: Hello +``` + +```cjs +const { Buffer } = require('node:buffer'); +const { websocketMask, websocketUnmask } = require('node:http'); + +const key = Buffer.from([0x37, 0xfa, 0x21, 0x3d]); +const payload = Buffer.from('Hello'); + +// Write a masked copy of the payload after a 6-byte frame header. +const frame = Buffer.alloc(6 + payload.length); +websocketMask(payload, key, frame, 6); +console.log(frame.subarray(6)); +// Prints: + +const received = frame.subarray(6); +websocketUnmask(received, key); +console.log(received.toString()); +// Prints: Hello +``` + +## `http.websocketUnmask(buffer, mask)` + + + +* `buffer` {Buffer|TypedArray|DataView} The data to unmask, in place. +* `mask` {Buffer|TypedArray|DataView} The 4-byte masking key. + +XORs every byte of `buffer` with `mask`, repeated, in place: byte `i` is XORed +with byte `i % 4` of `mask`. This is equivalent to +`http.websocketMask(buffer, mask, buffer)`, and is typically used to unmask a +received WebSocket frame payload ([RFC 6455, Section 5.3][]). See +[`http.websocketMask()`][] for an example. + +An error is thrown, and nothing is written, if `mask` is not exactly 4 bytes +long or if `buffer` is backed by an immutable `ArrayBuffer`. + ## Class: `WebSocket`