@expo/serve-sim 0.3.2 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/serve-sim",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Evan Bacon",
package/src/middleware.ts CHANGED
@@ -24,7 +24,12 @@ import {
24
24
  peekDeviceSession,
25
25
  type HidSocket,
26
26
  } from "./device-session";
27
- import { assertPreviewAccess, assertUpgradeAccess } from "./session-auth";
27
+ import {
28
+ acceptedTokenSubprotocol,
29
+ assertPreviewAccess,
30
+ assertUpgradeAccess,
31
+ upgradeAuthHeaders,
32
+ } from "./session-auth";
28
33
  import {
29
34
  eventLogEventForAction,
30
35
  readEventLog,
@@ -654,26 +659,35 @@ function webSocketBinary(payload: Buffer<ArrayBufferLike>): Uint8Array<ArrayBuff
654
659
  * handshake doesn't flush under Bun). Writes the 101 response and resumes the
655
660
  * socket on success; on a missing key writes 400 and returns false.
656
661
  */
657
- function writeWebSocketAccept(req: SimReq, socket: Socket): boolean {
662
+ function writeWebSocketAccept(req: SimReq, socket: Socket, execToken: string): boolean {
658
663
  const key = req.headers["sec-websocket-key"];
659
664
  if (typeof key !== "string") {
660
665
  socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
661
666
  return false;
662
667
  }
663
668
  const accept = createHash("sha1").update(key + WS_ACCEPT_GUID).digest("base64");
669
+ // A client that offered subprotocols fails the handshake unless one is named back.
670
+ const subprotocol = acceptedTokenSubprotocol(req.headers, execToken);
664
671
  socket.write(
665
672
  "HTTP/1.1 101 Switching Protocols\r\n" +
666
673
  "Upgrade: websocket\r\n" +
667
674
  "Connection: Upgrade\r\n" +
668
675
  `Sec-WebSocket-Accept: ${accept}\r\n` +
676
+ (subprotocol ? `Sec-WebSocket-Protocol: ${subprotocol}\r\n` : "") +
669
677
  "\r\n",
670
678
  );
671
679
  socket.resume();
672
680
  return true;
673
681
  }
674
682
 
675
- function bridgeWebSocketFrames(req: SimReq, socket: Socket, head: Buffer, upstreamUrl: string): void {
676
- if (!writeWebSocketAccept(req, socket)) return;
683
+ function bridgeWebSocketFrames(
684
+ req: SimReq,
685
+ socket: Socket,
686
+ head: Buffer,
687
+ upstreamUrl: string,
688
+ execToken: string,
689
+ ): void {
690
+ if (!writeWebSocketAccept(req, socket, execToken)) return;
677
691
 
678
692
  const upstream = new WebSocket(upstreamUrl);
679
693
  upstream.binaryType = "arraybuffer";
@@ -927,13 +941,21 @@ function rawHidSocket(socket: Socket, head: Buffer): HidSocket {
927
941
  closed = true;
928
942
  for (const cb of closeCbs) cb();
929
943
  };
930
- const shutdown = () => {
944
+ const shutdown = (code?: number, reason = "") => {
931
945
  fireClose();
932
- try { socket.end(websocketFrame(0x8, Buffer.alloc(0))); } catch {}
933
- try { socket.destroy(); } catch {}
946
+ const payload = code === undefined ? Buffer.alloc(0) : Buffer.alloc(2 + Buffer.byteLength(reason));
947
+ if (code !== undefined) {
948
+ payload.writeUInt16BE(code);
949
+ payload.write(reason, 2);
950
+ }
951
+ try {
952
+ socket.end(websocketFrame(0x8, payload));
953
+ socket.destroySoon();
954
+ } catch { socket.destroy(); }
934
955
  };
935
956
 
936
957
  const drain = () => {
958
+ if (closed) return;
937
959
  for (;;) {
938
960
  let frame: ParsedWebSocketFrame | null;
939
961
  try {
@@ -971,6 +993,7 @@ function rawHidSocket(socket: Socket, head: Buffer): HidSocket {
971
993
  function attachHidInProcess(
972
994
  req: SimReq,
973
995
  socket: Socket,
996
+ execToken: string,
974
997
  head: Buffer,
975
998
  device: string | null,
976
999
  initialStreamSettings?: StreamSettings,
@@ -982,7 +1005,7 @@ function attachHidInProcess(
982
1005
  } catch {
983
1006
  return false;
984
1007
  }
985
- if (!writeWebSocketAccept(req, socket)) return true; // bad request handled
1008
+ if (!writeWebSocketAccept(req, socket, execToken)) return true; // bad request handled
986
1009
  session.attachHidSocket(rawHidSocket(socket, head));
987
1010
  return true;
988
1011
  }
@@ -1525,8 +1548,6 @@ export interface SimMiddlewareOptions {
1525
1548
  * decides access. Loopback is always allowed.
1526
1549
  */
1527
1550
  corsOrigins?: string[];
1528
- /** @deprecated Use `corsOrigins`. */
1529
- metricsCorsOrigins?: string[];
1530
1551
  frameAncestors?: string[];
1531
1552
  /** Public page the Share button copies instead of this preview's address. */
1532
1553
  shareUrl?: string;
@@ -1702,7 +1723,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
1702
1723
  // can't read this value (it's only injected into the preview page's config).
1703
1724
  const execToken = options?.execToken ?? randomBytes(32).toString("base64url");
1704
1725
  const requirePreviewToken = options?.requirePreviewToken ?? false;
1705
- const corsOrigins = [...(options?.corsOrigins ?? []), ...(options?.metricsCorsOrigins ?? [])];
1726
+ const corsOrigins = [...(options?.corsOrigins ?? [])];
1706
1727
  const frameAncestors = options?.frameAncestors ?? [];
1707
1728
  const shareUrl = options?.shareUrl;
1708
1729
  // The proxied DevTools frontend sits behind the same cookie, so its document needs the policy too.
@@ -2665,13 +2686,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
2665
2686
  // their own, so gate them here too.
2666
2687
  if (
2667
2688
  !assertUpgradeAccess(
2668
- {
2669
- authorization: req.headers.authorization,
2670
- cookie: req.headers.cookie,
2671
- origin: req.headers.origin,
2672
- host: req.headers.host,
2673
- "sec-fetch-site": req.headers["sec-fetch-site"],
2674
- },
2689
+ upgradeAuthHeaders(req.headers),
2675
2690
  execToken,
2676
2691
  { required: requirePreviewToken },
2677
2692
  )
@@ -2687,7 +2702,13 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
2687
2702
  (async () => {
2688
2703
  try {
2689
2704
  const bridge = await getInspectWebKitBridge();
2690
- bridgeWebSocketFrames(req, socket, head, `ws://127.0.0.1:${bridge.port}${devtoolsTarget.upstreamPath}`);
2705
+ bridgeWebSocketFrames(
2706
+ req,
2707
+ socket,
2708
+ head,
2709
+ `ws://127.0.0.1:${bridge.port}${devtoolsTarget.upstreamPath}`,
2710
+ execToken,
2711
+ );
2691
2712
  } catch (err) {
2692
2713
  const message = err instanceof Error ? err.message : "Failed to start inspect-webkit";
2693
2714
  socket.end(`HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n${message}`);
@@ -2702,7 +2723,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
2702
2723
  const device = helperTarget.device ?? selectedDevice;
2703
2724
  if (helperTarget.upstreamPath === "/ws") {
2704
2725
  // HID input is delivered to the in-process DeviceSession.
2705
- if (attachHidInProcess(req, socket, head, device, streamSettings)) return;
2726
+ if (attachHidInProcess(req, socket, execToken, head, device, streamSettings)) return;
2706
2727
  socket.end("HTTP/1.1 404 Not Found\r\n\r\n");
2707
2728
  return;
2708
2729
  }
@@ -2717,6 +2738,7 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
2717
2738
  const execWebSocketHandler = createExecWebSocketHandler({
2718
2739
  path: `${base}/exec-ws`,
2719
2740
  execToken,
2741
+ corsOrigins,
2720
2742
  ssePrefixes: [
2721
2743
  `${base}/api/events`,
2722
2744
  `${base}/api/event-log/events`,
@@ -2740,17 +2762,14 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
2740
2762
  });
2741
2763
 
2742
2764
  fetchMiddleware.handleWebSocket = (request: Request, websocket: UpgradeHandlerWebSocket): boolean => {
2765
+ // Before any refusal below can close it: an unhandled error from a peer that keeps sending
2766
+ // would exit the process.
2767
+ websocket.on("error", () => websocket.close());
2743
2768
  // Embedded hosts forward accepted sockets and bypass the request gate. The exec channel
2744
- // re-checks the token in its first frame; the helper HID socket does not.
2769
+ // re-checks the token itself; the helper HID socket does not.
2745
2770
  if (
2746
2771
  !assertUpgradeAccess(
2747
- {
2748
- authorization: request.headers.get("authorization") ?? undefined,
2749
- cookie: request.headers.get("cookie") ?? undefined,
2750
- origin: request.headers.get("origin") ?? undefined,
2751
- host: request.headers.get("host") ?? undefined,
2752
- "sec-fetch-site": request.headers.get("sec-fetch-site") ?? undefined,
2753
- },
2772
+ upgradeAuthHeaders(request),
2754
2773
  execToken,
2755
2774
  { required: requirePreviewToken },
2756
2775
  )
package/src/native.ts CHANGED
@@ -75,6 +75,7 @@ interface NativeAddon {
75
75
  ) => SimCaptureHandle;
76
76
  axDescribe(udid: string): Promise<string>;
77
77
  axFrontmost(udid: string): Promise<string>;
78
+ axTypeKeyboardCharacter(udid: string, character: string): Promise<boolean>;
78
79
  setHardwareKeyboard(udid: string, enabled: boolean): Promise<boolean>;
79
80
  }
80
81
 
@@ -377,6 +378,10 @@ export function axFrontmostAsync(udid: string): Promise<string> {
377
378
  return load().axFrontmost(udid);
378
379
  }
379
380
 
381
+ export function axTypeKeyboardCharacterAsync(udid: string, character: string): Promise<boolean> {
382
+ return load().axTypeKeyboardCharacter(udid, character);
383
+ }
384
+
380
385
  /**
381
386
  * Connect/disconnect the device's hardware keyboard (⌘⇧K). Disconnecting makes
382
387
  * the guest show its on-screen keyboard. Per-device; resolves true when the