Compare commits

...
Author SHA1 Message Date
Rob Hogan b55a871fa6 dev-middleware: Don't assume device-relative and debugger-relative URLs have the same port - remove CORS reliance
Summary:
Currently, if a device is connected to the bundler via, say, `http://10.0.2.2:8081` (Android default), and a debugger is opened on `http://localhost:8081`, we rewrite hostnames `10.0.2.2.`<->`localhost` so that URLs are correct relative to each.

However, if the debugger is on a different port or protocol, this breaks down - because we only rewrite hostnames.

This fixes that by using the debugger's connecting `Host` header and `encrypted` state to derive the base URL of the server relative to the frontend. We then update the rewriting logic to use this actual full origin (protocol + host) in place of the device-relative origin.

Changelog:
[General][Fixed] dev-middleware: Fix URL rewriting where device and debugger reach the server on different ports/protocols.

Differential Revision: D66077627
2024-11-21 07:10:15 -08:00
Rob Hogan 4effc43f41 dev-middleware: Refactor urlRegex rewriting, regex-escape IP4 addresses (#47872)
Summary:

Largely a refactoring to the way we currently rewrite `url`/`urlRegex` in `Debugger.setBreakpointByURL` CDP requests (debugger->target). 

Rewriting regexes is fragile, it only really works if we can assume `'localhost'` appears literally and that ports and protocols don't need changing. The intention here is to freeze the current behaviour so as not to break anyone relying on it (if anyone is), and decouple it from more robust rewriting we want to generalise.

Also adds simple regex escaping to host names (always IPv4 addresses) we inject into regex patterns, since the previous approach could've led to false matches in unlikely edge cases.

Changelog:
[General][Fixed] dev-middleware: Regex-escape IP addresses in urlRegex replacements

Reviewed By: huntie

Differential Revision: D66238782
2024-11-21 07:10:15 -08:00
3 changed files with 83 additions and 48 deletions
@@ -391,7 +391,7 @@ describe.each(['HTTP', 'HTTPS'])(
},
});
expect(setBreakpointByUrlRegexMessage.params.urlRegex).toEqual(
`${sourceHost}:${serverRef.port}|example.com:2000`,
`${sourceHost.replaceAll('.', '\\.')}:${serverRef.port}|example.com:2000`,
);
} finally {
device.close();
@@ -60,13 +60,15 @@ const FILE_PREFIX = 'file://';
type DebuggerConnection = {
// Debugger web socket connection
socket: WS,
// If we replaced address (like '10.0.2.2') to localhost we need to store original
// address because Chrome uses URL or urlRegex params (instead of scriptId) to set breakpoints.
originalSourceURLAddress?: string,
// If we replaced a device-relative origin (like 'http://10.0.2.2:8082') with
// debugger-relative, we store the original address to reverse the operation
// on messages back from the frontend, such as setting breakpoints.
originalSourceURLOrigin?: string,
prependedFilePrefix: boolean,
pageId: string,
userAgent: string | null,
customHandler: ?CustomMessageHandler,
debuggerRelativeBaseUrl: URL,
};
const REACT_NATIVE_RELOADABLE_PAGE_ID = '-1';
@@ -264,6 +266,7 @@ export default class Device {
oldDebugger.socket.removeAllListeners();
this.#deviceSocket.close();
this.handleDebuggerConnection(oldDebugger.socket, oldDebugger.pageId, {
debuggerRelativeBaseUrl: oldDebugger.debuggerRelativeBaseUrl,
userAgent: oldDebugger.userAgent,
});
}
@@ -294,7 +297,11 @@ export default class Device {
handleDebuggerConnection(
socket: WS,
pageId: string,
metadata: $ReadOnly<{
{
debuggerRelativeBaseUrl,
userAgent,
}: $ReadOnly<{
debuggerRelativeBaseUrl: URL,
userAgent: string | null,
}>,
) {
@@ -305,7 +312,8 @@ export default class Device {
if (!page) {
debug(
`Got new debugger connection for page ${pageId} of ${this.#name}, but no such page exists`,
`Got new debugger connection via ${debuggerRelativeBaseUrl.href} for ` +
`page ${pageId} of ${this.#name}, but no such page exists`,
);
socket.close();
return;
@@ -319,20 +327,24 @@ export default class Device {
this.#deviceEventReporter?.logConnection('debugger', {
pageId,
frontendUserAgent: metadata.userAgent,
frontendUserAgent: userAgent,
});
const debuggerInfo = {
socket,
prependedFilePrefix: false,
pageId,
userAgent: metadata.userAgent,
userAgent: userAgent,
customHandler: null,
debuggerRelativeBaseUrl,
};
this.#debuggerConnection = debuggerInfo;
debug(`Got new debugger connection for page ${pageId} of ${this.#name}`);
debug(
`Got new debugger connection via ${debuggerRelativeBaseUrl.href} for ` +
`page ${pageId} of ${this.#name}`,
);
if (this.#debuggerConnection && this.#createCustomMessageHandler) {
this.#debuggerConnection.customHandler = this.#createCustomMessageHandler(
@@ -386,7 +398,7 @@ export default class Device {
const debuggerRequest = JSON.parse(message);
this.#deviceEventReporter?.logRequest(debuggerRequest, 'debugger', {
pageId: this.#debuggerConnection?.pageId ?? null,
frontendUserAgent: metadata.userAgent,
frontendUserAgent: userAgent,
prefersFuseboxFrontend: this.#isPageFuseboxFrontend(
this.#debuggerConnection?.pageId,
),
@@ -690,9 +702,8 @@ export default class Device {
// This is not exposed to the debugger.
const serverRelativeUrl = new URL(sourceMapURL.href);
// Rewrite device-relative URLs to localhost-relative URLs for the
// debugger.
// TODO: Fix the assumption that localhost:[same port] is correct.
// Rewrite device-relative URLs to de debugger-relative URLs for the
// frontend.
if (
// sourceMapURL is a device-relative url to the server.
// May or may not be reachable from the frontend.
@@ -704,11 +715,14 @@ export default class Device {
REWRITE_HOSTS_TO_LOCALHOST.has(this.#deviceRelativeBaseUrl.hostname)
) {
const debuggerRelativeURL = new URL(sourceMapURL.href);
debuggerRelativeURL.hostname = 'localhost';
debuggerRelativeURL.host =
debuggerInfo.debuggerRelativeBaseUrl.host;
debuggerRelativeURL.protocol =
debuggerInfo.debuggerRelativeBaseUrl.protocol;
serverRelativeUrl.host = this.#serverRelativeBaseUrl.host;
serverRelativeUrl.protocol = this.#serverRelativeBaseUrl.protocol;
debuggerInfo.originalSourceURLAddress =
this.#deviceRelativeBaseUrl.hostname;
debuggerInfo.originalSourceURLOrigin =
this.#deviceRelativeBaseUrl.origin;
payload.params.sourceMapURL = debuggerRelativeURL.href;
}
@@ -730,9 +744,8 @@ export default class Device {
}
}
if ('url' in params) {
const originalParamsUrl = params.url;
let serverRelativeUrl = originalParamsUrl;
const parsedUrl = this.#tryParseHTTPURL(originalParamsUrl);
let serverRelativeUrl = params.url;
const parsedUrl = this.#tryParseHTTPURL(params.url);
// Rewrite device-relative URLs pointing to the server so that they're
// reachable from the frontend.
if (
@@ -748,10 +761,11 @@ export default class Device {
) {
// URL is device-relative and points to the host - rewrite it to
// use localhost.
parsedUrl.hostname = 'localhost';
parsedUrl.host = debuggerInfo.debuggerRelativeBaseUrl.host;
parsedUrl.protocol = debuggerInfo.debuggerRelativeBaseUrl.protocol;
payload.params.url = parsedUrl.href;
debuggerInfo.originalSourceURLAddress =
this.#deviceRelativeBaseUrl.hostname;
debuggerInfo.originalSourceURLOrigin =
this.#deviceRelativeBaseUrl.origin;
// Determine the server-relative URL.
parsedUrl.host = this.#serverRelativeBaseUrl.host;
@@ -869,36 +883,54 @@ export default class Device {
debuggerInfo: DebuggerConnection,
): CDPRequest<'Debugger.setBreakpointByUrl'> {
// If we replaced Android emulator's address to localhost we need to change it back.
if (debuggerInfo.originalSourceURLAddress != null) {
const processedReq = {...req, params: {...req.params}};
if (processedReq.params.url != null) {
processedReq.params.url = processedReq.params.url.replace(
'localhost',
debuggerInfo.originalSourceURLAddress,
);
const {
debuggerRelativeBaseUrl,
originalSourceURLOrigin,
prependedFilePrefix,
} = debuggerInfo;
const processedReq = {...req, params: {...req.params}};
if (originalSourceURLOrigin != null && processedReq.params.url != null) {
processedReq.params.url = processedReq.params.url.replace(
debuggerRelativeBaseUrl.origin,
originalSourceURLOrigin,
);
if (
processedReq.params.url &&
processedReq.params.url.startsWith(FILE_PREFIX) &&
debuggerInfo.prependedFilePrefix
) {
// Remove fake URL prefix if we modified URL in #processMessageFromDeviceLegacy.
// $FlowFixMe[incompatible-use]
processedReq.params.url = processedReq.params.url.slice(
FILE_PREFIX.length,
);
}
}
if (processedReq.params.urlRegex != null) {
processedReq.params.urlRegex = processedReq.params.urlRegex.replace(
/localhost/g,
// $FlowFixMe[incompatible-call]
debuggerInfo.originalSourceURLAddress,
if (
processedReq.params.url &&
processedReq.params.url.startsWith(FILE_PREFIX) &&
prependedFilePrefix
) {
// Remove fake URL prefix if we modified URL in #processMessageFromDeviceLegacy.
// $FlowFixMe[incompatible-use]
processedReq.params.url = processedReq.params.url.slice(
FILE_PREFIX.length,
);
}
return processedReq;
}
return req;
// Retain special case rewriting of localhost to device-relative IPs
// within regex patterns. We don't rewrite the protocol here because
// these patterns typically come from CDT reinterpreting the source URL
// `file://host/path` into the regex `host/path|file://host/path`. See:
//
// https://github.com/ChromeDevTools/devtools-frontend/blob/f913cc6d76f2e2639c05b11ba673fc880b5490dd/front_end/core/sdk/DebuggerModel.ts#L505
//
// This has always been fragile and probably unnecessary - we don't set
// `file://` source URLs. It can be removed when we drop support for
// legacy targets, if not sooner.
if (
REWRITE_HOSTS_TO_LOCALHOST.has(this.#deviceRelativeBaseUrl.hostname) &&
this.#deviceRelativeBaseUrl.port === debuggerRelativeBaseUrl.port &&
debuggerRelativeBaseUrl.hostname === 'localhost' &&
processedReq.params.urlRegex != null
) {
processedReq.params.urlRegex = processedReq.params.urlRegex.replaceAll(
'localhost',
// regex-escape IPv4
this.#deviceRelativeBaseUrl.hostname.replaceAll('.', '\\.'),
);
}
return processedReq;
}
#processDebuggerGetScriptSource(
@@ -290,6 +290,8 @@ export default class InspectorProxy implements InspectorProxyQueries {
const query = url.parse(req.url || '', true).query || {};
const deviceId = query.device;
const pageId = query.page;
const debuggerRelativeBaseUrl =
getBaseUrlFromRequest(req) ?? this.#serverBaseUrl;
if (deviceId == null || pageId == null) {
throw new Error('Incorrect URL - must provide device and page IDs');
@@ -303,6 +305,7 @@ export default class InspectorProxy implements InspectorProxyQueries {
this.#startHeartbeat(socket, DEBUGGER_HEARTBEAT_INTERVAL_MS);
device.handleDebuggerConnection(socket, pageId, {
debuggerRelativeBaseUrl,
userAgent: req.headers['user-agent'] ?? query.userAgent ?? null,
});
} catch (e) {