@expo/serve-sim 0.2.3 → 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 +79 -79
- package/dist/native/serve-sim-native.node +0 -0
- package/dist/serve-sim.js +114 -114
- package/package.json +1 -1
- package/src/connect-to-fetch.ts +9 -0
- package/src/middleware.ts +41 -31
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") {
|
|
@@ -1502,10 +1488,12 @@ export interface SimMiddlewareOptions {
|
|
|
1502
1488
|
/** Stream transport and codec settings for the preview. */
|
|
1503
1489
|
streamSettings?: StreamSettings;
|
|
1504
1490
|
/**
|
|
1505
|
-
* Origins allowed to read
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
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.
|
|
1508
1494
|
*/
|
|
1495
|
+
corsOrigins?: string[];
|
|
1496
|
+
/** @deprecated Use `corsOrigins`. */
|
|
1509
1497
|
metricsCorsOrigins?: string[];
|
|
1510
1498
|
frameAncestors?: string[];
|
|
1511
1499
|
/** Public page the Share button copies instead of this preview's address. */
|
|
@@ -1548,7 +1536,6 @@ export function handleMetricsRequest(
|
|
|
1548
1536
|
res: SimRes,
|
|
1549
1537
|
state: ServeSimState | null,
|
|
1550
1538
|
samplerCache: MetricsSamplerCache = metricsSamplerCache,
|
|
1551
|
-
corsOrigins: readonly string[] = [],
|
|
1552
1539
|
tracker: ForegroundTrackerCache = foregroundTracker,
|
|
1553
1540
|
): void {
|
|
1554
1541
|
if (!state) {
|
|
@@ -1561,7 +1548,6 @@ export function handleMetricsRequest(
|
|
|
1561
1548
|
"Cache-Control": "no-cache",
|
|
1562
1549
|
Connection: "keep-alive",
|
|
1563
1550
|
"X-Accel-Buffering": "no",
|
|
1564
|
-
...corsAllowOriginHeaders(req.headers.origin, corsOrigins),
|
|
1565
1551
|
});
|
|
1566
1552
|
res.write(":\n\n");
|
|
1567
1553
|
// Keep the foreground tail warm for this stream's lifetime so the sampler can scope to the
|
|
@@ -1594,9 +1580,13 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
1594
1580
|
// can't read this value (it's only injected into the preview page's config).
|
|
1595
1581
|
const execToken = options?.execToken ?? randomBytes(32).toString("base64url");
|
|
1596
1582
|
const requirePreviewToken = options?.requirePreviewToken ?? false;
|
|
1597
|
-
const
|
|
1583
|
+
const corsOrigins = [...(options?.corsOrigins ?? []), ...(options?.metricsCorsOrigins ?? [])];
|
|
1598
1584
|
const frameAncestors = options?.frameAncestors ?? [];
|
|
1599
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
|
+
: {};
|
|
1600
1590
|
|
|
1601
1591
|
// Simulator-settings requests run in-process (just the underlying simctl /
|
|
1602
1592
|
// ax-tool spawn) instead of round-tripping a full `node <cli>` exec per
|
|
@@ -1646,6 +1636,25 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
1646
1636
|
const rawUrl: string = req.url ?? "";
|
|
1647
1637
|
const qIndex = rawUrl.indexOf("?");
|
|
1648
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
|
+
}
|
|
1649
1658
|
const requestedDevice = queryDevice(rawUrl);
|
|
1650
1659
|
const selectedDevice = requestedDevice ?? options?.device ?? null;
|
|
1651
1660
|
const devtoolsFrontendBase = base === "/" ? "/devtools-frontend" : `${base}/devtools-frontend`;
|
|
@@ -1691,10 +1700,13 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
1691
1700
|
const upstream = await fetch(
|
|
1692
1701
|
`https://chrome-devtools-frontend.appspot.com/serve_rev/@${DEVTOOLS_FRONTEND_REV}/${assetPath}${upstreamSuffix}`,
|
|
1693
1702
|
);
|
|
1703
|
+
const contentType = upstream.headers.get("content-type");
|
|
1704
|
+
const isDocument = contentType?.startsWith("text/html") ?? false;
|
|
1694
1705
|
const headers: Record<string, string> = {
|
|
1695
|
-
|
|
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 : {}),
|
|
1696
1709
|
};
|
|
1697
|
-
const contentType = upstream.headers.get("content-type");
|
|
1698
1710
|
if (contentType) headers["Content-Type"] = contentType;
|
|
1699
1711
|
res.writeHead(upstream.status, headers);
|
|
1700
1712
|
res.end(Buffer.from(await upstream.arrayBuffer()));
|
|
@@ -1738,9 +1750,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
1738
1750
|
res.writeHead(200, {
|
|
1739
1751
|
"Content-Type": "text/html; charset=utf-8",
|
|
1740
1752
|
"Cache-Control": "no-store",
|
|
1741
|
-
...
|
|
1742
|
-
? { "Content-Security-Policy": frameAncestorsPolicy(frameAncestors) }
|
|
1743
|
-
: {}),
|
|
1753
|
+
...framePolicyHeaders,
|
|
1744
1754
|
});
|
|
1745
1755
|
res.end(html);
|
|
1746
1756
|
return;
|
|
@@ -2185,7 +2195,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2185
2195
|
if (url === base + "/api/screenshot") {
|
|
2186
2196
|
if (req.method !== "POST") {
|
|
2187
2197
|
res.writeHead(405, {
|
|
2188
|
-
|
|
2198
|
+
"Cache-Control": "no-store",
|
|
2189
2199
|
"Content-Type": "text/plain; charset=utf-8",
|
|
2190
2200
|
});
|
|
2191
2201
|
res.end("method not allowed");
|
|
@@ -2194,7 +2204,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2194
2204
|
let udid = selectedDevice;
|
|
2195
2205
|
if (udid && !isSimulatorUdid(udid)) {
|
|
2196
2206
|
res.writeHead(400, {
|
|
2197
|
-
|
|
2207
|
+
"Cache-Control": "no-store",
|
|
2198
2208
|
"Content-Type": "application/json",
|
|
2199
2209
|
});
|
|
2200
2210
|
res.end(JSON.stringify({ ok: false, error: "Invalid simulator device ID" }));
|
|
@@ -2206,7 +2216,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2206
2216
|
}
|
|
2207
2217
|
if (!udid) {
|
|
2208
2218
|
res.writeHead(400, {
|
|
2209
|
-
|
|
2219
|
+
"Cache-Control": "no-store",
|
|
2210
2220
|
"Content-Type": "application/json",
|
|
2211
2221
|
});
|
|
2212
2222
|
res.end(JSON.stringify({ ok: false, error: "No booted simulator to screenshot" }));
|
|
@@ -2229,7 +2239,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2229
2239
|
});
|
|
2230
2240
|
const png = await readFile(file);
|
|
2231
2241
|
res.writeHead(200, {
|
|
2232
|
-
|
|
2242
|
+
"Cache-Control": "no-store",
|
|
2233
2243
|
"Content-Type": "image/png",
|
|
2234
2244
|
});
|
|
2235
2245
|
res.end(png);
|
|
@@ -2239,7 +2249,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2239
2249
|
(typeof stderr === "string" && stderr.trim()) ||
|
|
2240
2250
|
(err instanceof Error ? err.message : String(err));
|
|
2241
2251
|
res.writeHead(500, {
|
|
2242
|
-
|
|
2252
|
+
"Cache-Control": "no-store",
|
|
2243
2253
|
"Content-Type": "application/json",
|
|
2244
2254
|
});
|
|
2245
2255
|
res.end(JSON.stringify({ ok: false, error: message }));
|
|
@@ -2463,7 +2473,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
|
|
|
2463
2473
|
if (url === base + "/metrics") {
|
|
2464
2474
|
const states = await readServeSimStates();
|
|
2465
2475
|
const state = selectServeSimState(states, selectedDevice);
|
|
2466
|
-
handleMetricsRequest(req, res, state, metricsSamplerCache
|
|
2476
|
+
handleMetricsRequest(req, res, state, metricsSamplerCache);
|
|
2467
2477
|
return;
|
|
2468
2478
|
}
|
|
2469
2479
|
|