react-native-flip
Version:
222 lines (197 loc) • 7.14 kB
Flow
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
;
const Device = require('./Device');
const WS = require('ws');
const debug = require('debug')('Metro:InspectorProxy');
const url = require('url');
import type {
JsonPagesListResponse,
JsonVersionResponse,
Page,
PageDescription,
} from './types';
import type {Server as HttpServer, IncomingMessage, ServerResponse} from 'http';
import type {Server as HttpsServer} from 'https';
const WS_DEVICE_URL = '/inspector/device';
const WS_DEBUGGER_URL = '/inspector/debug';
const PAGES_LIST_JSON_URL = '/json';
const PAGES_LIST_JSON_URL_2 = '/json/list';
const PAGES_LIST_JSON_VERSION_URL = '/json/version';
const INTERNAL_ERROR_CODE = 1011;
/**
* Main Inspector Proxy class that connects JavaScript VM inside Android/iOS apps and JS debugger.
*/
class InspectorProxy {
// Root of the project used for relative to absolute source path conversion.
_projectRoot: string;
// Maps device ID to Device instance.
_devices: Map<number, Device>;
// Internal counter for device IDs -- just gets incremented for each new device.
_deviceCounter: number = 0;
// We store server's address with port (like '127.0.0.1:8081') to be able to build URLs
// (devtoolsFrontendUrl and webSocketDebuggerUrl) for page descriptions. These URLs are used
// by debugger to know where to connect.
_serverAddressWithPort: string = '';
constructor(projectRoot: string) {
this._projectRoot = projectRoot;
this._devices = new Map();
}
// Process HTTP request sent to server. We only respond to 2 HTTP requests:
// 1. /json/version returns Chrome debugger protocol version that we use
// 2. /json and /json/list returns list of page descriptions (list of inspectable apps).
// This list is combined from all the connected devices.
processRequest(
request: IncomingMessage,
response: ServerResponse,
next: (?Error) => mixed,
) {
if (
request.url === PAGES_LIST_JSON_URL ||
request.url === PAGES_LIST_JSON_URL_2
) {
// Build list of pages from all devices.
let result = [];
Array.from(this._devices.entries()).forEach(
([deviceId: number, device: Device]) => {
result = result.concat(
device
.getPagesList()
.map((page: Page) =>
this._buildPageDescription(deviceId, device, page),
),
);
},
);
this._sendJsonResponse(response, result);
} else if (request.url === PAGES_LIST_JSON_VERSION_URL) {
this._sendJsonResponse(response, {
Browser: 'Mobile JavaScript',
'Protocol-Version': '1.1',
});
} else {
next();
}
}
// Adds websocket listeners to the provided HTTP/HTTPS server.
addWebSocketListener(server: HttpServer | HttpsServer) {
const {port} = server.address();
if (server.address().family === 'IPv6') {
this._serverAddressWithPort = `[::]:${port}`;
} else {
this._serverAddressWithPort = `localhost:${port}`;
}
this._addDeviceConnectionHandler(server);
this._addDebuggerConnectionHandler(server);
}
// Converts page information received from device into PageDescription object
// that is sent to debugger.
_buildPageDescription(
deviceId: number,
device: Device,
page: Page,
): PageDescription {
const debuggerUrl = `${this._serverAddressWithPort}${WS_DEBUGGER_URL}?device=${deviceId}&page=${page.id}`;
const webSocketDebuggerUrl = 'ws://' + debuggerUrl;
const devtoolsFrontendUrl =
'chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=' +
encodeURIComponent(debuggerUrl);
return {
id: `${deviceId}-${page.id}`,
description: page.app,
title: page.title,
faviconUrl: 'https://reactjs.org/favicon.ico',
devtoolsFrontendUrl,
type: 'node',
webSocketDebuggerUrl,
vm: page.vm,
};
}
// Sends object as response to HTTP request.
// Just serializes object using JSON and sets required headers.
_sendJsonResponse(
response: ServerResponse,
object: JsonPagesListResponse | JsonVersionResponse,
) {
const data = JSON.stringify(object, null, 2);
response.writeHead(200, {
'Content-Type': 'application/json; charset=UTF-8',
'Cache-Control': 'no-cache',
'Content-Length': data.length.toString(),
Connection: 'close',
});
response.end(data);
}
// Adds websocket handler for device connections.
// Device connects to /inspector/device and passes device and app names as
// HTTP GET params.
// For each new websocket connection we parse device and app names and create
// new instance of Device class.
_addDeviceConnectionHandler(server: HttpServer | HttpsServer) {
const wss = new WS.Server({
server,
path: WS_DEVICE_URL,
perMessageDeflate: true,
});
// $FlowFixMe[value-as-type]
wss.on('connection', async (socket: WS) => {
try {
const query = url.parse(socket.upgradeReq.url || '', true).query || {};
const deviceName = query.name || 'Unknown';
const appName = query.app || 'Unknown';
const deviceId = this._deviceCounter++;
this._devices.set(
deviceId,
new Device(deviceId, deviceName, appName, socket, this._projectRoot),
);
debug(`Got new connection: device=${deviceName}, app=${appName}`);
socket.on('close', () => {
this._devices.delete(deviceId);
debug(`Device ${deviceName} disconnected.`);
});
} catch (e) {
console.error('error', e);
socket.close(INTERNAL_ERROR_CODE, e);
}
});
}
// Adds websocket handler for debugger connections.
// Debugger connects to webSocketDebuggerUrl that we return as part of page description
// in /json response.
// When debugger connects we try to parse device and page IDs from the query and pass
// websocket object to corresponding Device instance.
_addDebuggerConnectionHandler(server: HttpServer | HttpsServer) {
const wss = new WS.Server({
server,
path: WS_DEBUGGER_URL,
perMessageDeflate: false,
});
// $FlowFixMe[value-as-type]
wss.on('connection', async (socket: WS) => {
try {
const query = url.parse(socket.upgradeReq.url || '', true).query || {};
const deviceId = query.device;
const pageId = query.page;
if (deviceId == null || pageId == null) {
throw new Error('Incorrect URL - must provide device and page IDs');
}
const device = this._devices.get(parseInt(deviceId, 10));
if (device == null) {
throw new Error('Unknown device with ID ' + deviceId);
}
device.handleDebuggerConnection(socket, pageId);
} catch (e) {
console.error(e);
socket.close(INTERNAL_ERROR_CODE, e);
}
});
}
}
module.exports = InspectorProxy;