@sproutboat/runtime 0.40.0 → 0.42.0
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 +18 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +303 -9
- package/src/sockets.test.ts +42 -0
- package/src/source.ts +15 -0
- package/src/subtle/README.md +4 -4
- package/src/subtle/subtle.c +19 -7
- package/src/subtle/subtle.test.ts +1 -1
- package/src/transport-broker.js +471 -0
- package/src/transport-embedded.js +182 -3
- package/src/wrap.ts +14 -0
- package/test/bearssl.ts +5 -4
- package/test/native-sockets-broker.ts +460 -0
|
@@ -3302,6 +3302,10 @@ typedef struct sb_wsc {
|
|
|
3302
3302
|
uint64_t rng;
|
|
3303
3303
|
char *host, *path, *headers, *protocols;
|
|
3304
3304
|
int port, tls;
|
|
3305
|
+
// Raw TCP (cloudflare:sockets connect()): no WebSocket framing, bytes both ways.
|
|
3306
|
+
int raw, starttls_req, shutdown_req, want_drain;
|
|
3307
|
+
size_t unsent, window;
|
|
3308
|
+
char sni[256];
|
|
3305
3309
|
} sb_wsc;
|
|
3306
3310
|
static pthread_mutex_t sb_wsc_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
3307
3311
|
static sb_wsc* sb_wscs = 0;
|
|
@@ -3496,6 +3500,7 @@ static long sb_wsio_pump(sb_wsc* c, sb_wsio* io, unsigned char* buf, size_t cap,
|
|
|
3496
3500
|
}
|
|
3497
3501
|
}
|
|
3498
3502
|
|
|
3503
|
+
static int sb_wsio_start_tls(sb_wsio* io, const char* host, char* why, size_t whylen);
|
|
3499
3504
|
static int sb_wsio_open(sb_wsio* io, const char* host, int port, int tls, char* why, size_t whylen) {
|
|
3500
3505
|
memset(io, 0, sizeof(*io));
|
|
3501
3506
|
io->fd = sb_tcp_connect(host, port, why, whylen);
|
|
@@ -3506,6 +3511,11 @@ static int sb_wsio_open(sb_wsio* io, const char* host, int port, int tls, char*
|
|
|
3506
3511
|
fcntl(io->fd, F_SETFL, fcntl(io->fd, F_GETFL) | O_NONBLOCK);
|
|
3507
3512
|
io->tls = tls;
|
|
3508
3513
|
if (!tls) return 0;
|
|
3514
|
+
return sb_wsio_start_tls(io, host, why, whylen);
|
|
3515
|
+
}
|
|
3516
|
+
// Starts TLS on an open fd (connect with secureTransport "on", or startTls()).
|
|
3517
|
+
static int sb_wsio_start_tls(sb_wsio* io, const char* host, char* why, size_t whylen) {
|
|
3518
|
+
io->tls = 1;
|
|
3509
3519
|
io->sc = (br_ssl_client_context*)malloc(sizeof(br_ssl_client_context));
|
|
3510
3520
|
io->xc = (br_x509_minimal_context*)malloc(sizeof(br_x509_minimal_context));
|
|
3511
3521
|
io->iobuf = (unsigned char*)malloc(BR_SSL_BUFSIZE_BIDI);
|
|
@@ -3759,6 +3769,91 @@ finish:
|
|
|
3759
3769
|
return 0;
|
|
3760
3770
|
}
|
|
3761
3771
|
|
|
3772
|
+
// Raw TCP: the thread for connect(). Events: 1 data, 3 error (reason in data), 4 connected (plain),
|
|
3773
|
+
// 6 end of input, 8 send queue drained (after a write left more than the high-water mark queued),
|
|
3774
|
+
// 9 TLS handshake finished.
|
|
3775
|
+
#define SB_TCP_READ_WINDOW (256u << 10)
|
|
3776
|
+
static void* sb_tcp_thread(void* arg) {
|
|
3777
|
+
sb_wsc* c = (sb_wsc*)arg;
|
|
3778
|
+
sb_wsio io;
|
|
3779
|
+
memset(&io, 0, sizeof(io));
|
|
3780
|
+
io.fd = -1;
|
|
3781
|
+
char why[512] = "";
|
|
3782
|
+
unsigned char buf[16384];
|
|
3783
|
+
unsigned char* wq = 0; size_t wq_len = 0, wq_cap = 0;
|
|
3784
|
+
int tls_waiting = 0, eof = 0, shut = 0;
|
|
3785
|
+
if (sb_wsio_open(&io, c->host, c->port, c->tls, why, sizeof(why)) != 0) { sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
|
|
3786
|
+
if (c->tls) tls_waiting = 1; else sb_wsc_push(c, 4, 0, "", 0);
|
|
3787
|
+
for (;;) {
|
|
3788
|
+
pthread_mutex_lock(&c->lock);
|
|
3789
|
+
int rel = c->released;
|
|
3790
|
+
size_t window = c->unacked;
|
|
3791
|
+
int st = c->starttls_req; c->starttls_req = 0;
|
|
3792
|
+
int sh = c->shutdown_req;
|
|
3793
|
+
char sni[256]; memcpy(sni, c->sni, sizeof(sni));
|
|
3794
|
+
if (c->outq_len) {
|
|
3795
|
+
if (wq_len + c->outq_len > wq_cap) {
|
|
3796
|
+
size_t cap = wq_cap ? wq_cap : 4096;
|
|
3797
|
+
while (cap < wq_len + c->outq_len) cap *= 2;
|
|
3798
|
+
unsigned char* grown = (unsigned char*)realloc(wq, cap);
|
|
3799
|
+
if (grown) { wq = grown; wq_cap = cap; }
|
|
3800
|
+
}
|
|
3801
|
+
if (wq_len + c->outq_len <= wq_cap) { memcpy(wq + wq_len, c->outq, c->outq_len); wq_len += c->outq_len; c->outq_len = 0; }
|
|
3802
|
+
}
|
|
3803
|
+
c->unsent = wq_len;
|
|
3804
|
+
pthread_mutex_unlock(&c->lock);
|
|
3805
|
+
if (rel) {
|
|
3806
|
+
if (wq_len) sb_wsio_pump(c, &io, buf, sizeof(buf), wq, &wq_len, 0, 0);
|
|
3807
|
+
goto finish;
|
|
3808
|
+
}
|
|
3809
|
+
if (st && !io.tls) {
|
|
3810
|
+
if (wq_len) sb_wsio_pump(c, &io, buf, sizeof(buf), wq, &wq_len, 0, 0);
|
|
3811
|
+
if (sb_wsio_start_tls(&io, sni[0] ? sni : c->host, why, sizeof(why)) != 0) { sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
|
|
3812
|
+
tls_waiting = 1;
|
|
3813
|
+
}
|
|
3814
|
+
long n = sb_wsio_pump(c, &io, buf, sizeof(buf), wq, &wq_len, !eof && window < SB_TCP_READ_WINDOW, tls_waiting ? 20 : -1);
|
|
3815
|
+
if (tls_waiting && io.tls && (br_ssl_engine_current_state(&io.sc->eng) & (BR_SSL_SENDAPP | BR_SSL_RECVAPP))) {
|
|
3816
|
+
tls_waiting = 0;
|
|
3817
|
+
sb_wsc_push(c, 9, 0, "", 0);
|
|
3818
|
+
}
|
|
3819
|
+
if (n == -2) continue;
|
|
3820
|
+
if (n < 0) {
|
|
3821
|
+
int err = io.tls ? br_ssl_engine_last_error(&io.sc->eng) : 0;
|
|
3822
|
+
if (err != 0 && err != BR_ERR_IO) { snprintf(why, sizeof(why), "TLS error %d", err); sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
|
|
3823
|
+
if (tls_waiting) { snprintf(why, sizeof(why), "connection closed during the TLS handshake"); sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
|
|
3824
|
+
if (!eof) { eof = 1; sb_wsc_push(c, 6, 0, "", 0); }
|
|
3825
|
+
// keep serving writes until released; with no reader the pump just waits for the wake pipe
|
|
3826
|
+
n = 0;
|
|
3827
|
+
}
|
|
3828
|
+
if (n == 0) sb_wsc_drain_wake(c);
|
|
3829
|
+
else sb_wsc_push(c, 1, 0, (const char*)buf, (size_t)n);
|
|
3830
|
+
if (sh && wq_len == 0 && !shut) {
|
|
3831
|
+
shut = 1;
|
|
3832
|
+
if (io.tls) br_ssl_engine_close(&io.sc->eng); else shutdown(io.fd, SHUT_WR);
|
|
3833
|
+
}
|
|
3834
|
+
int drained = 0;
|
|
3835
|
+
pthread_mutex_lock(&c->lock);
|
|
3836
|
+
c->unsent = wq_len;
|
|
3837
|
+
if (c->want_drain && c->outq_len + wq_len == 0) { c->want_drain = 0; drained = 1; }
|
|
3838
|
+
pthread_mutex_unlock(&c->lock);
|
|
3839
|
+
if (drained) sb_wsc_push(c, 8, 0, "", 0);
|
|
3840
|
+
}
|
|
3841
|
+
finish:
|
|
3842
|
+
sb_wsio_close(&io);
|
|
3843
|
+
free(wq);
|
|
3844
|
+
pthread_mutex_lock(&c->lock);
|
|
3845
|
+
c->finished = 1;
|
|
3846
|
+
int rel2 = c->released;
|
|
3847
|
+
pthread_mutex_unlock(&c->lock);
|
|
3848
|
+
if (rel2) {
|
|
3849
|
+
pthread_mutex_lock(&sb_wsc_lock);
|
|
3850
|
+
for (sb_wsc** at = &sb_wscs; *at; at = &(*at)->next) if (*at == c) { *at = c->next; break; }
|
|
3851
|
+
pthread_mutex_unlock(&sb_wsc_lock);
|
|
3852
|
+
sb_wsc_free(c);
|
|
3853
|
+
}
|
|
3854
|
+
return 0;
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3762
3857
|
static sb_wsc* sb_wsc_find(unsigned id) {
|
|
3763
3858
|
pthread_mutex_lock(&sb_wsc_lock);
|
|
3764
3859
|
sb_wsc* c = sb_wscs;
|
|
@@ -3787,6 +3882,52 @@ static void sb_wsc_drain(void) {
|
|
|
3787
3882
|
|
|
3788
3883
|
// The JS side's ops. data is borrowed for the call.
|
|
3789
3884
|
static long long sb_wsc_op(int op, unsigned id, const char* data, size_t len, size_t n, char* out, size_t cap) {
|
|
3885
|
+
if (op == 'T') {
|
|
3886
|
+
// data: host \n port \n mode (0 off, 1 on, 2 starttls) \n servername
|
|
3887
|
+
sb_wsc* c = (sb_wsc*)calloc(1, sizeof(sb_wsc));
|
|
3888
|
+
if (!c) return 0;
|
|
3889
|
+
char* copy = (char*)malloc(len + 1);
|
|
3890
|
+
if (!copy) { free(c); return 0; }
|
|
3891
|
+
memcpy(copy, data, len); copy[len] = 0;
|
|
3892
|
+
char* parts[4]; char* cur = copy;
|
|
3893
|
+
for (int i = 0; i < 4; i++) {
|
|
3894
|
+
parts[i] = cur;
|
|
3895
|
+
char* nl = strchr(cur, '\n');
|
|
3896
|
+
if (nl) { *nl = 0; cur = nl + 1; } else cur += strlen(cur);
|
|
3897
|
+
}
|
|
3898
|
+
c->raw = 1; c->window = SB_TCP_READ_WINDOW;
|
|
3899
|
+
c->host = strdup(parts[0]); c->port = atoi(parts[1]); c->tls = atoi(parts[2]) == 1;
|
|
3900
|
+
snprintf(c->sni, sizeof(c->sni), "%s", parts[3]);
|
|
3901
|
+
c->path = strdup(""); c->protocols = strdup(""); c->headers = strdup("");
|
|
3902
|
+
free(copy);
|
|
3903
|
+
if (!c->host || !c->path || !c->protocols || !c->headers || pipe(c->wake) != 0) { free(c->host); free(c->path); free(c->protocols); free(c->headers); free(c); return 0; }
|
|
3904
|
+
for (int k = 0; k < 2; k++) { fcntl(c->wake[k], F_SETFD, FD_CLOEXEC); fcntl(c->wake[k], F_SETFL, fcntl(c->wake[k], F_GETFL) | O_NONBLOCK); }
|
|
3905
|
+
pthread_mutex_init(&c->lock, 0);
|
|
3906
|
+
pthread_mutex_lock(&sb_wsc_lock);
|
|
3907
|
+
int live = 0;
|
|
3908
|
+
for (sb_wsc* k = sb_wscs; k; k = k->next) live++;
|
|
3909
|
+
if (live >= SB_WSC_MAX) { pthread_mutex_unlock(&sb_wsc_lock); sb_wsc_free(c); return 0; }
|
|
3910
|
+
sb_wsc_next = (sb_wsc_next + 1) & 0x7fffffffu;
|
|
3911
|
+
if (!sb_wsc_next) sb_wsc_next = 1;
|
|
3912
|
+
c->id = sb_wsc_next | SB_WSC_BIT;
|
|
3913
|
+
c->next = sb_wscs; sb_wscs = c;
|
|
3914
|
+
pthread_mutex_unlock(&sb_wsc_lock);
|
|
3915
|
+
pthread_attr_t attr;
|
|
3916
|
+
pthread_attr_init(&attr);
|
|
3917
|
+
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
|
3918
|
+
pthread_attr_setstacksize(&attr, 512 * 1024);
|
|
3919
|
+
pthread_t thread;
|
|
3920
|
+
int failed = pthread_create(&thread, &attr, sb_tcp_thread, c);
|
|
3921
|
+
pthread_attr_destroy(&attr);
|
|
3922
|
+
if (failed) {
|
|
3923
|
+
pthread_mutex_lock(&sb_wsc_lock);
|
|
3924
|
+
for (sb_wsc** at = &sb_wscs; *at; at = &(*at)->next) if (*at == c) { *at = c->next; break; }
|
|
3925
|
+
pthread_mutex_unlock(&sb_wsc_lock);
|
|
3926
|
+
sb_wsc_free(c);
|
|
3927
|
+
return 0;
|
|
3928
|
+
}
|
|
3929
|
+
return (long long)c->id;
|
|
3930
|
+
}
|
|
3790
3931
|
if (op == 'u') {
|
|
3791
3932
|
// data: host \n port \n tls(0|1) \n path \n protocols \n header lines (to the end)
|
|
3792
3933
|
sb_wsc* c = (sb_wsc*)calloc(1, sizeof(sb_wsc));
|
|
@@ -3836,7 +3977,40 @@ static long long sb_wsc_op(int op, unsigned id, const char* data, size_t len, si
|
|
|
3836
3977
|
sb_wsc* c = sb_wsc_find(id);
|
|
3837
3978
|
if (!c) return op == 'p' || op == 'k' || op == 'w' || op == 'r' ? 0 : -1;
|
|
3838
3979
|
switch (op) {
|
|
3839
|
-
case '
|
|
3980
|
+
case 'b': if (c->raw) {
|
|
3981
|
+
// raw bytes: queue them, say how many are still unsent, and ask for a drain event past the mark
|
|
3982
|
+
pthread_mutex_lock(&c->lock);
|
|
3983
|
+
size_t need = c->outq_len + len;
|
|
3984
|
+
if (need > c->outq_cap) {
|
|
3985
|
+
size_t cap = c->outq_cap ? c->outq_cap : 4096;
|
|
3986
|
+
while (cap < need) cap *= 2;
|
|
3987
|
+
unsigned char* grown = (unsigned char*)realloc(c->outq, cap);
|
|
3988
|
+
if (!grown) { pthread_mutex_unlock(&c->lock); return -1; }
|
|
3989
|
+
c->outq = grown; c->outq_cap = cap;
|
|
3990
|
+
}
|
|
3991
|
+
memcpy(c->outq + c->outq_len, data, len);
|
|
3992
|
+
c->outq_len += len;
|
|
3993
|
+
size_t queued = c->outq_len + c->unsent;
|
|
3994
|
+
if (queued > (256u << 10)) c->want_drain = 1;
|
|
3995
|
+
pthread_mutex_unlock(&c->lock);
|
|
3996
|
+
sb_wsc_wake_thread(c);
|
|
3997
|
+
return (long long)queued;
|
|
3998
|
+
}
|
|
3999
|
+
/* fall through */
|
|
4000
|
+
case 't': return sb_wsc_frame(c, op == 't' ? 1 : 2, data, len, 0, 0);
|
|
4001
|
+
case 'S':
|
|
4002
|
+
pthread_mutex_lock(&c->lock);
|
|
4003
|
+
snprintf(c->sni, sizeof(c->sni), "%.*s", (int)(len < sizeof(c->sni) - 1 ? len : sizeof(c->sni) - 1), data ? data : "");
|
|
4004
|
+
c->starttls_req = 1;
|
|
4005
|
+
pthread_mutex_unlock(&c->lock);
|
|
4006
|
+
sb_wsc_wake_thread(c);
|
|
4007
|
+
return 0;
|
|
4008
|
+
case 'H':
|
|
4009
|
+
pthread_mutex_lock(&c->lock);
|
|
4010
|
+
c->shutdown_req = 1;
|
|
4011
|
+
pthread_mutex_unlock(&c->lock);
|
|
4012
|
+
sb_wsc_wake_thread(c);
|
|
4013
|
+
return 0;
|
|
3840
4014
|
case 'c': {
|
|
3841
4015
|
pthread_mutex_lock(&c->lock);
|
|
3842
4016
|
int already = c->close_queued;
|
|
@@ -4962,8 +5136,13 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
4962
5136
|
if (op === "secret.get") {
|
|
4963
5137
|
// Secrets reach a standalone binary through the environment, the same
|
|
4964
5138
|
// channel the launcher and the supervisor use; there is nothing to decrypt.
|
|
4965
|
-
const
|
|
4966
|
-
if (!
|
|
5139
|
+
const raw = __sbEnv(String(msg.name));
|
|
5140
|
+
if (!raw) throw new Error("secret not set: " + msg.name);
|
|
5141
|
+
// __sbEnv hands back the environment's bytes as a bytestring, one char per byte, so a UTF-8 secret ("é" is
|
|
5142
|
+
// C3 A9) reached the app as "é". From compatibility date 2026-11-07 it is decoded as UTF-8, as workerd's
|
|
5143
|
+
// secrets are text. Bytes that are not valid UTF-8 pass through unchanged (__sbFromUtf8's rule). The broker
|
|
5144
|
+
// transport is not touched: its secrets arrive as JSON, already real strings.
|
|
5145
|
+
const value = __sbCompatDate() >= "2026-11-07" ? __sbFromUtf8(raw) : raw;
|
|
4967
5146
|
return { ok: true, value };
|
|
4968
5147
|
}
|
|
4969
5148
|
|
package/src/wrap.ts
CHANGED
|
@@ -137,6 +137,20 @@ function hasBindings(b: Bindings): boolean {
|
|
|
137
137
|
* leaves the specifier external for this.
|
|
138
138
|
*/
|
|
139
139
|
export function inlineWorkersImports(source: string): string {
|
|
140
|
+
// `cloudflare:sockets` (compatibility date 2026-11-17): connect() lives on a prelude global.
|
|
141
|
+
// postgres/cf loads it lazily: `await import("cloudflare:sockets")`.
|
|
142
|
+
source = source.replace(/\bimport\(\s*["']cloudflare:sockets["']\s*\)/g, "Promise.resolve(globalThis.__sbSockets)");
|
|
143
|
+
source = source.replace(
|
|
144
|
+
/^import\s*\{([^}]*)\}\s*from\s*["']cloudflare:sockets["'];?[ \t]*$/gm,
|
|
145
|
+
(_match, names: string) => {
|
|
146
|
+
const pairs = names
|
|
147
|
+
.split(",")
|
|
148
|
+
.map((part) => part.trim())
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.map((part) => part.replace(/^(\S+)\s+as\s+(\S+)$/, "$1: $2"));
|
|
151
|
+
return `const { ${pairs.join(", ")} } = globalThis.__sbSockets;`;
|
|
152
|
+
},
|
|
153
|
+
);
|
|
140
154
|
return source.replace(
|
|
141
155
|
/^import\s*\{([^}]*)\}\s*from\s*["']cloudflare:workers["'];?[ \t]*$/gm,
|
|
142
156
|
(_match, names: string) => {
|
package/test/bearssl.ts
CHANGED
|
@@ -7,9 +7,10 @@ import { join, resolve } from "node:path";
|
|
|
7
7
|
// The prelude carries crypto.subtle's native half on every transport, so a test that compiles a
|
|
8
8
|
// handler using crypto.subtle needs BearSSL on the include path and link line, as the CLI arranges
|
|
9
9
|
// for a build. Same pinned source as the CLI's src/bearssl.ts; built once into the toolchain cache.
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
10
|
+
const COMMIT = "7bea48e5e850ab4cafbe68d3765cdaba13a86d6f";
|
|
11
|
+
const VERSION = COMMIT.slice(0, 7);
|
|
12
|
+
const URL_ = `https://bearssl.org/gitweb/?p=BearSSL;a=snapshot;h=${COMMIT};sf=tgz`;
|
|
13
|
+
const SHA256 = "ed397832b1613582737d9c1ce01c1af74ad6a9e9c52a341e24433987cda0f372";
|
|
13
14
|
|
|
14
15
|
const walk = async (dir: string): Promise<string[]> =>
|
|
15
16
|
(
|
|
@@ -28,7 +29,7 @@ export async function bearsslEnv(): Promise<{ SB_EXTRA_CFLAGS: string; SB_EXTRA_
|
|
|
28
29
|
process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? join(homedir(), ".cache/sproutboat"),
|
|
29
30
|
`bearssl-${VERSION}-test`,
|
|
30
31
|
);
|
|
31
|
-
const tree = join(root, `
|
|
32
|
+
const tree = join(root, `BearSSL-${VERSION}`);
|
|
32
33
|
const objects = join(root, "obj");
|
|
33
34
|
const marker = join(objects, ".complete");
|
|
34
35
|
if (!existsSync(marker)) {
|