Skip to content

Commit acf384a

Browse files
robhoganfacebook-github-bot
authored andcommitted
dev-middleware: Redefine "serverBaseUrl" as server-relative, '/json/list' by requestor (#47628)
Summary: Pull Request resolved: #47628 `serverBaseUrl` is currently documented as: > The base URL to the dev server, as addressible from the local developer machine This is problematic in general because `dev-middleware` on a server doesn't necessarily know about where clients might be reaching it from, how tunnels or port-forwards are set up, etc., and this can change over the lifetime of the server and vary between clients. Indeed, our own use of `serverBaseUrl` from both `community-cli-plugin` and internally simply sets it to the host and port the dev server is listening on - ie it's the address of the dev server accessible *from the server*. This PR changes the docs, redefining `serverBaseUrl`, to match the way we currently specify it. One usage where we *do* want the previously documented behaviour is in responses to `/json/list` (`getPageDescriptions`) where the URLs in the response should be reachable by a browser requesting `/json/list`. Here, we use the request (host header, etc.) to attempt to get working base URL. History: It should be mentioned that this is the latest in a series of changes like this: - #39394 - #39456 Learning from those: - This change does *not* break Android emulators, which routes `10.0.2.2` to localhost, or other routed devices, because `/open-debugger` still uses server-relative URLs, and now formally delegates to `BrowserLauncher` to decide what to do with those URLs (internally, VSCode / `xdg-open` handles port forwarding) - Middleware configuration is no longer required to specify how it is reachable from clients. This sets up some subsequent changes for more robust handling of tunnelled connections. Changelog: [General][Breaking] dev-middleware: Frameworks should specify `serverBaseUrl` relative to the middleware host. Reviewed By: huntie Differential Revision: D65974487 fbshipit-source-id: 1face8fc7715df387f75b329e80932d8543ee419
1 parent ca9c563 commit acf384a

File tree

8 files changed

+117
-20
lines changed

8 files changed

+117
-20
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
import getBaseUrlFromRequest from '../utils/getBaseUrlFromRequest';
13+
14+
test('returns a base url based on req.headers.host', () => {
15+
expect(
16+
getBaseUrlFromRequest(makeRequest('localhost:8081', false))?.href,
17+
).toEqual('http://localhost:8081/');
18+
});
19+
20+
test('identifies https using socket.encrypted', () => {
21+
expect(
22+
getBaseUrlFromRequest(makeRequest('secure.net:8443', true))?.href,
23+
).toEqual('https://secure.net:8443/');
24+
});
25+
26+
test('works with ipv6 hosts', () => {
27+
expect(getBaseUrlFromRequest(makeRequest('[::1]:8081', false))?.href).toEqual(
28+
'http://[::1]:8081/',
29+
);
30+
});
31+
32+
test('returns null on an invalid host header', () => {
33+
expect(getBaseUrlFromRequest(makeRequest('local[]host', false))).toBeNull();
34+
});
35+
36+
test('returns null on an empty host header', () => {
37+
expect(getBaseUrlFromRequest(makeRequest(null, false))).toBeNull();
38+
});
39+
40+
function makeRequest(
41+
host: ?string,
42+
encrypted: boolean,
43+
): http$IncomingMessage<> | http$IncomingMessage<tls$TLSSocket> {
44+
// $FlowIgnore[incompatible-return] Partial mock of request
45+
return {
46+
socket: encrypted ? {encrypted: true} : {},
47+
headers: host != null ? {host} : {},
48+
};
49+
}

packages/dev-middleware/src/createDevMiddleware.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,8 @@ type Options = $ReadOnly<{
2828
projectRoot: string,
2929

3030
/**
31-
* The base URL to the dev server, as addressible from the local developer
32-
* machine. This is used in responses which return URLs to other endpoints,
33-
* e.g. the debugger frontend and inspector proxy targets.
34-
*
35-
* Example: `'http://localhost:8081'`.
31+
* The base URL to the dev server, as reachable from the machine on which
32+
* dev-middleware is hosted. Typically `http://localhost:${metroPort}`.
3633
*/
3734
serverBaseUrl: string,
3835

packages/dev-middleware/src/inspector-proxy/Device.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export default class Device {
133133
projectRoot: string,
134134
eventReporter: ?EventReporter,
135135
createMessageMiddleware: ?CreateCustomMessageHandlerFn,
136+
serverBaseUrl?: URL,
136137
) {
137138
this.#dangerouslyConstruct(
138139
id,

packages/dev-middleware/src/inspector-proxy/InspectorProxy.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {IncomingMessage, ServerResponse} from 'http';
2222
// $FlowFixMe[cannot-resolve-module] libdef missing in RN OSS
2323
import type {Timeout} from 'timers';
2424

25+
import getBaseUrlFromRequest from '../utils/getBaseUrlFromRequest';
2526
import Device from './Device';
2627
import nullthrows from 'nullthrows';
2728
// Import these from node:timers to get the correct Flow types.
@@ -47,7 +48,7 @@ export interface InspectorProxyQueries {
4748
* Returns list of page descriptions ordered by device connection order, then
4849
* page addition order.
4950
*/
50-
getPageDescriptions(): Array<PageDescription>;
51+
getPageDescriptions(requestorRelativeBaseUrl: URL): Array<PageDescription>;
5152
}
5253

5354
/**
@@ -57,8 +58,8 @@ export default class InspectorProxy implements InspectorProxyQueries {
5758
// Root of the project used for relative to absolute source path conversion.
5859
#projectRoot: string;
5960

60-
/** The base URL to the dev server from the developer machine. */
61-
#serverBaseUrl: string;
61+
// The base URL to the dev server from the dev-middleware host.
62+
#serverBaseUrl: URL;
6263

6364
// Maps device ID to Device instance.
6465
#devices: Map<string, Device>;
@@ -81,22 +82,27 @@ export default class InspectorProxy implements InspectorProxyQueries {
8182
customMessageHandler: ?CreateCustomMessageHandlerFn,
8283
) {
8384
this.#projectRoot = projectRoot;
84-
this.#serverBaseUrl = serverBaseUrl;
85+
this.#serverBaseUrl = new URL(serverBaseUrl);
8586
this.#devices = new Map();
8687
this.#eventReporter = eventReporter;
8788
this.#experiments = experiments;
8889
this.#customMessageHandler = customMessageHandler;
8990
}
9091

91-
getPageDescriptions(): Array<PageDescription> {
92+
getPageDescriptions(requestorRelativeBaseUrl: URL): Array<PageDescription> {
9293
// Build list of pages from all devices.
9394
let result: Array<PageDescription> = [];
9495
Array.from(this.#devices.entries()).forEach(([deviceId, device]) => {
9596
result = result.concat(
9697
device
9798
.getPagesList()
9899
.map((page: Page) =>
99-
this.#buildPageDescription(deviceId, device, page),
100+
this.#buildPageDescription(
101+
deviceId,
102+
device,
103+
page,
104+
requestorRelativeBaseUrl,
105+
),
100106
),
101107
);
102108
});
@@ -117,7 +123,12 @@ export default class InspectorProxy implements InspectorProxyQueries {
117123
pathname === PAGES_LIST_JSON_URL ||
118124
pathname === PAGES_LIST_JSON_URL_2
119125
) {
120-
this.#sendJsonResponse(response, this.getPageDescriptions());
126+
this.#sendJsonResponse(
127+
response,
128+
this.getPageDescriptions(
129+
getBaseUrlFromRequest(request) ?? this.#serverBaseUrl,
130+
),
131+
);
121132
} else if (pathname === PAGES_LIST_JSON_VERSION_URL) {
122133
this.#sendJsonResponse(response, {
123134
Browser: 'Mobile JavaScript',
@@ -143,8 +154,9 @@ export default class InspectorProxy implements InspectorProxyQueries {
143154
deviceId: string,
144155
device: Device,
145156
page: Page,
157+
requestorRelativeBaseUrl: URL,
146158
): PageDescription {
147-
const {host, protocol} = new URL(this.#serverBaseUrl);
159+
const {host, protocol} = requestorRelativeBaseUrl;
148160
const webSocketScheme = protocol === 'https:' ? 'wss' : 'ws';
149161

150162
const webSocketUrlWithoutProtocol = `${host}${WS_DEBUGGER_URL}?device=${deviceId}&page=${page.id}`;

packages/dev-middleware/src/middleware/openDebuggerMiddleware.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,14 @@ export default function openDebuggerMiddleware({
7272
...
7373
} = query;
7474

75-
const targets = inspectorProxy.getPageDescriptions().filter(
76-
// Only use targets with better reloading support
77-
app =>
78-
app.title === LEGACY_SYNTHETIC_PAGE_TITLE ||
79-
app.reactNative.capabilities?.nativePageReloads === true,
80-
);
75+
const targets = inspectorProxy
76+
.getPageDescriptions(new URL(serverBaseUrl))
77+
.filter(
78+
// Only use targets with better reloading support
79+
app =>
80+
app.title === LEGACY_SYNTHETIC_PAGE_TITLE ||
81+
app.reactNative.capabilities?.nativePageReloads === true,
82+
);
8183

8284
let target;
8385

packages/dev-middleware/src/types/BrowserLauncher.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export interface BrowserLauncher {
1818
* Attempt to open a debugger frontend URL in a browser app window,
1919
* optionally returning an object to control the launched browser instance.
2020
* The browser used should be capable of running Chrome DevTools.
21+
*
22+
* The provided url is based on serverBaseUrl, and therefore reachable from
23+
* the host of dev-middleware. Implementations are responsible for rewriting
24+
* this as necessary where the server is remote.
2125
*/
2226
launchDebuggerAppWindow: (url: string) => Promise<void>;
2327
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
// Determine the base URL (scheme and host) used by a client to reach this
13+
// server.
14+
//
15+
// TODO: Support X-Forwarded-Host, etc. for trusted proxies
16+
export default function getBaseUrlFromRequest(
17+
req: http$IncomingMessage<tls$TLSSocket> | http$IncomingMessage<net$Socket>,
18+
): ?URL {
19+
const hostHeader = req.headers.host;
20+
if (hostHeader == null) {
21+
return null;
22+
}
23+
// `encrypted` is always true for TLS sockets and undefined for net
24+
// https://github.com/nodejs/node/issues/41863#issuecomment-1030709186
25+
const scheme = req.socket.encrypted === true ? 'https' : 'http';
26+
const url = `${scheme}://${req.headers.host}`;
27+
return URL.canParse(url) ? new URL(url) : null;
28+
}

packages/dev-middleware/src/utils/getDevToolsFrontendUrl.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ function getWsParam({
6565
const serverHost = new URL(devServerUrl).host;
6666
let value;
6767
if (wsUrl.host === serverHost) {
68-
// Use a path-absolute (host-relative) URL
68+
// Use a path-absolute (host-relative) URL if the WS server and frontend
69+
// server are colocated. This is more robust for cases where the frontend
70+
// may actually load through a tunnel or proxy, and the WS connection
71+
// should therefore do the same.
72+
//
6973
// Depends on https://github.com/facebookexperimental/rn-chrome-devtools-frontend/pull/4
7074
value = wsUrl.pathname + wsUrl.search + wsUrl.hash;
7175
} else {

0 commit comments

Comments
 (0)