@threadedstack/wmd-web 0.1.18 → 0.1.20
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 +24 -3
- package/RELEASE.json +28 -44
- package/guest/arm64/Image +1 -1
- package/guest/arm64/Image-4k +1 -1
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +4 -4
- package/relay/hosted.ts +32 -18
- package/relay/main.ts +1 -1
- package/relay/relay.ts +0 -1
- package/relay/token.ts +5 -34
- package/relay/wisp.ts +5 -37
- package/src/blob.js +12 -0
- package/src/boot.js +9 -1
- package/src/build/builder.js +197 -12
- package/src/build/context.js +3 -2
- package/src/host/disk-worker.js +8 -1
- package/src/host/disk.js +1 -1
- package/src/host/jit.js +36 -0
- package/src/image-files.js +2 -1
- package/src/net/stack.js +153 -1
- package/src/registry/load-tar.js +2 -1
- package/wmdpack.wasm +0 -0
package/README.md
CHANGED
|
@@ -159,10 +159,11 @@ The interface this host implements is in [docs/host-interface.md](docs/host-inte
|
|
|
159
159
|
|
|
160
160
|
`createBuilder` runs BuildKit (moby/buildkit v0.33.0) in a privileged builder machine in the page and builds a
|
|
161
161
|
Dockerfile there. The result is `{index, packs}`, the shape `pullImage` returns, so `bootWmd` can run it at once.
|
|
162
|
-
Chromium, Firefox and WebKit all pass the same 18 build steps
|
|
162
|
+
Chromium, Firefox and WebKit all pass the same 18 build steps, and Node and Go projects for arm64 and amd64 (plan
|
|
163
|
+
`docs/plans/web-buildkit-impl.md`, section 10).
|
|
163
164
|
|
|
164
165
|
```js
|
|
165
|
-
import { createBuilder } from 'wmd-web/build'
|
|
166
|
+
import { buildImage, createBuilder } from 'wmd-web/build'
|
|
166
167
|
|
|
167
168
|
const builder = await createBuilder({
|
|
168
169
|
vmUrl, files, // as bootWmd: the engine and the builder arch's guest files
|
|
@@ -176,6 +177,9 @@ const builder = await createBuilder({
|
|
|
176
177
|
const r = await builder.build({ context: { Dockerfile: 'FROM alpine:3.20\nRUN echo hi > /x\n' } })
|
|
177
178
|
// r: {index, packs, log, path: 'fast'|'buildkit', ms, cached}
|
|
178
179
|
await builder.close()
|
|
180
|
+
|
|
181
|
+
// or, for one build: createBuilder + build + close in one call
|
|
182
|
+
const r2 = await buildImage({ ...builderOptions, context, timeoutMs: 30 * 60_000 })
|
|
179
183
|
```
|
|
180
184
|
|
|
181
185
|
- **The cache stays.** The disk outlives the builder and the visit. After a reload, or after a tab was killed
|
|
@@ -190,7 +194,24 @@ await builder.close()
|
|
|
190
194
|
for layer the same as BuildKit's (40 compared cases): under 0.4 s once its base image is pulled, where a warm
|
|
191
195
|
BuildKit takes 5 to 14 s.
|
|
192
196
|
- **Errors:** `BuildError {step, tail, reason}`: `reason` is `'no-space'`, `'quota'`, `'disk'`, `'timeout'`, `'pin'`
|
|
193
|
-
or `'secret'`. Cancelling with `signal` stops the step and leaves nothing behind.
|
|
197
|
+
or `'secret'`. Cancelling with `signal` stops the step and leaves nothing behind. A failed build also carries `jit`
|
|
198
|
+
(the engine's last JIT messages), `net` (the guest network's counters and events), and, when the machine itself
|
|
199
|
+
stopped, `machine` (why, and its last output).
|
|
200
|
+
- **RAM and vCPUs.** The default is 1536 MiB and 2 vCPUs (`ram`, `cpus`). Small builds and the proxy project in the
|
|
201
|
+
plan fit. A build that compiles a native addon or bundles a large app needed 3072 MiB and 4 vCPUs (plan 10.5); a
|
|
202
|
+
single `RUN` step must finish within `timeoutMs`, since BuildKit resumes from its cache only between steps.
|
|
203
|
+
- **WebKit** builds Go and Node images and the real projects of the plan, but slower: its code space fills and new
|
|
204
|
+
code runs in the interpreter for a while (a `{t:'slow', reason:'webkit-code-space'}` event says so).
|
|
205
|
+
- In WebKit, node in `RUN` steps runs its JavaScript in the interpreter (D-553, `nodeInterpreted`, an
|
|
206
|
+
`{t:'node-interpreted'}` event): node's own JIT writes code faster than WebKit's code pool takes it, and a
|
|
207
|
+
1,600-package `pnpm` install did not end in 27 minutes. A preload bound at `/dev/.wmd-node.js` and named in
|
|
208
|
+
`NODE_OPTIONS` (`--require`) turns off V8's compilers; WebAssembly still works. The image and the cache keys do
|
|
209
|
+
not change. `nodeInterpreted: false` turns it off; `true` turns it on elsewhere.
|
|
210
|
+
- npm closes a TLS handshake that is not done 15 s after the connect. The guest network lets 4 handshakes go at
|
|
211
|
+
once, sends a ClientHello that waited on a fresh connection, and sends an unanswered one again once (D-552); it
|
|
212
|
+
dials a stream again when the remote closes it before any byte crossed (D-551).
|
|
213
|
+
- WebKit cannot read a Blob of 128 MiB or more whole (`NotReadableError`). Read the `tar` Blob a build returns
|
|
214
|
+
with `blobBytes(blob)` from `wmd-web/build`, which reads it in slices (D-554).
|
|
194
215
|
- **The builder images** are built with `make web-builder` (wmd builds them; Docker Desktop is not used) into
|
|
195
216
|
`web/dist/builder/builder-<arch>.tar`, and their sha256 is pinned in `src/build/pin.js`. `make web-release` puts
|
|
196
217
|
them next to the release tgz.
|
package/RELEASE.json
CHANGED
|
@@ -1,66 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wmd-web",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"commit": "
|
|
5
|
-
"previous": "wmd-web-v0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"commit": "f0089e9f12e73d951b5a3a47f962764edc1c39a3",
|
|
5
|
+
"previous": "wmd-web-v0.5.0",
|
|
6
6
|
"decisions": [
|
|
7
|
-
"D-
|
|
8
|
-
"D-
|
|
9
|
-
"D-494",
|
|
10
|
-
"D-498",
|
|
11
|
-
"D-505",
|
|
12
|
-
"D-506",
|
|
13
|
-
"D-507",
|
|
14
|
-
"D-509",
|
|
15
|
-
"D-511",
|
|
16
|
-
"D-516",
|
|
17
|
-
"D-518",
|
|
18
|
-
"D-531",
|
|
19
|
-
"D-533",
|
|
20
|
-
"D-539",
|
|
21
|
-
"D-542",
|
|
22
|
-
"D-544",
|
|
23
|
-
"D-545",
|
|
24
|
-
"D-547",
|
|
25
|
-
"D-548"
|
|
7
|
+
"D-552",
|
|
8
|
+
"D-554"
|
|
26
9
|
],
|
|
27
|
-
"vmSources": "
|
|
10
|
+
"vmSources": "543f2ee1a363719c2a973fff350d54a54d325286",
|
|
28
11
|
"guest": {
|
|
29
12
|
"arch": "arm64",
|
|
30
13
|
"files": {
|
|
31
14
|
"vm.wasm": {
|
|
32
|
-
"sha256": "
|
|
33
|
-
"bytes":
|
|
15
|
+
"sha256": "73bee71d7453a130f7991c7cead3bc12787050116217f635f9f523f584d2577d",
|
|
16
|
+
"bytes": 1562868
|
|
34
17
|
},
|
|
35
18
|
"Image": {
|
|
36
|
-
"sha256": "
|
|
19
|
+
"sha256": "6952a8d4f1bafec9c3e3b17e2c4af20ab5de8a7086956d087979804be2a01353",
|
|
37
20
|
"bytes": 39049224
|
|
38
21
|
},
|
|
39
22
|
"Image-4k": {
|
|
40
|
-
"sha256": "
|
|
23
|
+
"sha256": "47a1dcf06bcd1cc31fb4f02adbd1e035bc9e9a243fe57f3efd71b4a287581de2",
|
|
41
24
|
"bytes": 38828040
|
|
42
25
|
},
|
|
43
26
|
"initramfs.cpio.gz": {
|
|
44
|
-
"sha256": "
|
|
45
|
-
"bytes":
|
|
27
|
+
"sha256": "efa6eb2338cb01d87eeebce8035ed7a26b00edcfc3b9db963b38272715d2ee9d",
|
|
28
|
+
"bytes": 4683846
|
|
46
29
|
}
|
|
47
30
|
}
|
|
48
31
|
},
|
|
49
32
|
"files": {
|
|
50
|
-
"README.md": "
|
|
33
|
+
"README.md": "6962e410444bcf44ba6dd8498673e2de17ee4f8b96e1d29044a55b8f0dbcd8de",
|
|
51
34
|
"export-page/coi-sw.js": "0300e670b413cb2e0aae35843fc583546bbf6687df971ab6df41cfa3b0d494d7",
|
|
52
35
|
"export-page/index.html": "f5450c89e63601bbbbffef421f03491b231d1f143070012c1c5315d4b0059f43",
|
|
53
36
|
"export-page/run.css": "b57adf1e76553549c1f89ff157b7973409dedc21889cd3932948cd05b51adc6f",
|
|
54
37
|
"export-page/run.js": "717636a632a40469106ddf1383e198eaac5d83651b5866d0699ec04b82462b7f",
|
|
55
|
-
"guest/arm64/Image": "
|
|
56
|
-
"guest/arm64/Image-4k": "
|
|
57
|
-
"guest/arm64/initramfs.cpio.gz": "
|
|
58
|
-
"guest/arm64/vm.wasm": "
|
|
59
|
-
"package.json": "
|
|
38
|
+
"guest/arm64/Image": "6952a8d4f1bafec9c3e3b17e2c4af20ab5de8a7086956d087979804be2a01353",
|
|
39
|
+
"guest/arm64/Image-4k": "47a1dcf06bcd1cc31fb4f02adbd1e035bc9e9a243fe57f3efd71b4a287581de2",
|
|
40
|
+
"guest/arm64/initramfs.cpio.gz": "efa6eb2338cb01d87eeebce8035ed7a26b00edcfc3b9db963b38272715d2ee9d",
|
|
41
|
+
"guest/arm64/vm.wasm": "73bee71d7453a130f7991c7cead3bc12787050116217f635f9f523f584d2577d",
|
|
42
|
+
"package.json": "7d43c05047d0a3ea56b4a7709f48a60de32e15c3bdf328d55435c46637981b40",
|
|
60
43
|
"src/base.js": "982f8e46dbb727ec542a494ebdb884ccd19a31ff4e091425b2d1fc01b0d4754a",
|
|
61
|
-
"src/
|
|
62
|
-
"src/
|
|
63
|
-
"src/build/
|
|
44
|
+
"src/blob.js": "bb2a2f0d87ce236280a7be2f246c926010dfe07f649c27da5e70d8eb5eaf7061",
|
|
45
|
+
"src/boot.js": "b1819d2b8abd46fd42f10a986719a311bedca705fc368a4cd19cc121b04b57d2",
|
|
46
|
+
"src/build/builder.js": "0d5355c8570b0ecedce25602ff7f60c49b7daaed669426328f05f52e26048213",
|
|
47
|
+
"src/build/context.js": "d38097c61dbdadc4d41290c010425a4422f499aba473fa2b83f2a5c41e543571",
|
|
64
48
|
"src/build/dockerfile.js": "5957fe3d4ac8ff8b312f980ee04bd1d98ad4373a61f9a33e02f1ff1108b6ea53",
|
|
65
49
|
"src/build/dockerignore.js": "8fc03d09da020f43ea483979b1183891b71d76f012379ab7813838ea0e53c546",
|
|
66
50
|
"src/build/fast.js": "bf7373a43cce995b21751c317a58d18438f9fd2f670266b314b1151ef3cdb9e8",
|
|
@@ -93,8 +77,8 @@
|
|
|
93
77
|
"src/host/clock.js": "aba6af3a8b1a985ae340892222e4b482d33afe20f5c8b3a4c7403ce87d1e247c",
|
|
94
78
|
"src/host/ctl.js": "ca2ebbb940c9668cb0573c959cd2ff8982f96d58dfdb8dce39d304b89e033dd7",
|
|
95
79
|
"src/host/disk-map.js": "3d7e0d7d488ce25fc6a0e4dc0a9b1c8fc2af96bac1c03491a1d30be392af268b",
|
|
96
|
-
"src/host/disk-worker.js": "
|
|
97
|
-
"src/host/disk.js": "
|
|
80
|
+
"src/host/disk-worker.js": "6e82bfeb4aa3f8b02bd7cb2de7427def26114e016f6b05196c8485d7fc8315ee",
|
|
81
|
+
"src/host/disk.js": "e941e62cc2d9a83582d53c80c09c5f0f682508f77390237bedff4051c489b2cd",
|
|
98
82
|
"src/host/errno.js": "26021d85962084be2c43c98fb3accafc2a8f11fbf3c8a5b8d484bd6a088ae4db",
|
|
99
83
|
"src/host/fs.js": "d6753f74db35b7b01baa8709fc7fdcdfc8d638dfe42f454b0a833b22111e8f4a",
|
|
100
84
|
"src/host/gcnudge.js": "4a6569cb73ae24a11fc4ac8c467380bd9efba0bb5fa9867b797bc4f153ab15ab",
|
|
@@ -103,7 +87,7 @@
|
|
|
103
87
|
"src/host/jit-hub-core.js": "f8d39dd0e4cbd42568bf56a8a7105f321d33bcf2d5faf8756d45a1ed692bd344",
|
|
104
88
|
"src/host/jit-hub.js": "cf3fc9f9d4e3fb50c41d6de5e2523f380d3c6c1feaf61c15d90cde89aec2f4c3",
|
|
105
89
|
"src/host/jit-snap.js": "3932f36047c9edeb7f59282d1c37fef19c8f5a9fe4bd0192fb93aa64030fe51d",
|
|
106
|
-
"src/host/jit.js": "
|
|
90
|
+
"src/host/jit.js": "72ef7334506fde2b3724cee93b52a987fbd6d45473b8de93d914db8a80ae9e17",
|
|
107
91
|
"src/host/layout.js": "d5bc5aa9292630b204438df5d6278fda6daa2a65acc061e1c57e0272b32fecc8",
|
|
108
92
|
"src/host/monitor.js": "ae1741ba35668bcc6192642246494433b175668933921634c699af5a0a69acc0",
|
|
109
93
|
"src/host/p9mount.js": "43246601fc1e27d5294742e4482ed166a50c665cb5f526c3646a47dd27e06150",
|
|
@@ -113,14 +97,14 @@
|
|
|
113
97
|
"src/host/wasi.js": "fbafa239b8fb4083f7e8c910b1adab38901e8e7cffd247284a9abb2847eaf4bd",
|
|
114
98
|
"src/host/wmd-imports.js": "3a00562f7c9c7787ab3841b14dde5d903a78f43ed19c43a2f5e31fd8a14c09d2",
|
|
115
99
|
"src/host/yamux.js": "f4044f14f7e836224bdb4b1e53e59c565ee478b83b2e428fffb9f7a048768f79",
|
|
116
|
-
"src/image-files.js": "
|
|
100
|
+
"src/image-files.js": "19f90a7d3c8accae5093e53bb85fe7e158ac1dd18866283722171eccb1d7e37a",
|
|
117
101
|
"src/net/dhcp-dns.js": "fdfa7c7d5cbbb4a8774b7dee195bf2df22a0c3fe8f20ac9694b24485fa8f25d5",
|
|
118
102
|
"src/net/net-worker.js": "0a2ce998f69807e386ded7ff1bee53687a1d59ed76d7ef46e2eb6f958b14ad38",
|
|
119
103
|
"src/net/packet.js": "d658f26fa84324a88d7dbc26af9c4a9c10e977c3060f16f7db431496eacd1127",
|
|
120
104
|
"src/net/peer.js": "d83cf85639f6197fe26c877eb79f8c9afc017bf2362dc2b22a8b6bd0f45f3f19",
|
|
121
105
|
"src/net/proto.js": "abfc8cd6f4331cbc98fcb1b0bba0795ff3042cc7db10476673ead1723249eee0",
|
|
122
106
|
"src/net/relay-client.js": "4101de76dc0251db46839dc786cce677e4020795adf022bdce14bcf7edf8f864",
|
|
123
|
-
"src/net/stack.js": "
|
|
107
|
+
"src/net/stack.js": "b721e4e116b0611abfe8ec1c6f2e212efd89a11f745fef2222ca5de474d8c0a9",
|
|
124
108
|
"src/net/switch.js": "bb047fb1ceeb10a84b5475916ead916362accf0e6f42a86561653dfad0c6c50d",
|
|
125
109
|
"src/net/wisp-client.js": "653961c4b735f2f74e5ea89e75c387f15ec8b8d9ed4c45d06c7d6b2a04968d42",
|
|
126
110
|
"src/net/wisp-transport.js": "2b71201f3c686078c867e8995bf5c1cd6d47a0d415f476f3a33fbd5729a55f74",
|
|
@@ -128,7 +112,7 @@
|
|
|
128
112
|
"src/registry/client.js": "fd38550e9afd82c5cb498513d245c33357a71697b3b2e6dd78538934bcc9d879",
|
|
129
113
|
"src/registry/converter.js": "1fb8294fd5275f6772c27130cc9ba70bd46fafaf440884d42448036618229f37",
|
|
130
114
|
"src/registry/hub.js": "cd672323bd1f96bd1d7e9eed80d4f36891e919e456fc0deb378fa0c7e3b6f249",
|
|
131
|
-
"src/registry/load-tar.js": "
|
|
115
|
+
"src/registry/load-tar.js": "219d6701f5c938e27e3ca36ade8fcea8114a5f2b1511496f0dbd60137290867f",
|
|
132
116
|
"src/registry/oci.js": "6a51ea16b34a65c0e732730f3e07d7ad16c50f6943a486ec05ca6aa3e98676a1",
|
|
133
117
|
"src/registry/pack-worker.js": "fd6798851cb7815dacfcb67fc78c41654209c5504d8fd343aae524e99de1c741",
|
|
134
118
|
"src/registry/pull.js": "24f2e01a76c9f7f27896af69b989f8c97f0ca31712b3e8110aaa5b5e3148b04b",
|
package/guest/arm64/Image
CHANGED
package/guest/arm64/Image-4k
CHANGED
|
Binary file
|
package/guest/arm64/vm.wasm
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threadedstack/wmd-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://github.com/threadedstack/wmd-releases",
|
|
6
6
|
"repository": {
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"description": "Browser host for wmd's guest vm.wasm (Web Workers + shared memory + WASI preview1)",
|
|
11
11
|
"wmd": {
|
|
12
|
-
"tag": "v0.1.
|
|
13
|
-
"commit": "
|
|
14
|
-
"web": "0.
|
|
12
|
+
"tag": "v0.1.20",
|
|
13
|
+
"commit": "f0089e9f12e73d951b5a3a47f962764edc1c39a3",
|
|
14
|
+
"web": "0.5.1"
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
|
17
17
|
"main": "./src/boot.js",
|
package/relay/hosted.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { Bucket, type Session, type Stream } from './engine.ts'
|
|
|
18
18
|
import { type Addr, type Cidr, addrString, cidrContains, hostedBlockedReason, parseCidr, parseIP } from './policy.ts'
|
|
19
19
|
|
|
20
20
|
const HOUR = 3_600_000
|
|
21
|
+
const MINUTE = 60_000
|
|
21
22
|
|
|
22
23
|
export type HostedOptions = {
|
|
23
24
|
abuseContact: string
|
|
@@ -26,11 +27,12 @@ export type HostedOptions = {
|
|
|
26
27
|
now?: () => number // ms since the epoch (tests)
|
|
27
28
|
warn?: (line: string) => void // one-line operator notices (stderr)
|
|
28
29
|
sites?: string[] // A: page origin patterns served (required: none listed serves no page)
|
|
29
|
-
sitesPerAddrPerHour?: number // B: different sites per address group per hour (
|
|
30
|
+
sitesPerAddrPerHour?: number // B: different sites per address group per hour (800)
|
|
30
31
|
sitesPerNetPerHour?: number // B: per network (800)
|
|
31
32
|
monthBytes?: number // C: bytes relayed per UTC month (the operator's figure from the provider's free outbound data); 0 = no cap
|
|
32
33
|
usageFile?: string // C: the month's counter survives a restart here
|
|
33
34
|
dialLimitMs?: number // D, G: the engine's dial limit (connectTimeoutMs, 10 s)
|
|
35
|
+
failedDialsPerMin?: number // D: failed connections per address group within a minute before any is slowed (20)
|
|
34
36
|
fdLimit?: number // E: the open-file limit (read from the host when absent)
|
|
35
37
|
fdOpen?: number // E: descriptors open at start (read from the host when absent)
|
|
36
38
|
dnsServers?: number // E: resolvers the relay asks (each may hold a UDP and a TCP socket)
|
|
@@ -57,7 +59,6 @@ export type Handle = { ip: string; grp: string; origin: string; end: (why: Why)
|
|
|
57
59
|
/** What a session holds from the governor until it ends. */
|
|
58
60
|
export type Admission = {
|
|
59
61
|
grp: string
|
|
60
|
-
publish: boolean
|
|
61
62
|
net: string
|
|
62
63
|
handle: Handle
|
|
63
64
|
fair: Bucket | null
|
|
@@ -257,10 +258,11 @@ export function createGovernor(o: HostedOptions) {
|
|
|
257
258
|
const warn = o.warn ?? ((m: string) => void process.stderr.write(`${m}\n`))
|
|
258
259
|
const host = o.fdLimit === undefined || o.fdOpen === undefined ? hostLimits() : null
|
|
259
260
|
const c = {
|
|
260
|
-
sitesPerAddrPerHour: o.sitesPerAddrPerHour ??
|
|
261
|
+
sitesPerAddrPerHour: o.sitesPerAddrPerHour ?? 800,
|
|
261
262
|
sitesPerNetPerHour: o.sitesPerNetPerHour ?? 800,
|
|
262
263
|
monthBytes: o.monthBytes ?? 0,
|
|
263
264
|
dialLimitMs: o.dialLimitMs ?? 10_000,
|
|
265
|
+
failedDialsPerMin: o.failedDialsPerMin ?? 20,
|
|
264
266
|
fdLimit: o.fdLimit ?? host!.fdLimit,
|
|
265
267
|
fdOpen: o.fdOpen ?? host!.fdOpen,
|
|
266
268
|
dnsServers: o.dnsServers ?? getServers().length,
|
|
@@ -404,7 +406,7 @@ export function createGovernor(o: HostedOptions) {
|
|
|
404
406
|
return m
|
|
405
407
|
}
|
|
406
408
|
const oldest = (m: Map<string, number>) => Math.min(...m.values()) + HOUR
|
|
407
|
-
function sites(grp: string, host: string
|
|
409
|
+
function sites(grp: string, host: string): Why | null {
|
|
408
410
|
const t = now()
|
|
409
411
|
const site = host.toLowerCase().replace(/\.$/, '')
|
|
410
412
|
const net = coarseKey(grp)
|
|
@@ -415,9 +417,8 @@ export function createGovernor(o: HostedOptions) {
|
|
|
415
417
|
fresh(a, t)
|
|
416
418
|
fresh(n, t)
|
|
417
419
|
if (!a.has(site)) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
return { rule: 'too-many-sites', words: `${max} different sites in an hour from this address${publish ? ' while publishing' : ''}: a new site waits until one ages out; sites already reached keep working`, until: oldest(a) }
|
|
420
|
+
if (a.size >= c.sitesPerAddrPerHour) {
|
|
421
|
+
return { rule: 'too-many-sites', words: `${c.sitesPerAddrPerHour} different sites in an hour from this address: a new site waits until one ages out; sites already reached keep working`, until: oldest(a) }
|
|
421
422
|
}
|
|
422
423
|
if (!n.has(site) && n.size >= c.sitesPerNetPerHour) {
|
|
423
424
|
return { rule: 'too-many-sites', words: `${c.sitesPerNetPerHour} different sites in an hour from this network: a new site waits until one ages out; sites already reached keep working`, until: oldest(n) }
|
|
@@ -429,12 +430,16 @@ export function createGovernor(o: HostedOptions) {
|
|
|
429
430
|
}
|
|
430
431
|
|
|
431
432
|
// ---- D
|
|
432
|
-
const dials = new Map<string, { w: number; gate: number; told: boolean }>()
|
|
433
|
+
const dials = new Map<string, { w: number; gate: number; told: boolean; fails: number[] }>()
|
|
433
434
|
const dialOf = (grp: string) => {
|
|
434
435
|
let d = dials.get(grp)
|
|
435
|
-
if (!d) dials.set(grp, (d = { w: 0, gate: 0, told: false }))
|
|
436
|
+
if (!d) dials.set(grp, (d = { w: 0, gate: 0, told: false, fails: [] }))
|
|
436
437
|
return d
|
|
437
438
|
}
|
|
439
|
+
const recentFails = (d: { fails: number[] }, t: number) => {
|
|
440
|
+
while (d.fails.length && t - d.fails[0] >= MINUTE) d.fails.shift()
|
|
441
|
+
return d.fails.length
|
|
442
|
+
}
|
|
438
443
|
function dialGate(s: Session): Why | { waitMs: number } | null {
|
|
439
444
|
const d = dials.get(grpOf(s))
|
|
440
445
|
if (!d || !d.w) return null
|
|
@@ -454,7 +459,7 @@ export function createGovernor(o: HostedOptions) {
|
|
|
454
459
|
const grpOf = (s: Session): string => s.hx?.grp ?? s.p.grp
|
|
455
460
|
|
|
456
461
|
/** A session from `ip` (address group `grp`) with this page origin is about to open. */
|
|
457
|
-
function admitSession(i: { ip: string; grp: string; origin: string;
|
|
462
|
+
function admitSession(i: { ip: string; grp: string; origin: string; end: (why: Why) => void; tell?: Tell; claimed?: boolean }): { ok: true; adm: Admission } | ({ ok: false } & Why) {
|
|
458
463
|
const no = (w: Why) => ({ ok: false as const, ...w })
|
|
459
464
|
const denied = deniedClient(i.ip, i.origin)
|
|
460
465
|
if (denied) return no(denied)
|
|
@@ -469,7 +474,7 @@ export function createGovernor(o: HostedOptions) {
|
|
|
469
474
|
live.add(handle)
|
|
470
475
|
let done = false
|
|
471
476
|
const adm: Admission = {
|
|
472
|
-
grp: i.grp,
|
|
477
|
+
grp: i.grp, net: coarseKey(i.grp), handle, fair: c.linkBytesPerSec ? new Bucket(Infinity) : null, sent: 0, waiting: false,
|
|
473
478
|
release() {
|
|
474
479
|
if (done) return
|
|
475
480
|
done = true
|
|
@@ -490,7 +495,7 @@ export function createGovernor(o: HostedOptions) {
|
|
|
490
495
|
function admitConnect(s: Session, host: string, _port: number): Why | { waitMs: number } | null {
|
|
491
496
|
const closed = notAccepting()
|
|
492
497
|
if (closed) return closed
|
|
493
|
-
const crawl = sites(grpOf(s), host
|
|
498
|
+
const crawl = sites(grpOf(s), host)
|
|
494
499
|
if (crawl) return crawl
|
|
495
500
|
return dialGate(s)
|
|
496
501
|
}
|
|
@@ -506,15 +511,22 @@ export function createGovernor(o: HostedOptions) {
|
|
|
506
511
|
/** D: a dial was refused, found no route, timed out or named a host that does not exist, after `ms`. */
|
|
507
512
|
function dialFailed(s: Session, ms: number) {
|
|
508
513
|
const d = dialOf(grpOf(s))
|
|
514
|
+
const t = now()
|
|
515
|
+
d.fails.push(t)
|
|
516
|
+
if (d.fails.length > c.failedDialsPerMin) d.fails.shift()
|
|
517
|
+
if (!d.w && recentFails(d, t) < c.failedDialsPerMin) return
|
|
509
518
|
d.w = Math.min(c.dialLimitMs, Math.max(1, Math.round(ms), d.w * 2))
|
|
510
519
|
}
|
|
511
520
|
function dialOk(s: Session) {
|
|
512
521
|
const d = dials.get(grpOf(s))
|
|
513
522
|
if (!d) return
|
|
523
|
+
if (!d.w) return
|
|
514
524
|
d.w = Math.floor(d.w / 2)
|
|
515
525
|
if (!d.w) {
|
|
516
|
-
|
|
526
|
+
d.gate = 0
|
|
517
527
|
if (d.told) s.hx?.handle.tell?.(3, 'failed-dials', 0, '')
|
|
528
|
+
d.told = false
|
|
529
|
+
if (!recentFails(d, now())) dials.delete(grpOf(s))
|
|
518
530
|
}
|
|
519
531
|
}
|
|
520
532
|
const dialWait = (s: Session) => dials.get(grpOf(s))?.w ?? 0
|
|
@@ -618,12 +630,13 @@ export function createGovernor(o: HostedOptions) {
|
|
|
618
630
|
ticker.unref?.()
|
|
619
631
|
const saver = o.usageFile ? setInterval(saveUsage, 15_000) : null
|
|
620
632
|
saver?.unref?.()
|
|
621
|
-
|
|
633
|
+
function sweep() {
|
|
622
634
|
const t = now()
|
|
623
635
|
for (const m of [addrSites, netSites]) for (const [k, x] of m) if (!fresh(x, t).size) m.delete(k)
|
|
624
|
-
for (const [k, d] of dials) if (d.gate + HOUR < t) dials.delete(k)
|
|
625
|
-
}
|
|
626
|
-
sweep
|
|
636
|
+
for (const [k, d] of dials) if (d.gate + HOUR < t && !recentFails(d, t)) dials.delete(k)
|
|
637
|
+
}
|
|
638
|
+
const sweeper = setInterval(sweep, 60_000)
|
|
639
|
+
sweeper.unref?.()
|
|
627
640
|
|
|
628
641
|
const state = () => {
|
|
629
642
|
roll()
|
|
@@ -686,6 +699,7 @@ export function createGovernor(o: HostedOptions) {
|
|
|
686
699
|
bytes,
|
|
687
700
|
limitWhy,
|
|
688
701
|
tick,
|
|
702
|
+
sweep,
|
|
689
703
|
slowed,
|
|
690
704
|
take,
|
|
691
705
|
waitMs,
|
|
@@ -697,7 +711,7 @@ export function createGovernor(o: HostedOptions) {
|
|
|
697
711
|
(c.monthBytes ? `; ${Math.round(c.monthBytes / 1e9)} GB a month, heavy users slowed first when the month runs ahead` : ''),
|
|
698
712
|
stop() {
|
|
699
713
|
clearInterval(ticker)
|
|
700
|
-
clearInterval(
|
|
714
|
+
clearInterval(sweeper)
|
|
701
715
|
if (saver) clearInterval(saver)
|
|
702
716
|
saveUsage()
|
|
703
717
|
},
|
package/relay/main.ts
CHANGED
|
@@ -79,7 +79,7 @@ const FLAG_KEYS = ['host', 'port', 'origins', 'keys', 'tlsCert', 'tlsKey', 'loca
|
|
|
79
79
|
const CAP_KEYS = ['maxConns', 'maxSessions', 'maxSessionsPerGroup', 'opensPerMin', 'connectsPerMin', 'dnsPerMin', 'rateDown', 'rateUp', 'maxBytes', 'selfAddrs', 'dnsServers', 'connectTimeoutMs', 'dnsTimeoutMs'] as const
|
|
80
80
|
const WISP_KEYS = ['path', 'buffer', 'maxPayload', 'maxHeldBytes', 'maxSessionHeldBytes', 'maxHeldTotal', 'maxGroupHeldBytes', 'instancesPerHour', 'maxPrincipals', 'preAuthPerAddr', 'handshakeMs', 'streamIdleMs', 'prefixKeys'] as const
|
|
81
81
|
// the numbers of the hosted rules, under "hosted" in a config file (hosted.ts HostedOptions; all whole numbers)
|
|
82
|
-
const HOSTED_KEYS = ['sitesPerAddrPerHour', 'sitesPerNetPerHour'] as const
|
|
82
|
+
const HOSTED_KEYS = ['sitesPerAddrPerHour', 'sitesPerNetPerHour', 'failedDialsPerMin'] as const
|
|
83
83
|
// keys of the limits plan §16 removed: a config that still has them starts, with one line saying they do nothing
|
|
84
84
|
const LEGACY_KEYS = new Set(['maxBytesPerDay', 'maxBytesPerMonth'])
|
|
85
85
|
const LEGACY_HOSTED_KEYS = new Set(['groupStreams', 'groupConnectsPerMin', 'groupBytesPerHour', 'groupDnsPerMin', 'coarseSessions', 'coarseInstancesPerHour', 'coarseRateDown', 'coarseRateUp', 'originTable', 'originSessions', 'originShare', 'otherOriginSessions', 'noOriginSessions', 'originsPerAddrPerHour', 'originRateDown', 'originRateUp', 'maxStreams', 'maxRateDown', 'maxRateUp', 'destConnectsPerMin', 'destStreams', 'distinctDests', 'failedConnects', 'failedWindowMs', 'failedCoolMs'])
|
package/relay/relay.ts
CHANGED
|
@@ -132,7 +132,6 @@ export function createRelay(o: RelayOptions) {
|
|
|
132
132
|
const r = health(url.pathname)
|
|
133
133
|
return req.method === 'HEAD' ? new Response(null, { status: r.status, headers: r.headers }) : r // same status and headers, no body
|
|
134
134
|
}
|
|
135
|
-
if (gov && wisp && url.pathname === '/publish-token') return wisp.publishToken(req, srv, url)
|
|
136
135
|
if (url.pathname === '/relay') {
|
|
137
136
|
const protos = req.headers.get('sec-websocket-protocol') || ''
|
|
138
137
|
if (wisp && protos.includes('wisp-v2') && !protos.includes(PROTOCOL)) return new Response(`this is the wmd-relay.v1 endpoint; Wisp is at ${wisp.path}\n`, { status: 400 })
|
package/relay/token.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// P-B10 relay: session tokens. A token is "v1.<payload>.<mac>", base64url without padding,
|
|
2
|
-
// where the payload is JSON {sid, exp (ms since the epoch), uid?, grp
|
|
2
|
+
// where the payload is JSON {sid, exp (ms since the epoch), uid?, grp?} and the mac is
|
|
3
3
|
// HMAC-SHA256 of "v1.<payload>" with the relay's secret. Whoever serves the page
|
|
4
4
|
// (test/serve.ts, or wmn's server) mints them with the same secret (TokenIssuer below); the
|
|
5
5
|
// browser presents one as a WebSocket subprotocol "t.<token>" (not in the URL, so it stays
|
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
// uid the guest identity the relay keeps caps for, across reconnects and fresh tokens
|
|
10
10
|
// (one guest machine of one page); defaults to sid
|
|
11
11
|
// grp the client the issuer saw (an address, a user): the relay caps sessions per grp
|
|
12
|
-
// kind "publish": a publish identity (TokenIssuer.issuePublish), which a hosted relay gives a higher sites-an-hour
|
|
13
|
-
// count (hosted.ts). Only the signature makes it one: the relay never takes a client's word for it
|
|
14
12
|
//
|
|
15
13
|
// Parsing is strict: a token is exactly one encoding of its payload and MAC, so the same
|
|
16
14
|
// token never verifies under two spellings.
|
|
@@ -21,20 +19,19 @@ const b64u = (b: Buffer) => b.toString('base64url')
|
|
|
21
19
|
const B64U = /^[A-Za-z0-9_-]+$/
|
|
22
20
|
const ID = /^[A-Za-z0-9_.:\/-]{1,128}$/
|
|
23
21
|
|
|
24
|
-
export type TokenFields = { sid?: string; uid?: string; grp?: string;
|
|
22
|
+
export type TokenFields = { sid?: string; uid?: string; grp?: string; ttlMs?: number; now?: number }
|
|
25
23
|
|
|
26
|
-
export function mintToken(secret: string, { sid = b64u(randomBytes(12)), uid, grp,
|
|
24
|
+
export function mintToken(secret: string, { sid = b64u(randomBytes(12)), uid, grp, ttlMs = 60 * 60_000, now = Date.now() }: TokenFields = {}): string {
|
|
27
25
|
if (!secret || secret.length < 16) throw new Error('relay secret: at least 16 characters')
|
|
28
26
|
const body: Record<string, unknown> = { sid, exp: now + ttlMs }
|
|
29
27
|
if (uid) body.uid = uid
|
|
30
28
|
if (grp) body.grp = grp
|
|
31
|
-
if (kind) body.kind = kind
|
|
32
29
|
const payload = b64u(Buffer.from(JSON.stringify(body)))
|
|
33
30
|
const mac = createHmac('sha256', secret).update(`v1.${payload}`).digest()
|
|
34
31
|
return `v1.${payload}.${b64u(mac)}`
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
export type TokenInfo = { sid: string; exp: number; uid?: string; grp?: string
|
|
34
|
+
export type TokenInfo = { sid: string; exp: number; uid?: string; grp?: string }
|
|
38
35
|
|
|
39
36
|
/** The token's session, or an error string. */
|
|
40
37
|
export function verifyToken(secret: string, token: string, now = Date.now()): TokenInfo | string {
|
|
@@ -55,9 +52,8 @@ export function verifyToken(secret: string, token: string, now = Date.now()): To
|
|
|
55
52
|
}
|
|
56
53
|
if (typeof info !== 'object' || !info || typeof info.sid !== 'string' || !ID.test(info.sid) || typeof info.exp !== 'number') return 'bad token'
|
|
57
54
|
for (const k of ['uid', 'grp'] as const) if (info[k] !== undefined && (typeof info[k] !== 'string' || !ID.test(info[k]!))) return 'bad token'
|
|
58
|
-
if (info.kind !== undefined && info.kind !== 'publish') return 'bad token'
|
|
59
55
|
if (info.exp <= now) return 'token expired'
|
|
60
|
-
return { sid: info.sid, exp: info.exp, uid: info.uid, grp: info.grp
|
|
56
|
+
return { sid: info.sid, exp: info.exp, uid: info.uid, grp: info.grp }
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
/** The key a client's limits are kept under: IPv6 addresses by their /64 (one subscriber holds 2^64 of them). */
|
|
@@ -83,7 +79,6 @@ export function clientKey(client: string): string {
|
|
|
83
79
|
* an hour. At most `maxClients` clients are remembered (the least recently used go first).
|
|
84
80
|
*/
|
|
85
81
|
export const MIN_TTL_MS = 60_000
|
|
86
|
-
export const PUBLISH_TTL_MS = 60 * 60_000
|
|
87
82
|
export class TokenIssuer {
|
|
88
83
|
perMinute: number
|
|
89
84
|
instancesPerHour: number
|
|
@@ -91,7 +86,6 @@ export class TokenIssuer {
|
|
|
91
86
|
maxClients: number
|
|
92
87
|
minTtlMs: number
|
|
93
88
|
clients = new Map<string, { times: number[]; instances: Map<string, number> }>() // least recently used first
|
|
94
|
-
publishing = new Map<string, { instance: string; token: string; until: number }>() // per client: its live publish identity, in the order they end
|
|
95
89
|
constructor(public secret: string, { perMinute = 60, instancesPerHour = 20, maxTtlMs = 60 * 60_000, maxClients = 10_000, minTtlMs = MIN_TTL_MS } = {}) {
|
|
96
90
|
this.perMinute = perMinute
|
|
97
91
|
this.instancesPerHour = instancesPerHour
|
|
@@ -119,27 +113,4 @@ export class TokenIssuer {
|
|
|
119
113
|
const ttl = Math.min(Math.max(ttlMs, this.minTtlMs), this.maxTtlMs)
|
|
120
114
|
return { token: mintToken(this.secret, { uid: `${key}/${instance}`, grp: key, ttlMs: ttl, now }) }
|
|
121
115
|
}
|
|
122
|
-
/** A publish identity for this client, for one hour; at most one per client at a time. The page that holds it gets the
|
|
123
|
-
same token again (a reconnect); another page of the client is refused until its hour is over. When `maxClients`
|
|
124
|
-
identities are live, a new one is refused (503) until the first ends: a live one is never dropped. */
|
|
125
|
-
issuePublish(client: string, instance: string, now = Date.now()): { token: string; until: number } | { error: string; status: number; until?: number } {
|
|
126
|
-
if (!ID.test(client)) return { error: 'bad client', status: 400 }
|
|
127
|
-
if (!/^[A-Za-z0-9_-]{8,64}$/.test(instance || '')) return { error: 'instance: 8-64 characters [A-Za-z0-9_-]', status: 400 }
|
|
128
|
-
const key = clientKey(client)
|
|
129
|
-
for (const [k, p] of this.publishing) {
|
|
130
|
-
if (p.until > now) break
|
|
131
|
-
this.publishing.delete(k)
|
|
132
|
-
}
|
|
133
|
-
const live = this.publishing.get(key)
|
|
134
|
-
if (live && live.instance === instance) return { token: live.token, until: live.until }
|
|
135
|
-
if (live) return { error: `this address already has a publish identity; another can be had in ${Math.ceil((live.until - now) / 60_000)} min`, status: 429, until: live.until }
|
|
136
|
-
if (this.publishing.size >= this.maxClients) {
|
|
137
|
-
const first = this.publishing.values().next().value!
|
|
138
|
-
return { error: `the relay is giving out as many publish identities as it can hold; try again in ${Math.ceil((first.until - now) / 60_000)} min`, status: 503, until: first.until }
|
|
139
|
-
}
|
|
140
|
-
const until = now + PUBLISH_TTL_MS
|
|
141
|
-
const token = mintToken(this.secret, { uid: `${key}/publish.${instance}`, grp: key, kind: 'publish', ttlMs: PUBLISH_TTL_MS, now })
|
|
142
|
-
this.publishing.set(key, { instance, token, until })
|
|
143
|
-
return { token, until }
|
|
144
|
-
}
|
|
145
116
|
}
|
package/relay/wisp.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// W59: the Wisp v2 adapter (src/net/wisp.js, docs/wisp-wmd-extensions.md) over the relay engine.
|
|
2
2
|
//
|
|
3
3
|
// Who may use it (plan §5):
|
|
4
|
-
// - token: the HMAC `t.<token>` subprotocol, as wmd-relay.v1 (wmn's bundled relay)
|
|
5
|
-
// identity it issues itself (relay.ts /publish-token), used only from the address it was given to
|
|
4
|
+
// - token: the HMAC `t.<token>` subprotocol, as wmd-relay.v1 (wmn's bundled relay)
|
|
6
5
|
// - open: no key (the standalone relay's default); identity = the client's instance id (0xE7)
|
|
7
6
|
// plus its address group, bandwidth shared per address
|
|
8
7
|
// - keys (optional): Wisp password auth, scrypt hashes; a relay with keys refuses keyless clients
|
|
@@ -24,7 +23,7 @@ import { type AllowEntry, type Engine, type Principal, type Session, type Shared
|
|
|
24
23
|
import { type Governor, type Why, resolveClient } from './hosted.ts'
|
|
25
24
|
import { type OwnerKey, verifySecret } from './keys.ts'
|
|
26
25
|
import type { Cidr } from './policy.ts'
|
|
27
|
-
import {
|
|
26
|
+
import { clientKey, verifyToken } from './token.ts'
|
|
28
27
|
|
|
29
28
|
export type WispOptions = {
|
|
30
29
|
enabled?: boolean
|
|
@@ -185,32 +184,6 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
185
184
|
return reply(body, status)
|
|
186
185
|
}
|
|
187
186
|
|
|
188
|
-
const issuer = w.hosted && o.secret ? new TokenIssuer(o.secret) : null
|
|
189
|
-
|
|
190
|
-
/** Hosted: GET /publish-token?instance=ID from a listed page gives {token, until}, a publish identity for this address
|
|
191
|
-
(token.ts issuePublish), or {error, until?} in words. The page's origin may read the answer. */
|
|
192
|
-
function publishToken(req: Request, srv: any, url: URL): Response {
|
|
193
|
-
const origin = req.headers.get('origin') || ''
|
|
194
|
-
if (!issuer || origin.length > 256 || !w.hosted!.siteAllowed(origin)) {
|
|
195
|
-
log({ ev: 'publish-token-refused', why: 'not-allowed-site' })
|
|
196
|
-
return reply('this page is not one of the sites this relay serves\n', 403)
|
|
197
|
-
}
|
|
198
|
-
const headers = { 'access-control-allow-origin': origin, vary: 'Origin', 'cache-control': 'no-store' }
|
|
199
|
-
if (req.method !== 'GET') return new Response(`GET only\n${w.hosted!.contactLine}`, { status: 405, headers })
|
|
200
|
-
const ip = clientIp(req, srv)
|
|
201
|
-
if (typeof ip !== 'string') {
|
|
202
|
-
for (const [k, v] of Object.entries(headers)) ip.headers.set(k, v)
|
|
203
|
-
return ip
|
|
204
|
-
}
|
|
205
|
-
const r = issuer.issuePublish(ip, url.searchParams.get('instance') || '')
|
|
206
|
-
if ('token' in r) {
|
|
207
|
-
log({ ev: 'publish-token', addr: clientKey(ip), origin })
|
|
208
|
-
return Response.json(r, { headers })
|
|
209
|
-
}
|
|
210
|
-
log({ ev: 'publish-token-refused', why: r.error, addr: clientKey(ip) })
|
|
211
|
-
return Response.json({ error: r.error, ...(r.until ? { until: r.until } : {}), abuseContact: w.hosted!.contact }, { status: r.status, headers })
|
|
212
|
-
}
|
|
213
|
-
|
|
214
187
|
async function fetch(req: Request, srv: any, url: URL): Promise<Response | undefined> {
|
|
215
188
|
const ip = clientIp(req, srv)
|
|
216
189
|
if (typeof ip !== 'string') return ip
|
|
@@ -237,13 +210,11 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
237
210
|
let uid = ''
|
|
238
211
|
let grp = ''
|
|
239
212
|
let exp = Infinity
|
|
240
|
-
let publish = false
|
|
241
213
|
let p: Principal | null = null
|
|
242
214
|
const tok = protos.find(x => x.startsWith('t.'))
|
|
243
215
|
if (tok && o.secret) {
|
|
244
216
|
const info = verifyToken(o.secret, tok.slice(2))
|
|
245
217
|
if (typeof info === 'string') return refuse(401, `${info}\n`, info, { origin })
|
|
246
|
-
if (w.hosted && info.grp !== addr) return refuse(401, 'this token was given to another address\n', 'token of another address', { addr })
|
|
247
218
|
if (engine.hasSession(info.sid)) {
|
|
248
219
|
stats.refusedSessions++
|
|
249
220
|
return reply('session already connected\n', 409)
|
|
@@ -253,9 +224,7 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
253
224
|
uid = info.uid || info.sid
|
|
254
225
|
grp = info.grp || uid
|
|
255
226
|
exp = info.exp
|
|
256
|
-
|
|
257
|
-
// a hosted publish session shares its address's bandwidth with the address's open sessions
|
|
258
|
-
const a = engine.admit(uid, grp, exp, { proto: 'wisp' }, w.hosted && publish ? sharedFor(grp) : undefined)
|
|
227
|
+
const a = engine.admit(uid, grp, exp, { proto: 'wisp' })
|
|
259
228
|
if (!a.ok) return reply(a.body, a.status)
|
|
260
229
|
p = a.p
|
|
261
230
|
} else if (rest) {
|
|
@@ -302,7 +271,7 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
302
271
|
const full = w.hosted?.claimHandshake() ?? null
|
|
303
272
|
const headers: Record<string, string> = {}
|
|
304
273
|
if (v2 && protos.length) headers['Sec-WebSocket-Protocol'] = protos.includes(SUBPROTOCOL) ? SUBPROTOCOL : protos[0]
|
|
305
|
-
const data = { kind: 'wisp', v2, mode, addr, ip, sid, uid, grp, exp,
|
|
274
|
+
const data = { kind: 'wisp', v2, mode, addr, ip, sid, uid, grp, exp, p, key, origin, hs: null as any, hsTimer: null as any, pre, claimed: !!w.hosted && !full, refuse: full as Why | null, at: performance.now() }
|
|
306
275
|
const ok = srv.upgrade(req, Object.keys(headers).length ? { data, headers } : { data }) // Bun refuses an empty headers object
|
|
307
276
|
if (!ok) {
|
|
308
277
|
release(data)
|
|
@@ -448,7 +417,7 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
448
417
|
tell(REASON_KIND.REFUSED, why.rule, why.until ?? 0, why.words)
|
|
449
418
|
ws.close(4011, why.words.slice(0, 120))
|
|
450
419
|
}
|
|
451
|
-
adm = w.hosted.admitSession({ ip: d.ip, grp: d.grp, origin: d.origin,
|
|
420
|
+
adm = w.hosted.admitSession({ ip: d.ip, grp: d.grp, origin: d.origin, end, tell, claimed: d.claimed })
|
|
452
421
|
if (!adm.ok) return refuseHs(ws, client, adm)
|
|
453
422
|
d.claimed = false // the session holds the socket's count now
|
|
454
423
|
d.rel = adm.adm.release
|
|
@@ -695,7 +664,6 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
695
664
|
ws.close(1013, 'the relay is full')
|
|
696
665
|
},
|
|
697
666
|
fetch,
|
|
698
|
-
publishToken,
|
|
699
667
|
open: open_,
|
|
700
668
|
message,
|
|
701
669
|
close,
|
package/src/blob.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// D-554: WebKit (Playwright's WebKit 26 here) cannot read a Blob of 128 MiB or more whole: arrayBuffer() and stream()
|
|
2
|
+
// fail with NotReadableError ("The I/O read operation failed."). Slices of the same Blob read. Measured with 128, 256
|
|
3
|
+
// and 400 MiB Blobs; 64 MiB read whole. Readers of large slices (a tar entry, a layer) go through here too.
|
|
4
|
+
const SLICE = 16 << 20
|
|
5
|
+
|
|
6
|
+
/** A Blob's bytes, read in 16 MiB slices. */
|
|
7
|
+
export async function blobBytes(b) {
|
|
8
|
+
if (b.size <= SLICE) return new Uint8Array(await b.arrayBuffer())
|
|
9
|
+
const out = new Uint8Array(b.size)
|
|
10
|
+
for (let o = 0; o < b.size; o += SLICE) out.set(new Uint8Array(await b.slice(o, o + SLICE).arrayBuffer()), o)
|
|
11
|
+
return out
|
|
12
|
+
}
|