UNPKG

@sentry/node

Version:
325 lines (265 loc) 8 kB
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