Compare commits

...
Author SHA1 Message Date
Rob Hogan 05fd9a19e9 dev-middleware: Use serverBaseUrl for local->server fetches (#47653)
Summary:

## Context

Currently, when `nativeSourceCodeFetching == false`, `inspector-proxy` attempts to pre-fetch source maps, given the URL from a `Debugger.scriptParsed` event, and embeds them into `Debugger.scriptParsed`'s `sourceMapURL` using a data URI.

This was originally to support frontends that did not perform HTTP requests or were blocked (eg by CORS), but we're retaining it for the moment because it's more performant than lazy loading the source map.

Similarly, we perform middleware->server fetches to respond to `Debugger.getScriptSource` events.

To make these fetches for URLs that target `10.0.2.2` (ie, addressable from within an Android emulator) (etc), we rewrite `10.0.2.2`->`localhost` and perform a `fetch` from the Node process running dev-middleware.

## The problem

Consider a setup where:
 - Metro is running on a remote server, listening on `8081`.
 - Dev machine tunnels `localhost:8082` -> remote `8081`.
 - An app is running on an Android emulator on the dev machine, with bundle URL configured to `10.0.2.2:8082`.

In this case, we'll rewrite `10.0.2.2:8082` to `localhost:8082`, which *is* reachable and correct from the dev machine, but *not* from the machine where Metro is running, so the `fetch` of a source map from the inspector proxy will fail.

## Motivation

This might seem like a niche case, but it's part of fixing a series of unsafe assumptions that currently prevent us from running DevTools on an arbitrary port.

## This fix

Preserve the current behaviour (simple `10.0.2.2`<=>`localhost`) for URLs sent to the frontend, but construct a separate, server-relative URL, using the configured `serverBaseUrl`, for `fetch` calls within dev-middleware.

Changelog:
[General][Fixed] RN DevTools: Fix fetching sources and source maps when the dev-server is remote and not tunnelled via the same port+protocol.

Reviewed By: huntie

Differential Revision: D65993910
2024-11-18 15:34:47 -08:00
2 changed files with 85 additions and 84 deletions
@@ -71,6 +71,17 @@ type DebuggerConnection = {
const REACT_NATIVE_RELOADABLE_PAGE_ID = '-1';
export type DeviceOptions = $ReadOnly<{
id: string,
name: string,
app: string,
socket: WS,
projectRoot: string,
eventReporter: ?EventReporter,
createMessageMiddleware: ?CreateCustomMessageHandlerFn,
serverRelativeBaseUrl: URL,
}>;
/**
* Device class represents single device connection to Inspector Proxy. Each device
* can have multiple inspectable pages.
@@ -125,41 +136,29 @@ export default class Device {
#connectedPageIds: Set<string> = new Set();
constructor(
id: string,
name: string,
app: string,
socket: WS,
projectRoot: string,
eventReporter: ?EventReporter,
createMessageMiddleware: ?CreateCustomMessageHandlerFn,
serverBaseUrl?: URL,
) {
this.#dangerouslyConstruct(
id,
name,
app,
socket,
projectRoot,
eventReporter,
createMessageMiddleware,
);
// A base HTTP(S) URL to the server, relative to this server.
#serverRelativeBaseUrl: URL;
constructor(deviceOptions: DeviceOptions) {
this.#dangerouslyConstruct(deviceOptions);
}
#dangerouslyConstruct(
id: string,
name: string,
app: string,
socket: WS,
projectRoot: string,
eventReporter: ?EventReporter,
createMessageMiddleware: ?CreateCustomMessageHandlerFn,
) {
#dangerouslyConstruct({
id,
name,
app,
socket,
projectRoot,
eventReporter,
createMessageMiddleware,
serverRelativeBaseUrl,
}: DeviceOptions) {
this.#id = id;
this.#name = name;
this.#app = app;
this.#deviceSocket = socket;
this.#projectRoot = projectRoot;
this.#serverRelativeBaseUrl = serverRelativeBaseUrl;
this.#deviceEventReporter = eventReporter
? new DeviceEventReporter(eventReporter, {
deviceId: id,
@@ -239,23 +238,15 @@ export default class Device {
* This hack attempts to allow users to reload the app, either as result of a
* crash, or manually reloading, without having to restart the debugger.
*/
dangerouslyRecreateDevice(
id: string,
name: string,
app: string,
socket: WS,
projectRoot: string,
eventReporter: ?EventReporter,
createMessageMiddleware: ?CreateCustomMessageHandlerFn,
) {
dangerouslyRecreateDevice(deviceOptions: DeviceOptions) {
invariant(
id === this.#id,
deviceOptions.id === this.#id,
'dangerouslyRecreateDevice() can only be used for the same device ID',
);
const oldDebugger = this.#debuggerConnection;
if (this.#app !== app || this.#name !== name) {
if (this.#app !== deviceOptions.app || this.#name !== deviceOptions.name) {
this.#deviceSocket.close();
this.#terminateDebuggerConnection();
}
@@ -270,15 +261,7 @@ export default class Device {
});
}
this.#dangerouslyConstruct(
id,
name,
app,
socket,
projectRoot,
eventReporter,
createMessageMiddleware,
);
this.#dangerouslyConstruct(deviceOptions);
}
getName(): string {
@@ -692,25 +675,29 @@ export default class Device {
) {
const params = payload.params;
if ('sourceMapURL' in params) {
for (const hostToRewrite of REWRITE_HOSTS_TO_LOCALHOST) {
if (params.sourceMapURL.includes(hostToRewrite)) {
payload.params.sourceMapURL = params.sourceMapURL.replace(
hostToRewrite,
'localhost',
);
debuggerInfo.originalSourceURLAddress = hostToRewrite;
}
}
const sourceMapURL = this.#tryParseHTTPURL(params.sourceMapURL);
if (sourceMapURL) {
const serverRelativeUrl = new URL(sourceMapURL.href);
for (const hostToRewrite of REWRITE_HOSTS_TO_LOCALHOST) {
if (params.sourceMapURL.includes(hostToRewrite)) {
payload.params.sourceMapURL = params.sourceMapURL.replace(
hostToRewrite,
'localhost',
);
debuggerInfo.originalSourceURLAddress = hostToRewrite;
serverRelativeUrl.host = this.#serverRelativeBaseUrl.host;
serverRelativeUrl.protocol = this.#serverRelativeBaseUrl.protocol;
}
}
// Some debug clients do not support fetching HTTP URLs. If the
// message headed to the debug client identifies the source map with
// an HTTP URL, fetch the content here and convert the content to a
// Data URL (which is more widely supported) before passing the
// message to the debug client.
try {
const sourceMap = await this.#fetchText(sourceMapURL);
const sourceMap = await this.#fetchText(serverRelativeUrl);
payload.params.sourceMapURL =
'data:application/json;charset=utf-8;base64,' +
Buffer.from(sourceMap).toString('base64');
@@ -722,10 +709,23 @@ export default class Device {
}
}
if ('url' in params) {
for (const hostToRewrite of REWRITE_HOSTS_TO_LOCALHOST) {
if (params.url.includes(hostToRewrite)) {
payload.params.url = params.url.replace(hostToRewrite, 'localhost');
debuggerInfo.originalSourceURLAddress = hostToRewrite;
const originalParamsUrl = params.url;
let serverRelativeUrl = originalParamsUrl;
const parsedUrl = this.#tryParseHTTPURL(originalParamsUrl);
if (parsedUrl) {
for (const hostToRewrite of REWRITE_HOSTS_TO_LOCALHOST) {
if (parsedUrl.hostname === hostToRewrite) {
// URL is device-relative and points to the host - rewrite it to
// use localhost.
parsedUrl.hostname = 'localhost';
payload.params.url = parsedUrl.href;
debuggerInfo.originalSourceURLAddress = hostToRewrite;
// Determine the server-relative URL.
parsedUrl.host = this.#serverRelativeBaseUrl.host;
parsedUrl.protocol = this.#serverRelativeBaseUrl.protocol;
serverRelativeUrl = parsedUrl.href;
}
}
}
@@ -738,9 +738,13 @@ export default class Device {
debuggerInfo.prependedFilePrefix = true;
}
// $FlowFixMe[prop-missing]
if (params.scriptId != null) {
this.#scriptIdToSourcePathMapping.set(params.scriptId, params.url);
if ('scriptId' in params && params.scriptId != null) {
// Set a server-relative URL to locally fetch source by script ID
// on Debugger.getScriptSource.
this.#scriptIdToSourcePathMapping.set(
params.scriptId,
serverRelativeUrl,
);
}
}
}
@@ -907,6 +911,7 @@ export default class Device {
if (pathToSource != null) {
const httpURL = this.#tryParseHTTPURL(pathToSource);
if (httpURL) {
// URL is server-relatve, so we should be able to fetch it from here.
this.#fetchText(httpURL).then(
text => sendSuccessResponse(text),
err =>
@@ -12,6 +12,7 @@
import type {EventReporter} from '../types/EventReporter';
import type {Experiments} from '../types/Experiments';
import type {CreateCustomMessageHandlerFn} from './CustomMessageHandler';
import type {DeviceOptions} from './Device';
import type {
JsonPagesListResponse,
JsonVersionResponse,
@@ -227,27 +228,22 @@ export default class InspectorProxy implements InspectorProxyQueries {
const oldDevice = this.#devices.get(deviceId);
let newDevice;
const deviceOptions: DeviceOptions = {
id: deviceId,
name: deviceName,
app: appName,
socket,
projectRoot: this.#projectRoot,
eventReporter: this.#eventReporter,
createMessageMiddleware: this.#customMessageHandler,
serverRelativeBaseUrl: this.#serverBaseUrl,
};
if (oldDevice) {
oldDevice.dangerouslyRecreateDevice(
deviceId,
deviceName,
appName,
socket,
this.#projectRoot,
this.#eventReporter,
this.#customMessageHandler,
);
oldDevice.dangerouslyRecreateDevice(deviceOptions);
newDevice = oldDevice;
} else {
newDevice = new Device(
deviceId,
deviceName,
appName,
socket,
this.#projectRoot,
this.#eventReporter,
this.#customMessageHandler,
);
newDevice = new Device(deviceOptions);
}
this.#devices.set(deviceId, newDevice);