1/* 2 * Copyright Node.js contributors. All rights reserved. 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to 6 * deal in the Software without restriction, including without limitation the 7 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 * sell copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in 12 * all copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 * IN THE SOFTWARE. 21 */ 22'use strict'; 23const Buffer = require('buffer').Buffer; 24const crypto = require('crypto'); 25const { EventEmitter } = require('events'); 26const http = require('http'); 27const URL = require('url'); 28const util = require('util'); 29 30const debuglog = util.debuglog('inspect'); 31 32const kOpCodeText = 0x1; 33const kOpCodeClose = 0x8; 34 35const kFinalBit = 0x80; 36const kReserved1Bit = 0x40; 37const kReserved2Bit = 0x20; 38const kReserved3Bit = 0x10; 39const kOpCodeMask = 0xF; 40const kMaskBit = 0x80; 41const kPayloadLengthMask = 0x7F; 42 43const kMaxSingleBytePayloadLength = 125; 44const kMaxTwoBytePayloadLength = 0xFFFF; 45const kTwoBytePayloadLengthField = 126; 46const kEightBytePayloadLengthField = 127; 47const kMaskingKeyWidthInBytes = 4; 48 49function isEmpty(obj) { 50 return Object.keys(obj).length === 0; 51} 52 53function unpackError({ code, message, data }) { 54 const err = new Error(`${message} - ${data}`); 55 err.code = code; 56 Error.captureStackTrace(err, unpackError); 57 return err; 58} 59 60function encodeFrameHybi17(payload) { 61 var i; 62 63 const dataLength = payload.length; 64 65 let singleByteLength; 66 let additionalLength; 67 if (dataLength > kMaxTwoBytePayloadLength) { 68 singleByteLength = kEightBytePayloadLengthField; 69 additionalLength = Buffer.alloc(8); 70 let remaining = dataLength; 71 for (i = 0; i < 8; ++i) { 72 additionalLength[7 - i] = remaining & 0xFF; 73 remaining >>= 8; 74 } 75 } else if (dataLength > kMaxSingleBytePayloadLength) { 76 singleByteLength = kTwoBytePayloadLengthField; 77 additionalLength = Buffer.alloc(2); 78 additionalLength[0] = (dataLength & 0xFF00) >> 8; 79 additionalLength[1] = dataLength & 0xFF; 80 } else { 81 additionalLength = Buffer.alloc(0); 82 singleByteLength = dataLength; 83 } 84 85 const header = Buffer.from([ 86 kFinalBit | kOpCodeText, 87 kMaskBit | singleByteLength, 88 ]); 89 90 const mask = Buffer.alloc(4); 91 const masked = Buffer.alloc(dataLength); 92 for (i = 0; i < dataLength; ++i) { 93 masked[i] = payload[i] ^ mask[i % kMaskingKeyWidthInBytes]; 94 } 95 96 return Buffer.concat([header, additionalLength, mask, masked]); 97} 98 99function decodeFrameHybi17(data) { 100 const dataAvailable = data.length; 101 const notComplete = { closed: false, payload: null, rest: data }; 102 let payloadOffset = 2; 103 if ((dataAvailable - payloadOffset) < 0) return notComplete; 104 105 const firstByte = data[0]; 106 const secondByte = data[1]; 107 108 const final = (firstByte & kFinalBit) !== 0; 109 const reserved1 = (firstByte & kReserved1Bit) !== 0; 110 const reserved2 = (firstByte & kReserved2Bit) !== 0; 111 const reserved3 = (firstByte & kReserved3Bit) !== 0; 112 const opCode = firstByte & kOpCodeMask; 113 const masked = (secondByte & kMaskBit) !== 0; 114 const compressed = reserved1; 115 if (compressed) { 116 throw new Error('Compressed frames not supported'); 117 } 118 if (!final || reserved2 || reserved3) { 119 throw new Error('Only compression extension is supported'); 120 } 121 122 if (masked) { 123 throw new Error('Masked server frame - not supported'); 124 } 125 126 let closed = false; 127 switch (opCode) { 128 case kOpCodeClose: 129 closed = true; 130 break; 131 case kOpCodeText: 132 break; 133 default: 134 throw new Error(`Unsupported op code ${opCode}`); 135 } 136 137 let payloadLength = secondByte & kPayloadLengthMask; 138 switch (payloadLength) { 139 case kTwoBytePayloadLengthField: 140 payloadOffset += 2; 141 payloadLength = (data[2] << 8) + data[3]; 142 break; 143 144 case kEightBytePayloadLengthField: 145 payloadOffset += 8; 146 payloadLength = 0; 147 for (var i = 0; i < 8; ++i) { 148 payloadLength <<= 8; 149 payloadLength |= data[2 + i]; 150 } 151 break; 152 153 default: 154 // Nothing. We already have the right size. 155 } 156 if ((dataAvailable - payloadOffset - payloadLength) < 0) return notComplete; 157 158 const payloadEnd = payloadOffset + payloadLength; 159 return { 160 payload: data.slice(payloadOffset, payloadEnd), 161 rest: data.slice(payloadEnd), 162 closed, 163 }; 164} 165 166class Client extends EventEmitter { 167 constructor() { 168 super(); 169 this.handleChunk = this._handleChunk.bind(this); 170 171 this._port = undefined; 172 this._host = undefined; 173 174 this.reset(); 175 } 176 177 _handleChunk(chunk) { 178 this._unprocessed = Buffer.concat([this._unprocessed, chunk]); 179 180 while (this._unprocessed.length > 2) { 181 const { 182 closed, 183 payload: payloadBuffer, 184 rest 185 } = decodeFrameHybi17(this._unprocessed); 186 this._unprocessed = rest; 187 188 if (closed) { 189 this.reset(); 190 return; 191 } 192 if (payloadBuffer === null) break; 193 194 const payloadStr = payloadBuffer.toString(); 195 debuglog('< %s', payloadStr); 196 const lastChar = payloadStr[payloadStr.length - 1]; 197 if (payloadStr[0] !== '{' || lastChar !== '}') { 198 throw new Error(`Payload does not look like JSON: ${payloadStr}`); 199 } 200 let payload; 201 try { 202 payload = JSON.parse(payloadStr); 203 } catch (parseError) { 204 parseError.string = payloadStr; 205 throw parseError; 206 } 207 208 const { id, method, params, result, error } = payload; 209 if (id) { 210 const handler = this._pending[id]; 211 if (handler) { 212 delete this._pending[id]; 213 handler(error, result); 214 } 215 } else if (method) { 216 this.emit('debugEvent', method, params); 217 this.emit(method, params); 218 } else { 219 throw new Error(`Unsupported response: ${payloadStr}`); 220 } 221 } 222 } 223 224 reset() { 225 if (this._http) { 226 this._http.destroy(); 227 } 228 this._http = null; 229 this._lastId = 0; 230 this._socket = null; 231 this._pending = {}; 232 this._unprocessed = Buffer.alloc(0); 233 } 234 235 callMethod(method, params) { 236 return new Promise((resolve, reject) => { 237 if (!this._socket) { 238 reject(new Error('Use `run` to start the app again.')); 239 return; 240 } 241 const data = { id: ++this._lastId, method, params }; 242 this._pending[data.id] = (error, result) => { 243 if (error) reject(unpackError(error)); 244 else resolve(isEmpty(result) ? undefined : result); 245 }; 246 const json = JSON.stringify(data); 247 debuglog('> %s', json); 248 this._socket.write(encodeFrameHybi17(Buffer.from(json))); 249 }); 250 } 251 252 _fetchJSON(urlPath) { 253 return new Promise((resolve, reject) => { 254 const httpReq = http.get({ 255 host: this._host, 256 port: this._port, 257 path: urlPath, 258 }); 259 260 const chunks = []; 261 262 function onResponse(httpRes) { 263 function parseChunks() { 264 const resBody = Buffer.concat(chunks).toString(); 265 if (httpRes.statusCode !== 200) { 266 reject(new Error(`Unexpected ${httpRes.statusCode}: ${resBody}`)); 267 return; 268 } 269 try { 270 resolve(JSON.parse(resBody)); 271 } catch (parseError) { 272 reject(new Error(`Response didn't contain JSON: ${resBody}`)); 273 return; 274 } 275 } 276 277 httpRes.on('error', reject); 278 httpRes.on('data', (chunk) => chunks.push(chunk)); 279 httpRes.on('end', parseChunks); 280 } 281 282 httpReq.on('error', reject); 283 httpReq.on('response', onResponse); 284 }); 285 } 286 287 connect(port, host) { 288 this._port = port; 289 this._host = host; 290 return this._discoverWebsocketPath() 291 .then((urlPath) => this._connectWebsocket(urlPath)); 292 } 293 294 _discoverWebsocketPath() { 295 return this._fetchJSON('/json') 296 .then(([{ webSocketDebuggerUrl }]) => 297 URL.parse(webSocketDebuggerUrl).path); 298 } 299 300 _connectWebsocket(urlPath) { 301 this.reset(); 302 303 const key1 = crypto.randomBytes(16).toString('base64'); 304 debuglog('request websocket', key1); 305 306 const httpReq = this._http = http.request({ 307 host: this._host, 308 port: this._port, 309 path: urlPath, 310 headers: { 311 Connection: 'Upgrade', 312 Upgrade: 'websocket', 313 'Sec-WebSocket-Key': key1, 314 'Sec-WebSocket-Version': '13', 315 }, 316 }); 317 httpReq.on('error', (e) => { 318 this.emit('error', e); 319 }); 320 httpReq.on('response', (httpRes) => { 321 if (httpRes.statusCode >= 400) { 322 process.stderr.write(`Unexpected HTTP code: ${httpRes.statusCode}\n`); 323 httpRes.pipe(process.stderr); 324 } else { 325 httpRes.pipe(process.stderr); 326 } 327 }); 328 329 const handshakeListener = (res, socket) => { 330 // TODO: we *could* validate res.headers[sec-websocket-accept] 331 debuglog('websocket upgrade'); 332 333 this._socket = socket; 334 socket.on('data', this.handleChunk); 335 socket.on('close', () => { 336 this.emit('close'); 337 }); 338 339 this.emit('ready'); 340 }; 341 342 return new Promise((resolve, reject) => { 343 this.once('error', reject); 344 this.once('ready', resolve); 345 346 httpReq.on('upgrade', handshakeListener); 347 httpReq.end(); 348 }); 349 } 350} 351 352module.exports = Client; 353