@sproutboat/runtime 0.2.0 → 0.4.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 +59 -0
- package/README.md +29 -2
- package/package.json +1 -1
- package/src/client-ip.test.js +66 -0
- package/src/native-fetch-prelude.js +648 -2
- package/src/transport-embedded.js +175 -4
- package/src/wrap.ts +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `crypto.subtle` subset (baronunread/sproutboat#133): `digest` (SHA-256 / SHA-384
|
|
8
|
+
/ SHA-512) and HMAC `importKey` / `sign` / `verify` over a raw key. Enough for
|
|
9
|
+
JWTs and hand-rolled sessions; no ECDSA, AES or key wrapping.
|
|
10
|
+
|
|
11
|
+
Backed by ~300 lines of reference SHA-2 as inline C rather than BearSSL —
|
|
12
|
+
BearSSL links only in `--standalone` builds and the prelude C is shared with the
|
|
13
|
+
broker transport, so a link dependency would break non-standalone builds. Pure C
|
|
14
|
+
is transport-independent and lets a handler drop a vendored pure-JS SHA-256.
|
|
15
|
+
Verified against NIST vectors on both transports; surface is exactly standard so
|
|
16
|
+
it deletes cleanly when Porffor ships Web Crypto (CanadaHonk/porffor#347).
|
|
17
|
+
- Rate Limiting binding (baronunread/sproutboat#69): `env.<NAME>.limit({ key }) ->
|
|
18
|
+
{ success }`.
|
|
19
|
+
|
|
20
|
+
- Config: `ratelimiters: [{ binding, limit, period }]` in `sproutboat.jsonc` —
|
|
21
|
+
at most `limit` calls per `period` seconds, per key.
|
|
22
|
+
- A fixed-window counter (`ratelimit` table) on **both** transports: the
|
|
23
|
+
embedded one for standalone binaries, and the broker (`ratelimit.check` op,
|
|
24
|
+
in the replay-dedup set) for deployed sprouts. `limit` / `period` travel in
|
|
25
|
+
the message so the op stays stateless.
|
|
26
|
+
- `ponytail`: fixed window, so a burst across the boundary can briefly reach
|
|
27
|
+
~2x; swap for a two-window weighted count if that matters.
|
|
28
|
+
- `crypto.scryptVerify(password, salt, expected, { N, r, p })` (baronunread/sproutboat#153):
|
|
29
|
+
a scrypt (RFC 7914) implementation as inline C — PBKDF2-HMAC-SHA256 (reusing
|
|
30
|
+
#133's HMAC) + Salsa20/8 + BlockMix + ROMix.
|
|
31
|
+
|
|
32
|
+
Deliberately verify-only (re-derive and constant-time compare, no exposed raw
|
|
33
|
+
`scrypt()`): a migration path for password hashes made by Node/Bun `scrypt`
|
|
34
|
+
(e.g. `N=32768 r=8 p=3`), not a KDF blessed for new credentials — those should
|
|
35
|
+
use HMAC/PBKDF2 via `crypto.subtle`. Verified against the RFC 7914 §12 vector.
|
|
36
|
+
`N*r` is capped so a bad config cannot ask for gigabytes of scratch.
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- Embedded transport: set `PRAGMA busy_timeout=5000` on every connection. Within
|
|
41
|
+
one binary all binding ops are serial so there is no self-contention, but when
|
|
42
|
+
several copies of a standalone binary share a data dir behind `SO_REUSEPORT`
|
|
43
|
+
(the #154 scaling recipe) a writer now waits for the WAL lock instead of
|
|
44
|
+
failing `SQLITE_BUSY`.
|
|
45
|
+
|
|
46
|
+
## 0.3.0
|
|
47
|
+
|
|
48
|
+
### Minor Changes
|
|
49
|
+
|
|
50
|
+
- Standalone runtime: client IP, D1 online backup, and a prepared-statement cache.
|
|
51
|
+
|
|
52
|
+
- `request.cf.clientIp` is populated from the connection's remote address
|
|
53
|
+
(`x-sb-remote-addr`, appended by the server and stripped from client input),
|
|
54
|
+
with `SB_TRUSTED_PROXIES` resolving `X-Forwarded-For` behind a reverse proxy.
|
|
55
|
+
IPv4-mapped IPv6 peers are folded to dotted form. (baronunread/sproutboat#163)
|
|
56
|
+
- `env.<D1>.backup(name?)` — a Sproutboat extension: an online, integrity-checked
|
|
57
|
+
single-file snapshot via `VACUUM INTO`, on both the embedded and broker
|
|
58
|
+
transports. New `d1.backup` op. (baronunread/sproutboat#164)
|
|
59
|
+
- The embedded transport caches prepared statements per database (FIFO, 32/db)
|
|
60
|
+
instead of recompiling the SQL on every binding op. (baronunread/sproutboat#155)
|
|
61
|
+
|
|
3
62
|
## 0.2.0
|
|
4
63
|
|
|
5
64
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
# `@sproutboat/runtime`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
The sprout runtime as a unit: the binding and trigger wrapper, handler
|
|
4
|
+
source validation, both transports (broker and embedded), and the
|
|
5
5
|
native-fetch prelude. These move as one because `wrap.ts` locates the
|
|
6
6
|
prelude and transports by file URL beside itself.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
bun add @sproutboat/runtime
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { preludePath, transportPath, wrapNativeFetchHandler } from "@sproutboat/runtime";
|
|
14
|
+
import { validateHttpSyncSource } from "@sproutboat/runtime";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
|
|
19
|
+
- `wrapNativeFetchHandler(...)` turns a user's `export default { fetch }`
|
|
20
|
+
into a native-fetch module with bindings, triggers, and env installed.
|
|
21
|
+
`Bindings`, `EMPTY_BINDINGS`, `readVarsFromEnv`, `readBindingsFromEnv`,
|
|
22
|
+
and `neutraliseExports` travel with it.
|
|
23
|
+
- `preludePath` / `transportPath(transport)` locate the prelude and the
|
|
24
|
+
`broker` / `embedded` transports on disk. The prelude is read as text
|
|
25
|
+
and prepended before compilation, never imported.
|
|
26
|
+
- `TRANSPORT_MARKER`, `BASELINE_COMPATIBILITY_DATE`, `Transport` type.
|
|
27
|
+
- `validateHttpSyncSource(source)` checks a bundled handler module
|
|
28
|
+
(`SourceValidation`), used by project checks on both sides.
|
|
29
|
+
|
|
30
|
+
The prelude shims the Web API surface handlers expect (URL, Response,
|
|
31
|
+
crypto random values, binding shims, trigger dispatch) and is identical
|
|
32
|
+
for both transports, which is what lets one conformance suite hold them
|
|
33
|
+
honest.
|
package/package.json
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// #163 — the client-IP resolution helpers live inline in native-fetch-prelude.js
|
|
2
|
+
// (the prelude is prepended as text and cannot import), so this test lifts that
|
|
3
|
+
// one pure-JS block out and exercises it directly. If the block moves or its
|
|
4
|
+
// boundary markers change, this fails loudly rather than silently testing nothing.
|
|
5
|
+
import { expect, test } from "bun:test";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const src = readFileSync(fileURLToPath(new URL("./native-fetch-prelude.js", import.meta.url)), "utf8");
|
|
10
|
+
const start = src.indexOf("function __sbParseHex(");
|
|
11
|
+
const end = src.indexOf("globalThis.__sbEntry");
|
|
12
|
+
if (start === -1 || end === -1 || end < start) throw new Error("client-ip helper block not found in prelude");
|
|
13
|
+
|
|
14
|
+
let env = {};
|
|
15
|
+
// oxlint-disable-next-line no-function-constructor -- lifting a pure block out of the text-only prelude for test
|
|
16
|
+
const load = new Function(
|
|
17
|
+
"__envMap",
|
|
18
|
+
`${src.slice(start, end)}
|
|
19
|
+
const __sbEnv = (k) => __envMap[k];
|
|
20
|
+
return { __sbNormalizeIp, __sbIpInCidr, __sbIpTrusted, __sbClientIp, __sbSplitList };`,
|
|
21
|
+
);
|
|
22
|
+
const H = load(new Proxy({}, { get: (_, k) => env[k] }));
|
|
23
|
+
|
|
24
|
+
const req = (headers) => ({ headers: { get: (k) => headers[k.toLowerCase()] ?? null } });
|
|
25
|
+
|
|
26
|
+
test("__sbNormalizeIp folds IPv4-mapped IPv6 to dotted quad", () => {
|
|
27
|
+
expect(H.__sbNormalizeIp("0000:0000:0000:0000:0000:ffff:7f00:0001")).toBe("127.0.0.1");
|
|
28
|
+
expect(H.__sbNormalizeIp("::ffff:198.51.100.9")).toBe("198.51.100.9");
|
|
29
|
+
expect(H.__sbNormalizeIp("203.0.113.7")).toBe("203.0.113.7");
|
|
30
|
+
expect(H.__sbNormalizeIp("2001:db8::1")).toBe("2001:db8::1"); // real IPv6 untouched
|
|
31
|
+
expect(H.__sbNormalizeIp("")).toBe("");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("__sbIpInCidr does IPv4 range math without sign errors above 2^31", () => {
|
|
35
|
+
expect(H.__sbIpInCidr("10.1.2.3", "10.0.0.0/8")).toBe(true);
|
|
36
|
+
expect(H.__sbIpInCidr("11.0.0.1", "10.0.0.0/8")).toBe(false);
|
|
37
|
+
expect(H.__sbIpInCidr("203.0.113.7", "203.0.113.7")).toBe(true); // bare IP
|
|
38
|
+
expect(H.__sbIpInCidr("192.168.1.1", "0.0.0.0/0")).toBe(true);
|
|
39
|
+
// 224.x is > 2^31 as a uint32 — the divide-not-mask path.
|
|
40
|
+
expect(H.__sbIpInCidr("224.0.0.5", "224.0.0.0/24")).toBe(true);
|
|
41
|
+
expect(H.__sbIpInCidr("224.0.1.5", "224.0.0.0/24")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("__sbClientIp: no trusted proxies means the peer wins, XFF ignored", () => {
|
|
45
|
+
env = {};
|
|
46
|
+
const r = req({ "x-sb-remote-addr": "::ffff:203.0.113.7", "x-forwarded-for": "1.2.3.4" });
|
|
47
|
+
expect(H.__sbClientIp(r)).toBe("203.0.113.7");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("__sbClientIp: a trusted peer resolves the rightmost untrusted XFF hop", () => {
|
|
51
|
+
env = { SB_TRUSTED_PROXIES: "127.0.0.0/8, 10.0.0.0/8" };
|
|
52
|
+
expect(H.__sbClientIp(req({ "x-sb-remote-addr": "127.0.0.1", "x-forwarded-for": "203.0.113.7, 10.1.2.3" }))).toBe(
|
|
53
|
+
"203.0.113.7",
|
|
54
|
+
);
|
|
55
|
+
// every hop trusted → fall back to the peer
|
|
56
|
+
expect(H.__sbClientIp(req({ "x-sb-remote-addr": "127.0.0.1", "x-forwarded-for": "10.9.9.9" }))).toBe("127.0.0.1");
|
|
57
|
+
// mapped-v6 entry in the chain is normalised
|
|
58
|
+
expect(
|
|
59
|
+
H.__sbClientIp(req({ "x-sb-remote-addr": "10.0.0.1", "x-forwarded-for": "::ffff:198.51.100.9, 10.1.2.3" })),
|
|
60
|
+
).toBe("198.51.100.9");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("__sbClientIp: an untrusted peer cannot use XFF even if it sends one", () => {
|
|
64
|
+
env = { SB_TRUSTED_PROXIES: "10.0.0.0/8" };
|
|
65
|
+
expect(H.__sbClientIp(req({ "x-sb-remote-addr": "198.51.100.9", "x-forwarded-for": "1.1.1.1" }))).toBe("198.51.100.9");
|
|
66
|
+
});
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
// json(). This adds, additively, the rest of the WHATWG surface Worker code
|
|
5
5
|
// expects: URLSearchParams (read + write), URL.prototype.searchParams and the
|
|
6
6
|
// protocol/host/hostname/port/hash accessors, static Response.json,
|
|
7
|
-
// crypto.randomUUID / crypto.getRandomValues,
|
|
8
|
-
// feature-detected; delete a
|
|
7
|
+
// crypto.randomUUID / crypto.getRandomValues, crypto.subtle (digest + HMAC
|
|
8
|
+
// sign/verify, #133), and structuredClone. Each is feature-detected; delete a
|
|
9
|
+
// block once Porffor ships that global. crypto.scryptVerify (#153) is a
|
|
10
|
+
// Sproutboat extension, not WHATWG, and stays.
|
|
9
11
|
// Tracked upstream in patches/UPSTREAM.md.
|
|
10
12
|
//
|
|
11
13
|
// Declared before it is referenced: a getter body that names a later top-level
|
|
@@ -276,6 +278,141 @@ if (globalThis.crypto.randomUUID == null) {
|
|
|
276
278
|
};
|
|
277
279
|
}
|
|
278
280
|
|
|
281
|
+
// #133 — a WebCrypto subset: crypto.subtle.digest (SHA-256/384/512) and HMAC
|
|
282
|
+
// sign/verify over a raw key. Enough for JWTs and hand-rolled sessions; not a
|
|
283
|
+
// complete SubtleCrypto (no ECDSA, no AES, no key wrapping). The surface is
|
|
284
|
+
// exactly standard so it can be deleted when Porffor ships Web Crypto
|
|
285
|
+
// (CanadaHonk/porffor#347). Backed by the inline-C SHA-2 above, not a link
|
|
286
|
+
// dependency, so it works on the broker transport too.
|
|
287
|
+
function __sbHashBits(algo) {
|
|
288
|
+
const n = String((algo && algo.name) || algo || "").toUpperCase();
|
|
289
|
+
if (n === "SHA-256" || n === "SHA256") return "256";
|
|
290
|
+
if (n === "SHA-384" || n === "SHA384") return "384";
|
|
291
|
+
if (n === "SHA-512" || n === "SHA512") return "512";
|
|
292
|
+
return "";
|
|
293
|
+
}
|
|
294
|
+
// -> a latin1 string, one char per byte. Strings are UTF-8 encoded (matching
|
|
295
|
+
// TextEncoder); ArrayBuffer / typed-array input is copied byte for byte.
|
|
296
|
+
function __sbToBytes(input) {
|
|
297
|
+
if (input == null) return "";
|
|
298
|
+
if (__sbIsStr(input)) {
|
|
299
|
+
let s = "";
|
|
300
|
+
for (let i = 0; i < input.length; i++) {
|
|
301
|
+
const c = input.charCodeAt(i);
|
|
302
|
+
if (c < 0x80) s += String.fromCharCode(c);
|
|
303
|
+
else if (c < 0x800) s += String.fromCharCode(0xc0 | (c >> 6)) + String.fromCharCode(0x80 | (c & 0x3f));
|
|
304
|
+
else if (c >= 0xd800 && c < 0xdc00 && i + 1 < input.length) {
|
|
305
|
+
const cp = 0x10000 + ((c - 0xd800) << 10) + (input.charCodeAt(++i) - 0xdc00);
|
|
306
|
+
s +=
|
|
307
|
+
String.fromCharCode(0xf0 | (cp >> 18)) +
|
|
308
|
+
String.fromCharCode(0x80 | ((cp >> 12) & 0x3f)) +
|
|
309
|
+
String.fromCharCode(0x80 | ((cp >> 6) & 0x3f)) +
|
|
310
|
+
String.fromCharCode(0x80 | (cp & 0x3f));
|
|
311
|
+
} else
|
|
312
|
+
s +=
|
|
313
|
+
String.fromCharCode(0xe0 | (c >> 12)) +
|
|
314
|
+
String.fromCharCode(0x80 | ((c >> 6) & 0x3f)) +
|
|
315
|
+
String.fromCharCode(0x80 | (c & 0x3f));
|
|
316
|
+
}
|
|
317
|
+
return s;
|
|
318
|
+
}
|
|
319
|
+
const view = input.length !== undefined && input.buffer !== undefined ? input : new Uint8Array(input);
|
|
320
|
+
let s = "";
|
|
321
|
+
for (let i = 0; i < view.length; i++) s += String.fromCharCode(view[i] & 0xff);
|
|
322
|
+
return s;
|
|
323
|
+
}
|
|
324
|
+
function __sbBufFrom(latin1) {
|
|
325
|
+
const b = new Uint8Array(latin1.length);
|
|
326
|
+
for (let i = 0; i < latin1.length; i++) b[i] = latin1.charCodeAt(i) & 0xff;
|
|
327
|
+
return b.buffer;
|
|
328
|
+
}
|
|
329
|
+
// Constant-time compare of two latin1 byte strings.
|
|
330
|
+
function __sbEqCt(a, b) {
|
|
331
|
+
if (a.length !== b.length) return false;
|
|
332
|
+
let diff = 0;
|
|
333
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
334
|
+
return diff === 0;
|
|
335
|
+
}
|
|
336
|
+
// An even-length all-hex string -> its bytes; anything else -> its raw bytes.
|
|
337
|
+
function __sbHexOrBytes(value) {
|
|
338
|
+
if (__sbIsStr(value) && value.length > 0 && value.length % 2 === 0) {
|
|
339
|
+
let out = "";
|
|
340
|
+
for (let i = 0; i < value.length; i++) {
|
|
341
|
+
const c = value.charCodeAt(i);
|
|
342
|
+
const d = c >= 48 && c <= 57 ? c - 48 : c >= 97 && c <= 102 ? c - 87 : c >= 65 && c <= 70 ? c - 55 : -1;
|
|
343
|
+
if (d < 0) return __sbToBytes(value);
|
|
344
|
+
if (i % 2 === 0) out += String.fromCharCode(d << 4);
|
|
345
|
+
else out = out.slice(0, -1) + String.fromCharCode(out.charCodeAt(out.length - 1) | d);
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
return __sbToBytes(value);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (globalThis.crypto.subtle == null) {
|
|
353
|
+
globalThis.crypto.subtle = {
|
|
354
|
+
async digest(algo, data) {
|
|
355
|
+
const bits = __sbHashBits(algo);
|
|
356
|
+
if (!bits) throw new Error("crypto.subtle.digest: unsupported algorithm");
|
|
357
|
+
const out = __sbDigestRaw(bits, __sbToBytes(data));
|
|
358
|
+
if (!out) throw new Error("crypto.subtle.digest failed");
|
|
359
|
+
return __sbBufFrom(out);
|
|
360
|
+
},
|
|
361
|
+
async importKey(format, keyData, algo, extractable, usages) {
|
|
362
|
+
if (format !== "raw") throw new Error("crypto.subtle.importKey: only format 'raw' is supported");
|
|
363
|
+
const name = String((algo && algo.name) || algo || "").toUpperCase();
|
|
364
|
+
if (name !== "HMAC") throw new Error("crypto.subtle.importKey: only HMAC keys are supported");
|
|
365
|
+
const bits = __sbHashBits((algo && algo.hash) || "SHA-256");
|
|
366
|
+
if (!bits) throw new Error("crypto.subtle.importKey: unsupported hash");
|
|
367
|
+
return {
|
|
368
|
+
type: "secret",
|
|
369
|
+
extractable: !!extractable,
|
|
370
|
+
algorithm: { name: "HMAC", hash: { name: "SHA-" + bits } },
|
|
371
|
+
usages: usages || [],
|
|
372
|
+
__sbKey: __sbToBytes(keyData),
|
|
373
|
+
__sbBits: bits,
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
async sign(algo, key, data) {
|
|
377
|
+
if (!key || key.__sbKey == null) throw new Error("crypto.subtle.sign: only HMAC keys from importKey are supported");
|
|
378
|
+
const out = __sbHmacRaw(key.__sbBits, key.__sbKey, __sbToBytes(data));
|
|
379
|
+
if (!out) throw new Error("crypto.subtle.sign failed");
|
|
380
|
+
return __sbBufFrom(out);
|
|
381
|
+
},
|
|
382
|
+
async verify(algo, key, signature, data) {
|
|
383
|
+
if (!key || key.__sbKey == null) throw new Error("crypto.subtle.verify: only HMAC keys are supported");
|
|
384
|
+
const mac = __sbHmacRaw(key.__sbBits, key.__sbKey, __sbToBytes(data));
|
|
385
|
+
if (!mac) return false;
|
|
386
|
+
return __sbEqCt(mac, __sbToBytes(signature));
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// #153 — scrypt verification, for migrating password hashes made elsewhere
|
|
392
|
+
// (Node/Bun `scrypt`, N/r/p). Not on WebCrypto; deliberately verify-only, so it
|
|
393
|
+
// is a migration tool rather than a KDF blessed for new credentials — new
|
|
394
|
+
// credentials should use HMAC/PBKDF2 via crypto.subtle. `expected` is the stored
|
|
395
|
+
// hash: a hex string, or bytes (ArrayBuffer / Uint8Array).
|
|
396
|
+
if (globalThis.crypto.scryptVerify == null) {
|
|
397
|
+
globalThis.crypto.scryptVerify = function (password, salt, expected, params) {
|
|
398
|
+
const p = params || {};
|
|
399
|
+
const N = p.N || p.cost || 16384;
|
|
400
|
+
const r = p.r || p.blockSize || 8;
|
|
401
|
+
const par = p.p || p.parallelization || 1;
|
|
402
|
+
const want = __sbHexOrBytes(expected);
|
|
403
|
+
if (want.length === 0) return false;
|
|
404
|
+
const dk = __sbScryptRaw(
|
|
405
|
+
__sbToBytes(password),
|
|
406
|
+
__sbToBytes(salt),
|
|
407
|
+
String(N),
|
|
408
|
+
String(r),
|
|
409
|
+
String(par),
|
|
410
|
+
String(want.length),
|
|
411
|
+
);
|
|
412
|
+
return dk.length === want.length && __sbEqCt(dk, want);
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
279
416
|
// ---------------------------------------------------------------------------
|
|
280
417
|
// Bindings: env.<KV>, env.<SECRET>, env.<D1>, env.<R2>, and globalThis.fetch,
|
|
281
418
|
// backed by a Bun broker on a loopback TCP port. The transport is inline C —
|
|
@@ -300,10 +437,291 @@ Porffor.c`
|
|
|
300
437
|
#include <fcntl.h>
|
|
301
438
|
#include <errno.h>
|
|
302
439
|
#include <time.h>
|
|
440
|
+
#include <stdint.h>
|
|
303
441
|
|
|
304
442
|
u32 porf_native_fetch_alloc_bytestring(const char* input, size_t len);
|
|
305
443
|
int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned);
|
|
306
444
|
|
|
445
|
+
// --- #133: SHA-2 + HMAC ---------------------------------------------------
|
|
446
|
+
// Standalone links BearSSL, but a deployed sprout does not, and this inline C
|
|
447
|
+
// is shared by both transports — so a plain reference SHA-2 rather than a link
|
|
448
|
+
// dependency. It also lets a handler drop a vendored pure-JS SHA-256. Exposed
|
|
449
|
+
// as crypto.subtle.digest / sign / verify (HMAC), which deletes cleanly when
|
|
450
|
+
// Porffor ships Web Crypto (CanadaHonk/porffor#347).
|
|
451
|
+
#define SB_ROR32(x, n) (((x) >> (n)) | ((x) << (32 - (n))))
|
|
452
|
+
#define SB_ROR64(x, n) (((x) >> (n)) | ((x) << (64 - (n))))
|
|
453
|
+
|
|
454
|
+
typedef struct { uint32_t h[8]; uint64_t len; unsigned char buf[64]; size_t n; } sb_sha256_ctx;
|
|
455
|
+
static const uint32_t sb_k256[64] = {
|
|
456
|
+
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
|
457
|
+
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
|
458
|
+
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
|
459
|
+
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
|
460
|
+
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
|
461
|
+
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
|
462
|
+
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
|
463
|
+
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 };
|
|
464
|
+
static void sb_sha256_block(sb_sha256_ctx* c, const unsigned char* p) {
|
|
465
|
+
uint32_t w[64], a,b,cc,d,e,f,g,h,t1,t2;
|
|
466
|
+
for (int i = 0; i < 16; i++)
|
|
467
|
+
w[i] = ((uint32_t)p[i*4]<<24)|((uint32_t)p[i*4+1]<<16)|((uint32_t)p[i*4+2]<<8)|((uint32_t)p[i*4+3]);
|
|
468
|
+
for (int i = 16; i < 64; i++) {
|
|
469
|
+
uint32_t s0 = SB_ROR32(w[i-15],7) ^ SB_ROR32(w[i-15],18) ^ (w[i-15]>>3);
|
|
470
|
+
uint32_t s1 = SB_ROR32(w[i-2],17) ^ SB_ROR32(w[i-2],19) ^ (w[i-2]>>10);
|
|
471
|
+
w[i] = w[i-16] + s0 + w[i-7] + s1;
|
|
472
|
+
}
|
|
473
|
+
a=c->h[0];b=c->h[1];cc=c->h[2];d=c->h[3];e=c->h[4];f=c->h[5];g=c->h[6];h=c->h[7];
|
|
474
|
+
for (int i = 0; i < 64; i++) {
|
|
475
|
+
uint32_t S1 = SB_ROR32(e,6) ^ SB_ROR32(e,11) ^ SB_ROR32(e,25);
|
|
476
|
+
uint32_t ch = (e & f) ^ (~e & g);
|
|
477
|
+
t1 = h + S1 + ch + sb_k256[i] + w[i];
|
|
478
|
+
uint32_t S0 = SB_ROR32(a,2) ^ SB_ROR32(a,13) ^ SB_ROR32(a,22);
|
|
479
|
+
uint32_t maj = (a & b) ^ (a & cc) ^ (b & cc);
|
|
480
|
+
t2 = S0 + maj;
|
|
481
|
+
h=g;g=f;f=e;e=d+t1;d=cc;cc=b;b=a;a=t1+t2;
|
|
482
|
+
}
|
|
483
|
+
c->h[0]+=a;c->h[1]+=b;c->h[2]+=cc;c->h[3]+=d;c->h[4]+=e;c->h[5]+=f;c->h[6]+=g;c->h[7]+=h;
|
|
484
|
+
}
|
|
485
|
+
static void sb_sha256_init(sb_sha256_ctx* c) {
|
|
486
|
+
c->h[0]=0x6a09e667;c->h[1]=0xbb67ae85;c->h[2]=0x3c6ef372;c->h[3]=0xa54ff53a;
|
|
487
|
+
c->h[4]=0x510e527f;c->h[5]=0x9b05688c;c->h[6]=0x1f83d9ab;c->h[7]=0x5be0cd19;c->len=0;c->n=0;
|
|
488
|
+
}
|
|
489
|
+
static void sb_sha256_update(sb_sha256_ctx* c, const unsigned char* p, size_t n) {
|
|
490
|
+
c->len += n;
|
|
491
|
+
while (n) {
|
|
492
|
+
size_t k = 64 - c->n; if (k > n) k = n;
|
|
493
|
+
memcpy(c->buf + c->n, p, k); c->n += k; p += k; n -= k;
|
|
494
|
+
if (c->n == 64) { sb_sha256_block(c, c->buf); c->n = 0; }
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
static void sb_sha256_final(sb_sha256_ctx* c, unsigned char out[32]) {
|
|
498
|
+
uint64_t bits = c->len * 8;
|
|
499
|
+
unsigned char pad = 0x80;
|
|
500
|
+
sb_sha256_update(c, &pad, 1);
|
|
501
|
+
unsigned char z = 0;
|
|
502
|
+
while (c->n != 56) sb_sha256_update(c, &z, 1);
|
|
503
|
+
unsigned char lb[8];
|
|
504
|
+
for (int i = 0; i < 8; i++) lb[i] = (unsigned char)(bits >> (56 - i*8));
|
|
505
|
+
sb_sha256_update(c, lb, 8);
|
|
506
|
+
for (int i = 0; i < 8; i++) {
|
|
507
|
+
out[i*4]=(unsigned char)(c->h[i]>>24);out[i*4+1]=(unsigned char)(c->h[i]>>16);
|
|
508
|
+
out[i*4+2]=(unsigned char)(c->h[i]>>8);out[i*4+3]=(unsigned char)c->h[i];
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
typedef struct { uint64_t h[8]; uint64_t lenhi, lenlo; unsigned char buf[128]; size_t n; } sb_sha512_ctx;
|
|
513
|
+
static const uint64_t sb_k512[80] = {
|
|
514
|
+
0x428a2f98d728ae22ULL,0x7137449123ef65cdULL,0xb5c0fbcfec4d3b2fULL,0xe9b5dba58189dbbcULL,
|
|
515
|
+
0x3956c25bf348b538ULL,0x59f111f1b605d019ULL,0x923f82a4af194f9bULL,0xab1c5ed5da6d8118ULL,
|
|
516
|
+
0xd807aa98a3030242ULL,0x12835b0145706fbeULL,0x243185be4ee4b28cULL,0x550c7dc3d5ffb4e2ULL,
|
|
517
|
+
0x72be5d74f27b896fULL,0x80deb1fe3b1696b1ULL,0x9bdc06a725c71235ULL,0xc19bf174cf692694ULL,
|
|
518
|
+
0xe49b69c19ef14ad2ULL,0xefbe4786384f25e3ULL,0x0fc19dc68b8cd5b5ULL,0x240ca1cc77ac9c65ULL,
|
|
519
|
+
0x2de92c6f592b0275ULL,0x4a7484aa6ea6e483ULL,0x5cb0a9dcbd41fbd4ULL,0x76f988da831153b5ULL,
|
|
520
|
+
0x983e5152ee66dfabULL,0xa831c66d2db43210ULL,0xb00327c898fb213fULL,0xbf597fc7beef0ee4ULL,
|
|
521
|
+
0xc6e00bf33da88fc2ULL,0xd5a79147930aa725ULL,0x06ca6351e003826fULL,0x142929670a0e6e70ULL,
|
|
522
|
+
0x27b70a8546d22ffcULL,0x2e1b21385c26c926ULL,0x4d2c6dfc5ac42aedULL,0x53380d139d95b3dfULL,
|
|
523
|
+
0x650a73548baf63deULL,0x766a0abb3c77b2a8ULL,0x81c2c92e47edaee6ULL,0x92722c851482353bULL,
|
|
524
|
+
0xa2bfe8a14cf10364ULL,0xa81a664bbc423001ULL,0xc24b8b70d0f89791ULL,0xc76c51a30654be30ULL,
|
|
525
|
+
0xd192e819d6ef5218ULL,0xd69906245565a910ULL,0xf40e35855771202aULL,0x106aa07032bbd1b8ULL,
|
|
526
|
+
0x19a4c116b8d2d0c8ULL,0x1e376c085141ab53ULL,0x2748774cdf8eeb99ULL,0x34b0bcb5e19b48a8ULL,
|
|
527
|
+
0x391c0cb3c5c95a63ULL,0x4ed8aa4ae3418acbULL,0x5b9cca4f7763e373ULL,0x682e6ff3d6b2b8a3ULL,
|
|
528
|
+
0x748f82ee5defb2fcULL,0x78a5636f43172f60ULL,0x84c87814a1f0ab72ULL,0x8cc702081a6439ecULL,
|
|
529
|
+
0x90befffa23631e28ULL,0xa4506cebde82bde9ULL,0xbef9a3f7b2c67915ULL,0xc67178f2e372532bULL,
|
|
530
|
+
0xca273eceea26619cULL,0xd186b8c721c0c207ULL,0xeada7dd6cde0eb1eULL,0xf57d4f7fee6ed178ULL,
|
|
531
|
+
0x06f067aa72176fbaULL,0x0a637dc5a2c898a6ULL,0x113f9804bef90daeULL,0x1b710b35131c471bULL,
|
|
532
|
+
0x28db77f523047d84ULL,0x32caab7b40c72493ULL,0x3c9ebe0a15c9bebcULL,0x431d67c49c100d4cULL,
|
|
533
|
+
0x4cc5d4becb3e42b6ULL,0x597f299cfc657e2aULL,0x5fcb6fab3ad6faecULL,0x6c44198c4a475817ULL };
|
|
534
|
+
static void sb_sha512_block(sb_sha512_ctx* c, const unsigned char* p) {
|
|
535
|
+
uint64_t w[80], a,b,cc,d,e,f,g,h,t1,t2;
|
|
536
|
+
for (int i = 0; i < 16; i++) {
|
|
537
|
+
w[i] = 0; for (int j = 0; j < 8; j++) w[i] = (w[i]<<8) | p[i*8+j];
|
|
538
|
+
}
|
|
539
|
+
for (int i = 16; i < 80; i++) {
|
|
540
|
+
uint64_t s0 = SB_ROR64(w[i-15],1) ^ SB_ROR64(w[i-15],8) ^ (w[i-15]>>7);
|
|
541
|
+
uint64_t s1 = SB_ROR64(w[i-2],19) ^ SB_ROR64(w[i-2],61) ^ (w[i-2]>>6);
|
|
542
|
+
w[i] = w[i-16] + s0 + w[i-7] + s1;
|
|
543
|
+
}
|
|
544
|
+
a=c->h[0];b=c->h[1];cc=c->h[2];d=c->h[3];e=c->h[4];f=c->h[5];g=c->h[6];h=c->h[7];
|
|
545
|
+
for (int i = 0; i < 80; i++) {
|
|
546
|
+
uint64_t S1 = SB_ROR64(e,14) ^ SB_ROR64(e,18) ^ SB_ROR64(e,41);
|
|
547
|
+
uint64_t ch = (e & f) ^ (~e & g);
|
|
548
|
+
t1 = h + S1 + ch + sb_k512[i] + w[i];
|
|
549
|
+
uint64_t S0 = SB_ROR64(a,28) ^ SB_ROR64(a,34) ^ SB_ROR64(a,39);
|
|
550
|
+
uint64_t maj = (a & b) ^ (a & cc) ^ (b & cc);
|
|
551
|
+
t2 = S0 + maj;
|
|
552
|
+
h=g;g=f;f=e;e=d+t1;d=cc;cc=b;b=a;a=t1+t2;
|
|
553
|
+
}
|
|
554
|
+
c->h[0]+=a;c->h[1]+=b;c->h[2]+=cc;c->h[3]+=d;c->h[4]+=e;c->h[5]+=f;c->h[6]+=g;c->h[7]+=h;
|
|
555
|
+
}
|
|
556
|
+
static void sb_sha512_init(sb_sha512_ctx* c, int is384) {
|
|
557
|
+
if (is384) {
|
|
558
|
+
c->h[0]=0xcbbb9d5dc1059ed8ULL;c->h[1]=0x629a292a367cd507ULL;c->h[2]=0x9159015a3070dd17ULL;c->h[3]=0x152fecd8f70e5939ULL;
|
|
559
|
+
c->h[4]=0x67332667ffc00b31ULL;c->h[5]=0x8eb44a8768581511ULL;c->h[6]=0xdb0c2e0d64f98fa7ULL;c->h[7]=0x47b5481dbefa4fa4ULL;
|
|
560
|
+
} else {
|
|
561
|
+
c->h[0]=0x6a09e667f3bcc908ULL;c->h[1]=0xbb67ae8584caa73bULL;c->h[2]=0x3c6ef372fe94f82bULL;c->h[3]=0xa54ff53a5f1d36f1ULL;
|
|
562
|
+
c->h[4]=0x510e527fade682d1ULL;c->h[5]=0x9b05688c2b3e6c1fULL;c->h[6]=0x1f83d9abfb41bd6bULL;c->h[7]=0x5be0cd19137e2179ULL;
|
|
563
|
+
}
|
|
564
|
+
c->lenhi=0;c->lenlo=0;c->n=0;
|
|
565
|
+
}
|
|
566
|
+
static void sb_sha512_update(sb_sha512_ctx* c, const unsigned char* p, size_t n) {
|
|
567
|
+
uint64_t add = n; if ((c->lenlo += add) < add) c->lenhi++;
|
|
568
|
+
while (n) {
|
|
569
|
+
size_t k = 128 - c->n; if (k > n) k = n;
|
|
570
|
+
memcpy(c->buf + c->n, p, k); c->n += k; p += k; n -= k;
|
|
571
|
+
if (c->n == 128) { sb_sha512_block(c, c->buf); c->n = 0; }
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
static void sb_sha512_final(sb_sha512_ctx* c, unsigned char* out, int is384) {
|
|
575
|
+
uint64_t bhi = (c->lenhi << 3) | (c->lenlo >> 61), blo = c->lenlo << 3;
|
|
576
|
+
unsigned char pad = 0x80;
|
|
577
|
+
sb_sha512_update(c, &pad, 1);
|
|
578
|
+
unsigned char z = 0;
|
|
579
|
+
while (c->n != 112) sb_sha512_update(c, &z, 1);
|
|
580
|
+
unsigned char lb[16];
|
|
581
|
+
for (int i = 0; i < 8; i++) lb[i] = (unsigned char)(bhi >> (56 - i*8));
|
|
582
|
+
for (int i = 0; i < 8; i++) lb[8+i] = (unsigned char)(blo >> (56 - i*8));
|
|
583
|
+
sb_sha512_update(c, lb, 16);
|
|
584
|
+
int words = is384 ? 6 : 8;
|
|
585
|
+
for (int i = 0; i < words; i++)
|
|
586
|
+
for (int j = 0; j < 8; j++) out[i*8+j] = (unsigned char)(c->h[i] >> (56 - j*8));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// One-shot digest. algo: 256 | 384 | 512. out must hold 32/48/64 bytes.
|
|
590
|
+
// Returns the digest length, or 0 for an unknown algo.
|
|
591
|
+
static size_t sb_digest(int algo, const unsigned char* msg, size_t mlen, unsigned char* out) {
|
|
592
|
+
if (algo == 256) { sb_sha256_ctx c; sb_sha256_init(&c); sb_sha256_update(&c, msg, mlen); sb_sha256_final(&c, out); return 32; }
|
|
593
|
+
if (algo == 384) { sb_sha512_ctx c; sb_sha512_init(&c, 1); sb_sha512_update(&c, msg, mlen); sb_sha512_final(&c, out, 1); return 48; }
|
|
594
|
+
if (algo == 512) { sb_sha512_ctx c; sb_sha512_init(&c, 0); sb_sha512_update(&c, msg, mlen); sb_sha512_final(&c, out, 0); return 64; }
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// HMAC(algo) per RFC 2104. Block size is 64 for SHA-256, 128 for SHA-384/512.
|
|
599
|
+
static size_t sb_hmac(int algo, const unsigned char* key, size_t klen,
|
|
600
|
+
const unsigned char* msg, size_t mlen, unsigned char* out) {
|
|
601
|
+
size_t bs = algo == 256 ? 64 : 128;
|
|
602
|
+
unsigned char k[128], ipad[128], opad[128], inner[64];
|
|
603
|
+
memset(k, 0, sizeof(k));
|
|
604
|
+
if (klen > bs) { sb_digest(algo, key, klen, k); }
|
|
605
|
+
else memcpy(k, key, klen);
|
|
606
|
+
for (size_t i = 0; i < bs; i++) { ipad[i] = k[i] ^ 0x36; opad[i] = k[i] ^ 0x5c; }
|
|
607
|
+
size_t dl;
|
|
608
|
+
if (algo == 256) {
|
|
609
|
+
sb_sha256_ctx c; sb_sha256_init(&c);
|
|
610
|
+
sb_sha256_update(&c, ipad, bs); sb_sha256_update(&c, msg, mlen); sb_sha256_final(&c, inner);
|
|
611
|
+
sb_sha256_ctx o; sb_sha256_init(&o);
|
|
612
|
+
sb_sha256_update(&o, opad, bs); sb_sha256_update(&o, inner, 32); sb_sha256_final(&o, out);
|
|
613
|
+
dl = 32;
|
|
614
|
+
} else {
|
|
615
|
+
int is384 = algo == 384; dl = is384 ? 48 : 64;
|
|
616
|
+
sb_sha512_ctx c; sb_sha512_init(&c, is384);
|
|
617
|
+
sb_sha512_update(&c, ipad, bs); sb_sha512_update(&c, msg, mlen); sb_sha512_final(&c, inner, is384);
|
|
618
|
+
sb_sha512_ctx o; sb_sha512_init(&o, is384);
|
|
619
|
+
sb_sha512_update(&o, opad, bs); sb_sha512_update(&o, inner, dl); sb_sha512_final(&o, out, is384);
|
|
620
|
+
}
|
|
621
|
+
return dl;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// --- #153: scrypt (RFC 7914), for verifying pre-existing password hashes ----
|
|
625
|
+
// PBKDF2-HMAC-SHA256 (reuses sb_hmac) + Salsa20/8 + BlockMix + ROMix. Exposed
|
|
626
|
+
// only as crypto.scryptVerify: a migration path for hashes made elsewhere
|
|
627
|
+
// (Node/Bun scrypt), not a blessed KDF for new credentials.
|
|
628
|
+
static void sb_pbkdf2_hmac256(const unsigned char* pw, size_t pwlen,
|
|
629
|
+
const unsigned char* salt, size_t saltlen,
|
|
630
|
+
uint32_t iters, unsigned char* out, size_t dklen) {
|
|
631
|
+
unsigned char block[64], u[32], t[32];
|
|
632
|
+
uint32_t i = 1;
|
|
633
|
+
size_t done = 0;
|
|
634
|
+
while (done < dklen) {
|
|
635
|
+
unsigned char* s2 = (unsigned char*)malloc(saltlen + 4);
|
|
636
|
+
memcpy(s2, salt, saltlen);
|
|
637
|
+
s2[saltlen] = (unsigned char)(i >> 24); s2[saltlen+1] = (unsigned char)(i >> 16);
|
|
638
|
+
s2[saltlen+2] = (unsigned char)(i >> 8); s2[saltlen+3] = (unsigned char)i;
|
|
639
|
+
sb_hmac(256, pw, pwlen, s2, saltlen + 4, u);
|
|
640
|
+
free(s2);
|
|
641
|
+
memcpy(t, u, 32);
|
|
642
|
+
// iters is 1 for scrypt's use of PBKDF2, but support the general case.
|
|
643
|
+
for (uint32_t j = 1; j < iters; j++) {
|
|
644
|
+
sb_hmac(256, pw, pwlen, u, 32, u);
|
|
645
|
+
for (int k = 0; k < 32; k++) t[k] ^= u[k];
|
|
646
|
+
}
|
|
647
|
+
memcpy(block, t, 32);
|
|
648
|
+
size_t take = dklen - done; if (take > 32) take = 32;
|
|
649
|
+
memcpy(out + done, block, take);
|
|
650
|
+
done += take; i++;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
static void sb_salsa20_8(uint32_t out[16], const uint32_t in[16]) {
|
|
654
|
+
uint32_t x[16];
|
|
655
|
+
for (int i = 0; i < 16; i++) x[i] = in[i];
|
|
656
|
+
for (int i = 0; i < 4; i++) {
|
|
657
|
+
x[ 4] ^= SB_ROR32(x[ 0] + x[12], 32 - 7); x[ 8] ^= SB_ROR32(x[ 4] + x[ 0], 32 - 9);
|
|
658
|
+
x[12] ^= SB_ROR32(x[ 8] + x[ 4], 32 - 13); x[ 0] ^= SB_ROR32(x[12] + x[ 8], 32 - 18);
|
|
659
|
+
x[ 9] ^= SB_ROR32(x[ 5] + x[ 1], 32 - 7); x[13] ^= SB_ROR32(x[ 9] + x[ 5], 32 - 9);
|
|
660
|
+
x[ 1] ^= SB_ROR32(x[13] + x[ 9], 32 - 13); x[ 5] ^= SB_ROR32(x[ 1] + x[13], 32 - 18);
|
|
661
|
+
x[14] ^= SB_ROR32(x[10] + x[ 6], 32 - 7); x[ 2] ^= SB_ROR32(x[14] + x[10], 32 - 9);
|
|
662
|
+
x[ 6] ^= SB_ROR32(x[ 2] + x[14], 32 - 13); x[10] ^= SB_ROR32(x[ 6] + x[ 2], 32 - 18);
|
|
663
|
+
x[ 3] ^= SB_ROR32(x[15] + x[11], 32 - 7); x[ 7] ^= SB_ROR32(x[ 3] + x[15], 32 - 9);
|
|
664
|
+
x[11] ^= SB_ROR32(x[ 7] + x[ 3], 32 - 13); x[15] ^= SB_ROR32(x[11] + x[ 7], 32 - 18);
|
|
665
|
+
x[ 1] ^= SB_ROR32(x[ 0] + x[ 3], 32 - 7); x[ 2] ^= SB_ROR32(x[ 1] + x[ 0], 32 - 9);
|
|
666
|
+
x[ 3] ^= SB_ROR32(x[ 2] + x[ 1], 32 - 13); x[ 0] ^= SB_ROR32(x[ 3] + x[ 2], 32 - 18);
|
|
667
|
+
x[ 6] ^= SB_ROR32(x[ 5] + x[ 4], 32 - 7); x[ 7] ^= SB_ROR32(x[ 6] + x[ 5], 32 - 9);
|
|
668
|
+
x[ 4] ^= SB_ROR32(x[ 7] + x[ 6], 32 - 13); x[ 5] ^= SB_ROR32(x[ 4] + x[ 7], 32 - 18);
|
|
669
|
+
x[11] ^= SB_ROR32(x[10] + x[ 9], 32 - 7); x[ 8] ^= SB_ROR32(x[11] + x[10], 32 - 9);
|
|
670
|
+
x[ 9] ^= SB_ROR32(x[ 8] + x[11], 32 - 13); x[10] ^= SB_ROR32(x[ 9] + x[ 8], 32 - 18);
|
|
671
|
+
x[12] ^= SB_ROR32(x[15] + x[14], 32 - 7); x[13] ^= SB_ROR32(x[12] + x[15], 32 - 9);
|
|
672
|
+
x[14] ^= SB_ROR32(x[13] + x[12], 32 - 13); x[15] ^= SB_ROR32(x[14] + x[13], 32 - 18);
|
|
673
|
+
}
|
|
674
|
+
for (int i = 0; i < 16; i++) out[i] = x[i] + in[i];
|
|
675
|
+
}
|
|
676
|
+
// BlockMix: B is 2r 64-byte blocks (as uint32 words). Result into out.
|
|
677
|
+
static void sb_blockmix(const uint32_t* b, uint32_t* out, size_t r) {
|
|
678
|
+
uint32_t x[16], y[16];
|
|
679
|
+
memcpy(x, b + (2*r - 1) * 16, 64);
|
|
680
|
+
for (size_t i = 0; i < 2*r; i++) {
|
|
681
|
+
for (int k = 0; k < 16; k++) x[k] ^= b[i*16 + k];
|
|
682
|
+
sb_salsa20_8(y, x);
|
|
683
|
+
memcpy(x, y, 64);
|
|
684
|
+
size_t dst = (i % 2 == 0) ? (i/2) : (r + (i-1)/2);
|
|
685
|
+
memcpy(out + dst*16, x, 64);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
static uint64_t sb_integerify(const uint32_t* b, size_t r) {
|
|
689
|
+
const uint32_t* p = b + (2*r - 1) * 16;
|
|
690
|
+
return ((uint64_t)p[0]) | (((uint64_t)p[1]) << 32);
|
|
691
|
+
}
|
|
692
|
+
// scrypt(pw, salt, N, r, p) -> dk[dklen]. Returns 0 on success, -1 on bad
|
|
693
|
+
// params or allocation failure. Caps N*r so a bad config cannot ask for GB.
|
|
694
|
+
static int sb_scrypt(const unsigned char* pw, size_t pwlen, const unsigned char* salt, size_t saltlen,
|
|
695
|
+
uint64_t N, uint32_t r, uint32_t p, unsigned char* dk, size_t dklen) {
|
|
696
|
+
if (r == 0 || p == 0 || N < 2 || (N & (N - 1)) != 0) return -1;
|
|
697
|
+
if ((uint64_t)r * N > (1ULL << 20)) return -1; // <= 128 MiB of V
|
|
698
|
+
size_t blk = 128 * (size_t)r; // one B block, bytes
|
|
699
|
+
unsigned char* b = (unsigned char*)malloc(blk * p);
|
|
700
|
+
uint32_t* v = (uint32_t*)malloc(blk * (size_t)N);
|
|
701
|
+
uint32_t* xy = (uint32_t*)malloc(blk * 2);
|
|
702
|
+
if (!b || !v || !xy) { free(b); free(v); free(xy); return -1; }
|
|
703
|
+
sb_pbkdf2_hmac256(pw, pwlen, salt, saltlen, 1, b, blk * p);
|
|
704
|
+
for (uint32_t i = 0; i < p; i++) {
|
|
705
|
+
uint32_t* x = (uint32_t*)(b + (size_t)i * blk);
|
|
706
|
+
uint32_t* y = xy;
|
|
707
|
+
for (uint64_t j = 0; j < N; j++) {
|
|
708
|
+
memcpy(v + j * (blk / 4), x, blk);
|
|
709
|
+
sb_blockmix(x, y, r);
|
|
710
|
+
memcpy(x, y, blk);
|
|
711
|
+
}
|
|
712
|
+
for (uint64_t j = 0; j < N; j++) {
|
|
713
|
+
uint64_t k = sb_integerify(x, r) & (N - 1);
|
|
714
|
+
const uint32_t* vk = v + k * (blk / 4);
|
|
715
|
+
for (size_t w = 0; w < blk / 4; w++) x[w] ^= vk[w];
|
|
716
|
+
sb_blockmix(x, y, r);
|
|
717
|
+
memcpy(x, y, blk);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
sb_pbkdf2_hmac256(pw, pwlen, b, blk * p, 1, dk, dklen);
|
|
721
|
+
free(b); free(v); free(xy);
|
|
722
|
+
return 0;
|
|
723
|
+
}
|
|
724
|
+
|
|
307
725
|
// Fill buf with n bytes from the OS CSPRNG. /dev/urandom is present on Linux and
|
|
308
726
|
// macOS and inside the bubblewrap sandbox; blocking is not a concern after the
|
|
309
727
|
// pool is seeded. Returns 0, or -1 if the source could not be read in full.
|
|
@@ -354,6 +772,101 @@ function __sbRandomBytes(nStr) {
|
|
|
354
772
|
return out;
|
|
355
773
|
}
|
|
356
774
|
|
|
775
|
+
// #133 — one-shot SHA-2 digest. `algoStr` is "256"|"384"|"512"; `data` is a
|
|
776
|
+
// latin1 string (one char per byte). Returns the raw digest as a bytestring.
|
|
777
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
|
|
778
|
+
function __sbDigestRaw(algoStr, data) {
|
|
779
|
+
let out = "";
|
|
780
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
781
|
+
Porffor.c`
|
|
782
|
+
const char* __as; size_t __asl; char* __aso = 0;
|
|
783
|
+
porf_native_fetch_read_value(algoStr, &__as, &__asl, &__aso);
|
|
784
|
+
char __ab[8]; size_t __ak = __asl < 7 ? __asl : 7;
|
|
785
|
+
memcpy(__ab, __as, __ak); __ab[__ak] = 0;
|
|
786
|
+
if (__aso) free(__aso);
|
|
787
|
+
int __algo = atoi(__ab);
|
|
788
|
+
|
|
789
|
+
const char* __d; size_t __dl; char* __do = 0;
|
|
790
|
+
porf_native_fetch_read_value(data, &__d, &__dl, &__do);
|
|
791
|
+
unsigned char __out[64];
|
|
792
|
+
size_t __n = sb_digest(__algo, (const unsigned char*)__d, __dl, __out);
|
|
793
|
+
if (__do) free(__do);
|
|
794
|
+
if (__n) out = porf_box((f64)porf_native_fetch_alloc_bytestring((const char*)__out, __n), 195);
|
|
795
|
+
`;
|
|
796
|
+
return out;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// #133 — HMAC-SHA-2. `algoStr` "256"|"384"|"512"; `key` and `data` latin1.
|
|
800
|
+
// Returns the raw MAC as a bytestring.
|
|
801
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
|
|
802
|
+
function __sbHmacRaw(algoStr, key, data) {
|
|
803
|
+
let out = "";
|
|
804
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
805
|
+
Porffor.c`
|
|
806
|
+
const char* __as; size_t __asl; char* __aso = 0;
|
|
807
|
+
porf_native_fetch_read_value(algoStr, &__as, &__asl, &__aso);
|
|
808
|
+
char __ab[8]; size_t __ak = __asl < 7 ? __asl : 7;
|
|
809
|
+
memcpy(__ab, __as, __ak); __ab[__ak] = 0;
|
|
810
|
+
if (__aso) free(__aso);
|
|
811
|
+
int __algo = atoi(__ab);
|
|
812
|
+
|
|
813
|
+
const char* __k; size_t __kl; char* __ko = 0;
|
|
814
|
+
porf_native_fetch_read_value(key, &__k, &__kl, &__ko);
|
|
815
|
+
unsigned char* __kc = (unsigned char*)malloc(__kl ? __kl : 1);
|
|
816
|
+
if (__kc) memcpy(__kc, __k, __kl);
|
|
817
|
+
if (__ko) free(__ko);
|
|
818
|
+
|
|
819
|
+
const char* __d; size_t __dl; char* __do = 0;
|
|
820
|
+
porf_native_fetch_read_value(data, &__d, &__dl, &__do);
|
|
821
|
+
unsigned char __out[64];
|
|
822
|
+
size_t __n = __kc ? sb_hmac(__algo, __kc, __kl, (const unsigned char*)__d, __dl, __out) : 0;
|
|
823
|
+
if (__do) free(__do);
|
|
824
|
+
if (__kc) free(__kc);
|
|
825
|
+
if (__n) out = porf_box((f64)porf_native_fetch_alloc_bytestring((const char*)__out, __n), 195);
|
|
826
|
+
`;
|
|
827
|
+
return out;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// #153 — scrypt derivation. All params are decimal strings. Returns the derived
|
|
831
|
+
// key as a bytestring, or '' on bad params / allocation failure.
|
|
832
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
|
|
833
|
+
function __sbScryptRaw(pw, salt, nStr, rStr, pStr, dkLenStr) {
|
|
834
|
+
let out = "";
|
|
835
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
836
|
+
Porffor.c`
|
|
837
|
+
const char* __p; size_t __pl; char* __po = 0;
|
|
838
|
+
porf_native_fetch_read_value(pw, &__p, &__pl, &__po);
|
|
839
|
+
unsigned char* __pc = (unsigned char*)malloc(__pl ? __pl : 1);
|
|
840
|
+
if (__pc) memcpy(__pc, __p, __pl);
|
|
841
|
+
if (__po) free(__po);
|
|
842
|
+
|
|
843
|
+
const char* __s; size_t __sl; char* __so = 0;
|
|
844
|
+
porf_native_fetch_read_value(salt, &__s, &__sl, &__so);
|
|
845
|
+
unsigned char* __sc = (unsigned char*)malloc(__sl ? __sl : 1);
|
|
846
|
+
if (__sc) memcpy(__sc, __s, __sl);
|
|
847
|
+
if (__so) free(__so);
|
|
848
|
+
|
|
849
|
+
long __N = 0, __r = 0, __pp = 0, __dk = 0;
|
|
850
|
+
{ const char* q; size_t ql; char* qo = 0; char nb[24];
|
|
851
|
+
porf_native_fetch_read_value(nStr, &q, &ql, &qo); { size_t k = ql < 23 ? ql : 23; memcpy(nb, q, k); nb[k] = 0; } if (qo) free(qo); __N = atol(nb);
|
|
852
|
+
porf_native_fetch_read_value(rStr, &q, &ql, &qo); { size_t k = ql < 23 ? ql : 23; memcpy(nb, q, k); nb[k] = 0; } if (qo) free(qo); __r = atol(nb);
|
|
853
|
+
porf_native_fetch_read_value(pStr, &q, &ql, &qo); { size_t k = ql < 23 ? ql : 23; memcpy(nb, q, k); nb[k] = 0; } if (qo) free(qo); __pp = atol(nb);
|
|
854
|
+
porf_native_fetch_read_value(dkLenStr, &q, &ql, &qo); { size_t k = ql < 23 ? ql : 23; memcpy(nb, q, k); nb[k] = 0; } if (qo) free(qo); __dk = atol(nb);
|
|
855
|
+
}
|
|
856
|
+
if (__pc && __sc && __dk > 0 && __dk <= 1024) {
|
|
857
|
+
unsigned char* __d = (unsigned char*)malloc((size_t)__dk);
|
|
858
|
+
if (__d) {
|
|
859
|
+
if (sb_scrypt(__pc, __pl, __sc, __sl, (uint64_t)__N, (uint32_t)__r, (uint32_t)__pp, __d, (size_t)__dk) == 0)
|
|
860
|
+
out = porf_box((f64)porf_native_fetch_alloc_bytestring((const char*)__d, (size_t)__dk), 195);
|
|
861
|
+
free(__d);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (__pc) free(__pc);
|
|
865
|
+
if (__sc) free(__sc);
|
|
866
|
+
`;
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
|
|
357
870
|
// #63 — every request carries the protocol version it was built against and an
|
|
358
871
|
// id unique to this process. The id is what makes a resend safe: the transport
|
|
359
872
|
// retries the *same bytes*, so a broker that already applied the request can
|
|
@@ -421,6 +934,14 @@ function __sbMakeD1(dbName) {
|
|
|
421
934
|
__sbRpc("d1.exec", { db: dbName, sql: String(sql) });
|
|
422
935
|
return { count: (String(sql).match(/;/g) || []).length, duration: 0 };
|
|
423
936
|
},
|
|
937
|
+
// Sproutboat extension (not in CF Workers D1): an online, integrity-checked
|
|
938
|
+
// snapshot of this database. Standalone only for now — the broker transport
|
|
939
|
+
// returns an error until hosted backup (#139) covers it. `name` lands under
|
|
940
|
+
// `<data-dir>/backups/`; omit it for a timestamped default. See #164.
|
|
941
|
+
backup(name) {
|
|
942
|
+
const r = __sbRpc("d1.backup", { db: dbName, name: name == null ? "" : String(name) });
|
|
943
|
+
return { path: r.path, bytes: r.bytes };
|
|
944
|
+
},
|
|
424
945
|
};
|
|
425
946
|
}
|
|
426
947
|
|
|
@@ -585,6 +1106,21 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
585
1106
|
target[b.binding] = __sbMakeDONamespace(b.binding, b.className);
|
|
586
1107
|
}
|
|
587
1108
|
|
|
1109
|
+
// #69 — env.<NAME>.limit({ key }) -> { success }. Fixed window: at most
|
|
1110
|
+
// `limit` calls per `period` seconds for a given key. limit/period travel in
|
|
1111
|
+
// the message so the op stays stateless (the transport has no config).
|
|
1112
|
+
// Returns sync like the other shims (CF's is a Promise; the runtime is sync).
|
|
1113
|
+
for (let i = 0; i < (bindings.ratelimiters || []).length; i++) {
|
|
1114
|
+
const rl = bindings.ratelimiters[i];
|
|
1115
|
+
target[rl.binding] = {
|
|
1116
|
+
limit(options) {
|
|
1117
|
+
const key = options && options.key != null ? String(options.key) : "";
|
|
1118
|
+
const r = __sbRpc("ratelimit.check", { name: rl.binding, key, limit: rl.limit, period: rl.period });
|
|
1119
|
+
return { success: !!r.success };
|
|
1120
|
+
},
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
588
1124
|
// Static assets: env.<ASSETS>.fetch(request) -> broker `assets.get`. The edge
|
|
589
1125
|
// already serves matching files directly; the sprout only calls this for paths
|
|
590
1126
|
// it wants to own (SPA fallback, auth-gated files). The transport keeps the
|
|
@@ -844,9 +1380,119 @@ function __sbTriggerAuthed(request) {
|
|
|
844
1380
|
return request.headers.get("x-sb-token") === want;
|
|
845
1381
|
}
|
|
846
1382
|
|
|
1383
|
+
// #163 — the connection's remote address, for `request.cf.clientIp`.
|
|
1384
|
+
//
|
|
1385
|
+
// `x-sb-remote-addr` is the TCP peer, appended by the server (see
|
|
1386
|
+
// patches/UPSTREAM.md #163) and stripped from anything a client sends, so it
|
|
1387
|
+
// cannot be forged. With no proxy in front, that is the client. Behind one, set
|
|
1388
|
+
// `SB_TRUSTED_PROXIES` to a comma-separated list of trusted CIDRs (or bare IPs):
|
|
1389
|
+
// when the peer is trusted, the client is the rightmost `x-forwarded-for` entry
|
|
1390
|
+
// that is not itself a trusted hop.
|
|
1391
|
+
//
|
|
1392
|
+
// ponytail: IPv4 CIDR ranges + exact-string match (which covers a bare IPv6).
|
|
1393
|
+
// An IPv6 *prefix* in SB_TRUSTED_PROXIES matches nothing — widen __sbIpInCidr if
|
|
1394
|
+
// a deployment ever fronts its sprout with an IPv6 proxy range.
|
|
1395
|
+
function __sbParseHex(s) {
|
|
1396
|
+
let n = 0;
|
|
1397
|
+
for (let i = 0; i < s.length; i++) {
|
|
1398
|
+
const c = s.charCodeAt(i);
|
|
1399
|
+
let d = -1;
|
|
1400
|
+
if (c >= 48 && c <= 57) d = c - 48;
|
|
1401
|
+
else if (c >= 97 && c <= 102) d = c - 87;
|
|
1402
|
+
else if (c >= 65 && c <= 70) d = c - 55;
|
|
1403
|
+
if (d < 0) return -1;
|
|
1404
|
+
n = n * 16 + d;
|
|
1405
|
+
}
|
|
1406
|
+
return n;
|
|
1407
|
+
}
|
|
1408
|
+
// uWS hands back IPv4-mapped IPv6 for IPv4 clients on a dual-stack socket
|
|
1409
|
+
// (`::ffff:1.2.3.4`, or fully expanded `0:0:0:0:0:ffff:0102:0304`). Fold those
|
|
1410
|
+
// to the dotted IPv4 so CIDR matching and `clientIp` see a plain address.
|
|
1411
|
+
function __sbNormalizeIp(ip) {
|
|
1412
|
+
const s = String(ip || "");
|
|
1413
|
+
if (s.indexOf(":") === -1) return s;
|
|
1414
|
+
const low = s.toLowerCase();
|
|
1415
|
+
const dotted = low.indexOf("::ffff:");
|
|
1416
|
+
if (dotted === 0 && low.indexOf(".") !== -1) return low.slice(7);
|
|
1417
|
+
const g = low.split(":");
|
|
1418
|
+
if (g.length === 8) {
|
|
1419
|
+
let mapped = __sbParseHex(g[5]) === 0xffff;
|
|
1420
|
+
for (let i = 0; i < 5; i++) if (__sbParseHex(g[i] || "0") !== 0) mapped = false;
|
|
1421
|
+
if (mapped) {
|
|
1422
|
+
const hi = __sbParseHex(g[6] || "0");
|
|
1423
|
+
const lo = __sbParseHex(g[7] || "0");
|
|
1424
|
+
if (hi >= 0 && lo >= 0) {
|
|
1425
|
+
return ((hi / 256) | 0) + "." + (hi & 0xff) + "." + ((lo / 256) | 0) + "." + (lo & 0xff);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return s;
|
|
1430
|
+
}
|
|
1431
|
+
function __sbTrim(s) {
|
|
1432
|
+
let a = 0;
|
|
1433
|
+
let b = s.length;
|
|
1434
|
+
while (a < b && (s.charCodeAt(a) === 32 || s.charCodeAt(a) === 9)) a++;
|
|
1435
|
+
while (b > a && (s.charCodeAt(b - 1) === 32 || s.charCodeAt(b - 1) === 9)) b--;
|
|
1436
|
+
return s.slice(a, b);
|
|
1437
|
+
}
|
|
1438
|
+
function __sbSplitList(raw) {
|
|
1439
|
+
const out = [];
|
|
1440
|
+
const parts = String(raw || "").split(",");
|
|
1441
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1442
|
+
const v = __sbTrim(parts[i]);
|
|
1443
|
+
if (v) out.push(v);
|
|
1444
|
+
}
|
|
1445
|
+
return out;
|
|
1446
|
+
}
|
|
1447
|
+
function __sbIpToLong(ip) {
|
|
1448
|
+
const p = String(ip).split(".");
|
|
1449
|
+
if (p.length !== 4) return -1;
|
|
1450
|
+
let n = 0;
|
|
1451
|
+
for (let i = 0; i < 4; i++) {
|
|
1452
|
+
if (p[i] === "" || p[i].length > 3) return -1;
|
|
1453
|
+
const o = Number(p[i]);
|
|
1454
|
+
if (!(o >= 0 && o <= 255) || o !== (o | 0)) return -1;
|
|
1455
|
+
n = n * 256 + o;
|
|
1456
|
+
}
|
|
1457
|
+
return n;
|
|
1458
|
+
}
|
|
1459
|
+
function __sbIpInCidr(ip, cidr) {
|
|
1460
|
+
const slash = cidr.indexOf("/");
|
|
1461
|
+
if (slash === -1) return ip === cidr; // bare IP: exact match, covers IPv6
|
|
1462
|
+
const addr = __sbIpToLong(ip);
|
|
1463
|
+
const base = __sbIpToLong(cidr.slice(0, slash));
|
|
1464
|
+
const bits = Number(cidr.slice(slash + 1));
|
|
1465
|
+
if (addr < 0 || base < 0 || !(bits >= 0 && bits <= 32)) return false;
|
|
1466
|
+
if (bits === 0) return true;
|
|
1467
|
+
if (bits === 32) return addr === base;
|
|
1468
|
+
// Divide rather than mask: a 32-bit `&` in JS is signed and would miscompare
|
|
1469
|
+
// addresses above 2^31.
|
|
1470
|
+
const size = Math.pow(2, 32 - bits);
|
|
1471
|
+
return Math.floor(addr / size) === Math.floor(base / size);
|
|
1472
|
+
}
|
|
1473
|
+
function __sbIpTrusted(ip, list) {
|
|
1474
|
+
for (let i = 0; i < list.length; i++) if (__sbIpInCidr(ip, list[i])) return true;
|
|
1475
|
+
return false;
|
|
1476
|
+
}
|
|
1477
|
+
function __sbClientIp(request) {
|
|
1478
|
+
const peer = __sbNormalizeIp(request.headers.get("x-sb-remote-addr") || "");
|
|
1479
|
+
const trusted = __sbSplitList(__sbEnv("SB_TRUSTED_PROXIES"));
|
|
1480
|
+
if (!trusted.length || !__sbIpTrusted(peer, trusted)) return peer;
|
|
1481
|
+
const xff = __sbSplitList(request.headers.get("x-forwarded-for"));
|
|
1482
|
+
for (let i = xff.length - 1; i >= 0; i--) {
|
|
1483
|
+
const hop = __sbNormalizeIp(xff[i]);
|
|
1484
|
+
if (!__sbIpTrusted(hop, trusted)) return hop;
|
|
1485
|
+
}
|
|
1486
|
+
return peer;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
847
1489
|
globalThis.__sbEntry = function (handlers, request) {
|
|
848
1490
|
const trigger = request.headers.get("x-sb-trigger");
|
|
849
1491
|
if (!trigger) {
|
|
1492
|
+
// #163 — expose the resolved client IP the Workers way, before the handler runs.
|
|
1493
|
+
const __cf = request.cf || {};
|
|
1494
|
+
__cf.clientIp = __sbClientIp(request);
|
|
1495
|
+
request.cf = __cf;
|
|
850
1496
|
// #28 — per-invocation CPU time. One fetch turn per process (serial), so the
|
|
851
1497
|
// process CPU delta across the handler is this invocation's CPU.
|
|
852
1498
|
// ponytail: serial-turn assumption; revisit if the profile ever allows
|
|
@@ -35,6 +35,7 @@ extern int sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const
|
|
|
35
35
|
extern int sqlite3_step(sqlite3_stmt*);
|
|
36
36
|
extern int sqlite3_finalize(sqlite3_stmt*);
|
|
37
37
|
extern int sqlite3_reset(sqlite3_stmt*);
|
|
38
|
+
extern int sqlite3_clear_bindings(sqlite3_stmt*);
|
|
38
39
|
extern int sqlite3_column_count(sqlite3_stmt*);
|
|
39
40
|
extern int sqlite3_column_type(sqlite3_stmt*, int);
|
|
40
41
|
extern const unsigned char* sqlite3_column_text(sqlite3_stmt*, int);
|
|
@@ -50,6 +51,7 @@ extern const void* sqlite3_column_blob(sqlite3_stmt*, int);
|
|
|
50
51
|
extern int sqlite3_changes(sqlite3*);
|
|
51
52
|
extern int64_t sqlite3_last_insert_rowid(sqlite3*);
|
|
52
53
|
extern const char* sqlite3_errmsg(sqlite3*);
|
|
54
|
+
extern int sqlite3_close(sqlite3*);
|
|
53
55
|
|
|
54
56
|
#define SB_SQLITE_ROW 100
|
|
55
57
|
#define SB_SQLITE_DONE 101
|
|
@@ -94,8 +96,13 @@ static int sb_db_for(const char* path) {
|
|
|
94
96
|
// copy it saves once the file holds a few large objects. The page cache is
|
|
95
97
|
// capped instead, and journal_size_limit stops the WAL staying huge after one
|
|
96
98
|
// big write.
|
|
99
|
+
// busy_timeout: within one binary every binding op is serial, so there is no
|
|
100
|
+
// self-contention. It matters when the same data dir is served by several
|
|
101
|
+
// copies of the binary behind SO_REUSEPORT (#154 scaling recipe): WAL takes
|
|
102
|
+
// concurrent readers plus one writer across processes, and this makes a
|
|
103
|
+
// writer wait for the lock instead of failing SQLITE_BUSY.
|
|
97
104
|
sqlite3_exec(db,
|
|
98
|
-
"PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"
|
|
105
|
+
"PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;"
|
|
99
106
|
"PRAGMA cache_size=-2000; PRAGMA journal_size_limit=16777216;",
|
|
100
107
|
0, 0, 0);
|
|
101
108
|
sb_dbs[sb_db_count] = db;
|
|
@@ -103,6 +110,42 @@ static int sb_db_for(const char* path) {
|
|
|
103
110
|
return sb_db_count++;
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
// #155 — a prepared-statement cache per database. Every binding op (KV, D1, DO
|
|
114
|
+
// storage, queue, AE) funnels through sb_sql_run, which used to prepare and
|
|
115
|
+
// finalize on every call — measured at ~0.6 ms/op, that compile step was most
|
|
116
|
+
// of an op's cost. Keyed by exact SQL text; reset and re-bound on reuse.
|
|
117
|
+
//
|
|
118
|
+
// ponytail: FIFO eviction over 32 slots, linear scan, no lock (the runtime
|
|
119
|
+
// serves one turn at a time). Move to LRU + a hash if a real app keeps >32 hot
|
|
120
|
+
// statements per db, or if execution ever overlaps. Entries are never finalized
|
|
121
|
+
// on shutdown — sb_db_for never closes a handle, the process owns it for life.
|
|
122
|
+
#define SB_STMT_CACHE 32
|
|
123
|
+
typedef struct { char* sql; sqlite3_stmt* st; } sb_cached_stmt;
|
|
124
|
+
static sb_cached_stmt sb_stmt_cache[SB_MAX_DB][SB_STMT_CACHE];
|
|
125
|
+
static int sb_stmt_fifo[SB_MAX_DB];
|
|
126
|
+
|
|
127
|
+
static sqlite3_stmt* sb_stmt_get(int idx, sqlite3* db, const char* sql) {
|
|
128
|
+
sb_cached_stmt* slots = sb_stmt_cache[idx];
|
|
129
|
+
for (int i = 0; i < SB_STMT_CACHE; i++) {
|
|
130
|
+
if (slots[i].sql && strcmp(slots[i].sql, sql) == 0) {
|
|
131
|
+
sqlite3_reset(slots[i].st);
|
|
132
|
+
sqlite3_clear_bindings(slots[i].st);
|
|
133
|
+
return slots[i].st;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
sqlite3_stmt* st = 0;
|
|
137
|
+
if (sqlite3_prepare_v2(db, sql, -1, &st, 0) != 0 || !st) return 0;
|
|
138
|
+
int slot = sb_stmt_fifo[idx];
|
|
139
|
+
sb_stmt_fifo[idx] = (slot + 1) % SB_STMT_CACHE;
|
|
140
|
+
if (slots[slot].st) sqlite3_finalize(slots[slot].st);
|
|
141
|
+
free(slots[slot].sql);
|
|
142
|
+
size_t n = strlen(sql);
|
|
143
|
+
slots[slot].sql = (char*)malloc(n + 1);
|
|
144
|
+
if (slots[slot].sql) memcpy(slots[slot].sql, sql, n + 1);
|
|
145
|
+
slots[slot].st = st;
|
|
146
|
+
return st;
|
|
147
|
+
}
|
|
148
|
+
|
|
106
149
|
// --- a growable output buffer, for building the reply JSON ------------------
|
|
107
150
|
typedef struct { char* p; size_t len; size_t cap; } sb_buf;
|
|
108
151
|
static void sb_buf_need(sb_buf* b, size_t extra) {
|
|
@@ -319,8 +362,8 @@ static char* sb_sql_run(const char* path, const char* sql, const char* params) {
|
|
|
319
362
|
int idx = sb_db_for(path);
|
|
320
363
|
if (idx < 0) { sb_puts(&b, "{\"ok\":false,\"error\":\"cannot open database\"}"); return b.p; }
|
|
321
364
|
sqlite3* db = sb_dbs[idx];
|
|
322
|
-
sqlite3_stmt* st =
|
|
323
|
-
if (
|
|
365
|
+
sqlite3_stmt* st = sb_stmt_get(idx, db, sql);
|
|
366
|
+
if (!st) {
|
|
324
367
|
sb_puts(&b, "{\"ok\":false,\"error\":");
|
|
325
368
|
const char* m = sqlite3_errmsg(db);
|
|
326
369
|
sb_putjson(&b, m, strlen(m));
|
|
@@ -361,7 +404,9 @@ static char* sb_sql_run(const char* path, const char* sql, const char* params) {
|
|
|
361
404
|
char tail[96];
|
|
362
405
|
int k = snprintf(tail, 96, "],\"changes\":%d,\"rowid\":%lld}", sqlite3_changes(db), (long long)sqlite3_last_insert_rowid(db));
|
|
363
406
|
sb_put(&b, tail, (size_t)k);
|
|
364
|
-
|
|
407
|
+
// Reset, don't finalize: the statement stays compiled in sb_stmt_cache. Reset
|
|
408
|
+
// now (not just on next reuse) so a SELECT stops holding its read transaction.
|
|
409
|
+
sqlite3_reset(st);
|
|
365
410
|
if (rc != SB_SQLITE_DONE && rc != SB_SQLITE_ROW) {
|
|
366
411
|
free(b.p);
|
|
367
412
|
sb_buf e = { 0, 0, 0 };
|
|
@@ -374,6 +419,67 @@ static char* sb_sql_run(const char* path, const char* sql, const char* params) {
|
|
|
374
419
|
return b.p;
|
|
375
420
|
}
|
|
376
421
|
|
|
422
|
+
// #164 — an online, consistent single-file snapshot of a database, via
|
|
423
|
+
// VACUUM INTO. Runs against the live db with no downtime (SQLite holds a read
|
|
424
|
+
// transaction for the copy) and writes a fresh, defragmented file with no WAL
|
|
425
|
+
// sidecars — exactly what you copy off the box. The copy is then reopened and
|
|
426
|
+
// PRAGMA integrity_check'd, so a bad backup fails here, not on restore.
|
|
427
|
+
// Returns {"ok":true,"bytes":n} or {"ok":false,"error":"..."}.
|
|
428
|
+
static char* sb_sql_backup(const char* src_path, const char* dest_path) {
|
|
429
|
+
sb_buf b = { 0, 0, 0 };
|
|
430
|
+
int idx = sb_db_for(src_path);
|
|
431
|
+
if (idx < 0) { sb_puts(&b, "{\"ok\":false,\"error\":\"cannot open database\"}"); return b.p; }
|
|
432
|
+
sqlite3* db = sb_dbs[idx];
|
|
433
|
+
|
|
434
|
+
sb_mkdirs(dest_path);
|
|
435
|
+
unlink(dest_path); // VACUUM INTO refuses to overwrite an existing file
|
|
436
|
+
|
|
437
|
+
sqlite3_stmt* st = 0;
|
|
438
|
+
if (sqlite3_prepare_v2(db, "VACUUM INTO ?1", -1, &st, 0) != 0 || !st) {
|
|
439
|
+
sb_puts(&b, "{\"ok\":false,\"error\":");
|
|
440
|
+
const char* m = sqlite3_errmsg(db);
|
|
441
|
+
sb_putjson(&b, m, strlen(m));
|
|
442
|
+
sb_puts(&b, "}");
|
|
443
|
+
return b.p;
|
|
444
|
+
}
|
|
445
|
+
sqlite3_bind_text(st, 1, dest_path, -1, SB_SQLITE_TRANSIENT);
|
|
446
|
+
int rc = sqlite3_step(st);
|
|
447
|
+
sqlite3_finalize(st);
|
|
448
|
+
if (rc != SB_SQLITE_DONE) {
|
|
449
|
+
sb_puts(&b, "{\"ok\":false,\"error\":");
|
|
450
|
+
const char* m = sqlite3_errmsg(db);
|
|
451
|
+
sb_putjson(&b, m, strlen(m));
|
|
452
|
+
sb_puts(&b, "}");
|
|
453
|
+
return b.p;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
sqlite3* chk = 0;
|
|
457
|
+
int ok = 0;
|
|
458
|
+
if (sqlite3_open(dest_path, &chk) == 0 && chk) {
|
|
459
|
+
sqlite3_stmt* cs = 0;
|
|
460
|
+
if (sqlite3_prepare_v2(chk, "PRAGMA integrity_check", -1, &cs, 0) == 0 && cs) {
|
|
461
|
+
if (sqlite3_step(cs) == SB_SQLITE_ROW) {
|
|
462
|
+
const unsigned char* r = sqlite3_column_text(cs, 0);
|
|
463
|
+
ok = r && strcmp((const char*)r, "ok") == 0;
|
|
464
|
+
}
|
|
465
|
+
sqlite3_finalize(cs);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (chk) sqlite3_close(chk);
|
|
469
|
+
if (!ok) {
|
|
470
|
+
unlink(dest_path);
|
|
471
|
+
sb_puts(&b, "{\"ok\":false,\"error\":\"integrity check failed on the backup\"}");
|
|
472
|
+
return b.p;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
struct stat sbuf;
|
|
476
|
+
long long bytes = stat(dest_path, &sbuf) == 0 ? (long long)sbuf.st_size : -1;
|
|
477
|
+
char head[64];
|
|
478
|
+
int k = snprintf(head, sizeof(head), "{\"ok\":true,\"bytes\":%lld}", bytes);
|
|
479
|
+
sb_put(&b, head, (size_t)k);
|
|
480
|
+
return b.p;
|
|
481
|
+
}
|
|
482
|
+
|
|
377
483
|
// --- outbound HTTP + HTTPS ---------------------------------------------------
|
|
378
484
|
// Plain sockets for http, BearSSL for https, with the Mozilla root set compiled
|
|
379
485
|
// in (see src/bearssl.ts). Both directions share the request builder and the
|
|
@@ -894,6 +1000,33 @@ function __sbSqlScriptRaw(path, sql) {
|
|
|
894
1000
|
return res;
|
|
895
1001
|
}
|
|
896
1002
|
|
|
1003
|
+
// #164 — VACUUM INTO a fresh file. Two strings in, JSON out. Same pattern as
|
|
1004
|
+
// __sbSqlScriptRaw.
|
|
1005
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
1006
|
+
function __sbD1BackupRaw(src, dest) {
|
|
1007
|
+
let res = "";
|
|
1008
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
1009
|
+
Porffor.c`
|
|
1010
|
+
const char* __s; size_t __sl; char* __so = 0;
|
|
1011
|
+
porf_native_fetch_read_value(src, &__s, &__sl, &__so);
|
|
1012
|
+
char* __src = (char*)malloc(__sl + 1); memcpy(__src, __s, __sl); __src[__sl] = 0;
|
|
1013
|
+
if (__so) free(__so);
|
|
1014
|
+
|
|
1015
|
+
const char* __d; size_t __dl; char* __do = 0;
|
|
1016
|
+
porf_native_fetch_read_value(dest, &__d, &__dl, &__do);
|
|
1017
|
+
char* __dest = (char*)malloc(__dl + 1); memcpy(__dest, __d, __dl); __dest[__dl] = 0;
|
|
1018
|
+
if (__do) free(__do);
|
|
1019
|
+
|
|
1020
|
+
char* __out = sb_sql_backup(__src, __dest);
|
|
1021
|
+
free(__src); free(__dest);
|
|
1022
|
+
if (__out) {
|
|
1023
|
+
res = porf_box((f64)porf_native_fetch_alloc_bytestring(__out, strlen(__out)), 195);
|
|
1024
|
+
free(__out);
|
|
1025
|
+
}
|
|
1026
|
+
`;
|
|
1027
|
+
return res;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
897
1030
|
// Outbound HTTP. String params so C can read each directly; the JS side has
|
|
898
1031
|
// already split the URL and enforced the allowlist. `tlsFlag` is "1" or "0" —
|
|
899
1032
|
// a string like the rest, so the marshalling stays uniform.
|
|
@@ -1051,6 +1184,10 @@ function __sbEnsureSchema() {
|
|
|
1051
1184
|
s,
|
|
1052
1185
|
"CREATE TABLE IF NOT EXISTS ae (dataset TEXT NOT NULL, ts INTEGER NOT NULL, indexes_json TEXT NOT NULL, blobs_json TEXT NOT NULL, doubles_json TEXT NOT NULL)",
|
|
1053
1186
|
);
|
|
1187
|
+
__sbSql(
|
|
1188
|
+
s,
|
|
1189
|
+
"CREATE TABLE IF NOT EXISTS ratelimit (name TEXT NOT NULL, key TEXT NOT NULL, window_start INTEGER NOT NULL, count INTEGER NOT NULL, PRIMARY KEY (name, key))",
|
|
1190
|
+
);
|
|
1054
1191
|
}
|
|
1055
1192
|
|
|
1056
1193
|
function __sbHex(n) {
|
|
@@ -1199,6 +1336,20 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1199
1336
|
return { ok: true, results: one.results, meta: one.meta, success: true };
|
|
1200
1337
|
}
|
|
1201
1338
|
|
|
1339
|
+
if (op === "d1.backup") {
|
|
1340
|
+
const src = __sbD1Path(String(msg.db));
|
|
1341
|
+
// A caller-supplied name must be a plain basename; anything else falls back
|
|
1342
|
+
// to the timestamped default, so nothing can climb out of backups/.
|
|
1343
|
+
const given = String(msg.name || "");
|
|
1344
|
+
const name = /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(given)
|
|
1345
|
+
? given
|
|
1346
|
+
: String(msg.db) + "-" + new Date().toISOString().replace(/[:.]/g, "-") + ".sqlite";
|
|
1347
|
+
const dest = __sbDir() + "/backups/" + name;
|
|
1348
|
+
const reply = JSON.parse(__sbD1BackupRaw(src, dest));
|
|
1349
|
+
if (reply.ok === false) throw new Error("d1 backup: " + reply.error);
|
|
1350
|
+
return { ok: true, path: dest, bytes: reply.bytes };
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1202
1353
|
if (op === "r2.put") {
|
|
1203
1354
|
const body = String(msg.body == null ? "" : msg.body);
|
|
1204
1355
|
const etag = __sbHex(16);
|
|
@@ -1371,6 +1522,26 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1371
1522
|
return { ok: true, count: total.rows.length ? Number(total.rows[0][0]) : 0, rows };
|
|
1372
1523
|
}
|
|
1373
1524
|
|
|
1525
|
+
if (op === "ratelimit.check") {
|
|
1526
|
+
// #69 — fixed window. limit/period come in the message (the transport holds
|
|
1527
|
+
// no config). One upsert: same window -> count+1, new window -> reset to 1.
|
|
1528
|
+
// ponytail: fixed window, so a burst straddling the boundary can briefly
|
|
1529
|
+
// reach ~2x limit. Swap for a two-window weighted count if that matters.
|
|
1530
|
+
const limit = Math.max(Number(msg.limit) || 1, 1);
|
|
1531
|
+
const period = Math.max(Number(msg.period) || 60, 1);
|
|
1532
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1533
|
+
const windowStart = now - (now % period);
|
|
1534
|
+
const r = __sbSql(
|
|
1535
|
+
store,
|
|
1536
|
+
"INSERT INTO ratelimit (name, key, window_start, count) VALUES (?1, ?2, ?3, 1) " +
|
|
1537
|
+
"ON CONFLICT (name, key) DO UPDATE SET count = CASE WHEN window_start = ?3 THEN count + 1 ELSE 1 END, " +
|
|
1538
|
+
"window_start = ?3 RETURNING count",
|
|
1539
|
+
[String(msg.name), String(msg.key == null ? "" : msg.key), windowStart],
|
|
1540
|
+
);
|
|
1541
|
+
const count = r.rows.length ? Number(r.rows[0][0]) : 1;
|
|
1542
|
+
return { ok: true, success: count <= limit };
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1374
1545
|
throw new Error("unknown op: " + op);
|
|
1375
1546
|
}
|
|
1376
1547
|
|
package/src/wrap.ts
CHANGED
|
@@ -65,6 +65,9 @@ export type Bindings = {
|
|
|
65
65
|
* it resolves to is a runtime input, not part of the artifact. */
|
|
66
66
|
services: Array<{ binding: string; service: string }>;
|
|
67
67
|
crons: string[];
|
|
68
|
+
/** #69 — rate limiters: `env.<binding>.limit({ key }) -> { success }`, a
|
|
69
|
+
* fixed window of `limit` calls per `period` seconds, per key. */
|
|
70
|
+
ratelimiters: Array<{ binding: string; limit: number; period: number }>;
|
|
68
71
|
/** Static-asset binding name for `env.<NAME>.fetch(request)`; `""` when assets are edge-only. */
|
|
69
72
|
assets: string;
|
|
70
73
|
};
|
|
@@ -80,6 +83,7 @@ export const EMPTY_BINDINGS: Bindings = {
|
|
|
80
83
|
do: [],
|
|
81
84
|
services: [],
|
|
82
85
|
crons: [],
|
|
86
|
+
ratelimiters: [],
|
|
83
87
|
assets: "",
|
|
84
88
|
};
|
|
85
89
|
|
|
@@ -94,6 +98,7 @@ function hasBindings(b: Bindings): boolean {
|
|
|
94
98
|
b.analytics.length > 0 ||
|
|
95
99
|
b.do.length > 0 ||
|
|
96
100
|
b.services.length > 0 ||
|
|
101
|
+
b.ratelimiters.length > 0 ||
|
|
97
102
|
b.assets !== ""
|
|
98
103
|
);
|
|
99
104
|
}
|
|
@@ -251,6 +256,15 @@ export function readBindingsFromEnv(): Bindings {
|
|
|
251
256
|
}
|
|
252
257
|
}
|
|
253
258
|
}
|
|
259
|
+
const isCount = (v: VarsJson): v is number => Number(v) === v && Number.isInteger(v) && v >= 1;
|
|
260
|
+
const ratelimiters: Array<{ binding: string; limit: number; period: number }> = [];
|
|
261
|
+
if (Array.isArray(parsed.ratelimiters)) {
|
|
262
|
+
for (const entry of parsed.ratelimiters) {
|
|
263
|
+
if (isVarsObject(entry) && isVarsString(entry.binding) && isCount(entry.limit) && isCount(entry.period)) {
|
|
264
|
+
ratelimiters.push({ binding: entry.binding, limit: entry.limit, period: entry.period });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
254
268
|
return {
|
|
255
269
|
kv: strings(parsed.kv),
|
|
256
270
|
secrets: strings(parsed.secrets),
|
|
@@ -262,6 +276,7 @@ export function readBindingsFromEnv(): Bindings {
|
|
|
262
276
|
do: dos,
|
|
263
277
|
services,
|
|
264
278
|
crons: strings(parsed.crons),
|
|
279
|
+
ratelimiters,
|
|
265
280
|
assets: isVarsString(parsed.assets) ? parsed.assets : "",
|
|
266
281
|
};
|
|
267
282
|
}
|