'use strict' ;
const { Writable } = require('stream' );
const PerMessageDeflate = require('./permessage-deflate' );
const {
BINARY_TYPES,
EMPTY_BUFFER,
kStatusCode,
kWebSocket
} = require('./constants' );
const { concat, toArrayBuffer, unmask } = require('./buffer-util' );
const { isValidStatusCode, isValidUTF8 } = require('./validation' );
const GET_INFO = 0 ;
const GET_PAYLOAD_LENGTH_16 = 1 ;
const GET_PAYLOAD_LENGTH_64 = 2 ;
const GET_MASK = 3 ;
const GET_DATA = 4 ;
const INFLATING = 5 ;
/**
* HyBi Receiver implementation .
*
* @ extends Writable
*/
class Receiver extends Writable {
/**
* Creates a Receiver instance .
*
* @ param { Object } [ options ] Options object
* @ param { String } [ options . binaryType = nodebuffer ] The type for binary data
* @ param { Object } [ options . extensions ] An object containing the negotiated
* extensions
* @ param { Boolean } [ options . isServer = false ] Specifies whether to operate in
* client or server mode
* @ param { Number } [ options . maxPayload = 0 ] The maximum allowed message length
* @ param { Boolean } [ options . skipUTF8Validation = false ] Specifies whether or
* not to skip UTF - 8 validation for text and close messages
*/
constructor(options = {}) {
super ();
this ._binaryType = options.binaryType || BINARY_TYPES[0 ];
this ._extensions = options.extensions || {};
this ._isServer = !!options.isServer;
this ._maxPayload = options.maxPayload | 0 ;
this ._skipUTF8Validation = !!options.skipUTF8Validation;
this [kWebSocket] = undefined;
this ._bufferedBytes = 0 ;
this ._buffers = [];
this ._compressed = false ;
this ._payloadLength = 0 ;
this ._mask = undefined;
this ._fragmented = 0 ;
this ._masked = false ;
this ._fin = false ;
this ._opcode = 0 ;
this ._totalPayloadLength = 0 ;
this ._messageLength = 0 ;
this ._fragments = [];
this ._state = GET_INFO;
this ._loop = false ;
}
/**
* Implements ` Writable . prototype . _ write ( ) ` .
*
* @ param { Buffer } chunk The chunk of data to write
* @ param { String } encoding The character encoding of ` chunk `
* @ param { Function } cb Callback
* @ private
*/
_write(chunk, encoding, cb) {
if (this ._opcode === 0 x08 && this ._state == GET_INFO) return cb();
this ._bufferedBytes += chunk.length;
this ._buffers.push(chunk);
this .startLoop(cb);
}
/**
* Consumes ` n ` bytes from the buffered data .
*
* @ param { Number } n The number of bytes to consume
* @ return { Buffer } The consumed bytes
* @ private
*/
consume(n) {
this ._bufferedBytes -= n;
if (n === this ._buffers[0 ].length) return this ._buffers.shift();
if (n < this ._buffers[0 ].length) {
const buf = this ._buffers[0 ];
this ._buffers[0 ] = buf.slice(n);
return buf.slice(0 , n);
}
const dst = Buffer.allocUnsafe(n);
do {
const buf = this ._buffers[0 ];
const offset = dst.length - n;
if (n >= buf.length) {
dst.set(this ._buffers.shift(), offset);
} else {
dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset);
this ._buffers[0 ] = buf.slice(n);
}
n -= buf.length;
} while (n > 0 );
return dst;
}
/**
* Starts the parsing loop .
*
* @ param { Function } cb Callback
* @ private
*/
startLoop(cb) {
let err;
this ._loop = true ;
do {
switch (this ._state) {
case GET_INFO:
err = this .getInfo();
break ;
case GET_PAYLOAD_LENGTH_16:
err = this .getPayloadLength16();
break ;
case GET_PAYLOAD_LENGTH_64:
err = this .getPayloadLength64();
break ;
case GET_MASK:
this .getMask();
break ;
case GET_DATA:
err = this .getData(cb);
break ;
default :
// `INFLATING`
this ._loop = false ;
return ;
}
} while (this ._loop);
cb(err);
}
/**
* Reads the first two bytes of a frame .
*
* @ return { ( RangeError | undefined ) } A possible error
* @ private
*/
getInfo() {
if (this ._bufferedBytes < 2 ) {
this ._loop = false ;
return ;
}
const buf = this .consume(2 );
if ((buf[0 ] & 0 x30) !== 0 x00) {
this ._loop = false ;
return error(
RangeError,
'RSV2 and RSV3 must be clear' ,
true ,
1002 ,
'WS_ERR_UNEXPECTED_RSV_2_3'
);
}
const compressed = (buf[0 ] & 0 x40) === 0 x40;
if (compressed && !this ._extensions[PerMessageDeflate.extensionName]) {
this ._loop = false ;
return error(
RangeError,
'RSV1 must be clear' ,
true ,
1002 ,
'WS_ERR_UNEXPECTED_RSV_1'
);
}
this ._fin = (buf[0 ] & 0 x80) === 0 x80;
this ._opcode = buf[0 ] & 0 x0f;
this ._payloadLength = buf[1 ] & 0 x7f;
if (this ._opcode === 0 x00) {
if (compressed) {
this ._loop = false ;
return error(
RangeError,
'RSV1 must be clear' ,
true ,
1002 ,
'WS_ERR_UNEXPECTED_RSV_1'
);
}
if (!this ._fragmented) {
this ._loop = false ;
return error(
RangeError,
'invalid opcode 0' ,
true ,
1002 ,
'WS_ERR_INVALID_OPCODE'
);
}
this ._opcode = this ._fragmented;
} else if (this ._opcode === 0 x01 || this ._opcode === 0 x02) {
if (this ._fragmented) {
this ._loop = false ;
return error(
RangeError,
`invalid opcode ${this ._opcode}`,
true ,
1002 ,
'WS_ERR_INVALID_OPCODE'
);
}
this ._compressed = compressed;
} else if (this ._opcode > 0 x07 && this ._opcode < 0 x0b) {
if (!this ._fin) {
this ._loop = false ;
return error(
RangeError,
'FIN must be set' ,
true ,
1002 ,
'WS_ERR_EXPECTED_FIN'
);
}
if (compressed) {
this ._loop = false ;
return error(
RangeError,
'RSV1 must be clear' ,
true ,
1002 ,
'WS_ERR_UNEXPECTED_RSV_1'
);
}
if (this ._payloadLength > 0 x7d) {
this ._loop = false ;
return error(
RangeError,
`invalid payload length ${this ._payloadLength}`,
true ,
1002 ,
'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
);
}
} else {
this ._loop = false ;
return error(
RangeError,
`invalid opcode ${this ._opcode}`,
true ,
1002 ,
'WS_ERR_INVALID_OPCODE'
);
}
if (!this ._fin && !this ._fragmented) this ._fragmented = this ._opcode;
this ._masked = (buf[1 ] & 0 x80) === 0 x80;
if (this ._isServer) {
if (!this ._masked) {
this ._loop = false ;
return error(
RangeError,
'MASK must be set' ,
true ,
1002 ,
'WS_ERR_EXPECTED_MASK'
);
}
} else if (this ._masked) {
this ._loop = false ;
return error(
RangeError,
'MASK must be clear' ,
true ,
1002 ,
'WS_ERR_UNEXPECTED_MASK'
);
}
if (this ._payloadLength === 126 ) this ._state = GET_PAYLOAD_LENGTH_16;
else if (this ._payloadLength === 127 ) this ._state = GET_PAYLOAD_LENGTH_64;
else return this .haveLength();
}
/**
* Gets extended payload length ( 7 + 16 ) .
*
* @ return { ( RangeError | undefined ) } A possible error
* @ private
*/
getPayloadLength16() {
if (this ._bufferedBytes < 2 ) {
this ._loop = false ;
return ;
}
this ._payloadLength = this .consume(2 ).readUInt16BE(0 );
return this .haveLength();
}
/**
* Gets extended payload length ( 7 + 64 ) .
*
* @ return { ( RangeError | undefined ) } A possible error
* @ private
*/
getPayloadLength64() {
if (this ._bufferedBytes < 8 ) {
this ._loop = false ;
return ;
}
const buf = this .consume(8 );
const num = buf.readUInt32BE(0 );
//
// The maximum safe integer in JavaScript is 2^53 - 1. An error is returned
// if payload length is greater than this number.
//
if (num > Math.pow(2 , 53 - 32 ) - 1 ) {
this ._loop = false ;
return error(
RangeError,
'Unsupported WebSocket frame: payload length > 2^53 - 1' ,
false ,
1009 ,
'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'
);
}
this ._payloadLength = num * Math.pow(2 , 32 ) + buf.readUInt32BE(4 );
return this .haveLength();
}
/**
* Payload length has been read .
*
* @ return { ( RangeError | undefined ) } A possible error
* @ private
*/
haveLength() {
if (this ._payloadLength && this ._opcode < 0 x08) {
this ._totalPayloadLength += this ._payloadLength;
if (this ._totalPayloadLength > this ._maxPayload && this ._maxPayload > 0 ) {
this ._loop = false ;
return error(
RangeError,
'Max payload size exceeded' ,
false ,
1009 ,
'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
);
}
}
if (this ._masked) this ._state = GET_MASK;
else this ._state = GET_DATA;
}
/**
* Reads mask bytes .
*
* @ private
*/
getMask() {
if (this ._bufferedBytes < 4 ) {
this ._loop = false ;
return ;
}
this ._mask = this .consume(4 );
this ._state = GET_DATA;
}
/**
* Reads data bytes .
*
* @ param { Function } cb Callback
* @ return { ( Error | RangeError | undefined ) } A possible error
* @ private
*/
getData(cb) {
let data = EMPTY_BUFFER;
if (this ._payloadLength) {
if (this ._bufferedBytes < this ._payloadLength) {
this ._loop = false ;
return ;
}
data = this .consume(this ._payloadLength);
if (
this ._masked &&
(this ._mask[0 ] | this ._mask[1 ] | this ._mask[2 ] | this ._mask[3 ]) !== 0
) {
unmask(data, this ._mask);
}
}
if (this ._opcode > 0 x07) return this .controlMessage(data);
if (this ._compressed) {
this ._state = INFLATING;
this .decompress(data, cb);
return ;
}
if (data.length) {
//
// This message is not compressed so its length is the sum of the payload
// length of all fragments.
//
this ._messageLength = this ._totalPayloadLength;
this ._fragments.push(data);
}
return this .dataMessage();
}
/**
* Decompresses data .
*
* @ param { Buffer } data Compressed data
* @ param { Function } cb Callback
* @ private
*/
decompress(data, cb) {
const perMessageDeflate = this ._extensions[PerMessageDeflate.extensionName];
perMessageDeflate.decompress(data, this ._fin, (err, buf) => {
if (err) return cb(err);
if (buf.length) {
this ._messageLength += buf.length;
if (this ._messageLength > this ._maxPayload && this ._maxPayload > 0 ) {
return cb(
error(
RangeError,
'Max payload size exceeded' ,
false ,
1009 ,
'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
)
);
}
this ._fragments.push(buf);
}
const er = this .dataMessage();
if (er) return cb(er);
this .startLoop(cb);
});
}
/**
* Handles a data message .
*
* @ return { ( Error | undefined ) } A possible error
* @ private
*/
dataMessage() {
if (this ._fin) {
const messageLength = this ._messageLength;
const fragments = this ._fragments;
this ._totalPayloadLength = 0 ;
this ._messageLength = 0 ;
this ._fragmented = 0 ;
this ._fragments = [];
if (this ._opcode === 2 ) {
let data;
if (this ._binaryType === 'nodebuffer' ) {
data = concat(fragments, messageLength);
} else if (this ._binaryType === 'arraybuffer' ) {
data = toArrayBuffer(concat(fragments, messageLength));
} else {
data = fragments;
}
this .emit('message' , data, true );
} else {
const buf = concat(fragments, messageLength);
if (!this ._skipUTF8Validation && !isValidUTF8(buf)) {
this ._loop = false ;
return error(
Error,
'invalid UTF-8 sequence' ,
true ,
1007 ,
'WS_ERR_INVALID_UTF8'
);
}
this .emit('message' , buf, false );
}
}
this ._state = GET_INFO;
}
/**
* Handles a control message .
*
* @ param { Buffer } data Data to handle
* @ return { ( Error | RangeError | undefined ) } A possible error
* @ private
*/
controlMessage(data) {
if (this ._opcode === 0 x08) {
this ._loop = false ;
if (data.length === 0 ) {
this .emit('conclude' , 1005 , EMPTY_BUFFER);
this .end();
} else if (data.length === 1 ) {
return error(
RangeError,
'invalid payload length 1' ,
true ,
1002 ,
'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
);
} else {
const code = data.readUInt16BE(0 );
if (!isValidStatusCode(code)) {
return error(
RangeError,
`invalid status code ${code}`,
true ,
1002 ,
'WS_ERR_INVALID_CLOSE_CODE'
);
}
const buf = data.slice(2 );
if (!this ._skipUTF8Validation && !isValidUTF8(buf)) {
return error(
Error,
'invalid UTF-8 sequence' ,
true ,
1007 ,
'WS_ERR_INVALID_UTF8'
);
}
this .emit('conclude' , code, buf);
this .end();
}
} else if (this ._opcode === 0 x09) {
this .emit('ping' , data);
} else {
this .emit('pong' , data);
}
this ._state = GET_INFO;
}
}
module.exports = Receiver;
/**
* Builds an error object .
*
* @ param { function ( new : Error | RangeError ) } ErrorCtor The error constructor
* @ param { String } message The error message
* @ param { Boolean } prefix Specifies whether or not to add a default prefix to
* ` message `
* @ param { Number } statusCode The status code
* @ param { String } errorCode The exposed error code
* @ return { ( Error | RangeError ) } The error
* @ private
*/
function error(ErrorCtor, message, prefix, statusCode, errorCode) {
const err = new ErrorCtor(
prefix ? `Invalid WebSocket frame: ${message}` : message
);
Error.captureStackTrace(err, error);
err.code = errorCode;
err[kStatusCode] = statusCode;
return err;
}
Messung V0.5 in Prozent C=92 H=97 G=94
¤ Dauer der Verarbeitung: 0.14 Sekunden
(vorverarbeitet am 2026-08-26)
¤
*© Formatika GbR, Deutschland