1'use strict'; 2 3const { 4 ArrayPrototypePush, 5 ErrorCaptureStackTrace, 6 FunctionPrototypeBind, 7 JSONParse, 8 JSONStringify, 9 ObjectKeys, 10 Promise, 11} = primordials; 12 13const Buffer = require('buffer').Buffer; 14const crypto = require('crypto'); 15const { ERR_DEBUGGER_ERROR } = require('internal/errors').codes; 16const { EventEmitter } = require('events'); 17const http = require('http'); 18const URL = require('url'); 19 20const debuglog = require('internal/util/debuglog').debuglog('inspect'); 21 22const kOpCodeText = 0x1; 23const kOpCodeClose = 0x8; 24 25const kFinalBit = 0x80; 26const kReserved1Bit = 0x40; 27const kReserved2Bit = 0x20; 28const kReserved3Bit = 0x10; 29const kOpCodeMask = 0xF; 30const kMaskBit = 0x80; 31const kPayloadLengthMask = 0x7F; 32 33const kMaxSingleBytePayloadLength = 125; 34const kMaxTwoBytePayloadLength = 0xFFFF; 35const kTwoBytePayloadLengthField = 126; 36const kEightBytePayloadLengthField = 127; 37const kMaskingKeyWidthInBytes = 4; 38 39// This guid is defined in the Websocket Protocol RFC 40// https://tools.ietf.org/html/rfc6455#section-1.3 41const WEBSOCKET_HANDSHAKE_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; 42 43function unpackError({ code, message }) { 44 const err = new ERR_DEBUGGER_ERROR(`${message}`); 45 err.code = code; 46 ErrorCaptureStackTrace(err, unpackError); 47 return err; 48} 49 50function validateHandshake(requestKey, responseKey) { 51 const expectedResponseKeyBase = requestKey + WEBSOCKET_HANDSHAKE_GUID; 52 const shasum = crypto.createHash('sha1'); 53 shasum.update(expectedResponseKeyBase); 54 const shabuf = shasum.digest(); 55 56 if (shabuf.toString('base64') !== responseKey) { 57 throw new ERR_DEBUGGER_ERROR( 58 `WebSocket secret mismatch: ${requestKey} did not match ${responseKey}`, 59 ); 60 } 61} 62 63function encodeFrameHybi17(payload) { 64 const dataLength = payload.length; 65 66 let singleByteLength; 67 let additionalLength; 68 if (dataLength > kMaxTwoBytePayloadLength) { 69 singleByteLength = kEightBytePayloadLengthField; 70 additionalLength = Buffer.alloc(8); 71 let remaining = dataLength; 72 for (let i = 0; i < 8; ++i) { 73 additionalLength[7 - i] = remaining & 0xFF; 74 remaining >>= 8; 75 } 76 } else if (dataLength > kMaxSingleBytePayloadLength) { 77 singleByteLength = kTwoBytePayloadLengthField; 78 additionalLength = Buffer.alloc(2); 79 additionalLength[0] = (dataLength & 0xFF00) >> 8; 80 additionalLength[1] = dataLength & 0xFF; 81 } else { 82 additionalLength = Buffer.alloc(0); 83 singleByteLength = dataLength; 84 } 85 86 const header = Buffer.from([ 87 kFinalBit | kOpCodeText, 88 kMaskBit | singleByteLength, 89 ]); 90 91 const mask = Buffer.alloc(4); 92 const masked = Buffer.alloc(dataLength); 93 for (let i = 0; i < dataLength; ++i) { 94 masked[i] = payload[i] ^ mask[i % kMaskingKeyWidthInBytes]; 95 } 96 97 return Buffer.concat([header, additionalLength, mask, masked]); 98} 99 100function decodeFrameHybi17(data) { 101 const dataAvailable = data.length; 102 const notComplete = { closed: false, payload: null, rest: data }; 103 let payloadOffset = 2; 104 if ((dataAvailable - payloadOffset) < 0) return notComplete; 105 106 const firstByte = data[0]; 107 const secondByte = data[1]; 108 109 const final = (firstByte & kFinalBit) !== 0; 110 const reserved1 = (firstByte & kReserved1Bit) !== 0; 111 const reserved2 = (firstByte & kReserved2Bit) !== 0; 112 const reserved3 = (firstByte & kReserved3Bit) !== 0; 113 const opCode = firstByte & kOpCodeMask; 114 const masked = (secondByte & kMaskBit) !== 0; 115 const compressed = reserved1; 116 if (compressed) { 117 throw new ERR_DEBUGGER_ERROR('Compressed frames not supported'); 118 } 119 if (!final || reserved2 || reserved3) { 120 throw new ERR_DEBUGGER_ERROR('Only compression extension is supported'); 121 } 122 123 if (masked) { 124 throw new ERR_DEBUGGER_ERROR('Masked server frame - not supported'); 125 } 126 127 let closed = false; 128 switch (opCode) { 129 case kOpCodeClose: 130 closed = true; 131 break; 132 case kOpCodeText: 133 break; 134 default: 135 throw new ERR_DEBUGGER_ERROR(`Unsupported op code ${opCode}`); 136 } 137 138 let payloadLength = secondByte & kPayloadLengthMask; 139 switch (payloadLength) { 140 case kTwoBytePayloadLengthField: 141 payloadOffset += 2; 142 payloadLength = (data[2] << 8) + data[3]; 143 break; 144 145 case kEightBytePayloadLengthField: 146 payloadOffset += 8; 147 payloadLength = 0; 148 for (let i = 0; i < 8; ++i) { 149 payloadLength <<= 8; 150 payloadLength |= data[2 + i]; 151 } 152 break; 153 154 default: 155 // Nothing. We already have the right size. 156 } 157 if ((dataAvailable - payloadOffset - payloadLength) < 0) return notComplete; 158 159 const payloadEnd = payloadOffset + payloadLength; 160 return { 161 payload: data.slice(payloadOffset, payloadEnd), 162 rest: data.slice(payloadEnd), 163 closed, 164 }; 165} 166 167class Client extends EventEmitter { 168 constructor() { 169 super(); 170 this.handleChunk = FunctionPrototypeBind(this._handleChunk, this); 171 172 this._port = undefined; 173 this._host = undefined; 174 175 this.reset(); 176 } 177 178 _handleChunk(chunk) { 179 this._unprocessed = Buffer.concat([this._unprocessed, chunk]); 180 181 while (this._unprocessed.length > 2) { 182 const { 183 closed, 184 payload: payloadBuffer, 185 rest, 186 } = decodeFrameHybi17(this._unprocessed); 187 this._unprocessed = rest; 188 189 if (closed) { 190 this.reset(); 191 return; 192 } 193 if (payloadBuffer === null || payloadBuffer.length === 0) break; 194 195 const payloadStr = payloadBuffer.toString(); 196 debuglog('< %s', payloadStr); 197 const lastChar = payloadStr[payloadStr.length - 1]; 198 if (payloadStr[0] !== '{' || lastChar !== '}') { 199 throw new ERR_DEBUGGER_ERROR(`Payload does not look like JSON: ${payloadStr}`); 200 } 201 let payload; 202 try { 203 payload = JSONParse(payloadStr); 204 } catch (parseError) { 205 parseError.string = payloadStr; 206 throw parseError; 207 } 208 209 const { id, method, params, result, error } = payload; 210 if (id) { 211 const handler = this._pending[id]; 212 if (handler) { 213 delete this._pending[id]; 214 handler(error, result); 215 } 216 } else if (method) { 217 this.emit('debugEvent', method, params); 218 this.emit(method, params); 219 } else { 220 throw new ERR_DEBUGGER_ERROR(`Unsupported response: ${payloadStr}`); 221 } 222 } 223 } 224 225 reset() { 226 if (this._http) { 227 this._http.destroy(); 228 } 229 if (this._socket) { 230 this._socket.destroy(); 231 } 232 this._http = null; 233 this._lastId = 0; 234 this._socket = null; 235 this._pending = {}; 236 this._unprocessed = Buffer.alloc(0); 237 } 238 239 callMethod(method, params) { 240 return new Promise((resolve, reject) => { 241 if (!this._socket) { 242 reject(new ERR_DEBUGGER_ERROR('Use `run` to start the app again.')); 243 return; 244 } 245 const data = { id: ++this._lastId, method, params }; 246 this._pending[data.id] = (error, result) => { 247 if (error) reject(unpackError(error)); 248 else resolve(ObjectKeys(result).length ? result : undefined); 249 }; 250 const json = JSONStringify(data); 251 debuglog('> %s', json); 252 this._socket.write(encodeFrameHybi17(Buffer.from(json))); 253 }); 254 } 255 256 _fetchJSON(urlPath) { 257 return new Promise((resolve, reject) => { 258 const httpReq = http.get({ 259 host: this._host, 260 port: this._port, 261 path: urlPath, 262 }); 263 264 const chunks = []; 265 266 function onResponse(httpRes) { 267 function parseChunks() { 268 const resBody = Buffer.concat(chunks).toString(); 269 if (httpRes.statusCode !== 200) { 270 reject(new ERR_DEBUGGER_ERROR(`Unexpected ${httpRes.statusCode}: ${resBody}`)); 271 return; 272 } 273 try { 274 resolve(JSONParse(resBody)); 275 } catch { 276 reject(new ERR_DEBUGGER_ERROR(`Response didn't contain JSON: ${resBody}`)); 277 278 } 279 } 280 281 httpRes.on('error', reject); 282 httpRes.on('data', (chunk) => ArrayPrototypePush(chunks, chunk)); 283 httpRes.on('end', parseChunks); 284 } 285 286 httpReq.on('error', reject); 287 httpReq.on('response', onResponse); 288 }); 289 } 290 291 async connect(port, host) { 292 this._port = port; 293 this._host = host; 294 const urlPath = await this._discoverWebsocketPath(); 295 return this._connectWebsocket(urlPath); 296 } 297 298 async _discoverWebsocketPath() { 299 const { 0: { webSocketDebuggerUrl } } = await this._fetchJSON('/json'); 300 return URL.parse(webSocketDebuggerUrl).path; 301 } 302 303 _connectWebsocket(urlPath) { 304 this.reset(); 305 306 const requestKey = crypto.randomBytes(16).toString('base64'); 307 debuglog('request WebSocket', requestKey); 308 309 const httpReq = this._http = http.request({ 310 host: this._host, 311 port: this._port, 312 path: urlPath, 313 headers: { 314 'Connection': 'Upgrade', 315 'Upgrade': 'websocket', 316 'Sec-WebSocket-Key': requestKey, 317 'Sec-WebSocket-Version': '13', 318 }, 319 }); 320 httpReq.on('error', (e) => { 321 this.emit('error', e); 322 }); 323 httpReq.on('response', (httpRes) => { 324 if (httpRes.statusCode >= 400) { 325 process.stderr.write(`Unexpected HTTP code: ${httpRes.statusCode}\n`); 326 httpRes.pipe(process.stderr); 327 } else { 328 httpRes.pipe(process.stderr); 329 } 330 }); 331 332 const handshakeListener = (res, socket) => { 333 validateHandshake(requestKey, res.headers['sec-websocket-accept']); 334 debuglog('websocket upgrade'); 335 336 this._socket = socket; 337 socket.on('data', this.handleChunk); 338 socket.on('close', () => { 339 this.emit('close'); 340 }); 341 342 this.emit('ready'); 343 }; 344 345 return new Promise((resolve, reject) => { 346 this.once('error', reject); 347 this.once('ready', resolve); 348 349 httpReq.on('upgrade', handshakeListener); 350 httpReq.end(); 351 }); 352 } 353} 354 355module.exports = Client; 356