@expo/serve-sim 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/middleware.js +80 -80
- package/dist/native/serve-sim-native.node +0 -0
- package/dist/serve-sim.js +119 -119
- package/package.json +1 -1
- package/src/connect-to-fetch.ts +9 -0
- package/src/middleware.ts +68 -35
package/package.json
CHANGED
package/src/connect-to-fetch.ts
CHANGED
|
@@ -53,6 +53,9 @@ export function connectToFetch(
|
|
|
53
53
|
|
|
54
54
|
let status = 200;
|
|
55
55
|
let responseHeaders = new Headers();
|
|
56
|
+
// Set before the status is written, the way `ServerResponse.setHeader` allows. `writeHead`
|
|
57
|
+
// values win, matching Node.
|
|
58
|
+
const presetHeaders = new Headers();
|
|
56
59
|
let headersSent = false;
|
|
57
60
|
let writableEnded = false;
|
|
58
61
|
let writableFinished = false;
|
|
@@ -96,6 +99,9 @@ export function connectToFetch(
|
|
|
96
99
|
const ensureResponse = () => {
|
|
97
100
|
if (headersSent) return;
|
|
98
101
|
headersSent = true;
|
|
102
|
+
for (const [name, value] of presetHeaders) {
|
|
103
|
+
if (!responseHeaders.has(name)) responseHeaders.set(name, value);
|
|
104
|
+
}
|
|
99
105
|
resolveOnce(new Response(statusAllowsBody() ? body : null, { status, headers: responseHeaders }));
|
|
100
106
|
};
|
|
101
107
|
const writeChunk = (chunk: Buffer | string | Uint8Array) => {
|
|
@@ -122,6 +128,9 @@ export function connectToFetch(
|
|
|
122
128
|
get writableNeedDrain() { return writableNeedDrain; },
|
|
123
129
|
get statusCode() { return status; },
|
|
124
130
|
set statusCode(nextStatus: number) { status = nextStatus; },
|
|
131
|
+
setHeader(name: string, value: string) {
|
|
132
|
+
presetHeaders.set(name, value);
|
|
133
|
+
},
|
|
125
134
|
writeHead(nextStatus: number, headers?: Record<string, string | number | string[]>) {
|
|
126
135
|
status = nextStatus;
|
|
127
136
|
if (headers) {
|
package/src/middleware.ts
CHANGED
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
closeDeviceSession,
|
|
22
22
|
getDeviceSession,
|
|
23
23
|
peekDeviceSession,
|
|
24
|
-
sendCorsPreflight,
|
|
25
24
|
type HidSocket,
|
|
26
25
|
} from "./device-session";
|
|
27
26
|
import { assertPreviewAccess, assertUpgradeAccess } from "./session-auth";
|
|
@@ -179,11 +178,6 @@ function isSimulatorUdid(value: string): boolean {
|
|
|
179
178
|
return /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i.test(value);
|
|
180
179
|
}
|
|
181
180
|
|
|
182
|
-
const SCREENSHOT_RESPONSE_HEADERS = {
|
|
183
|
-
"Access-Control-Allow-Origin": "*",
|
|
184
|
-
"Cache-Control": "no-store",
|
|
185
|
-
};
|
|
186
|
-
|
|
187
181
|
/** What to do with a persisted device state when reaping during a grid poll. */
|
|
188
182
|
type StaleStateAction = "keep" | "recycle-self" | "recycle-helper";
|
|
189
183
|
|
|
@@ -807,14 +801,6 @@ function serveHelperInProcess(
|
|
|
807
801
|
void handleCameraStatus(req, res, device);
|
|
808
802
|
return true;
|
|
809
803
|
}
|
|
810
|
-
if (
|
|
811
|
-
(endpoint === "/webrtc/offer" || endpoint === "/webrtc/close" || endpoint === "/webrtc/stats"
|
|
812
|
-
|| endpoint === "/stream-settings")
|
|
813
|
-
&& req.method === "OPTIONS"
|
|
814
|
-
) {
|
|
815
|
-
sendCorsPreflight(res);
|
|
816
|
-
return true;
|
|
817
|
-
}
|
|
818
804
|
// Polled once a second by the panel and the recorder, so creating a session here would start a
|
|
819
805
|
// capture nothing asked for, and re-create one every tick after the simulator is shut down.
|
|
820
806
|
if (endpoint === "/webrtc/stats") {
|
|
@@ -977,6 +963,8 @@ export function previewConfigForState(
|
|
|
977
963
|
execToken: string,
|
|
978
964
|
streamSettingsOrCodec?: StreamSettings | string,
|
|
979
965
|
proxyHelpers = false,
|
|
966
|
+
requirePreviewToken = false,
|
|
967
|
+
shareUrl?: string,
|
|
980
968
|
): ServeSimState & {
|
|
981
969
|
basePath: string;
|
|
982
970
|
logsEndpoint: string;
|
|
@@ -1003,6 +991,8 @@ export function previewConfigForState(
|
|
|
1003
991
|
codec?: string;
|
|
1004
992
|
streamSettings?: StreamSettings;
|
|
1005
993
|
proxyHelpers?: boolean;
|
|
994
|
+
requireToken?: boolean;
|
|
995
|
+
shareUrl?: string;
|
|
1006
996
|
} {
|
|
1007
997
|
const gridApiBase = (base === "" ? "" : base) + "/grid/api";
|
|
1008
998
|
const legacyCodec = typeof streamSettingsOrCodec === "string" ? streamSettingsOrCodec : undefined;
|
|
@@ -1037,6 +1027,8 @@ export function previewConfigForState(
|
|
|
1037
1027
|
...(legacyCodec ? { codec: legacyCodec } : {}),
|
|
1038
1028
|
...(streamSettings ? { streamSettings } : {}),
|
|
1039
1029
|
...(proxyHelpers ? { proxyHelpers: true } : {}),
|
|
1030
|
+
...(requirePreviewToken ? { requireToken: true } : {}),
|
|
1031
|
+
...(shareUrl ? { shareUrl } : {}),
|
|
1040
1032
|
};
|
|
1041
1033
|
}
|
|
1042
1034
|
|
|
@@ -1496,12 +1488,16 @@ export interface SimMiddlewareOptions {
|
|
|
1496
1488
|
/** Stream transport and codec settings for the preview. */
|
|
1497
1489
|
streamSettings?: StreamSettings;
|
|
1498
1490
|
/**
|
|
1499
|
-
* Origins allowed to read
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1491
|
+
* Origins allowed to read this preview cross-origin (e.g. a hosted dashboard).
|
|
1492
|
+
* Every route answers with the policy, so the token gate stays the thing that
|
|
1493
|
+
* decides access. Loopback is always allowed.
|
|
1502
1494
|
*/
|
|
1495
|
+
corsOrigins?: string[];
|
|
1496
|
+
/** @deprecated Use `corsOrigins`. */
|
|
1503
1497
|
metricsCorsOrigins?: string[];
|
|
1504
1498
|
frameAncestors?: string[];
|
|
1499
|
+
/** Public page the Share button copies instead of this preview's address. */
|
|
1500
|
+
shareUrl?: string;
|
|
1505
1501
|
/** @deprecated Use `streamSettings: { transport: "http", codec }`. */
|
|
1506
1502
|
codec?: string;
|
|
1507
1503
|
/**
|
|
@@ -1540,7 +1536,6 @@ export function handleMetricsRequest(
|
|
|
1540
1536
|
res: SimRes,
|
|
1541
1537
|
state: ServeSimState | null,
|
|
1542
1538
|
samplerCache: MetricsSamplerCache = metricsSamplerCache,
|
|
1543
|
-
corsOrigins: readonly string[] = [],
|
|
1544
1539
|
tracker: ForegroundTrackerCache = foregroundTracker,
|
|
1545
1540
|
): void {
|
|
1546
1541
|
if (!state) {
|
|
@@ -1553,7 +1548,6 @@ export function handleMetricsRequest(
|
|
|
1553
1548
|
"Cache-Control": "no-cache",
|
|
1554
1549
|
Connection: "keep-alive",
|
|
1555
1550
|
"X-Accel-Buffering": "no",
|
|
1556
|
-
...corsAllowOriginHeaders(req.headers.origin, corsOrigins),
|
|
1557
1551
|
});
|
|
1558
1552
|
res.write(":\n\n");
|
|
1559
1553
|
// Keep the foreground tail warm for this stream's lifetime so the sampler can scope to the
|
|
@@ -1586,8 +1580,13 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
1586
1580
|
// can't read this value (it's only injected into the preview page's config).
|
|
1587
1581
|
const execToken = options?.execToken ?? randomBytes(32).toString("base64url");
|
|
1588
1582
|
const requirePreviewToken = options?.requirePreviewToken ?? false;
|
|
1589
|
-
const
|
|
1583
|
+
const corsOrigins = [...(options?.corsOrigins ?? []), ...(options?.metricsCorsOrigins ?? [])];
|
|
1590
1584
|
const frameAncestors = options?.frameAncestors ?? [];
|
|
1585
|
+
const shareUrl = options?.shareUrl;
|
|
1586
|
+
// The proxied DevTools frontend sits behind the same cookie, so its document needs the policy too.
|
|
1587
|
+
const framePolicyHeaders: Record<string, string> = requirePreviewToken
|
|
1588
|
+
? { "Content-Security-Policy": frameAncestorsPolicy(frameAncestors) }
|
|
1589
|
+
: {};
|
|
1591
1590
|
|
|
1592
1591
|
// Simulator-settings requests run in-process (just the underlying simctl /
|
|
1593
1592
|
// ax-tool spawn) instead of round-tripping a full `node <cli>` exec per
|
|
@@ -1637,6 +1636,25 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
1637
1636
|
const rawUrl: string = req.url ?? "";
|
|
1638
1637
|
const qIndex = rawUrl.indexOf("?");
|
|
1639
1638
|
const url = qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
|
|
1639
|
+
const ownPath = url === base || url.startsWith(`${base}/`);
|
|
1640
|
+
// Whole-middleware, so a new route is reachable cross-origin without opting in.
|
|
1641
|
+
if (ownPath) {
|
|
1642
|
+
// Unconditional, including when the origin is refused: a cached copy carrying no policy
|
|
1643
|
+
// would otherwise be replayed to an origin that is allowed one.
|
|
1644
|
+
res.setHeader("Vary", "Origin");
|
|
1645
|
+
const corsHeaders = corsAllowOriginHeaders(req.headers.origin, corsOrigins);
|
|
1646
|
+
for (const [name, value] of Object.entries(corsHeaders)) res.setHeader(name, value);
|
|
1647
|
+
}
|
|
1648
|
+
// A preflight carries no cookie and no token, so it has to be answered before the gate.
|
|
1649
|
+
if (ownPath && req.method === "OPTIONS") {
|
|
1650
|
+
res.writeHead(204, {
|
|
1651
|
+
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
|
|
1652
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
1653
|
+
"Access-Control-Max-Age": "600",
|
|
1654
|
+
});
|
|
1655
|
+
res.end();
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1640
1658
|
const requestedDevice = queryDevice(rawUrl);
|
|
1641
1659
|
const selectedDevice = requestedDevice ?? options?.device ?? null;
|
|
1642
1660
|
const devtoolsFrontendBase = base === "/" ? "/devtools-frontend" : `${base}/devtools-frontend`;
|
|
@@ -1682,10 +1700,13 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
1682
1700
|
const upstream = await fetch(
|
|
1683
1701
|
`https://chrome-devtools-frontend.appspot.com/serve_rev/@${DEVTOOLS_FRONTEND_REV}/${assetPath}${upstreamSuffix}`,
|
|
1684
1702
|
);
|
|
1703
|
+
const contentType = upstream.headers.get("content-type");
|
|
1704
|
+
const isDocument = contentType?.startsWith("text/html") ?? false;
|
|
1685
1705
|
const headers: Record<string, string> = {
|
|
1686
|
-
|
|
1706
|
+
// A cached copy would otherwise outlive the policy it was fetched under.
|
|
1707
|
+
"Cache-Control": isDocument ? "no-store" : "public, max-age=604800",
|
|
1708
|
+
...(isDocument ? framePolicyHeaders : {}),
|
|
1687
1709
|
};
|
|
1688
|
-
const contentType = upstream.headers.get("content-type");
|
|
1689
1710
|
if (contentType) headers["Content-Type"] = contentType;
|
|
1690
1711
|
res.writeHead(upstream.status, headers);
|
|
1691
1712
|
res.end(Buffer.from(await upstream.arrayBuffer()));
|
|
@@ -1706,7 +1727,11 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
1706
1727
|
// Empty-state UI still polls /exec (boot/list helpers), so the page
|
|
1707
1728
|
// needs the bearer token even before a helper attaches. Inject a
|
|
1708
1729
|
// minimal config with just the basePath + token.
|
|
1709
|
-
const minimal = JSON.stringify({
|
|
1730
|
+
const minimal = JSON.stringify({
|
|
1731
|
+
basePath: base,
|
|
1732
|
+
execToken,
|
|
1733
|
+
...(shareUrl ? { shareUrl } : {}),
|
|
1734
|
+
});
|
|
1710
1735
|
html = html.replace(
|
|
1711
1736
|
"<!--__SIM_PREVIEW_CONFIG__-->",
|
|
1712
1737
|
`<script>window.__SIM_PREVIEW__=${minimal}</script>`,
|
|
@@ -1715,7 +1740,9 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
1715
1740
|
|
|
1716
1741
|
if (state) {
|
|
1717
1742
|
const remoteState = rewriteStateForRequestHost(state, hostForRequest(req), base, httpProtocolForRequest(req), proxyHelpers);
|
|
1718
|
-
const config = JSON.stringify(
|
|
1743
|
+
const config = JSON.stringify(
|
|
1744
|
+
previewConfigForState(remoteState, base, execToken, streamSettings, proxyHelpers, requirePreviewToken, shareUrl),
|
|
1745
|
+
);
|
|
1719
1746
|
const configScript = `<script>window.__SIM_PREVIEW__=${config}</script>`;
|
|
1720
1747
|
html = html.replace("<!--__SIM_PREVIEW_CONFIG__-->", configScript);
|
|
1721
1748
|
}
|
|
@@ -1723,9 +1750,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
1723
1750
|
res.writeHead(200, {
|
|
1724
1751
|
"Content-Type": "text/html; charset=utf-8",
|
|
1725
1752
|
"Cache-Control": "no-store",
|
|
1726
|
-
...
|
|
1727
|
-
? { "Content-Security-Policy": frameAncestorsPolicy(frameAncestors) }
|
|
1728
|
-
: {}),
|
|
1753
|
+
...framePolicyHeaders,
|
|
1729
1754
|
});
|
|
1730
1755
|
res.end(html);
|
|
1731
1756
|
return;
|
|
@@ -2153,7 +2178,13 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2153
2178
|
"Cache-Control": "no-store",
|
|
2154
2179
|
});
|
|
2155
2180
|
const remoteState = state ? rewriteStateForRequestHost(state, hostForRequest(req), base, httpProtocolForRequest(req), proxyHelpers) : null;
|
|
2156
|
-
res.end(
|
|
2181
|
+
res.end(
|
|
2182
|
+
JSON.stringify(
|
|
2183
|
+
remoteState
|
|
2184
|
+
? previewConfigForState(remoteState, base, execToken, streamSettings, proxyHelpers, requirePreviewToken, shareUrl)
|
|
2185
|
+
: null,
|
|
2186
|
+
),
|
|
2187
|
+
);
|
|
2157
2188
|
return;
|
|
2158
2189
|
}
|
|
2159
2190
|
|
|
@@ -2164,7 +2195,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2164
2195
|
if (url === base + "/api/screenshot") {
|
|
2165
2196
|
if (req.method !== "POST") {
|
|
2166
2197
|
res.writeHead(405, {
|
|
2167
|
-
|
|
2198
|
+
"Cache-Control": "no-store",
|
|
2168
2199
|
"Content-Type": "text/plain; charset=utf-8",
|
|
2169
2200
|
});
|
|
2170
2201
|
res.end("method not allowed");
|
|
@@ -2173,7 +2204,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2173
2204
|
let udid = selectedDevice;
|
|
2174
2205
|
if (udid && !isSimulatorUdid(udid)) {
|
|
2175
2206
|
res.writeHead(400, {
|
|
2176
|
-
|
|
2207
|
+
"Cache-Control": "no-store",
|
|
2177
2208
|
"Content-Type": "application/json",
|
|
2178
2209
|
});
|
|
2179
2210
|
res.end(JSON.stringify({ ok: false, error: "Invalid simulator device ID" }));
|
|
@@ -2185,7 +2216,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2185
2216
|
}
|
|
2186
2217
|
if (!udid) {
|
|
2187
2218
|
res.writeHead(400, {
|
|
2188
|
-
|
|
2219
|
+
"Cache-Control": "no-store",
|
|
2189
2220
|
"Content-Type": "application/json",
|
|
2190
2221
|
});
|
|
2191
2222
|
res.end(JSON.stringify({ ok: false, error: "No booted simulator to screenshot" }));
|
|
@@ -2208,7 +2239,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2208
2239
|
});
|
|
2209
2240
|
const png = await readFile(file);
|
|
2210
2241
|
res.writeHead(200, {
|
|
2211
|
-
|
|
2242
|
+
"Cache-Control": "no-store",
|
|
2212
2243
|
"Content-Type": "image/png",
|
|
2213
2244
|
});
|
|
2214
2245
|
res.end(png);
|
|
@@ -2218,7 +2249,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2218
2249
|
(typeof stderr === "string" && stderr.trim()) ||
|
|
2219
2250
|
(err instanceof Error ? err.message : String(err));
|
|
2220
2251
|
res.writeHead(500, {
|
|
2221
|
-
|
|
2252
|
+
"Cache-Control": "no-store",
|
|
2222
2253
|
"Content-Type": "application/json",
|
|
2223
2254
|
});
|
|
2224
2255
|
res.end(JSON.stringify({ ok: false, error: message }));
|
|
@@ -2289,7 +2320,9 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2289
2320
|
const state = selectServeSimState(states, selectedDevice);
|
|
2290
2321
|
const remoteState = state ? rewriteStateForRequestHost(state, hostForRequest(req), base, httpProtocolForRequest(req), proxyHelpers) : null;
|
|
2291
2322
|
return JSON.stringify(
|
|
2292
|
-
remoteState
|
|
2323
|
+
remoteState
|
|
2324
|
+
? previewConfigForState(remoteState, base, execToken, streamSettings, proxyHelpers, requirePreviewToken, shareUrl)
|
|
2325
|
+
: null,
|
|
2293
2326
|
);
|
|
2294
2327
|
};
|
|
2295
2328
|
|
|
@@ -2440,7 +2473,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2440
2473
|
if (url === base + "/metrics") {
|
|
2441
2474
|
const states = await readServeSimStates();
|
|
2442
2475
|
const state = selectServeSimState(states, selectedDevice);
|
|
2443
|
-
handleMetricsRequest(req, res, state, metricsSamplerCache
|
|
2476
|
+
handleMetricsRequest(req, res, state, metricsSamplerCache);
|
|
2444
2477
|
return;
|
|
2445
2478
|
}
|
|
2446
2479
|
|