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