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