UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

360 lines (309 loc) 10 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/debugger/inspect_client.js import { Buffer as Buffer } from "nstdlib/lib/buffer"; import * as crypto from "nstdlib/lib/crypto"; import { codes as __codes__ } from "nstdlib/lib/internal/errors"; import { EventEmitter } from "nstdlib/lib/events"; import * as http from "nstdlib/lib/http"; import { URL } from "nstdlib/lib/internal/url"; const { ERR_DEBUGGER_ERROR } = __codes__; const debuglog = require("internal/util/debuglog").debuglog("inspect"); const kOpCodeText = 0x1; const kOpCodeClose = 0x8; const kFinalBit = 0x80; const kReserved1Bit = 0x40; const kReserved2Bit = 0x20; const kReserved3Bit = 0x10; const kOpCodeMask = 0xf; const kMaskBit = 0x80; const kPayloadLengthMask = 0x7f; const kMaxSingleBytePayloadLength = 125; const kMaxTwoBytePayloadLength = 0xffff; const kTwoBytePayloadLengthField = 126; const kEightBytePayloadLengthField = 127; const kMaskingKeyWidthInBytes = 4; // This guid is defined in the Websocket Protocol RFC // https://tools.ietf.org/html/rfc6455#section-1.3 const WEBSOCKET_HANDSHAKE_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; function unpackError({ code, message }) { const err = new ERR_DEBUGGER_ERROR(`${message}`); err.code = code; Error.captureStackTrace(err, unpackError); return err; } function validateHandshake(requestKey, responseKey) { const expectedResponseKeyBase = requestKey + WEBSOCKET_HANDSHAKE_GUID; const shasum = crypto.createHash("sha1"); shasum.update(expectedResponseKeyBase); const shabuf = shasum.digest(); if (shabuf.toString("base64") !== responseKey) { throw new ERR_DEBUGGER_ERROR( `WebSocket secret mismatch: ${requestKey} did not match ${responseKey}`, ); } } function encodeFrameHybi17(payload) { const dataLength = payload.length; let singleByteLength; let additionalLength; if (dataLength > kMaxTwoBytePayloadLength) { singleByteLength = kEightBytePayloadLengthField; additionalLength = Buffer.alloc(8); let remaining = dataLength; for (let i = 0; i < 8; ++i) { additionalLength[7 - i] = remaining & 0xff; remaining >>= 8; } } else if (dataLength > kMaxSingleBytePayloadLength) { singleByteLength = kTwoBytePayloadLengthField; additionalLength = Buffer.alloc(2); additionalLength[0] = (dataLength & 0xff00) >> 8; additionalLength[1] = dataLength & 0xff; } else { additionalLength = Buffer.alloc(0); singleByteLength = dataLength; } const header = Buffer.from([ kFinalBit | kOpCodeText, kMaskBit | singleByteLength, ]); const mask = Buffer.alloc(4); const masked = Buffer.alloc(dataLength); for (let i = 0; i < dataLength; ++i) { masked[i] = payload[i] ^ mask[i % kMaskingKeyWidthInBytes]; } return Buffer.concat([header, additionalLength, mask, masked]); } function decodeFrameHybi17(data) { const dataAvailable = data.length; const notComplete = { closed: false, payload: null, rest: data }; let payloadOffset = 2; if (dataAvailable - payloadOffset < 0) return notComplete; const firstByte = data[0]; const secondByte = data[1]; const final = (firstByte & kFinalBit) !== 0; const reserved1 = (firstByte & kReserved1Bit) !== 0; const reserved2 = (firstByte & kReserved2Bit) !== 0; const reserved3 = (firstByte & kReserved3Bit) !== 0; const opCode = firstByte & kOpCodeMask; const masked = (secondByte & kMaskBit) !== 0; const compressed = reserved1; if (compressed) { throw new ERR_DEBUGGER_ERROR("Compressed frames not supported"); } if (!final || reserved2 || reserved3) { throw new ERR_DEBUGGER_ERROR("Only compression extension is supported"); } if (masked) { throw new ERR_DEBUGGER_ERROR("Masked server frame - not supported"); } let closed = false; switch (opCode) { case kOpCodeClose: closed = true; break; case kOpCodeText: break; default: throw new ERR_DEBUGGER_ERROR(`Unsupported op code ${opCode}`); } let payloadLength = secondByte & kPayloadLengthMask; switch (payloadLength) { case kTwoBytePayloadLengthField: payloadOffset += 2; payloadLength = (data[2] << 8) + data[3]; break; case kEightBytePayloadLengthField: payloadOffset += 8; payloadLength = 0; for (let i = 0; i < 8; ++i) { payloadLength <<= 8; payloadLength |= data[2 + i]; } break; default: // Nothing. We already have the right size. } if (dataAvailable - payloadOffset - payloadLength < 0) return notComplete; const payloadEnd = payloadOffset + payloadLength; return { payload: data.slice(payloadOffset, payloadEnd), rest: data.slice(payloadEnd), closed, }; } class Client extends EventEmitter { constructor() { super(); this.handleChunk = Function.prototype.bind.call(this._handleChunk, this); this._port = undefined; this._host = undefined; this.reset(); } _handleChunk(chunk) { this._unprocessed = Buffer.concat([this._unprocessed, chunk]); while (this._unprocessed.length > 2) { const { closed, payload: payloadBuffer, rest, } = decodeFrameHybi17(this._unprocessed); this._unprocessed = rest; if (closed) { this.reset(); return; } if (payloadBuffer === null || payloadBuffer.length === 0) break; const payloadStr = payloadBuffer.toString(); debuglog("< %s", payloadStr); const lastChar = payloadStr[payloadStr.length - 1]; if (payloadStr[0] !== "{" || lastChar !== "}") { throw new ERR_DEBUGGER_ERROR( `Payload does not look like JSON: ${payloadStr}`, ); } let payload; try { payload = JSONParse(payloadStr); } catch (parseError) { parseError.string = payloadStr; throw parseError; } const { id, method, params, result, error } = payload; if (id) { const handler = this._pending[id]; if (handler) { delete this._pending[id]; handler(error, result); } } else if (method) { this.emit("debugEvent", method, params); this.emit(method, params); } else { throw new ERR_DEBUGGER_ERROR(`Unsupported response: ${payloadStr}`); } } } reset() { if (this._http) { this._http.destroy(); } if (this._socket) { this._socket.destroy(); } this._http = null; this._lastId = 0; this._socket = null; this._pending = {}; this._unprocessed = Buffer.alloc(0); } callMethod(method, params) { return new Promise((resolve, reject) => { if (!this._socket) { reject(new ERR_DEBUGGER_ERROR("Use `run` to start the app again.")); return; } const data = { id: ++this._lastId, method, params }; this._pending[data.id] = (error, result) => { if (error) reject(unpackError(error)); else resolve(Object.keys(result).length ? result : undefined); }; const json = JSONStringify(data); debuglog("> %s", json); this._socket.write(encodeFrameHybi17(Buffer.from(json))); }); } _fetchJSON(urlPath) { return new Promise((resolve, reject) => { const httpReq = http.get({ host: this._host, port: this._port, path: urlPath, }); const chunks = []; function onResponse(httpRes) { function parseChunks() { const resBody = Buffer.concat(chunks).toString(); if (httpRes.statusCode !== 200) { reject( new ERR_DEBUGGER_ERROR( `Unexpected ${httpRes.statusCode}: ${resBody}`, ), ); return; } try { resolve(JSONParse(resBody)); } catch { reject( new ERR_DEBUGGER_ERROR( `Response didn't contain JSON: ${resBody}`, ), ); } } httpRes.on("error", reject); httpRes.on("data", (chunk) => Array.prototype.push.call(chunks, chunk)); httpRes.on("end", parseChunks); } httpReq.on("error", reject); httpReq.on("response", onResponse); }); } async connect(port, host) { this._port = port; this._host = host; const urlPath = await this._discoverWebsocketPath(); return this._connectWebsocket(urlPath); } async _discoverWebsocketPath() { const { 0: { webSocketDebuggerUrl }, } = await this._fetchJSON("/json"); const { pathname, search } = new URL(webSocketDebuggerUrl); return `${pathname}${search}`; } _connectWebsocket(urlPath) { this.reset(); const requestKey = crypto.randomBytes(16).toString("base64"); debuglog("request WebSocket", requestKey); const httpReq = (this._http = http.request({ host: this._host, port: this._port, path: urlPath, headers: { Connection: "Upgrade", Upgrade: "websocket", "Sec-WebSocket-Key": requestKey, "Sec-WebSocket-Version": "13", }, })); httpReq.on("error", (e) => { this.emit("error", e); }); httpReq.on("response", (httpRes) => { if (httpRes.statusCode >= 400) { process.stderr.write(`Unexpected HTTP code: ${httpRes.statusCode}\n`); httpRes.pipe(process.stderr); } else { httpRes.pipe(process.stderr); } }); const handshakeListener = (res, socket) => { validateHandshake(requestKey, res.headers["sec-websocket-accept"]); debuglog("websocket upgrade"); this._socket = socket; socket.on("data", this.handleChunk); socket.on("close", () => { this.emit("close"); }); this.emit("ready"); }; return new Promise((resolve, reject) => { this.once("error", reject); this.once("ready", resolve); httpReq.on("upgrade", handshakeListener); httpReq.end(); }); } } export default Client;