serve-sim 0.1.24 → 0.1.26
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 +2 -2
- package/Sources/SimCameraHelper/build.sh +1 -1
- package/Sources/SimCameraHelper/main.m +28 -17
- package/Sources/SimCameraInjector/SimCamFakes.h +86 -0
- package/Sources/SimCameraInjector/SimCamFakes.m +635 -0
- package/Sources/SimCameraInjector/SimCamFrameSource.h +26 -0
- package/Sources/SimCameraInjector/SimCamFrameSource.m +546 -0
- package/Sources/SimCameraInjector/SimCamLog.h +5 -0
- package/Sources/SimCameraInjector/SimCamLog.m +9 -0
- package/Sources/SimCameraInjector/SimCamSwizzles.h +3 -0
- package/Sources/SimCameraInjector/SimCamSwizzles.m +1014 -0
- package/Sources/SimCameraInjector/SimCameraInjector.m +9 -1150
- package/Sources/SimCameraInjector/build.sh +6 -1
- package/bin/serve-sim-bin +0 -0
- package/dist/middleware.js +25 -25
- package/dist/serve-sim.js +37 -34
- package/dist/simcam/libSimCameraInjector.dylib +0 -0
- package/dist/simcam/serve-sim-camera-helper +0 -0
- package/package.json +3 -1
- package/src/middleware.ts +125 -25
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serve-sim",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Evan Bacon",
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"src/state.ts",
|
|
36
36
|
"bin/serve-sim-bin",
|
|
37
37
|
"Sources/SimCameraInjector/SimCameraInjector.m",
|
|
38
|
+
"Sources/SimCameraInjector/SimCam*.h",
|
|
39
|
+
"Sources/SimCameraInjector/SimCam*.m",
|
|
38
40
|
"Sources/SimCameraInjector/include/SimCamShared.h",
|
|
39
41
|
"Sources/SimCameraInjector/build.sh",
|
|
40
42
|
"Sources/SimCameraHelper/main.m",
|
package/src/middleware.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { execSync, spawn, exec, execFile, type ChildProcess, type ExecException
|
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { createServer as createNetServer } from "net";
|
|
6
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
6
7
|
import type { IncomingMessage, ServerResponse } from "http";
|
|
7
8
|
import { createAxStreamerCache } from "./ax";
|
|
8
9
|
|
|
@@ -249,6 +250,43 @@ function endpoint(base: string, path: string, device: string): string {
|
|
|
249
250
|
return `${value}?device=${encodeURIComponent(device)}`;
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
export function previewConfigForState(
|
|
254
|
+
state: ServeSimState,
|
|
255
|
+
base: string,
|
|
256
|
+
serveSimBin: string,
|
|
257
|
+
execToken: string,
|
|
258
|
+
): ServeSimState & {
|
|
259
|
+
basePath: string;
|
|
260
|
+
logsEndpoint: string;
|
|
261
|
+
appStateEndpoint: string;
|
|
262
|
+
axEndpoint: string;
|
|
263
|
+
devtoolsEndpoint: string;
|
|
264
|
+
serveSimBin: string;
|
|
265
|
+
gridApiEndpoint: string;
|
|
266
|
+
gridStartEndpoint: string;
|
|
267
|
+
gridShutdownEndpoint: string;
|
|
268
|
+
gridMemoryEndpoint: string;
|
|
269
|
+
previewEndpoint: string;
|
|
270
|
+
execToken: string;
|
|
271
|
+
} {
|
|
272
|
+
const gridApiBase = (base === "" ? "" : base) + "/grid/api";
|
|
273
|
+
return {
|
|
274
|
+
...state,
|
|
275
|
+
basePath: base,
|
|
276
|
+
logsEndpoint: endpoint(base, "/logs", state.device),
|
|
277
|
+
appStateEndpoint: endpoint(base, "/appstate", state.device),
|
|
278
|
+
axEndpoint: endpoint(base, "/ax", state.device),
|
|
279
|
+
devtoolsEndpoint: endpoint(base, "/devtools", state.device),
|
|
280
|
+
serveSimBin,
|
|
281
|
+
gridApiEndpoint: gridApiBase,
|
|
282
|
+
gridStartEndpoint: gridApiBase + "/start",
|
|
283
|
+
gridShutdownEndpoint: gridApiBase + "/shutdown",
|
|
284
|
+
gridMemoryEndpoint: gridApiBase + "/memory",
|
|
285
|
+
previewEndpoint: base === "" ? "/" : base,
|
|
286
|
+
execToken,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
252
290
|
async function isLocalPortFree(port: number): Promise<boolean> {
|
|
253
291
|
return new Promise((resolve) => {
|
|
254
292
|
const server = createNetServer();
|
|
@@ -582,6 +620,27 @@ export interface SimMiddlewareOptions {
|
|
|
582
620
|
basePath?: string;
|
|
583
621
|
/** Pin this preview server to a specific simulator UDID. */
|
|
584
622
|
device?: string;
|
|
623
|
+
/**
|
|
624
|
+
* Per-session bearer token gating the `/exec` shell-exec route.
|
|
625
|
+
* Auto-generated if omitted. The token is injected into the preview HTML
|
|
626
|
+
* so the in-page UI can call `/exec` same-origin; LAN attackers and
|
|
627
|
+
* cross-origin pages cannot read it.
|
|
628
|
+
*/
|
|
629
|
+
execToken?: string;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function safeEqualString(a: string, b: string): boolean {
|
|
633
|
+
const ab = Buffer.from(a);
|
|
634
|
+
const bb = Buffer.from(b);
|
|
635
|
+
if (ab.length !== bb.length) return false;
|
|
636
|
+
return timingSafeEqual(ab, bb);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function isJsonContentType(value: string | undefined): boolean {
|
|
640
|
+
if (!value) return false;
|
|
641
|
+
// `application/json; charset=utf-8` etc. — only the media type matters.
|
|
642
|
+
const mediaType = value.split(";", 1)[0]!.trim().toLowerCase();
|
|
643
|
+
return mediaType === "application/json";
|
|
585
644
|
}
|
|
586
645
|
|
|
587
646
|
/**
|
|
@@ -595,6 +654,10 @@ export interface SimMiddlewareOptions {
|
|
|
595
654
|
*/
|
|
596
655
|
export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
597
656
|
const base = (options?.basePath ?? "/.sim").replace(/\/+$/, "");
|
|
657
|
+
// Per-process random token. Anyone who can read the preview HTML same-origin
|
|
658
|
+
// can call /exec; cross-origin pages and LAN clients cannot, because they
|
|
659
|
+
// can't read this value (it's only injected into the preview page's config).
|
|
660
|
+
const execToken = options?.execToken ?? randomBytes(32).toString("base64url");
|
|
598
661
|
|
|
599
662
|
return (req: SimReq, res: SimRes, next?: SimNext) => {
|
|
600
663
|
const rawUrl: string = req.url ?? "";
|
|
@@ -643,28 +706,19 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
643
706
|
const state = selectServeSimState(states, selectedDevice);
|
|
644
707
|
let html = loadHtml();
|
|
645
708
|
|
|
709
|
+
if (!state) {
|
|
710
|
+
// Empty-state UI still polls /exec (boot/list helpers), so the page
|
|
711
|
+
// needs the bearer token even before a helper attaches. Inject a
|
|
712
|
+
// minimal config with just the basePath + token.
|
|
713
|
+
const minimal = JSON.stringify({ basePath: base, execToken });
|
|
714
|
+
html = html.replace(
|
|
715
|
+
"<!--__SIM_PREVIEW_CONFIG__-->",
|
|
716
|
+
`<script>window.__SIM_PREVIEW__=${minimal}</script>`,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
646
720
|
if (state) {
|
|
647
|
-
|
|
648
|
-
// stream via fetch() (CORS is fine — serve-sim sends Access-Control-Allow-Origin: *)
|
|
649
|
-
// and connects to the WS directly (WS has no CORS).
|
|
650
|
-
const gridApiBase = (base === "" ? "" : base) + "/grid/api";
|
|
651
|
-
const config = JSON.stringify({
|
|
652
|
-
...state,
|
|
653
|
-
basePath: base,
|
|
654
|
-
logsEndpoint: endpoint(base, "/logs", state.device),
|
|
655
|
-
appStateEndpoint: endpoint(base, "/appstate", state.device),
|
|
656
|
-
axEndpoint: endpoint(base, "/ax", state.device),
|
|
657
|
-
devtoolsEndpoint: endpoint(base, "/devtools", state.device),
|
|
658
|
-
// Forward the absolute path of the running serve-sim entry script
|
|
659
|
-
// so the in-page Camera tool can shell out via `node <bin> camera`
|
|
660
|
-
// without depending on `serve-sim` being on PATH.
|
|
661
|
-
serveSimBin: serveSimBinPath(),
|
|
662
|
-
gridApiEndpoint: gridApiBase,
|
|
663
|
-
gridStartEndpoint: gridApiBase + "/start",
|
|
664
|
-
gridShutdownEndpoint: gridApiBase + "/shutdown",
|
|
665
|
-
gridMemoryEndpoint: gridApiBase + "/memory",
|
|
666
|
-
previewEndpoint: base === "" ? "/" : base,
|
|
667
|
-
});
|
|
721
|
+
const config = JSON.stringify(previewConfigForState(state, base, serveSimBinPath(), execToken));
|
|
668
722
|
const configScript = `<script>window.__SIM_PREVIEW__=${config}</script>`;
|
|
669
723
|
html = html.replace("<!--__SIM_PREVIEW_CONFIG__-->", configScript);
|
|
670
724
|
}
|
|
@@ -936,7 +990,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
936
990
|
"Content-Type": "application/json",
|
|
937
991
|
"Cache-Control": "no-store",
|
|
938
992
|
});
|
|
939
|
-
res.end(JSON.stringify(state
|
|
993
|
+
res.end(JSON.stringify(state ? previewConfigForState(state, base, serveSimBinPath(), execToken) : null));
|
|
940
994
|
return;
|
|
941
995
|
}
|
|
942
996
|
|
|
@@ -962,15 +1016,61 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
962
1016
|
return;
|
|
963
1017
|
}
|
|
964
1018
|
|
|
965
|
-
// POST /exec — run a shell command on the host.
|
|
966
|
-
//
|
|
967
|
-
//
|
|
1019
|
+
// POST /exec — run a shell command on the host. Gated by a per-process
|
|
1020
|
+
// bearer token injected only into the same-origin preview HTML, with
|
|
1021
|
+
// Content-Type + Origin checks to block CORS-simple CSRF (a malicious
|
|
1022
|
+
// page POSTing `text/plain` JSON to a dev server bound to a public iface)
|
|
1023
|
+
// and LAN attackers who can reach the port but can't read the token.
|
|
968
1024
|
if ((url === base + "/exec" || url === base + "/exec/") && req.method === "POST") {
|
|
1025
|
+
// 1. Reject anything that isn't a JSON request, killing the
|
|
1026
|
+
// `enctype="text/plain"` CORS-simple form-POST path.
|
|
1027
|
+
if (!isJsonContentType(req.headers["content-type"])) {
|
|
1028
|
+
res.writeHead(415, { "Content-Type": "application/json" });
|
|
1029
|
+
res.end(JSON.stringify({ stdout: "", stderr: "Unsupported Media Type", exitCode: 1 }));
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
// 2. If the browser supplied an Origin, require it match this server.
|
|
1033
|
+
// Same-origin XHR from the preview page sets Origin to our own URL;
|
|
1034
|
+
// a cross-origin page's Origin won't match.
|
|
1035
|
+
const origin = req.headers.origin;
|
|
1036
|
+
if (origin) {
|
|
1037
|
+
try {
|
|
1038
|
+
const originHost = new URL(origin).host;
|
|
1039
|
+
if (originHost !== req.headers.host) {
|
|
1040
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1041
|
+
res.end(JSON.stringify({ stdout: "", stderr: "Cross-origin request blocked", exitCode: 1 }));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
} catch {
|
|
1045
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1046
|
+
res.end(JSON.stringify({ stdout: "", stderr: "Invalid Origin", exitCode: 1 }));
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
// 3. Require the per-session bearer token. Cross-origin pages cannot
|
|
1051
|
+
// read it from window.__SIM_PREVIEW__; non-browser callers must
|
|
1052
|
+
// have copied it from the CLI output.
|
|
1053
|
+
const authHeader = req.headers.authorization ?? "";
|
|
1054
|
+
const match = /^Bearer\s+(.+)$/i.exec(authHeader);
|
|
1055
|
+
if (!match || !safeEqualString(match[1]!.trim(), execToken)) {
|
|
1056
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1057
|
+
res.end(JSON.stringify({ stdout: "", stderr: "Unauthorized", exitCode: 1 }));
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
969
1060
|
let body = "";
|
|
1061
|
+
let aborted = false;
|
|
970
1062
|
req.on("data", (chunk: Buffer | string) => {
|
|
971
1063
|
body += typeof chunk === "string" ? chunk : chunk.toString();
|
|
1064
|
+
// Cheap belt-and-braces cap so a runaway POST can't OOM the dev server.
|
|
1065
|
+
if (body.length > 4 * 1024 * 1024) {
|
|
1066
|
+
aborted = true;
|
|
1067
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
1068
|
+
res.end(JSON.stringify({ stdout: "", stderr: "Payload Too Large", exitCode: 1 }));
|
|
1069
|
+
req.destroy();
|
|
1070
|
+
}
|
|
972
1071
|
});
|
|
973
1072
|
req.on("end", () => {
|
|
1073
|
+
if (aborted) return;
|
|
974
1074
|
let command = "";
|
|
975
1075
|
try {
|
|
976
1076
|
command = (JSON.parse(body) as ExecRequestBody).command ?? "";
|