@sproutboat/runtime 0.41.0 → 0.42.1
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 +13 -0
- package/package.json +1 -1
- package/src/egress.test.ts +101 -70
- package/src/egress.ts +14 -9
- package/src/native-fetch-prelude.js +287 -2
- package/src/sockets.test.ts +42 -0
- package/src/source.ts +15 -0
- package/src/transport-broker.js +471 -0
- package/src/transport-embedded.js +197 -2
- package/src/wrap.ts +14 -0
- package/test/bearssl.ts +12 -10
- package/test/native-sockets-broker.ts +460 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.42.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- dbba349: fetch() and connect() now also refuse reserved space that carries no public service: AS112 and the old IPv4 anycast blocks (`192.31.196.0/24`, `192.52.193.0/24`, `192.88.99.0/24`, `192.175.48.0/24`), SIIT (`::ffff:0:0:0/96`), IETF protocol assignments including Teredo (`2001::/23`), `3fff::/20` and SRv6 (`5f00::/16`). Applies to the broker, the egress service and the standalone client alike; `SB_EGRESS_ALLOW` still overrides.
|
|
8
|
+
- cf519dd: Outbound TCP sockets now set `TCP_NODELAY`. Without it, Nagle's algorithm and the peer's delayed ACK stalled every fresh HTTPS `fetch()` by about 44 ms on Linux (each small TLS handshake flight waited for an ACK). This covers `fetch()`, outbound WebSockets and `connect()` from `cloudflare:sockets` on the embedded transport (standalone binaries, including sockets handed over by `SB_EGRESS_FD`), and the egress tunnel's connections to origins in `wire`. No compatibility date: only latency changes, no result, header or error does.
|
|
9
|
+
|
|
10
|
+
## 0.42.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 2baeae9: `connect()` from `cloudflare:sockets`, from compatibility date 2026-11-17: plain TCP, `secureTransport: "on"` and `"starttls"` with `startTls()`, `readable`/`writable` byte streams with write backpressure, `opened`, `closed`, `close()`, half-close, workerd's error shapes, and port 25 refused. It runs on both transports: the embedded one (standalone and sproutd) opens the connection in a thread per socket and does TLS with BearSSL (extra roots through `SB_CA_BUNDLE`); the broker one tunnels through the broker, which vets every address with the `fetch()` policy (`SB_EGRESS_ALLOW`, private ranges refused), terminates TLS, and hands the tunnel to the egress service where the broker has no network. Limits: 16 sockets per app. From the same date `navigator.userAgent` is `"Cloudflare-Workers"` and a bundled `Buffer`/`process` is accepted, so unmodified `pg`, `postgres` and Redis clients run. The toolchain gains three Porffor fixes that those drivers hit: bindings named like libc macros, call arguments evaluated in order, and a writable `navigator`.
|
|
15
|
+
|
|
3
16
|
## 0.41.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
package/package.json
CHANGED
package/src/egress.test.ts
CHANGED
|
@@ -2,13 +2,7 @@ import { expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
blockedRange,
|
|
7
|
-
egressAllowList,
|
|
8
|
-
egressRefusal,
|
|
9
|
-
parseAddress,
|
|
10
|
-
spliceEgressTable,
|
|
11
|
-
} from "./egress";
|
|
5
|
+
import { blockedRange, egressAllowList, egressRefusal, parseAddress, spliceEgressTable } from "./egress";
|
|
12
6
|
import { validateHttpSyncSource } from "./source";
|
|
13
7
|
import { wrapNativeFetchHandler } from "./wrap";
|
|
14
8
|
|
|
@@ -41,6 +35,17 @@ const BLOCKED = [
|
|
|
41
35
|
"fd12::1",
|
|
42
36
|
"fe80::1",
|
|
43
37
|
"ff02::1",
|
|
38
|
+
"192.31.196.1",
|
|
39
|
+
"192.52.193.1",
|
|
40
|
+
"192.88.99.1",
|
|
41
|
+
"192.175.48.1",
|
|
42
|
+
"::ffff:0:7f00:1",
|
|
43
|
+
"2001::1",
|
|
44
|
+
"2001:0:4136:e378:8000:63bf:3fff:fdd2",
|
|
45
|
+
"2001:1ff::1",
|
|
46
|
+
"3fff::1",
|
|
47
|
+
"3fff:fff:ffff::1",
|
|
48
|
+
"5f00::1",
|
|
44
49
|
];
|
|
45
50
|
const PUBLIC = [
|
|
46
51
|
"1.1.1.1",
|
|
@@ -54,23 +59,25 @@ const PUBLIC = [
|
|
|
54
59
|
"2002:808:808::1",
|
|
55
60
|
"2001:4860:4860::8888",
|
|
56
61
|
"2606:4700::1111",
|
|
62
|
+
"192.31.195.255",
|
|
63
|
+
"192.88.100.1",
|
|
64
|
+
"2001:200::1",
|
|
65
|
+
"4000::1",
|
|
66
|
+
"3ffe::1",
|
|
67
|
+
"5f01::1",
|
|
57
68
|
];
|
|
58
69
|
const ALLOW = "8.8.4.4, 10.9.9.9";
|
|
59
70
|
|
|
60
71
|
test("private and reserved addresses are blocked, public ones are not", () => {
|
|
61
|
-
for (const address of BLOCKED)
|
|
62
|
-
|
|
63
|
-
for (const address of PUBLIC)
|
|
64
|
-
expect([address, blockedRange(address)]).toEqual([address, null]);
|
|
72
|
+
for (const address of BLOCKED) expect([address, blockedRange(address)]).not.toEqual([address, null]);
|
|
73
|
+
for (const address of PUBLIC) expect([address, blockedRange(address)]).toEqual([address, null]);
|
|
65
74
|
expect(blockedRange("::ffff:127.0.0.1")).toBe("127.0.0.0/8");
|
|
66
75
|
expect(blockedRange("64:ff9b::a00:1")).toBe("10.0.0.0/8");
|
|
67
76
|
});
|
|
68
77
|
|
|
69
78
|
test("the operator allow list lets an exact address or everything through", () => {
|
|
70
79
|
expect(egressRefusal("db", "10.9.9.9", egressAllowList(ALLOW))).toBeNull();
|
|
71
|
-
expect(egressRefusal("db", "10.9.9.8", egressAllowList(ALLOW))).toContain(
|
|
72
|
-
"SB_EGRESS_ALLOW=10.9.9.8",
|
|
73
|
-
);
|
|
80
|
+
expect(egressRefusal("db", "10.9.9.8", egressAllowList(ALLOW))).toContain("SB_EGRESS_ALLOW=10.9.9.8");
|
|
74
81
|
expect(egressRefusal("db", "127.0.0.1", ["*"])).toBeNull();
|
|
75
82
|
expect(egressRefusal("x", "169.254.169.254", [])).toBe(
|
|
76
83
|
"fetch() refused: x resolves to 169.254.169.254, a private or reserved address (169.254.0.0/16). An operator can allow it with SB_EGRESS_ALLOW=169.254.169.254",
|
|
@@ -78,15 +85,7 @@ test("the operator allow list lets an exact address or everything through", () =
|
|
|
78
85
|
});
|
|
79
86
|
|
|
80
87
|
test("parseAddress rejects what is not an address", () => {
|
|
81
|
-
for (const bad of [
|
|
82
|
-
"",
|
|
83
|
-
"1.2.3",
|
|
84
|
-
"256.1.1.1",
|
|
85
|
-
"example.com",
|
|
86
|
-
"1::2::3",
|
|
87
|
-
"1:2:3:4:5:6:7:8:9",
|
|
88
|
-
"12345::",
|
|
89
|
-
])
|
|
88
|
+
for (const bad of ["", "1.2.3", "256.1.1.1", "example.com", "1::2::3", "1:2:3:4:5:6:7:8:9", "12345::"])
|
|
90
89
|
expect([bad, parseAddress(bad)]).toEqual([bad, null]);
|
|
91
90
|
});
|
|
92
91
|
|
|
@@ -99,10 +98,7 @@ test("the C table in transport-embedded.js is generated from this one (bun run g
|
|
|
99
98
|
|
|
100
99
|
test("the C check gives the same answer and message as the TS one", () => {
|
|
101
100
|
const source = readFileSync(transportFile, "utf8");
|
|
102
|
-
const block = source.slice(
|
|
103
|
-
source.indexOf("// EGRESS:BEGIN"),
|
|
104
|
-
source.indexOf("// EGRESS:END"),
|
|
105
|
-
);
|
|
101
|
+
const block = source.slice(source.indexOf("// EGRESS:BEGIN"), source.indexOf("// EGRESS:END"));
|
|
106
102
|
const dir = mkdtempSync(join(tmpdir(), "sb-egress-"));
|
|
107
103
|
try {
|
|
108
104
|
writeFileSync(
|
|
@@ -130,23 +126,14 @@ int main(int argc, char** argv) {
|
|
|
130
126
|
}
|
|
131
127
|
`,
|
|
132
128
|
);
|
|
133
|
-
const cc = Bun.spawnSync([
|
|
134
|
-
"cc",
|
|
135
|
-
"-Wall",
|
|
136
|
-
"-Werror",
|
|
137
|
-
"-o",
|
|
138
|
-
join(dir, "check"),
|
|
139
|
-
join(dir, "check.c"),
|
|
140
|
-
]);
|
|
129
|
+
const cc = Bun.spawnSync(["cc", "-Wall", "-Werror", "-o", join(dir, "check"), join(dir, "check.c")]);
|
|
141
130
|
expect(cc.stderr.toString()).toBe("");
|
|
142
131
|
const addresses = [...BLOCKED, ...PUBLIC, "10.9.9.9"];
|
|
143
132
|
const run = Bun.spawnSync([join(dir, "check"), ...addresses], {
|
|
144
133
|
env: { ...process.env, SB_EGRESS_ALLOW: ALLOW },
|
|
145
134
|
});
|
|
146
135
|
const fromC = run.stdout.toString().trimEnd().split("\n");
|
|
147
|
-
const fromTs = addresses.map(
|
|
148
|
-
(a) => egressRefusal("h", a, egressAllowList(ALLOW)) ?? "ok",
|
|
149
|
-
);
|
|
136
|
+
const fromTs = addresses.map((a) => egressRefusal("h", a, egressAllowList(ALLOW)) ?? "ok");
|
|
150
137
|
expect(fromC).toEqual(fromTs);
|
|
151
138
|
} finally {
|
|
152
139
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -156,21 +143,13 @@ int main(int argc, char** argv) {
|
|
|
156
143
|
test("#174: fetch() needs no config, so every sprout gets it", () => {
|
|
157
144
|
const handler = `export default { async fetch() { return await fetch("https://api.example.com/"); } };`;
|
|
158
145
|
expect(validateHttpSyncSource(handler)).toEqual({ ok: true });
|
|
159
|
-
const prelude = readFileSync(
|
|
160
|
-
|
|
161
|
-
"utf8",
|
|
162
|
-
);
|
|
163
|
-
expect(wrapNativeFetchHandler(handler, prelude)).toContain(
|
|
164
|
-
"__sbInstallFetch();",
|
|
165
|
-
);
|
|
146
|
+
const prelude = readFileSync(new URL("./native-fetch-prelude.js", import.meta.url), "utf8");
|
|
147
|
+
expect(wrapNativeFetchHandler(handler, prelude)).toContain("__sbInstallFetch();");
|
|
166
148
|
});
|
|
167
149
|
|
|
168
150
|
test("SB_EGRESS_FD: the host connects, the sprout gets the socket", () => {
|
|
169
151
|
const source = readFileSync(transportFile, "utf8");
|
|
170
|
-
const block = source.slice(
|
|
171
|
-
source.indexOf("// EGRESS-FD:BEGIN"),
|
|
172
|
-
source.indexOf("// EGRESS-FD:END"),
|
|
173
|
-
);
|
|
152
|
+
const block = source.slice(source.indexOf("// EGRESS-FD:BEGIN"), source.indexOf("// EGRESS-FD:END"));
|
|
174
153
|
const dir = mkdtempSync(join(tmpdir(), "sb-egress-fd-"));
|
|
175
154
|
try {
|
|
176
155
|
// A fake host on one end of a socketpair: "up" gets a socket whose peer says
|
|
@@ -184,6 +163,8 @@ test("SB_EGRESS_FD: the host connects, the sprout gets the socket", () => {
|
|
|
184
163
|
#include <string.h>
|
|
185
164
|
#include <unistd.h>
|
|
186
165
|
#include <sys/socket.h>
|
|
166
|
+
#include <netinet/in.h>
|
|
167
|
+
#include <netinet/tcp.h>
|
|
187
168
|
#include <sys/time.h>
|
|
188
169
|
#include <sys/wait.h>
|
|
189
170
|
${block}
|
|
@@ -261,18 +242,76 @@ int main(void) {
|
|
|
261
242
|
}
|
|
262
243
|
});
|
|
263
244
|
|
|
245
|
+
test("sb_tcp_connect sets TCP_NODELAY on the socket it returns", () => {
|
|
246
|
+
// Nagle plus the peer's delayed ACK stalled every fresh HTTPS fetch about 44 ms on Linux (a TLS
|
|
247
|
+
// handshake writes small flights). Timing is flaky on CI and loopback on macOS does not show
|
|
248
|
+
// it, so ask the kernel: connect to a local listener and read the option back.
|
|
249
|
+
const source = readFileSync(transportFile, "utf8");
|
|
250
|
+
const between = (from: string, to: string) => source.slice(source.indexOf(from), source.indexOf(to));
|
|
251
|
+
const dir = mkdtempSync(join(tmpdir(), "sb-nodelay-"));
|
|
252
|
+
try {
|
|
253
|
+
writeFileSync(
|
|
254
|
+
join(dir, "check.c"),
|
|
255
|
+
`#include <errno.h>
|
|
256
|
+
#include <pthread.h>
|
|
257
|
+
#include <netdb.h>
|
|
258
|
+
#include <stdio.h>
|
|
259
|
+
#include <stdlib.h>
|
|
260
|
+
#include <string.h>
|
|
261
|
+
#include <unistd.h>
|
|
262
|
+
#include <sys/socket.h>
|
|
263
|
+
#include <sys/time.h>
|
|
264
|
+
#include <sys/uio.h>
|
|
265
|
+
#include <netinet/in.h>
|
|
266
|
+
#include <netinet/tcp.h>
|
|
267
|
+
#include <arpa/inet.h>
|
|
268
|
+
${between("// EGRESS:BEGIN", "// EGRESS:END")}
|
|
269
|
+
${between("// EGRESS-FD:BEGIN", "// EGRESS-FD:END")}
|
|
270
|
+
${between("// TCP-CONNECT:BEGIN", "// TCP-CONNECT:END")}
|
|
271
|
+
int main(void) {
|
|
272
|
+
int listener = socket(AF_INET, SOCK_STREAM, 0);
|
|
273
|
+
struct sockaddr_in addr;
|
|
274
|
+
memset(&addr, 0, sizeof(addr));
|
|
275
|
+
addr.sin_family = AF_INET;
|
|
276
|
+
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
277
|
+
if (bind(listener, (struct sockaddr*)&addr, sizeof(addr)) || listen(listener, 1)) return 2;
|
|
278
|
+
socklen_t len = sizeof(addr);
|
|
279
|
+
getsockname(listener, (struct sockaddr*)&addr, &len);
|
|
280
|
+
char refused[512] = "";
|
|
281
|
+
int fd = sb_tcp_connect("127.0.0.1", ntohs(addr.sin_port), refused, sizeof(refused));
|
|
282
|
+
int value = -1;
|
|
283
|
+
socklen_t vlen = sizeof(value);
|
|
284
|
+
if (fd >= 0) getsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &value, &vlen);
|
|
285
|
+
printf("fd %s nodelay %s\\n", fd >= 0 ? "yes" : "no", value > 0 ? "on" : "off");
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
`,
|
|
289
|
+
);
|
|
290
|
+
const cc = Bun.spawnSync([
|
|
291
|
+
"cc",
|
|
292
|
+
"-Wall",
|
|
293
|
+
"-Werror",
|
|
294
|
+
"-Wno-unused-function",
|
|
295
|
+
"-Wno-unused-result",
|
|
296
|
+
"-o",
|
|
297
|
+
join(dir, "check"),
|
|
298
|
+
join(dir, "check.c"),
|
|
299
|
+
]);
|
|
300
|
+
expect(cc.stderr.toString()).toBe("");
|
|
301
|
+
const run = Bun.spawnSync([join(dir, "check")], {
|
|
302
|
+
env: { ...process.env, SB_EGRESS_ALLOW: "127.0.0.1", SB_EGRESS_FD: "" },
|
|
303
|
+
});
|
|
304
|
+
expect(run.stdout.toString().trim()).toBe("fd yes nodelay on");
|
|
305
|
+
} finally {
|
|
306
|
+
rmSync(dir, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
264
310
|
test("the Host header carries a non-default port", () => {
|
|
265
311
|
const source = readFileSync(transportFile, "utf8");
|
|
266
|
-
const between = (from: string, to: string) =>
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
"typedef struct { char* p; size_t len; size_t cap; } sb_buf;",
|
|
270
|
-
"static void sb_putjson(",
|
|
271
|
-
);
|
|
272
|
-
const builder = between(
|
|
273
|
-
"// body_len, not strlen(body)",
|
|
274
|
-
"// Turn a whole raw response",
|
|
275
|
-
);
|
|
312
|
+
const between = (from: string, to: string) => source.slice(source.indexOf(from), source.indexOf(to));
|
|
313
|
+
const buffer = between("typedef struct { char* p; size_t len; size_t cap; } sb_buf;", "static void sb_putjson(");
|
|
314
|
+
const builder = between("// body_len, not strlen(body)", "// Turn a whole raw response");
|
|
276
315
|
const dir = mkdtempSync(join(tmpdir(), "sb-host-"));
|
|
277
316
|
try {
|
|
278
317
|
writeFileSync(
|
|
@@ -318,10 +357,7 @@ int main(void) { show(80, 80); show(8080, 80); show(443, 443); show(8443, 443);
|
|
|
318
357
|
|
|
319
358
|
test("async fetch: requests run on threads and come back through the host", () => {
|
|
320
359
|
const source = readFileSync(transportFile, "utf8");
|
|
321
|
-
const block = source.slice(
|
|
322
|
-
source.indexOf("// ASYNC:BEGIN"),
|
|
323
|
-
source.indexOf("// ASYNC:END"),
|
|
324
|
-
);
|
|
360
|
+
const block = source.slice(source.indexOf("// ASYNC:BEGIN"), source.indexOf("// ASYNC:END"));
|
|
325
361
|
const dir = mkdtempSync(join(tmpdir(), "sb-async-"));
|
|
326
362
|
try {
|
|
327
363
|
// Stand-ins: a client that takes 200 ms, and a host that records which timers fire.
|
|
@@ -385,13 +421,8 @@ int main(void) {
|
|
|
385
421
|
.trimEnd()
|
|
386
422
|
.split("\n");
|
|
387
423
|
// 16 requests of 200 ms on 8 threads: two rounds, not sixteen.
|
|
388
|
-
expect(lines.slice(0, 16).sort()).toEqual(
|
|
389
|
-
|
|
390
|
-
);
|
|
391
|
-
expect(lines.slice(16)).toEqual([
|
|
392
|
-
"elapsed overlapped",
|
|
393
|
-
"outside a callback nothing",
|
|
394
|
-
]);
|
|
424
|
+
expect(lines.slice(0, 16).sort()).toEqual(Array.from({ length: 16 }, (_, i) => `${i + 1} h/${i + 1} body`).sort());
|
|
425
|
+
expect(lines.slice(16)).toEqual(["elapsed overlapped", "outside a callback nothing"]);
|
|
395
426
|
} finally {
|
|
396
427
|
rmSync(dir, { recursive: true, force: true });
|
|
397
428
|
}
|
package/src/egress.ts
CHANGED
|
@@ -41,6 +41,18 @@ export const BLOCKED_RANGES = [
|
|
|
41
41
|
"fe80::/10",
|
|
42
42
|
"fec0::/10",
|
|
43
43
|
"ff00::/8",
|
|
44
|
+
// Reserved space that carries no public service: AS112 and the IETF's old anycast
|
|
45
|
+
// blocks, SIIT (`::ffff:0:0:0/96`, which embeds an IPv4 address the mapped rule
|
|
46
|
+
// above does not see), IETF protocol assignments (includes Teredo, `2001::/32`),
|
|
47
|
+
// the second documentation block and SRv6 SIDs.
|
|
48
|
+
"192.31.196.0/24",
|
|
49
|
+
"192.52.193.0/24",
|
|
50
|
+
"192.88.99.0/24",
|
|
51
|
+
"192.175.48.0/24",
|
|
52
|
+
"::ffff:0:0:0/96",
|
|
53
|
+
"2001::/23",
|
|
54
|
+
"3fff::/20",
|
|
55
|
+
"5f00::/16",
|
|
44
56
|
] as const;
|
|
45
57
|
|
|
46
58
|
/** Parse an IPv4 or IPv6 literal to its bytes; `null` when it is neither. */
|
|
@@ -123,11 +135,7 @@ export function egressAllowList(raw: string | undefined): string[] {
|
|
|
123
135
|
|
|
124
136
|
/** Why `fetch()` may not connect to `address`, or `null` when it may. The
|
|
125
137
|
* message is identical in the C client; keep the two in step. */
|
|
126
|
-
export function egressRefusal(
|
|
127
|
-
host: string,
|
|
128
|
-
address: string,
|
|
129
|
-
allow: readonly string[],
|
|
130
|
-
): string | null {
|
|
138
|
+
export function egressRefusal(host: string, address: string, allow: readonly string[]): string | null {
|
|
131
139
|
if (allow.includes("*") || allow.includes(address)) return null;
|
|
132
140
|
const range = blockedRange(address);
|
|
133
141
|
if (range === null) return null;
|
|
@@ -149,8 +157,5 @@ const TABLE_BEGIN = /( *\/\/ EGRESS-TABLE:BEGIN[^\n]*\n)[\s\S]*?( *\/\/ EGRESS-T
|
|
|
149
157
|
/** `source` with the generated table between its EGRESS-TABLE markers replaced. */
|
|
150
158
|
export function spliceEgressTable(source: string): string {
|
|
151
159
|
if (!TABLE_BEGIN.test(source)) throw new Error("EGRESS-TABLE markers not found");
|
|
152
|
-
return source.replace(
|
|
153
|
-
TABLE_BEGIN,
|
|
154
|
-
(_, begin: string, end: string) => `${begin}${egressRangesC()}\n${end}`,
|
|
155
|
-
);
|
|
160
|
+
return source.replace(TABLE_BEGIN, (_, begin: string, end: string) => `${begin}${egressRangesC()}\n${end}`);
|
|
156
161
|
}
|
|
@@ -11,6 +11,8 @@ function __sbEnvValue() {
|
|
|
11
11
|
// Setters, not bare assignments in the tail: the template and every app must compile the prelude identically.
|
|
12
12
|
function __sbSetCompat(date) {
|
|
13
13
|
__sbCompatNow = date;
|
|
14
|
+
// navigator.userAgent as workerd has it: pg picks its Workers socket code on exactly this string.
|
|
15
|
+
if (date >= "2026-11-17") navigator.userAgent = "Cloudflare-Workers";
|
|
14
16
|
Request._cd = date;
|
|
15
17
|
if (date >= "2026-11-15" && Error.captureStackTrace === undefined) Error.captureStackTrace = __sbCaptureStackTrace;
|
|
16
18
|
}
|
|
@@ -14162,7 +14164,7 @@ function __sbWsOp(op, id, data, n) {
|
|
|
14162
14164
|
const unsigned int __id = id.type == 1 && id.val > 0 ? (unsigned int)id.val : 0;
|
|
14163
14165
|
const size_t __n = n.type == 1 && n.val > 0 ? (size_t)n.val : 0;
|
|
14164
14166
|
char __r[64]; __r[0] = 0;
|
|
14165
|
-
if (__op == 'u' || __op == 'W' || (__id & 0x80000000u)) {
|
|
14167
|
+
if (__op == 'u' || __op == 'T' || __op == 'W' || (__id & 0x80000000u)) {
|
|
14166
14168
|
long long __w = -1;
|
|
14167
14169
|
if (!sb_wsc_hook) snprintf(__r, sizeof(__r), "0");
|
|
14168
14170
|
else if (__op == 'W') snprintf(__r, sizeof(__r), "%d", sb_wsc_wakeable_hook && sb_wsc_wakeable_hook() ? 1 : 0);
|
|
@@ -14181,7 +14183,7 @@ function __sbWsOp(op, id, data, n) {
|
|
|
14181
14183
|
const char* __d = 0; size_t __dl = 0;
|
|
14182
14184
|
if (porf_native_fetch_read_raw_bytes(data, &__d, &__dl) == 0) __w = sb_wsc_hook('b', __id, __d, __dl, 0, 0, 0);
|
|
14183
14185
|
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
14184
|
-
} else if (__op == 't' || __op == 'c' || __op == 'u') {
|
|
14186
|
+
} else if (__op == 't' || __op == 'c' || __op == 'u' || __op == 'T' || __op == 'S') {
|
|
14185
14187
|
const char* __d; size_t __dl; char* __do = 0;
|
|
14186
14188
|
porf_native_fetch_read_value(data, &__d, &__dl, &__do);
|
|
14187
14189
|
__w = sb_wsc_hook(__op, __id, __d, __dl, __n, 0, 0);
|
|
@@ -14573,6 +14575,289 @@ function __sbWsPump(ws) {
|
|
|
14573
14575
|
}
|
|
14574
14576
|
}
|
|
14575
14577
|
|
|
14578
|
+
// ---------------------------------------------------------------------------
|
|
14579
|
+
// cloudflare:sockets connect(), from compatibility date 2026-11-17. A socket is a host
|
|
14580
|
+
// socket in the outbound WebSocket table (sb_wsc, raw mode); this end speaks the __sbWsOp ops:
|
|
14581
|
+
// "T" open (host, port, secureTransport mode, server name), "b" queue bytes (returns how many
|
|
14582
|
+
// are still unsent), "S" start TLS, "H" shut the write side, "p" "i" take an event, "k" ack,
|
|
14583
|
+
// "w" park a timer, "r" release. Events: 1 data, 3 error, 4 connected, 6 end of input,
|
|
14584
|
+
// 8 send queue drained, 9 TLS handshake done.
|
|
14585
|
+
const __SB_SOCKETS_DATE = "2026-11-17";
|
|
14586
|
+
const __SB_TCP_HWM = 262144;
|
|
14587
|
+
let __sbTcpOpen = 0;
|
|
14588
|
+
|
|
14589
|
+
function __sbTcpBadInt() {
|
|
14590
|
+
return new TypeError("The value cannot be converted because it is not an integer.");
|
|
14591
|
+
}
|
|
14592
|
+
function __sbTcpTarget(address) {
|
|
14593
|
+
let host;
|
|
14594
|
+
let port;
|
|
14595
|
+
if (__sbIsStr(address)) {
|
|
14596
|
+
let rest = address;
|
|
14597
|
+
let cut;
|
|
14598
|
+
if (rest.charAt(0) === "[") {
|
|
14599
|
+
const close = rest.indexOf("]");
|
|
14600
|
+
cut = close >= 0 && rest.charAt(close + 1) === ":" ? close + 1 : -1;
|
|
14601
|
+
} else cut = rest.lastIndexOf(":");
|
|
14602
|
+
if (cut < 0) {
|
|
14603
|
+
if (rest === "") throw new TypeError("Specified address is missing hostname.");
|
|
14604
|
+
throw new TypeError("Specified address is missing port.");
|
|
14605
|
+
}
|
|
14606
|
+
host = rest.slice(0, cut);
|
|
14607
|
+
port = rest.slice(cut + 1);
|
|
14608
|
+
} else if (address != null && __sbIsObj(address)) {
|
|
14609
|
+
host = address.hostname;
|
|
14610
|
+
port = address.port;
|
|
14611
|
+
if (host === undefined || host === "") throw new TypeError("Specified address is missing hostname.");
|
|
14612
|
+
if (port === undefined) throw new TypeError("Specified address is missing port.");
|
|
14613
|
+
} else throw new TypeError("connect() takes an address string or { hostname, port }.");
|
|
14614
|
+
const n = Number(port);
|
|
14615
|
+
if (!(Number.isInteger(n)) || String(port).trim() === "") throw __sbTcpBadInt();
|
|
14616
|
+
if (n < 0 || n > 65535) throw new RangeError("Port out of range.");
|
|
14617
|
+
return { host: String(host), port: n };
|
|
14618
|
+
}
|
|
14619
|
+
|
|
14620
|
+
function __sbTcpChunk(chunk) {
|
|
14621
|
+
if (__sbIsStr(chunk)) return new TextEncoder().encode(chunk);
|
|
14622
|
+
if (chunk instanceof ArrayBuffer) return new Uint8Array(chunk);
|
|
14623
|
+
if (ArrayBuffer.isView(chunk)) return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
14624
|
+
throw new TypeError("This TransformStream is being used as a byte stream, but received an object of non-ArrayBuffer/ArrayBufferView type on its writable side.");
|
|
14625
|
+
}
|
|
14626
|
+
|
|
14627
|
+
// Takes every event the host has, then parks a timer (or polls, where nothing can wake the loop).
|
|
14628
|
+
function __sbTcpPump(st) {
|
|
14629
|
+
if (st.id === 0) return;
|
|
14630
|
+
for (;;) {
|
|
14631
|
+
const head = __sbWsOp("p", st.id, "", 0);
|
|
14632
|
+
if (head === "") {
|
|
14633
|
+
if (st.id === 0) return;
|
|
14634
|
+
if (__sbWsCanWake()) {
|
|
14635
|
+
st.timer = setTimeout(() => __sbTcpPump(st), 2147483647);
|
|
14636
|
+
__sbWsOp("w", st.id, "", st.timer);
|
|
14637
|
+
} else st.timer = setTimeout(() => __sbTcpPump(st), 10);
|
|
14638
|
+
return;
|
|
14639
|
+
}
|
|
14640
|
+
const parts = head.split(" ");
|
|
14641
|
+
const kind = Number(parts[0]);
|
|
14642
|
+
const length = Number(parts[1]);
|
|
14643
|
+
const bytes = new Uint8Array(length);
|
|
14644
|
+
if (length > 0) __sbWsOp("i", st.id, bytes, 0);
|
|
14645
|
+
else __sbWsOp("i", st.id, new Uint8Array(0), 0);
|
|
14646
|
+
if (kind === 1) {
|
|
14647
|
+
st.inbox.push(bytes);
|
|
14648
|
+
} else if (kind === 4 || kind === 9) {
|
|
14649
|
+
if (st.opening) {
|
|
14650
|
+
const o = st.opening;
|
|
14651
|
+
st.opening = null;
|
|
14652
|
+
o.resolve({ remoteAddress: st.host + ":" + st.port, localAddress: null });
|
|
14653
|
+
}
|
|
14654
|
+
} else if (kind === 6) {
|
|
14655
|
+
st.eof = true;
|
|
14656
|
+
if (!st.allowHalfOpen) st.writeEnd = true;
|
|
14657
|
+
} else if (kind === 8) {
|
|
14658
|
+
const d = st.drain;
|
|
14659
|
+
st.drain = null;
|
|
14660
|
+
if (d) d();
|
|
14661
|
+
} else if (kind === 3) {
|
|
14662
|
+
// workerd words every connect failure the same; the host's reason stays on st.reason
|
|
14663
|
+
st.reason = new TextDecoder().decode(bytes);
|
|
14664
|
+
st.failed = new Error("proxy request failed, cannot connect to the specified address");
|
|
14665
|
+
st.eof = true;
|
|
14666
|
+
}
|
|
14667
|
+
__sbTcpSettle(st);
|
|
14668
|
+
if (st.id === 0) return;
|
|
14669
|
+
}
|
|
14670
|
+
}
|
|
14671
|
+
|
|
14672
|
+
// Applies state changes to the streams and promises.
|
|
14673
|
+
function __sbTcpSettle(st) {
|
|
14674
|
+
if (st.opening && st.failed) {
|
|
14675
|
+
const o = st.opening;
|
|
14676
|
+
st.opening = null;
|
|
14677
|
+
o.reject(st.failed);
|
|
14678
|
+
}
|
|
14679
|
+
const wake = st.pullWake;
|
|
14680
|
+
if (wake && (st.inbox.length > 0 || st.eof)) {
|
|
14681
|
+
st.pullWake = null;
|
|
14682
|
+
wake();
|
|
14683
|
+
}
|
|
14684
|
+
if (st.eof && st.inbox.length === 0 && !st.settled) {
|
|
14685
|
+
if (st.failed) {
|
|
14686
|
+
st.settled = true;
|
|
14687
|
+
st.closedReject(st.failed);
|
|
14688
|
+
st.release();
|
|
14689
|
+
} else if (!st.allowHalfOpen) {
|
|
14690
|
+
st.settled = true;
|
|
14691
|
+
st.closedResolve(undefined);
|
|
14692
|
+
if (st.writableController) st.writableController.error(new TypeError("This WritableStream has been closed."));
|
|
14693
|
+
st.release();
|
|
14694
|
+
}
|
|
14695
|
+
}
|
|
14696
|
+
}
|
|
14697
|
+
|
|
14698
|
+
function __sbTcpRelease(st) {
|
|
14699
|
+
const id = st.id;
|
|
14700
|
+
if (id === 0) return;
|
|
14701
|
+
st.id = 0;
|
|
14702
|
+
if (st.timer !== undefined) clearTimeout(st.timer);
|
|
14703
|
+
__sbWsOp("r", id, "", 0);
|
|
14704
|
+
__sbTcpOpen--;
|
|
14705
|
+
}
|
|
14706
|
+
|
|
14707
|
+
// One socket facade over a host connection. `st` is shared with the socket startTls() returns.
|
|
14708
|
+
function __sbTcpFacade(st, mode) {
|
|
14709
|
+
const closed = new Promise((resolve, reject) => {
|
|
14710
|
+
st.closedResolve = resolve;
|
|
14711
|
+
st.closedReject = reject;
|
|
14712
|
+
});
|
|
14713
|
+
// A rejected `closed` nobody awaits must not surface as an unhandled rejection.
|
|
14714
|
+
closed.catch(() => {});
|
|
14715
|
+
const opened = new Promise((resolve, reject) => {
|
|
14716
|
+
st.opening = { resolve, reject };
|
|
14717
|
+
});
|
|
14718
|
+
opened.catch(() => {});
|
|
14719
|
+
const readable = new ReadableStream(
|
|
14720
|
+
{
|
|
14721
|
+
start(controller) {
|
|
14722
|
+
st.readableController = controller;
|
|
14723
|
+
},
|
|
14724
|
+
pull(controller) {
|
|
14725
|
+
if (st.inbox.length > 0) {
|
|
14726
|
+
const chunk = st.inbox.shift();
|
|
14727
|
+
controller.enqueue(chunk);
|
|
14728
|
+
if (st.id !== 0) __sbWsOp("k", st.id, "", chunk.length);
|
|
14729
|
+
return undefined;
|
|
14730
|
+
}
|
|
14731
|
+
if (st.eof) {
|
|
14732
|
+
if (st.failed && !st.closedByUser) {
|
|
14733
|
+
// a failed connect reads as an empty stream, as in workerd
|
|
14734
|
+
}
|
|
14735
|
+
controller.close();
|
|
14736
|
+
return undefined;
|
|
14737
|
+
}
|
|
14738
|
+
return new Promise((resolve) => {
|
|
14739
|
+
st.pullWake = () => {
|
|
14740
|
+
if (st.inbox.length > 0) {
|
|
14741
|
+
const chunk = st.inbox.shift();
|
|
14742
|
+
controller.enqueue(chunk);
|
|
14743
|
+
if (st.id !== 0) __sbWsOp("k", st.id, "", chunk.length);
|
|
14744
|
+
} else controller.close();
|
|
14745
|
+
resolve();
|
|
14746
|
+
};
|
|
14747
|
+
});
|
|
14748
|
+
},
|
|
14749
|
+
cancel() {
|
|
14750
|
+
return undefined;
|
|
14751
|
+
},
|
|
14752
|
+
},
|
|
14753
|
+
{ highWaterMark: 0 },
|
|
14754
|
+
);
|
|
14755
|
+
const writable = new WritableStream(
|
|
14756
|
+
{
|
|
14757
|
+
start(controller) {
|
|
14758
|
+
st.writableController = controller;
|
|
14759
|
+
},
|
|
14760
|
+
write(chunk) {
|
|
14761
|
+
const bytes = __sbTcpChunk(chunk);
|
|
14762
|
+
if (st.id === 0) throw new TypeError("This WritableStream has been closed.");
|
|
14763
|
+
const queued = Number(__sbWsOp("b", st.id, __sbToBytes(bytes), 0));
|
|
14764
|
+
if (queued > __SB_TCP_HWM) return new Promise((resolve) => (st.drain = resolve));
|
|
14765
|
+
return undefined;
|
|
14766
|
+
},
|
|
14767
|
+
close() {
|
|
14768
|
+
if (st.id !== 0) __sbWsOp("H", st.id, "", 0);
|
|
14769
|
+
st.writeClosed = true;
|
|
14770
|
+
return undefined;
|
|
14771
|
+
},
|
|
14772
|
+
abort() {
|
|
14773
|
+
return undefined;
|
|
14774
|
+
},
|
|
14775
|
+
},
|
|
14776
|
+
{ highWaterMark: 1 },
|
|
14777
|
+
);
|
|
14778
|
+
const socket = {
|
|
14779
|
+
readable,
|
|
14780
|
+
writable,
|
|
14781
|
+
opened,
|
|
14782
|
+
closed,
|
|
14783
|
+
upgraded: false,
|
|
14784
|
+
secureTransport: mode,
|
|
14785
|
+
close() {
|
|
14786
|
+
st.closedByUser = true;
|
|
14787
|
+
if (st.writableController) st.writableController.error(new TypeError("This WritableStream belongs to an object that is closing."));
|
|
14788
|
+
const wake = st.pullWake;
|
|
14789
|
+
st.pullWake = null;
|
|
14790
|
+
if (!st.settled) {
|
|
14791
|
+
st.settled = true;
|
|
14792
|
+
st.closedResolve(undefined);
|
|
14793
|
+
}
|
|
14794
|
+
st.inbox.length = 0;
|
|
14795
|
+
st.eof = true;
|
|
14796
|
+
if (wake && st.readableController) st.readableController.error(new TypeError("This socket has been closed."));
|
|
14797
|
+
__sbTcpRelease(st);
|
|
14798
|
+
return Promise.resolve();
|
|
14799
|
+
},
|
|
14800
|
+
startTls(options) {
|
|
14801
|
+
if (mode !== "starttls" || st.upgradedOnce)
|
|
14802
|
+
throw new TypeError(st.upgradedOnce ? "The connection was closed before startTls could be started." : "The `secureTransport` socket option must be set to 'starttls' for startTls to be used.");
|
|
14803
|
+
if (readable.locked || writable.locked) throw new TypeError("Cannot startTls while the streams are locked.");
|
|
14804
|
+
st.upgradedOnce = true;
|
|
14805
|
+
let sni = st.host;
|
|
14806
|
+
if (options != null && options.expectedServerHostname != null) sni = String(options.expectedServerHostname);
|
|
14807
|
+
__sbWsOp("S", st.id, sni, 0);
|
|
14808
|
+
// the old socket ends once the upgrade is under way (workerd resolves its `closed`)
|
|
14809
|
+
const oldResolve = st.closedResolve;
|
|
14810
|
+
const next = __sbTcpFacade(st, "on");
|
|
14811
|
+
setTimeout(() => oldResolve(undefined), 0);
|
|
14812
|
+
return next;
|
|
14813
|
+
},
|
|
14814
|
+
};
|
|
14815
|
+
return socket;
|
|
14816
|
+
}
|
|
14817
|
+
|
|
14818
|
+
function __sbConnect(address, options) {
|
|
14819
|
+
if (__sbCompatDate() < __SB_SOCKETS_DATE)
|
|
14820
|
+
throw new Error("connect() needs compatibility_date " + __SB_SOCKETS_DATE + " or later");
|
|
14821
|
+
const target = __sbTcpTarget(address);
|
|
14822
|
+
let mode = "off";
|
|
14823
|
+
let allowHalfOpen = false;
|
|
14824
|
+
if (options != null) {
|
|
14825
|
+
if (options.secureTransport !== undefined) {
|
|
14826
|
+
mode = String(options.secureTransport);
|
|
14827
|
+
if (mode !== "off" && mode !== "on" && mode !== "starttls")
|
|
14828
|
+
throw new TypeError("Unsupported secureTransport value: " + mode);
|
|
14829
|
+
}
|
|
14830
|
+
allowHalfOpen = options.allowHalfOpen === true;
|
|
14831
|
+
}
|
|
14832
|
+
const st = {
|
|
14833
|
+
id: 0, host: target.host, port: target.port, inbox: [], eof: false, failed: null, settled: false,
|
|
14834
|
+
allowHalfOpen, pullWake: null, drain: null, timer: undefined, opening: null, upgradedOnce: false,
|
|
14835
|
+
closedByUser: false, writableController: null, readableController: null, reason: "", writeClosed: false, writeEnd: false,
|
|
14836
|
+
release: null, closedResolve: null, closedReject: null,
|
|
14837
|
+
};
|
|
14838
|
+
st.release = () => __sbTcpRelease(st);
|
|
14839
|
+
const socket = __sbTcpFacade(st, mode);
|
|
14840
|
+
if (target.port === 25) {
|
|
14841
|
+
st.failed = new Error("proxy request failed, cannot connect to the specified address");
|
|
14842
|
+
st.eof = true;
|
|
14843
|
+
setTimeout(() => __sbTcpSettle(st), 0);
|
|
14844
|
+
return socket;
|
|
14845
|
+
}
|
|
14846
|
+
const spec = target.host + "\n" + target.port + "\n" + (mode === "on" ? "1" : mode === "starttls" ? "2" : "0") + "\n" + target.host;
|
|
14847
|
+
const id = __sbTcpOpen >= 16 ? 0 : Number(__sbWsOp("T", 0, spec, 0));
|
|
14848
|
+
if (!(id > 0)) {
|
|
14849
|
+
st.failed = new Error(__sbTcpOpen >= 16 ? "Too many open connections" : "TCP sockets are not available on this host");
|
|
14850
|
+
st.eof = true;
|
|
14851
|
+
setTimeout(() => __sbTcpSettle(st), 0);
|
|
14852
|
+
return socket;
|
|
14853
|
+
}
|
|
14854
|
+
st.id = id;
|
|
14855
|
+
__sbTcpOpen++;
|
|
14856
|
+
__sbTcpPump(st);
|
|
14857
|
+
return socket;
|
|
14858
|
+
}
|
|
14859
|
+
globalThis.__sbSockets = { connect: __sbConnect };
|
|
14860
|
+
|
|
14576
14861
|
// ---------------------------------------------------------------------------
|
|
14577
14862
|
// Outbound WebSockets, from compatibility date 2026-11-01: new WebSocket(url, protocols) and
|
|
14578
14863
|
// fetch(url, { headers: { Upgrade: "websocket" } }), whose Response carries the connected
|