@amritk/nish-x86_64-linux 0.27.2 → 0.29.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.
Files changed (54) hide show
  1. package/INSTALL.md +16 -8
  2. package/bin/nish +0 -0
  3. package/docs/AI.md +118 -26
  4. package/package.json +1 -1
  5. package/runtime/core/host.d.ts +2 -1
  6. package/runtime/core/host.mjs +4 -0
  7. package/runtime/core.d.ts +47 -0
  8. package/runtime/core.mjs +67 -0
  9. package/runtime/nish.d.ts +176 -0
  10. package/runtime/nish.h +120 -6
  11. package/runtime/nish.mjs +24 -4
  12. package/runtime/runtime-checksum.c +270 -0
  13. package/runtime/runtime-net.c +386 -5
  14. package/runtime/runtime-os.c +59 -8
  15. package/runtime/runtime-parallel.c +81 -5
  16. package/runtime/runtime-sandbox.c +479 -0
  17. package/runtime/runtime-sha256.c +239 -0
  18. package/runtime/runtime-shm.c +541 -0
  19. package/runtime/runtime.c +827 -791
  20. package/scripts/build.sh +79 -28
  21. package/std/README.md +25 -13
  22. package/std/collections.ts +5 -5
  23. package/std/compress/checksum.ts +88 -75
  24. package/std/compress/deflate.ts +224 -0
  25. package/std/compress/gzip.ts +52 -0
  26. package/std/crypto/bcrypt.ts +816 -280
  27. package/std/crypto/hmac.ts +305 -2
  28. package/std/crypto/jwt.ts +1661 -0
  29. package/std/json-equal.ts +335 -0
  30. package/std/json-format.ts +1070 -0
  31. package/std/json-number.ts +428 -0
  32. package/std/json-pointer.ts +245 -0
  33. package/std/json-typed.ts +34 -0
  34. package/std/json-value.ts +510 -17
  35. package/std/json.ts +35 -6
  36. package/std/net/handler.ts +1348 -0
  37. package/std/net/http-client.ts +97 -16
  38. package/std/net/http-compress.ts +301 -16
  39. package/std/net/http1-client.ts +104 -19
  40. package/std/net/http1-server.ts +127 -24
  41. package/std/net/http1.ts +24 -4
  42. package/std/net/outbound.ts +409 -0
  43. package/std/net/postgres.ts +405 -0
  44. package/std/net/prefork.ts +344 -20
  45. package/std/net/websocket.ts +295 -14
  46. package/std/regex.ts +613 -52
  47. package/std/secret.ts +1 -1
  48. package/std/sync/ring.ts +587 -0
  49. package/std/sync/shm.ts +284 -0
  50. package/std/term-io.ts +35 -8
  51. package/std/term-rate.ts +13 -1
  52. package/std/threads.ts +35 -3
  53. package/std/yaml-json.ts +841 -0
  54. /package/runtime/{LICENSE-ryu → LICENSE-zmij} +0 -0
@@ -0,0 +1,409 @@
1
+ /**
2
+ * `nish/net/outbound` — the policy for a program that fetches URLs it did
3
+ * not choose: a webhook sender, a link previewer, an importer that takes a
4
+ * URL from a form. Without one, such a program is a proxy into its own
5
+ * network for whoever supplies the URL (server-side request forgery): a URL
6
+ * naming `127.0.0.1`, `169.254.169.254` or a name that resolves to either
7
+ * reaches services that trust the program's address.
8
+ *
9
+ * import { OutboundPolicy } from "nish/net/outbound";
10
+ * import { HttpOptions } from "nish/net/http-client";
11
+ *
12
+ * const options = new HttpOptions();
13
+ * options.outbound = new OutboundPolicy();
14
+ *
15
+ * **The check is on the address, after the lookup.** A name is never judged
16
+ * by its text: `nish/net/http-client` resolves it, drops every address
17
+ * `outboundAddressAllowed` refuses, and connects only to the bytes it kept,
18
+ * with no second lookup, so a name whose answer changes between a check and
19
+ * a connect (DNS rebinding) cannot move the connection. Every redirect hop is
20
+ * dialled the same way, so a public URL that redirects inward is refused at
21
+ * the hop.
22
+ *
23
+ * **Public** is the IANA special-purpose registries read strictly
24
+ * (RFC 6890, and the registries at iana.org/assignments/iana-ipv4-special-registry
25
+ * and iana-ipv6-special-registry). An IPv4 address is public unless it is in
26
+ * `0/8`, `10/8`, `100.64/10`, `127/8`, `169.254/16`, `172.16/12`,
27
+ * `192.0.0/24`, `192.0.2/24`, `192.88.99/24`, `192.168/16`, `198.18/15`,
28
+ * `198.51.100/24`, `203.0.113/24`, `224/4` or `240/4` (the broadcast
29
+ * address included), or is `168.63.129.16`, a cloud host's metadata and DNS
30
+ * service. An IPv6 address is public only in `2000::/3`, and then not in
31
+ * `2001::/23` (Teredo included), `2001:db8::/32` or `3fff::/20`; `2002::/16`
32
+ * (6to4) and `64:ff9b::/96` (NAT64) are judged by the IPv4 address they carry,
33
+ * as is `::ffff:0:0/96`, the form every IPv4 address takes in `nish:net`.
34
+ * Everything else, `::1`, `fc00::/7` and `fe80::/10` among it, is refused.
35
+ *
36
+ * `permit` lets a trusted prefix through anyway (an internal API the program
37
+ * is meant to reach), and `allowHost` narrows the names a URL may carry. The
38
+ * policy also caps a fetch's redirects, total time and body, and the client
39
+ * uses the stricter of these and its own options.
40
+ *
41
+ * Private helpers share the importing program's flat symbol namespace
42
+ * (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
43
+ *
44
+ * Written from RFC 6890, RFC 4291, RFC 6052, RFC 3056, RFC 4380 and the IANA
45
+ * special-purpose address registries, not ported from another implementation.
46
+ */
47
+ import { copyBytes } from "nish/bytes"
48
+ import { DNS_ADDRESS_BYTES } from "nish/net/dns-message"
49
+ import { dnsParseAddress } from "nish/net/dns-config"
50
+
51
+ /** The bytes of one `permit` entry: sixteen of address, then the prefix length in bits. */
52
+ const OUTBOUND_PERMIT_BYTES: i32 = 17
53
+
54
+ /** Where an IPv4 address starts in its sixteen-byte mapped form. */
55
+ const OUTBOUND_V4_AT: i32 = 12
56
+
57
+ /**
58
+ * What a client that fetches untrusted URLs may reach. A new policy refuses
59
+ * every address that is not public, allows any host name, and caps a fetch
60
+ * at 5 redirects, 30 seconds and an 8 MiB body.
61
+ */
62
+ export class OutboundPolicy {
63
+ // Declared widest first, so no byte of a policy is padding.
64
+ /** The names a URL may carry, lower-cased; empty for any. See `allowHost`. */
65
+ allowHosts: string[]
66
+ /** `OUTBOUND_PERMIT_BYTES` per prefix `permit` let through. */
67
+ permitted: u8[]
68
+ /** A fetch's body is cut off past this, or past the client's own cap if lower. */
69
+ maxBodyBytes: i64 = 8388608
70
+ /** A whole fetch ends after this, or the client's own budget if lower; 0 or less for the client's alone. */
71
+ totalTimeoutMs: i32 = 30000
72
+ /** At most this many redirects, or the client's own limit if lower. */
73
+ maxRedirects: i32 = 5
74
+
75
+ constructor() {
76
+ this.allowHosts = []
77
+ this.permitted = []
78
+ }
79
+
80
+ /**
81
+ * Adds `name` to the hosts a URL may name; once any is added, a URL (or a
82
+ * redirect) naming another is refused before it is looked up. Case and one
83
+ * trailing dot do not matter. A leading dot (`.example.com`) allows the
84
+ * name and every name under it. An address literal matches only itself,
85
+ * as the URL writes it (`::1` or `[::1]`, `127.0.0.1`), never as a suffix.
86
+ */
87
+ allowHost(name: string): void {
88
+ let entry: string = outboundLower(name)
89
+ if (entry.startsWith("[") && entry.endsWith("]")) {
90
+ entry = entry.substring(1, toI32(entry.length) - 1)
91
+ }
92
+ this.allowHosts.push(outboundTrimDot(entry))
93
+ }
94
+
95
+ /**
96
+ * Lets the addresses in `cidr` through though they are not public:
97
+ * `10.1.2.0/24`, `fd00:1::/64`, or one address alone. False, with nothing
98
+ * added, when `cidr` is not an address and a prefix length that fits it.
99
+ */
100
+ permit(cidr: string): boolean {
101
+ const n: i32 = toI32(cidr.length)
102
+ const slash: i32 = toI32(cidr.indexOf("/"))
103
+ const end: i32 = slash >= 0 ? slash : n
104
+ const text: u8[] = new Array<u8>(end)
105
+ let v4: boolean = true
106
+ for (let i: i32 = 0; i < toI32(text.length) && i < n; i += 1) {
107
+ const c: i32 = toI32(cidr.charCodeAt(i))
108
+ if (c > 127) {
109
+ return false
110
+ }
111
+ v4 = v4 && c !== 58
112
+ text[i] = toU8(c)
113
+ }
114
+ const address: u8[] = new Array<u8>(DNS_ADDRESS_BYTES)
115
+ if (end === 0 || !dnsParseAddress(text, 0, end, address, 0, 0)) {
116
+ return false
117
+ }
118
+ const width: i32 = v4 ? 32 : 128
119
+ let bits: i32 = width
120
+ if (slash >= 0) {
121
+ bits = outboundDecimal(cidr, slash + 1, n)
122
+ if (bits < 0 || bits > width) {
123
+ return false
124
+ }
125
+ }
126
+ for (let i: i32 = 0; i < 16 && i < toI32(address.length); i += 1) {
127
+ this.permitted.push(address[i])
128
+ }
129
+ this.permitted.push(toU8(v4 ? bits + 96 : bits))
130
+ return true
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Whether the sixteen address bytes at `addr[off]` are public, as the module
136
+ * comment defines it. Panics when sixteen bytes do not fit there.
137
+ */
138
+ export const outboundAddressPublic = (addr: u8[], off: i32): boolean => {
139
+ if (off < 0 || off > toI32(addr.length) - 16) {
140
+ panic("outboundAddressPublic: sixteen bytes do not fit at off")
141
+ }
142
+ const carried: i32 = outboundCarriedV4(addr, off)
143
+ if (carried >= 0) {
144
+ return outboundV4Public(addr, carried)
145
+ }
146
+ const b0: i32 = toI32(addr[off])
147
+ const b1: i32 = toI32(addr[off + 1])
148
+ const b2: i32 = toI32(addr[off + 2])
149
+ const b3: i32 = toI32(addr[off + 3])
150
+ if ((b0 & 0xe0) !== 0x20) {
151
+ // Outside 2000::/3: loopback, unspecified, unique local, link-local, multicast, the rest.
152
+ return false
153
+ }
154
+ if (b0 === 0x20 && b1 === 0x01 && (b2 & 0xfe) === 0) {
155
+ // 2001::/23, IETF protocol assignments, Teredo's 2001::/32 among them.
156
+ return false
157
+ }
158
+ if (b0 === 0x20 && b1 === 0x01 && b2 === 0x0d && b3 === 0xb8) {
159
+ // 2001:db8::/32, documentation.
160
+ return false
161
+ }
162
+ // 3fff::/20, documentation (RFC 9637).
163
+ return !(b0 === 0x3f && b1 === 0xff && (b2 & 0xf0) === 0)
164
+ }
165
+
166
+ /** Whether `policy` lets a connection go to the sixteen address bytes at `addr[off]`. */
167
+ export const outboundAddressAllowed = (policy: OutboundPolicy, addr: u8[], off: i32): boolean => {
168
+ if (outboundPermitted(policy, addr, off)) {
169
+ return true
170
+ }
171
+ const carried: i32 = outboundCarriedV4(addr, off)
172
+ if (carried < 0) {
173
+ return outboundAddressPublic(addr, off)
174
+ }
175
+ if (!outboundMapped(addr, off) && toI32(policy.permitted.length) > 0) {
176
+ // A permit names an IPv4 prefix in its ::ffff: form, so try the address a NAT64 or 6to4 one carries in that form too.
177
+ const mapped: u8[] = new Array<u8>(16)
178
+ mapped[10] = toU8(255)
179
+ mapped[11] = toU8(255)
180
+ copyBytes(mapped, OUTBOUND_V4_AT, addr, carried, 4)
181
+ if (outboundPermitted(policy, mapped, 0)) {
182
+ return true
183
+ }
184
+ }
185
+ return outboundV4Public(addr, carried)
186
+ }
187
+
188
+ /**
189
+ * Keeps the addresses of `count` `DNS_ADDRESS_BYTES` records in `addresses`
190
+ * that `policy` allows, moved to the front in their order, and answers how
191
+ * many. Refused records are left behind them, so the caller can name one.
192
+ */
193
+ export const outboundFilter = (policy: OutboundPolicy, addresses: u8[], count: i32): i32 => {
194
+ if (count < 0 || count * DNS_ADDRESS_BYTES > toI32(addresses.length)) {
195
+ panic("outboundFilter: count records do not fit in addresses")
196
+ }
197
+ let kept: i32 = 0
198
+ for (let i: i32 = 0; i < count; i += 1) {
199
+ if (!outboundAddressAllowed(policy, addresses, i * DNS_ADDRESS_BYTES)) {
200
+ continue
201
+ }
202
+ if (i !== kept) {
203
+ // Swap, so the refused record stays in the list for an error to name.
204
+ for (let k: i32 = 0; k < DNS_ADDRESS_BYTES; k += 1) {
205
+ const held: u8 = addresses[kept * DNS_ADDRESS_BYTES + k]
206
+ addresses[kept * DNS_ADDRESS_BYTES + k] = addresses[i * DNS_ADDRESS_BYTES + k]
207
+ addresses[i * DNS_ADDRESS_BYTES + k] = held
208
+ }
209
+ }
210
+ kept += 1
211
+ }
212
+ return kept
213
+ }
214
+
215
+ /**
216
+ * Whether `policy` lets a URL name `host`, as the URL parser gives it
217
+ * (lower-cased, an IPv6 literal without its brackets). Any host when the
218
+ * policy names none.
219
+ */
220
+ export const outboundHostAllowed = (policy: OutboundPolicy, host: string): boolean => {
221
+ if (policy.allowHosts.length === 0) {
222
+ return true
223
+ }
224
+ const name: string = outboundTrimDot(outboundLower(host))
225
+ const literal: boolean = outboundIsLiteral(name)
226
+ for (let i: i32 = 0; i < toI32(policy.allowHosts.length); i += 1) {
227
+ const entry: string = policy.allowHosts[i]
228
+ if (entry === name) {
229
+ return true
230
+ }
231
+ if (
232
+ !literal &&
233
+ entry.startsWith(".") &&
234
+ (name.endsWith(entry) || (toI32(name.length) + 1 === toI32(entry.length) && entry.endsWith(name)))
235
+ ) {
236
+ return true
237
+ }
238
+ }
239
+ return false
240
+ }
241
+
242
+ /** The sixteen address bytes at `addr[off]` as text: dotted for an IPv4 address, eight hex groups otherwise. */
243
+ export const outboundAddressText = (addr: u8[], off: i32): string => {
244
+ if (outboundMapped(addr, off)) {
245
+ const at: i32 = off + OUTBOUND_V4_AT
246
+ return `${addr[at]}.${addr[at + 1]}.${addr[at + 2]}.${addr[at + 3]}`
247
+ }
248
+ // Eight groups of at most four digits and seven colons.
249
+ const out: u8[] = new Array<u8>(39)
250
+ let at: i32 = 0
251
+ for (let g: i32 = 0; g < 8; g += 1) {
252
+ const v: i32 = toI32(addr[off + 2 * g]) * 256 + toI32(addr[off + 2 * g + 1])
253
+ if (g > 0 && at < toI32(out.length)) {
254
+ out[at] = toU8(58)
255
+ at += 1
256
+ }
257
+ for (let shift: i32 = 12; shift >= 0; shift -= 4) {
258
+ const d: i32 = (v >> shift) & 15
259
+ if (v >> shift !== 0 || shift === 0) {
260
+ if (at < toI32(out.length)) {
261
+ out[at] = toU8(d < 10 ? 48 + d : 87 + d)
262
+ at += 1
263
+ }
264
+ }
265
+ }
266
+ }
267
+ return String.fromBytes(out, 0, at)
268
+ }
269
+
270
+ // --- helpers -----------------------------------------------------------------
271
+
272
+ /** Whether one of `policy`'s permits covers the sixteen address bytes at `addr[off]`. */
273
+ const outboundPermitted = (policy: OutboundPolicy, addr: u8[], off: i32): boolean => {
274
+ const p: u8[] = policy.permitted
275
+ for (let at: i32 = 0; at + OUTBOUND_PERMIT_BYTES <= toI32(p.length); at += OUTBOUND_PERMIT_BYTES) {
276
+ const bitsAt: i32 = at + 16
277
+ if (bitsAt >= 0 && bitsAt < toI32(p.length) && outboundPrefixMatch(p, at, toI32(p[bitsAt]), addr, off)) {
278
+ return true
279
+ }
280
+ }
281
+ return false
282
+ }
283
+
284
+ /** Whether the sixteen address bytes at `addr[off]` are an IPv4-mapped address, `::ffff:0:0/96`. */
285
+ const outboundMapped = (addr: u8[], off: i32): boolean =>
286
+ outboundZeros(addr, off, 10) && addr[off + 10] === toU8(255) && addr[off + 11] === toU8(255)
287
+
288
+ /**
289
+ * Where the IPv4 address that the sixteen address bytes at `addr[off]` carry
290
+ * starts, or -1 for none: `::ffff:a.b.c.d`, how nish:net writes every IPv4
291
+ * address; `64:ff9b::/96`, the NAT64 well-known prefix (RFC 6052), in its
292
+ * last four bytes; `2002::/16`, 6to4 (RFC 3056), in the four after the prefix.
293
+ */
294
+ const outboundCarriedV4 = (addr: u8[], off: i32): i32 => {
295
+ if (outboundMapped(addr, off)) {
296
+ return off + OUTBOUND_V4_AT
297
+ }
298
+ const b0: i32 = toI32(addr[off])
299
+ const b1: i32 = toI32(addr[off + 1])
300
+ if (
301
+ b0 === 0 &&
302
+ b1 === 0x64 &&
303
+ addr[off + 2] === toU8(0xff) &&
304
+ addr[off + 3] === toU8(0x9b) &&
305
+ outboundZeros(addr, off + 4, 8)
306
+ ) {
307
+ return off + OUTBOUND_V4_AT
308
+ }
309
+ if (b0 === 0x20 && b1 === 0x02) {
310
+ return off + 2
311
+ }
312
+ return -1
313
+ }
314
+
315
+ /** Whether the IPv4 address in the four bytes at `addr[at]` is public. */
316
+ const outboundV4Public = (addr: u8[], at: i32): boolean => {
317
+ const a: i32 = toI32(addr[at])
318
+ const b: i32 = toI32(addr[at + 1])
319
+ const c: i32 = toI32(addr[at + 2])
320
+ const d: i32 = toI32(addr[at + 3])
321
+ return !(
322
+ a === 0 ||
323
+ a === 10 ||
324
+ a === 127 ||
325
+ a >= 224 ||
326
+ (a === 100 && b >= 64 && b <= 127) ||
327
+ (a === 169 && b === 254) ||
328
+ (a === 172 && b >= 16 && b <= 31) ||
329
+ (a === 192 && b === 0 && (c === 0 || c === 2)) ||
330
+ (a === 192 && b === 88 && c === 99) ||
331
+ (a === 192 && b === 168) ||
332
+ (a === 198 && (b === 18 || b === 19)) ||
333
+ (a === 198 && b === 51 && c === 100) ||
334
+ (a === 203 && b === 0 && c === 113) ||
335
+ (a === 168 && b === 63 && c === 129 && d === 16)
336
+ )
337
+ }
338
+
339
+ /** Whether the `n` bytes at `b[at]` are all zero. */
340
+ const outboundZeros = (b: u8[], at: i32, n: i32): boolean => {
341
+ for (let i: i32 = 0; i < n; i += 1) {
342
+ if (b[at + i] !== toU8(0)) {
343
+ return false
344
+ }
345
+ }
346
+ return true
347
+ }
348
+
349
+ /** Whether the first `bits` bits of `p[at]` and `addr[off]` agree. */
350
+ const outboundPrefixMatch = (p: u8[], at: i32, bits: i32, addr: u8[], off: i32): boolean => {
351
+ const whole: i32 = bits >> 3
352
+ for (let i: i32 = 0; i < whole; i += 1) {
353
+ if (p[at + i] !== addr[off + i]) {
354
+ return false
355
+ }
356
+ }
357
+ const rest: i32 = bits & 7
358
+ if (rest === 0) {
359
+ return true
360
+ }
361
+ const mask: i32 = (toI32(0xff) << (8 - rest)) & toI32(0xff)
362
+ return (toI32(p[at + whole]) & mask) === (toI32(addr[off + whole]) & mask)
363
+ }
364
+
365
+ /** `s[from..to)` as a decimal of one to three digits, or -1. */
366
+ const outboundDecimal = (s: string, from: i32, to: i32): i32 => {
367
+ if (to <= from || to - from > 3) {
368
+ return -1
369
+ }
370
+ let v: i32 = 0
371
+ for (let i: i32 = from; i < to; i += 1) {
372
+ const c: i32 = toI32(s.charCodeAt(i))
373
+ if (c < 48 || c > 57) {
374
+ return -1
375
+ }
376
+ v = v * 10 + c - 48
377
+ }
378
+ return v
379
+ }
380
+
381
+ /** Whether `name` is an address literal: it holds a colon, or its last label is all digits. */
382
+ const outboundIsLiteral = (name: string): boolean => {
383
+ if (name.indexOf(":") >= 0) {
384
+ return true
385
+ }
386
+ const n: i32 = toI32(name.length)
387
+ let i: i32 = n - 1
388
+ while (i >= 0 && name.charCodeAt(i) >= 48 && name.charCodeAt(i) <= 57) {
389
+ i -= 1
390
+ }
391
+ return i < n - 1 && (i < 0 || name.charCodeAt(i) === 46)
392
+ }
393
+
394
+ /** `s` with A to Z lower-cased. */
395
+ const outboundLower = (s: string): string => {
396
+ const n: i32 = toI32(s.length)
397
+ const out: u8[] = new Array<u8>(n)
398
+ for (let i: i32 = 0; i < toI32(out.length) && i < n; i += 1) {
399
+ const c: i32 = toI32(s.charCodeAt(i))
400
+ out[i] = c >= 65 && c <= 90 ? toU8(c + 32) : toU8(c)
401
+ }
402
+ return String.fromBytes(out, 0, n)
403
+ }
404
+
405
+ /** `s` without one trailing dot. */
406
+ const outboundTrimDot = (s: string): string => {
407
+ const n: i32 = toI32(s.length)
408
+ return n > 0 && s.charCodeAt(n - 1) === 46 ? s.substring(0, n - 1) : s
409
+ }