• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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