@sentry/node
Version:
Official Sentry SDK for Node.js
325 lines (265 loc) • 8 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
const crypto = require('crypto');
const events = require('events');
const http = require('http');
const url = require('url');
/* eslint-disable no-bitwise */
const OPCODES = {
CONTINUATION: 0,
TEXT: 1,
BINARY: 2,
TERMINATE: 8,
PING: 9,
PONG: 10,
};
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
function isCompleteFrame(frame) {
return Buffer.byteLength(frame.payload) >= frame.payloadLength;
}
function unmaskPayload(payload, mask, offset) {
if (mask === undefined) {
return payload;
}
for (let i = 0; i < payload.length; i++) {
payload[i] ^= mask[(offset + i) & 3];
}
return payload;
}
function buildFrame(opts) {
const { opcode, fin, data } = opts;
let offset = 6;
let dataLength = data.length;
if (dataLength >= 65536) {
offset += 8;
dataLength = 127;
} else if (dataLength > 125) {
offset += 2;
dataLength = 126;
}
const head = Buffer.allocUnsafe(offset);
head[0] = fin ? opcode | 128 : opcode;
head[1] = dataLength;
if (dataLength === 126) {
head.writeUInt16BE(data.length, 2);
} else if (dataLength === 127) {
head.writeUInt32BE(0, 2);
head.writeUInt32BE(data.length, 6);
}
const mask = crypto.randomBytes(4);
head[1] |= 128;
head[offset - 4] = mask[0];
head[offset - 3] = mask[1];
head[offset - 2] = mask[2];
head[offset - 1] = mask[3];
const masked = Buffer.alloc(dataLength);
for (let i = 0; i < dataLength; ++i) {
masked[i] = data[i] ^ mask[i & 3];
}
return Buffer.concat([head, masked]);
}
function parseFrame(buffer) {
const firstByte = buffer.readUInt8(0);
const isFinalFrame = Boolean((firstByte >>> 7) & 1);
const opcode = firstByte & 15;
const secondByte = buffer.readUInt8(1);
const isMasked = Boolean((secondByte >>> 7) & 1);
// Keep track of our current position as we advance through the buffer
let currentOffset = 2;
let payloadLength = secondByte & 127;
if (payloadLength > 125) {
if (payloadLength === 126) {
payloadLength = buffer.readUInt16BE(currentOffset);
currentOffset += 2;
} else if (payloadLength === 127) {
const leftPart = buffer.readUInt32BE(currentOffset);
currentOffset += 4;
// The maximum safe integer in JavaScript is 2^53 - 1. An error is returned
// if payload length is greater than this number.
if (leftPart >= Number.MAX_SAFE_INTEGER) {
throw new Error('Unsupported WebSocket frame: payload length > 2^53 - 1');
}
const rightPart = buffer.readUInt32BE(currentOffset);
currentOffset += 4;
payloadLength = leftPart * Math.pow(2, 32) + rightPart;
} else {
throw new Error('Unknown payload length');
}
}
// Get the masking key if one exists
let mask;
if (isMasked) {
mask = buffer.slice(currentOffset, currentOffset + 4);
currentOffset += 4;
}
const payload = unmaskPayload(buffer.slice(currentOffset), mask, 0);
return {
fin: isFinalFrame,
opcode,
mask,
payload,
payloadLength,
};
}
function createKey(key) {
return crypto.createHash('sha1').update(`${key}${GUID}`).digest('base64');
}
class WebSocketInterface extends events.EventEmitter {
constructor(socket) {
super();
// When a frame is set here then any additional continuation frames payloads will be appended
this._unfinishedFrame = undefined;
// When a frame is set here, all additional chunks will be appended until we reach the correct payloadLength
this._incompleteFrame = undefined;
this._socket = socket;
this._alive = true;
socket.on('data', buff => {
this._addBuffer(buff);
});
socket.on('error', (err) => {
if (err.code === 'ECONNRESET') {
this.emit('close');
} else {
this.emit('error');
}
});
socket.on('close', () => {
this.end();
});
}
end() {
if (!this._alive) {
return;
}
this._alive = false;
this.emit('close');
this._socket.end();
}
send(buff) {
this._sendFrame({
opcode: OPCODES.TEXT,
fin: true,
data: Buffer.from(buff),
});
}
_sendFrame(frameOpts) {
this._socket.write(buildFrame(frameOpts));
}
_completeFrame(frame) {
// If we have an unfinished frame then only allow continuations
const { _unfinishedFrame: unfinishedFrame } = this;
if (unfinishedFrame !== undefined) {
if (frame.opcode === OPCODES.CONTINUATION) {
unfinishedFrame.payload = Buffer.concat([
unfinishedFrame.payload,
unmaskPayload(frame.payload, unfinishedFrame.mask, unfinishedFrame.payload.length),
]);
if (frame.fin) {
this._unfinishedFrame = undefined;
this._completeFrame(unfinishedFrame);
}
return;
} else {
// Silently ignore the previous frame...
this._unfinishedFrame = undefined;
}
}
if (frame.fin) {
if (frame.opcode === OPCODES.PING) {
this._sendFrame({
opcode: OPCODES.PONG,
fin: true,
data: frame.payload,
});
} else {
// Trim off any excess payload
let excess;
if (frame.payload.length > frame.payloadLength) {
excess = frame.payload.slice(frame.payloadLength);
frame.payload = frame.payload.slice(0, frame.payloadLength);
}
this.emit('message', frame.payload);
if (excess !== undefined) {
this._addBuffer(excess);
}
}
} else {
this._unfinishedFrame = frame;
}
}
_addBufferToIncompleteFrame(incompleteFrame, buff) {
incompleteFrame.payload = Buffer.concat([
incompleteFrame.payload,
unmaskPayload(buff, incompleteFrame.mask, incompleteFrame.payload.length),
]);
if (isCompleteFrame(incompleteFrame)) {
this._incompleteFrame = undefined;
this._completeFrame(incompleteFrame);
}
}
_addBuffer(buff) {
// Check if we're still waiting for the rest of a payload
const { _incompleteFrame: incompleteFrame } = this;
if (incompleteFrame !== undefined) {
this._addBufferToIncompleteFrame(incompleteFrame, buff);
return;
}
// There needs to be atleast two values in the buffer for us to parse
// a frame from it.
// See: https://github.com/getsentry/sentry-javascript/issues/9307
if (buff.length <= 1) {
return;
}
const frame = parseFrame(buff);
if (isCompleteFrame(frame)) {
// Frame has been completed!
this._completeFrame(frame);
} else {
this._incompleteFrame = frame;
}
}
}
/**
* Creates a WebSocket client
*/
async function createWebSocketClient(rawUrl) {
const parts = url.parse(rawUrl);
return new Promise((resolve, reject) => {
const key = crypto.randomBytes(16).toString('base64');
const digest = createKey(key);
const req = http.request({
hostname: parts.hostname,
port: parts.port,
path: parts.path,
method: 'GET',
headers: {
Connection: 'Upgrade',
Upgrade: 'websocket',
'Sec-WebSocket-Key': key,
'Sec-WebSocket-Version': '13',
},
});
req.on('response', (res) => {
if (res.statusCode && res.statusCode >= 400) {
process.stderr.write(`Unexpected HTTP code: ${res.statusCode}\n`);
res.pipe(process.stderr);
} else {
res.pipe(process.stderr);
}
});
req.on('upgrade', (res, socket) => {
if (res.headers['sec-websocket-accept'] !== digest) {
socket.end();
reject(new Error(`Digest mismatch ${digest} !== ${res.headers['sec-websocket-accept']}`));
return;
}
const client = new WebSocketInterface(socket);
resolve(client);
});
req.on('error', err => {
reject(err);
});
req.end();
});
}
exports.createWebSocketClient = createWebSocketClient;
//# sourceMappingURL=websocket.js.map