You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

210 lines
6.8 KiB

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
"use strict";
const Device = require("./Device");
const debug = require("debug")("Metro:InspectorProxy");
const url = require("url");
const WS = require("ws");
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.
// Maps device ID to Device instance.
// Internal counter for device IDs -- just gets incremented for each new device.
_deviceCounter = 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 = "";
constructor(projectRoot) {
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, response, next) {
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, device]) => {
result = result.concat(
device
.getPagesList()
.map((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.
createWebSocketListeners(server) {
const { port } = server.address();
if (server.address().family === "IPv6") {
this._serverAddressWithPort = `[::1]:${port}`;
} else {
this._serverAddressWithPort = `localhost:${port}`;
}
return {
[WS_DEVICE_URL]: this._createDeviceConnectionWSServer(),
[WS_DEBUGGER_URL]: this._createDebuggerConnectionWSServer(),
};
} // Converts page information received from device into PageDescription object
// that is sent to debugger.
_buildPageDescription(deviceId, device, page) {
const debuggerUrl = `${this._serverAddressWithPort}${WS_DEBUGGER_URL}?device=${deviceId}&page=${page.id}`;
const webSocketDebuggerUrl = "ws://" + debuggerUrl;
const devtoolsFrontendUrl =
"devtools://devtools/bundled/js_app.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, object) {
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.
_createDeviceConnectionWSServer() {
const wss = new WS.Server({
noServer: true,
perMessageDeflate: true,
}); // $FlowFixMe[value-as-type]
wss.on("connection", async (socket, req) => {
try {
const query = url.parse(req.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) {
var _e$toString;
console.error("error", e);
socket.close(
INTERNAL_ERROR_CODE,
(_e$toString = e === null || e === void 0 ? void 0 : e.toString()) !==
null && _e$toString !== void 0
? _e$toString
: "Unknown error"
);
}
});
return wss;
} // Returns 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.
_createDebuggerConnectionWSServer() {
const wss = new WS.Server({
noServer: true,
perMessageDeflate: false,
}); // $FlowFixMe[value-as-type]
wss.on("connection", async (socket, req) => {
try {
const query = url.parse(req.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) {
var _e$toString2;
console.error(e);
socket.close(
INTERNAL_ERROR_CODE,
(_e$toString2 =
e === null || e === void 0 ? void 0 : e.toString()) !== null &&
_e$toString2 !== void 0
? _e$toString2
: "Unknown error"
);
}
});
return wss;
}
}
module.exports = InspectorProxy;