buninu 0.4.2 → 0.4.5
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/CHANGELOG.md +16 -0
- package/apps/bunmsh/CHANGELOG.md +34 -0
- package/apps/bunmsh/index.js +7 -0
- package/apps/bunmsh/package.json +3 -3
- package/apps/bunmsh/test/curl.test.js +1 -1
- package/apps/bunmsh/test/reference.test.js +26 -12
- package/apps/bunmsh/test/shell1.test.js +1065 -0
- package/apps/bunmsh/test/shell2.test.js +457 -0
- package/apps/bunmsh/test/shell3.test.js +741 -0
- package/apps/bunx/bunx.js +5 -1
- package/apps/jsgotty/gotty.js +26 -6
- package/apps/jsgotty/package-lock.json +2 -2
- package/apps/jsgotty/package.json +2 -1
- package/apps/jsgotty/tests/websocket-origin.test.js +54 -0
- package/apps/jsmdcui/README.md +14 -0
- package/apps/jsmdcui/package.json +1 -1
- package/apps/jsmdcui/runtime/help/cdp.md +6 -0
- package/apps/jsmdcui/runtime/help/help.md +14 -0
- package/apps/jsmdcui/runtime/jsplugins/cdp/cdp-server.js +112 -34
- package/apps/jsmdcui/runtime/jsplugins/cdp/cdp.js +3 -1
- package/apps/jsmdcui/src/index.js +10 -0
- package/apps/jsmdcui/tests/cdp-security.test.js +32 -0
- package/apps/jsmdcui/tests/cdp-websocket-test.html +46 -0
- package/apps/jsmdcui/tests/demo.test.js +1 -0
- package/package.json +3 -2
package/apps/bunx/bunx.js
CHANGED
|
@@ -144,7 +144,11 @@ if (needsInstall(bunInstallRoot, target, version, pkgName)) {
|
|
|
144
144
|
const install = Bun.spawnSync(installArgs, {
|
|
145
145
|
stdio: ["inherit", "inherit", "inherit"],
|
|
146
146
|
});
|
|
147
|
-
|
|
147
|
+
|
|
148
|
+
// Don't check exit code so even if
|
|
149
|
+
// bun install killed by SIGSYS @issue #39060
|
|
150
|
+
// bunx still runs with actually existing bin
|
|
151
|
+
//if (install.exitCode !== 0) process.exit(install.exitCode ?? 1);
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
if (!fs.existsSync(target)) {
|
package/apps/jsgotty/gotty.js
CHANGED
|
@@ -800,6 +800,28 @@ function basicAuthAuthorized(req, credential) {
|
|
|
800
800
|
return payload === credential;
|
|
801
801
|
}
|
|
802
802
|
|
|
803
|
+
// Match gorilla/websocket's default policy used by upstream GoTTY: non-browser
|
|
804
|
+
// clients without Origin are accepted, while browser connections must come
|
|
805
|
+
// from the same host. --ws-origin intentionally replaces this default check.
|
|
806
|
+
function webSocketOriginAllowed(req, originMatcher = null) {
|
|
807
|
+
const origin = req.headers.origin;
|
|
808
|
+
|
|
809
|
+
if (originMatcher) {
|
|
810
|
+
return originMatcher.test(origin || "");
|
|
811
|
+
}
|
|
812
|
+
if (!origin) {
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
const originHost = new URL(origin).host;
|
|
818
|
+
const requestHost = req.headers.host || "";
|
|
819
|
+
return originHost !== "" && originHost.toLowerCase() === requestHost.toLowerCase();
|
|
820
|
+
} catch {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
803
825
|
function sendBasicAuthRequired(res) {
|
|
804
826
|
res.writeHead(401, {
|
|
805
827
|
"WWW-Authenticate": 'Basic realm="GoTTY"',
|
|
@@ -2940,12 +2962,9 @@ function createServerRuntime(command, argv, options) {
|
|
|
2940
2962
|
return;
|
|
2941
2963
|
}
|
|
2942
2964
|
|
|
2943
|
-
if (originMatcher) {
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
socket.destroy();
|
|
2947
|
-
return;
|
|
2948
|
-
}
|
|
2965
|
+
if (!webSocketOriginAllowed(req, originMatcher)) {
|
|
2966
|
+
socket.destroy();
|
|
2967
|
+
return;
|
|
2949
2968
|
}
|
|
2950
2969
|
|
|
2951
2970
|
if (options.once && acceptedOnce && !options.reconnect) {
|
|
@@ -3295,6 +3314,7 @@ async function bootstrap() {
|
|
|
3295
3314
|
|
|
3296
3315
|
module.exports.CursorStateTracker = CursorStateTracker;
|
|
3297
3316
|
module.exports.KittyGraphicsParser = KittyGraphicsParser;
|
|
3317
|
+
module.exports.webSocketOriginAllowed = webSocketOriginAllowed;
|
|
3298
3318
|
|
|
3299
3319
|
if (require.main === module) {
|
|
3300
3320
|
if (KITTY_TRACE) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsgotty",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.11",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "jsgotty",
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.11",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"node-addon-api": "^7.1.1",
|
|
12
12
|
"ws": "^8.18.3"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
const { webSocketOriginAllowed } = require("../gotty.js");
|
|
4
|
+
|
|
5
|
+
function request(host, origin) {
|
|
6
|
+
return {
|
|
7
|
+
headers: {
|
|
8
|
+
host,
|
|
9
|
+
...(origin === undefined ? {} : { origin }),
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("WebSocket permits clients without Origin", () => {
|
|
15
|
+
assert.equal(webSocketOriginAllowed(request("127.0.0.1:8080")), true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("WebSocket permits same-origin browsers", () => {
|
|
19
|
+
assert.equal(
|
|
20
|
+
webSocketOriginAllowed(request("127.0.0.1:8080", "http://127.0.0.1:8080")),
|
|
21
|
+
true,
|
|
22
|
+
);
|
|
23
|
+
assert.equal(
|
|
24
|
+
webSocketOriginAllowed(request("Example.COM:8080", "https://example.com:8080")),
|
|
25
|
+
true,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("WebSocket rejects cross-origin and malformed browser origins", () => {
|
|
30
|
+
assert.equal(
|
|
31
|
+
webSocketOriginAllowed(request("127.0.0.1:8080", "https://attacker.example")),
|
|
32
|
+
false,
|
|
33
|
+
);
|
|
34
|
+
assert.equal(webSocketOriginAllowed(request("127.0.0.1:8080", "null")), false);
|
|
35
|
+
assert.equal(webSocketOriginAllowed(request("127.0.0.1:8080", "not a URL")), false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("--ws-origin regex replaces the same-origin default", () => {
|
|
39
|
+
const matcher = /^https:\/\/trusted\.example$/;
|
|
40
|
+
assert.equal(
|
|
41
|
+
webSocketOriginAllowed(
|
|
42
|
+
request("127.0.0.1:8080", "https://trusted.example"),
|
|
43
|
+
matcher,
|
|
44
|
+
),
|
|
45
|
+
true,
|
|
46
|
+
);
|
|
47
|
+
assert.equal(
|
|
48
|
+
webSocketOriginAllowed(
|
|
49
|
+
request("127.0.0.1:8080", "http://127.0.0.1:8080"),
|
|
50
|
+
matcher,
|
|
51
|
+
),
|
|
52
|
+
false,
|
|
53
|
+
);
|
|
54
|
+
});
|
package/apps/jsmdcui/README.md
CHANGED
|
@@ -867,6 +867,20 @@ Ctrl-E cdp 9000 --public
|
|
|
867
867
|
The default bind address is `127.0.0.1` and the default port is `9222`.
|
|
868
868
|
Use `--public` or `--address=0.0.0.0` only on a trusted network.
|
|
869
869
|
|
|
870
|
+
Like Chromium, jsmdcui rejects WebSocket connections carrying an `Origin`
|
|
871
|
+
header by default. This prevents ordinary web pages from controlling the TUI.
|
|
872
|
+
Non-browser automation clients, which omit `Origin`, continue to work. To
|
|
873
|
+
explicitly allow a trusted browser origin, use a comma-separated allowlist:
|
|
874
|
+
|
|
875
|
+
```sh
|
|
876
|
+
bun src/index.js --remote-debugging-port=9222 \
|
|
877
|
+
--remote-allow-origins=http://localhost:8000 demos/maze.md
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
`--remote-allow-origins=*` disables this protection and should only be used in
|
|
881
|
+
a trusted environment. CDP can evaluate JavaScript and has the same authority
|
|
882
|
+
as the jsmdcui process.
|
|
883
|
+
|
|
870
884
|
Once CDP is running, control the TUI with `Bun.WebView`. For more info, enter
|
|
871
885
|
jsmdcui and use `Ctrl-E` or `€` → `help cdp`.
|
|
872
886
|
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
// From the command line (Chrome-style):
|
|
19
19
|
// bunmicro --remote-debugging-port=9222 file.txt
|
|
20
20
|
// bunmicro --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0 file.txt
|
|
21
|
+
// bunmicro --remote-debugging-port=9222 --remote-allow-origins=http://localhost:8000 file.txt
|
|
22
|
+
//
|
|
23
|
+
// Browser WebSocket origins are rejected by default, as in Chromium.
|
|
24
|
+
// --remote-allow-origins accepts a comma-separated allowlist; `*` allows all
|
|
25
|
+
// origins and disables this protection. Non-browser clients omit Origin and
|
|
26
|
+
// continue to work without this flag.
|
|
21
27
|
//
|
|
22
28
|
// Once running, status bar shows: CDP@<port> server running
|
|
23
29
|
// Running cdp again shows: CDP@<port> already running
|
|
@@ -867,6 +867,20 @@ Ctrl-E cdp 9000 --public
|
|
|
867
867
|
The default bind address is `127.0.0.1` and the default port is `9222`.
|
|
868
868
|
Use `--public` or `--address=0.0.0.0` only on a trusted network.
|
|
869
869
|
|
|
870
|
+
Like Chromium, jsmdcui rejects WebSocket connections carrying an `Origin`
|
|
871
|
+
header by default. This prevents ordinary web pages from controlling the TUI.
|
|
872
|
+
Non-browser automation clients, which omit `Origin`, continue to work. To
|
|
873
|
+
explicitly allow a trusted browser origin, use a comma-separated allowlist:
|
|
874
|
+
|
|
875
|
+
```sh
|
|
876
|
+
bun src/index.js --remote-debugging-port=9222 \
|
|
877
|
+
--remote-allow-origins=http://localhost:8000 demos/maze.md
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
`--remote-allow-origins=*` disables this protection and should only be used in
|
|
881
|
+
a trusted environment. CDP can evaluate JavaScript and has the same authority
|
|
882
|
+
as the jsmdcui process.
|
|
883
|
+
|
|
870
884
|
Once CDP is running, control the TUI with `Bun.WebView`. For more info, enter
|
|
871
885
|
jsmdcui and use `Ctrl-E` or `€` → `help cdp`.
|
|
872
886
|
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
const CDP_DEFAULT_PORT = 9222;
|
|
2
2
|
const CDP_PROTOCOL_VERSION = "1.3";
|
|
3
3
|
const CDP_BROWSER_ID = "cdp-server";
|
|
4
|
+
const CDP_BROWSER_PATH = `/devtools/browser/${CDP_BROWSER_ID}`;
|
|
4
5
|
|
|
5
6
|
let serverLogEnabled = 0//true;
|
|
6
7
|
function serverLog(...args) {
|
|
7
|
-
if (
|
|
8
|
+
if ( process.env.JSMDCUI_CDP_DEBUG ||
|
|
9
|
+
serverLogEnabled )
|
|
10
|
+
{
|
|
11
|
+
console.log(...args);
|
|
12
|
+
}
|
|
8
13
|
}
|
|
9
14
|
function serverError(...args) {
|
|
10
|
-
if (
|
|
15
|
+
if ( process.env.JSMDCUI_CDP_DEBUG ||
|
|
16
|
+
serverLogEnabled )
|
|
17
|
+
{
|
|
18
|
+
console.error(...args);
|
|
19
|
+
}
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
export class CdpServer {
|
|
@@ -21,9 +30,10 @@ export class CdpServer {
|
|
|
21
30
|
return new CdpServer(context);
|
|
22
31
|
}
|
|
23
32
|
|
|
24
|
-
listen(port = CDP_DEFAULT_PORT, hostname = "127.0.0.1") {
|
|
33
|
+
listen(port = CDP_DEFAULT_PORT, hostname = "127.0.0.1", options = {}) {
|
|
25
34
|
const context = this.#context;
|
|
26
35
|
const state = createCdpState();
|
|
36
|
+
const remoteAllowOrigins = parseAllowedOrigins(options.remoteAllowOrigins);
|
|
27
37
|
|
|
28
38
|
const server = Bun.serve({
|
|
29
39
|
port,
|
|
@@ -31,25 +41,65 @@ export class CdpServer {
|
|
|
31
41
|
fetch(req, server) {
|
|
32
42
|
const url = new URL(req.url);
|
|
33
43
|
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
44
|
+
|
|
45
|
+
const origin = req.headers.get("origin");
|
|
46
|
+
const respond = async (response) => {
|
|
47
|
+
const text = await response.clone().text();
|
|
48
|
+
serverLog(
|
|
49
|
+
`[CdpServer] HTTP ${req.method} ${pathname} -> ${response.status}`,
|
|
50
|
+
text,
|
|
51
|
+
);
|
|
52
|
+
return response;
|
|
53
|
+
};
|
|
54
|
+
|
|
34
55
|
serverLog(`[CdpServer] HTTP ${req.method} ${pathname}`);
|
|
35
|
-
|
|
56
|
+
|
|
57
|
+
serverLog(`[CdpServer] Origin ${origin}`);
|
|
58
|
+
|
|
59
|
+
if (!isSafeHostHeader(req.headers.get("host"))) {
|
|
60
|
+
return respond(new Response("Host header must be an IP address or localhost", {
|
|
61
|
+
status: 500,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
66
|
+
if (!isOriginAllowed(origin, remoteAllowOrigins)) {
|
|
67
|
+
return respond(new Response(
|
|
68
|
+
`Rejected an incoming WebSocket connection from the ${origin} origin. `
|
|
69
|
+
+ `Use --remote-allow-origins=${origin} to allow it, or `
|
|
70
|
+
+ "--remote-allow-origins=* to allow all origins.",
|
|
71
|
+
{ status: 403 },
|
|
72
|
+
));
|
|
73
|
+
}
|
|
74
|
+
if (pathname !== CDP_BROWSER_PATH) {
|
|
75
|
+
return respond(new Response("Not Found", { status: 404 }));
|
|
76
|
+
}
|
|
77
|
+
if (server.upgrade(req)) {
|
|
78
|
+
serverLog(
|
|
79
|
+
`[CdpServer] HTTP ${req.method} ${pathname} -> 101`,
|
|
80
|
+
"Switching Protocols",
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
return respond(new Response("WebSocket upgrade failed", { status: 500 }));
|
|
85
|
+
}
|
|
36
86
|
|
|
37
87
|
const host = req.headers.get("host") ?? `127.0.0.1:${port}`;
|
|
38
88
|
const webSocketDebuggerUrl = `ws://${host}/devtools/browser/${CDP_BROWSER_ID}`;
|
|
39
89
|
|
|
40
90
|
if (pathname === "/json/version") {
|
|
41
|
-
return jsonResponse({
|
|
91
|
+
return respond(jsonResponse({
|
|
42
92
|
Browser: "CdpServer/1.0",
|
|
43
93
|
"Protocol-Version": CDP_PROTOCOL_VERSION,
|
|
44
94
|
"User-Agent": "CdpServer/1.0",
|
|
45
95
|
"V8-Version": Bun.version,
|
|
46
96
|
"WebKit-Version": "537.36",
|
|
47
97
|
webSocketDebuggerUrl,
|
|
48
|
-
});
|
|
98
|
+
}));
|
|
49
99
|
}
|
|
50
100
|
|
|
51
101
|
if (pathname === "/json" || pathname === "/json/list") {
|
|
52
|
-
return jsonResponse(
|
|
102
|
+
return respond(jsonResponse(
|
|
53
103
|
[...state.targets.values()].map((target) => ({
|
|
54
104
|
id: target.targetId,
|
|
55
105
|
type: target.type,
|
|
@@ -57,14 +107,14 @@ export class CdpServer {
|
|
|
57
107
|
url: target.url,
|
|
58
108
|
webSocketDebuggerUrl,
|
|
59
109
|
}))
|
|
60
|
-
);
|
|
110
|
+
));
|
|
61
111
|
}
|
|
62
112
|
|
|
63
|
-
return jsonResponse({
|
|
113
|
+
return respond(jsonResponse({
|
|
64
114
|
server: "CdpServer",
|
|
65
115
|
port,
|
|
66
116
|
webSocketDebuggerUrl,
|
|
67
|
-
});
|
|
117
|
+
}));
|
|
68
118
|
},
|
|
69
119
|
websocket: {
|
|
70
120
|
open(ws) {
|
|
@@ -73,6 +123,11 @@ export class CdpServer {
|
|
|
73
123
|
async message(ws, raw) {
|
|
74
124
|
let id;
|
|
75
125
|
let sessionId;
|
|
126
|
+
const send = (message) => {
|
|
127
|
+
const text = JSON.stringify(message);
|
|
128
|
+
serverLog("[CdpServer] WebSocket response", text);
|
|
129
|
+
ws.send(text);
|
|
130
|
+
};
|
|
76
131
|
try {
|
|
77
132
|
const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
78
133
|
const msg = JSON.parse(text);
|
|
@@ -82,13 +137,12 @@ export class CdpServer {
|
|
|
82
137
|
serverLog(`[CdpServer] -> ${method}`, params ?? "");
|
|
83
138
|
|
|
84
139
|
const emit = (eventMethod, eventParams) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
);
|
|
140
|
+
serverLog(`[CdpServer] <- ${eventMethod} EVENT`, eventParams ?? {});
|
|
141
|
+
send({
|
|
142
|
+
method: eventMethod,
|
|
143
|
+
params: eventParams,
|
|
144
|
+
...(sessionId ? { sessionId } : {}),
|
|
145
|
+
});
|
|
92
146
|
};
|
|
93
147
|
const result = await dispatch(
|
|
94
148
|
context,
|
|
@@ -99,25 +153,22 @@ export class CdpServer {
|
|
|
99
153
|
emit
|
|
100
154
|
);
|
|
101
155
|
serverLog(`[CdpServer] <- ${method} OK`, result ?? "");
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
})
|
|
108
|
-
);
|
|
156
|
+
send({
|
|
157
|
+
id,
|
|
158
|
+
result: result ?? {},
|
|
159
|
+
...(sessionId ? { sessionId } : {}),
|
|
160
|
+
});
|
|
109
161
|
} catch (err) {
|
|
110
162
|
serverError(`[CdpServer] <- ERROR ${err.message}`);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
);
|
|
163
|
+
serverLog("[CdpServer] <- ERROR", { id, sessionId, message: err.message });
|
|
164
|
+
send({
|
|
165
|
+
id,
|
|
166
|
+
error: {
|
|
167
|
+
code: -32601,
|
|
168
|
+
message: err.message,
|
|
169
|
+
},
|
|
170
|
+
...(sessionId ? { sessionId } : {}),
|
|
171
|
+
});
|
|
121
172
|
}
|
|
122
173
|
},
|
|
123
174
|
close(ws) {
|
|
@@ -131,6 +182,33 @@ export class CdpServer {
|
|
|
131
182
|
}
|
|
132
183
|
}
|
|
133
184
|
|
|
185
|
+
export function parseAllowedOrigins(value) {
|
|
186
|
+
const origins = Array.isArray(value) ? value : String(value ?? "").split(",");
|
|
187
|
+
return new Set(origins.map((origin) => origin.trim().toLowerCase()).filter(Boolean));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function isOriginAllowed(origin, allowedOrigins = new Set()) {
|
|
191
|
+
if (origin == null) return true;
|
|
192
|
+
const normalized = origin.toLowerCase();
|
|
193
|
+
return allowedOrigins.has("*") || allowedOrigins.has(normalized);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function isSafeHostHeader(header) {
|
|
197
|
+
if (!header) return true;
|
|
198
|
+
try {
|
|
199
|
+
let hostname = new URL(`http://${header}`).hostname.toLowerCase();
|
|
200
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) {
|
|
201
|
+
hostname = hostname.slice(1, -1);
|
|
202
|
+
}
|
|
203
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) return true;
|
|
204
|
+
if (hostname.includes(":")) return /^[0-9a-f:.]+$/i.test(hostname);
|
|
205
|
+
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname)
|
|
206
|
+
&& hostname.split(".").every((part) => Number(part) <= 255);
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
134
212
|
function jsonResponse(value, status = 200) {
|
|
135
213
|
return new Response(JSON.stringify(value), {
|
|
136
214
|
status,
|
|
@@ -65,6 +65,8 @@ micro.on("init", () => {
|
|
|
65
65
|
micro.MakeCommand("cdp", async (bp, args) =>
|
|
66
66
|
{
|
|
67
67
|
const addrFlag = args.find(a => a.startsWith("--address="))?.slice("--address=".length);
|
|
68
|
+
const allowOrigins = args.find(a => a.startsWith("--remote-allow-origins="))
|
|
69
|
+
?.slice("--remote-allow-origins=".length);
|
|
68
70
|
const isPublic = args.includes("--public");
|
|
69
71
|
const port = parseInt(args.find(a => /^\d+$/.test(a))) || parseInt(Bun.env.CDP_PORT) || 9222;
|
|
70
72
|
const hostname = addrFlag ?? (isPublic ? "0.0.0.0" : "127.0.0.1");
|
|
@@ -123,7 +125,7 @@ micro.on("init", () => {
|
|
|
123
125
|
micro.cdpPort = port;
|
|
124
126
|
CdpServer
|
|
125
127
|
.create(micro.cdpContext)
|
|
126
|
-
.listen(port, hostname);
|
|
128
|
+
.listen(port, hostname, { remoteAllowOrigins: allowOrigins });
|
|
127
129
|
|
|
128
130
|
const addr = isPublic ? `0.0.0.0:${port}` : `127.0.0.1:${port}`;
|
|
129
131
|
micro.TermMessage(`CDP@${addr} server running 伺服器啟動了`)
|
|
@@ -1119,6 +1119,7 @@ function parseArgs(argv) {
|
|
|
1119
1119
|
cdpMaze: false,
|
|
1120
1120
|
cdpPort: 0,
|
|
1121
1121
|
cdpAddress: "",
|
|
1122
|
+
cdpAllowOrigins: "",
|
|
1122
1123
|
kittyMode: kittyModeFromEnvironment(process.env.JSMDCUI_KITTY_MODE),
|
|
1123
1124
|
settings: new Map(),
|
|
1124
1125
|
};
|
|
@@ -1197,6 +1198,10 @@ function parseArgs(argv) {
|
|
|
1197
1198
|
flags.cdpAddress = arg.slice("--remote-debugging-address=".length);
|
|
1198
1199
|
} else if (arg === "--remote-debugging-address") {
|
|
1199
1200
|
flags.cdpAddress = argv[++i] ?? "";
|
|
1201
|
+
} else if (arg.startsWith("--remote-allow-origins=")) {
|
|
1202
|
+
flags.cdpAllowOrigins = arg.slice("--remote-allow-origins=".length);
|
|
1203
|
+
} else if (arg === "--remote-allow-origins") {
|
|
1204
|
+
flags.cdpAllowOrigins = argv[++i] ?? "";
|
|
1200
1205
|
} else if (arg.startsWith("-") && arg.length > 1 && i + 1 < argv.length) {
|
|
1201
1206
|
flags.settings.set(arg.slice(1), argv[++i]);
|
|
1202
1207
|
} else {
|
|
@@ -1306,6 +1311,8 @@ CDP(Chrome DevTools Protocol):
|
|
|
1306
1311
|
Start CDP (Chrome DevTools Protocol) server on PORT at launch
|
|
1307
1312
|
--remote-debugging-address=ADDRESS
|
|
1308
1313
|
Bind CDP server to ADDRESS (default: 127.0.0.1); use 0.0.0.0 for all interfaces
|
|
1314
|
+
--remote-allow-origins=ORIGINS
|
|
1315
|
+
Allow comma-separated browser WebSocket origins; no browser origins are allowed by default
|
|
1309
1316
|
|
|
1310
1317
|
Information:
|
|
1311
1318
|
--help, -h, -help
|
|
@@ -9072,6 +9079,9 @@ async function main() {
|
|
|
9072
9079
|
if (flags.cdpPort) {
|
|
9073
9080
|
const cdpArgs = [flags.cdpPort];
|
|
9074
9081
|
if (flags.cdpAddress) cdpArgs.push(`--address=${flags.cdpAddress}`);
|
|
9082
|
+
if (flags.cdpAllowOrigins) {
|
|
9083
|
+
cdpArgs.push(`--remote-allow-origins=${flags.cdpAllowOrigins}`);
|
|
9084
|
+
}
|
|
9075
9085
|
await app.handleCommand(`cdp ${cdpArgs.join(" ")}`);
|
|
9076
9086
|
}
|
|
9077
9087
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
isOriginAllowed,
|
|
4
|
+
isSafeHostHeader,
|
|
5
|
+
parseAllowedOrigins,
|
|
6
|
+
} from "../runtime/jsplugins/cdp/cdp-server.js";
|
|
7
|
+
|
|
8
|
+
describe("Chromium-compatible CDP request checks", () => {
|
|
9
|
+
test("rejects browser origins by default but permits clients without Origin", () => {
|
|
10
|
+
expect(isOriginAllowed(null)).toBe(true);
|
|
11
|
+
expect(isOriginAllowed(undefined)).toBe(true);
|
|
12
|
+
expect(isOriginAllowed("http://localhost:8000")).toBe(false);
|
|
13
|
+
expect(isOriginAllowed("null")).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("allows only explicitly configured origins or wildcard", () => {
|
|
17
|
+
const exact = parseAllowedOrigins("http://localhost:8000, HTTPS://EXAMPLE.COM");
|
|
18
|
+
expect(isOriginAllowed("http://localhost:8000", exact)).toBe(true);
|
|
19
|
+
expect(isOriginAllowed("https://example.com", exact)).toBe(true);
|
|
20
|
+
expect(isOriginAllowed("http://127.0.0.1:8000", exact)).toBe(false);
|
|
21
|
+
expect(isOriginAllowed("https://evil.example", parseAllowedOrigins("*"))).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("accepts IP and localhost Host headers but rejects DNS names", () => {
|
|
25
|
+
expect(isSafeHostHeader("127.0.0.1:9222")).toBe(true);
|
|
26
|
+
expect(isSafeHostHeader("[::1]:9222")).toBe(true);
|
|
27
|
+
expect(isSafeHostHeader("localhost:9222")).toBe(true);
|
|
28
|
+
expect(isSafeHostHeader("devtools.localhost:9222")).toBe(true);
|
|
29
|
+
expect(isSafeHostHeader("attacker.example:9222")).toBe(false);
|
|
30
|
+
expect(isSafeHostHeader("127.0.0.999:9222")).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<meta charset="utf-8">
|
|
3
|
+
<title>jsmdcui CDP WebSocket test</title>
|
|
4
|
+
|
|
5
|
+
<button id="test">Try CDP connection</button>
|
|
6
|
+
<pre id="status">Ready.</pre>
|
|
7
|
+
|
|
8
|
+
<script>
|
|
9
|
+
const status = document.querySelector("#status");
|
|
10
|
+
const endpoint = "ws://127.0.0.1:9222/devtools/browser/cdp-server";
|
|
11
|
+
|
|
12
|
+
function log(message) {
|
|
13
|
+
status.textContent += `\n${message}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function testCdp() {
|
|
17
|
+
status.textContent = `Connecting to ${endpoint}`;
|
|
18
|
+
const socket = new WebSocket(endpoint);
|
|
19
|
+
|
|
20
|
+
socket.addEventListener("open", () => {
|
|
21
|
+
log("WebSocket connected. Sending Input.insertText...");
|
|
22
|
+
|
|
23
|
+
socket.send(JSON.stringify({
|
|
24
|
+
id: 1,
|
|
25
|
+
method: "Input.insertText",
|
|
26
|
+
params: { text: "hello from web page" },
|
|
27
|
+
}));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
socket.addEventListener("message", (event) => {
|
|
31
|
+
log(`CDP response: ${event.data}`);
|
|
32
|
+
socket.close();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
socket.addEventListener("error", () => {
|
|
36
|
+
log("WebSocket error or browser blocked the connection.");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
socket.addEventListener("close", (event) => {
|
|
40
|
+
log(`WebSocket closed (code ${event.code}).`);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
document.querySelector("#test").addEventListener("click", testCdp);
|
|
45
|
+
testCdp();
|
|
46
|
+
</script>
|
|
@@ -26,6 +26,7 @@ test("--help describes the non-overwriting demo behavior", () => {
|
|
|
26
26
|
expect(output.match(/Open it in the TUI and write 5 generated files beside it/g)?.length).toBe(2);
|
|
27
27
|
expect(output).not.toContain("Alias for --demo-image-processor");
|
|
28
28
|
expect(output).toContain("--cdp-maze");
|
|
29
|
+
expect(output).toContain("--remote-allow-origins=ORIGINS");
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
test("--demo-list lists root and automatically discovered demos", () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buninu",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Buninu is a portable, self-bootstrapping Unix-like userspace built on Bun that runs across operating systems",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/init.js",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
"exitAfterCmd": false,
|
|
27
27
|
"backToConsole": true,
|
|
28
|
-
"xdgDataHome": null
|
|
28
|
+
"xdgDataHome": null,
|
|
29
|
+
"hideExtraKeys": false
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
32
|
"start": "bun ./apps/jsgotty/gotty.js --reconnect -r -w --webgl -a 127.0.0.1",
|