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