@aloud/runner 0.3.1 → 0.3.3
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 +12 -5
- package/dist/cli.js +309 -107
- package/package.json +1 -1
- package/src/cli.ts +199 -35
- package/src/config/policy.ts +1 -1
- package/src/discovery.ts +28 -18
- package/src/loop.ts +20 -3
- package/src/protocol/presence.ts +83 -0
- package/src/run/execute.ts +1 -1
- package/src/run/sanitise.ts +36 -14
- package/src/ui/output.ts +2 -1
- package/src/version.ts +1 -1
package/src/run/execute.ts
CHANGED
|
@@ -145,7 +145,7 @@ export async function executeLease(deps: ExecuteDeps): Promise<ExecuteResult> {
|
|
|
145
145
|
const workers = new GuardedWorkerFactory(
|
|
146
146
|
deps.workers ?? new PlaywrightWorkerFactory(),
|
|
147
147
|
effective.allowedHosts,
|
|
148
|
-
deps.local,
|
|
148
|
+
{ ...deps.local, allowPrivateNetwork: effective.allowPrivateNetwork },
|
|
149
149
|
);
|
|
150
150
|
|
|
151
151
|
coordinator = new RunCoordinator(
|
package/src/run/sanitise.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
evaluateTargetUrl,
|
|
3
|
+
hostPermitted,
|
|
3
4
|
intersectHosts,
|
|
4
5
|
leasePermits,
|
|
5
6
|
normaliseHosts,
|
|
7
|
+
targetNetworkAccess,
|
|
6
8
|
type JobLease,
|
|
7
9
|
type StudySnapshot,
|
|
8
10
|
} from "@aloud/core";
|
|
@@ -31,6 +33,8 @@ export class SnapshotRefused extends Error {
|
|
|
31
33
|
export interface SanitisedSnapshot {
|
|
32
34
|
snapshot: StudySnapshot;
|
|
33
35
|
allowedHosts: string[];
|
|
36
|
+
/** False for an automatic public grant, which keeps DNS rebinding out of the local network. */
|
|
37
|
+
allowPrivateNetwork: boolean;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export async function sanitiseSnapshot(
|
|
@@ -44,38 +48,56 @@ export async function sanitiseSnapshot(
|
|
|
44
48
|
const granted = normaliseHosts(lease.allowedHosts);
|
|
45
49
|
const permitted = normaliseHosts(local.allowedHosts);
|
|
46
50
|
|
|
47
|
-
// The server already did this intersection. Doing it again here is the point: a server that got
|
|
48
|
-
// it wrong, or lied about it, changes nothing on this machine.
|
|
49
|
-
const effective = intersectHosts(granted, permitted);
|
|
50
|
-
if (effective.length === 0) {
|
|
51
|
-
throw new SnapshotRefused(
|
|
52
|
-
`This study wants ${granted.join(", ") || "nothing"}, and this machine allows ` +
|
|
53
|
-
`${permitted.join(", ") || "nothing"}. Nothing in common, so nothing will be opened. ` +
|
|
54
|
-
"Add a host with `aloud allow <host>` if that is wrong.",
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
51
|
const startUrl = snapshot.study?.startUrl;
|
|
59
52
|
if (typeof startUrl !== "string" || startUrl.length === 0) {
|
|
60
53
|
throw new SnapshotRefused("This study has no start URL.");
|
|
61
54
|
}
|
|
62
55
|
|
|
63
|
-
//
|
|
64
|
-
|
|
56
|
+
// The lease is still the outer boundary. Automatic access never invents a host the lease did not
|
|
57
|
+
// name; it only decides whether that lease host needs a durable local permission as well.
|
|
58
|
+
const verdict = leasePermits({ ...lease, allowedHosts: granted }, startUrl, nowIso);
|
|
65
59
|
if (!verdict.allowed) throw new SnapshotRefused(verdict.reason);
|
|
66
60
|
|
|
61
|
+
const access = await targetNetworkAccess(startUrl, resolver);
|
|
62
|
+
if (access.kind === "blocked") throw new SnapshotRefused(access.reason);
|
|
63
|
+
|
|
64
|
+
let effective: string[];
|
|
65
|
+
let allowPrivateNetwork: boolean;
|
|
66
|
+
if (access.kind === "public") {
|
|
67
|
+
// A covering lease entry proves the start host was in scope, but the browser receives only the
|
|
68
|
+
// exact start hostname. A hostile lease cannot smuggle an unrelated domain—or a broad parent
|
|
69
|
+
// that grants sibling subdomains—into the automatic browser policy.
|
|
70
|
+
effective = granted.some((entry) => hostPermitted(access.hostname, [entry]))
|
|
71
|
+
? [access.hostname]
|
|
72
|
+
: [];
|
|
73
|
+
allowPrivateNetwork = false;
|
|
74
|
+
} else {
|
|
75
|
+
// Private and loopback access still needs both halves of the durable permission. The server can
|
|
76
|
+
// offer it, but cannot make this intersection non-empty.
|
|
77
|
+
effective = intersectHosts(granted, permitted);
|
|
78
|
+
allowPrivateNetwork = local.allowPrivateNetwork;
|
|
79
|
+
}
|
|
80
|
+
if (effective.length === 0) {
|
|
81
|
+
throw new SnapshotRefused(
|
|
82
|
+
`This private or local study wants ${granted.join(", ") || "nothing"}, and this machine allows ` +
|
|
83
|
+
`${permitted.join(", ") || "nothing"}. Nothing in common, so nothing will be opened. ` +
|
|
84
|
+
"Add the private host with `aloud allow <host>` if that is intentional.",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
67
88
|
// And then the same check the browser worker will make, so a refusal happens before anything is
|
|
68
89
|
// launched rather than three browsers in. Cloud metadata endpoints are refused unconditionally
|
|
69
90
|
// in here, regardless of the private-network setting.
|
|
70
91
|
const target = await evaluateTargetUrl(startUrl, {
|
|
71
92
|
allowedDomains: effective,
|
|
72
|
-
allowPrivateNetwork
|
|
93
|
+
allowPrivateNetwork,
|
|
73
94
|
...(resolver ? { resolver } : {}),
|
|
74
95
|
});
|
|
75
96
|
if (!target.allowed) throw new SnapshotRefused(target.reason);
|
|
76
97
|
|
|
77
98
|
return {
|
|
78
99
|
allowedHosts: effective,
|
|
100
|
+
allowPrivateNetwork,
|
|
79
101
|
snapshot: {
|
|
80
102
|
...snapshot,
|
|
81
103
|
environment: {
|
package/src/ui/output.ts
CHANGED
|
@@ -39,7 +39,8 @@ export class TerminalReporter implements RunReporter {
|
|
|
39
39
|
this.say("");
|
|
40
40
|
this.say(` Chromium ${input.chromium ? GREEN + "ready" + RESET : YELLOW + "not installed yet" + RESET}`);
|
|
41
41
|
this.say(` Connection ${input.server}`);
|
|
42
|
-
this.say(`
|
|
42
|
+
this.say(` Public sites ${GREEN}automatic per study${RESET}`);
|
|
43
|
+
this.say(` Private/local ${input.allowedHosts.join(", ") || "nothing approved"}`);
|
|
43
44
|
this.say("");
|
|
44
45
|
}
|
|
45
46
|
|
package/src/version.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* package.json beside it to read, and importing one into the source trips the composite build's
|
|
11
11
|
* rootDir. `version.test.ts` asserts this matches, so the drift this invites cannot survive CI.
|
|
12
12
|
*/
|
|
13
|
-
export const RUNNER_VERSION = "0.3.
|
|
13
|
+
export const RUNNER_VERSION = "0.3.3";
|
|
14
14
|
|
|
15
15
|
/** The header the server reads it from. */
|
|
16
16
|
export const RUNNER_VERSION_HEADER = "x-aloud-runner-version";
|