@sproutboat/runtime 0.10.0 → 0.11.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @sproutboat/runtime
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d0e2c05: `request.cf` gains `httpProtocol`, `tlsVersion`, and `tlsCipher` (baronunread/sproutboat#128). Like `clientIp` (#163), these are only populated when the direct peer is listed in `SB_TRUSTED_PROXIES` and it forwarded the corresponding `x-sb-http-protocol` / `x-sb-tls-version` / `x-sb-tls-cipher` header — otherwise they stay absent rather than a guessed or faked value. `colo`, ASN, and geo fields remain out of scope (no GeoIP database on the box).
8
+
3
9
  ## 0.10.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -17,7 +17,7 @@ const load = new Function(
17
17
  "__envMap",
18
18
  `${src.slice(start, end)}
19
19
  const __sbEnv = (k) => __envMap[k];
20
- return { __sbNormalizeIp, __sbIpInCidr, __sbIpTrusted, __sbClientIp, __sbSplitList };`,
20
+ return { __sbNormalizeIp, __sbIpInCidr, __sbIpTrusted, __sbClientIp, __sbSplitList, __sbTrustedProxyHeader };`,
21
21
  );
22
22
  const H = load(new Proxy({}, { get: (_, k) => env[k] }));
23
23
 
@@ -64,3 +64,22 @@ test("__sbClientIp: an untrusted peer cannot use XFF even if it sends one", () =
64
64
  env = { SB_TRUSTED_PROXIES: "10.0.0.0/8" };
65
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
66
  });
67
+
68
+ test("__sbTrustedProxyHeader: no trusted proxies configured means the header is ignored", () => {
69
+ env = {};
70
+ const r = req({ "x-sb-remote-addr": "127.0.0.1", "x-sb-http-protocol": "HTTP/2" });
71
+ expect(H.__sbTrustedProxyHeader(r, "x-sb-http-protocol")).toBeUndefined();
72
+ });
73
+
74
+ test("__sbTrustedProxyHeader: an untrusted direct peer cannot fake it", () => {
75
+ env = { SB_TRUSTED_PROXIES: "10.0.0.0/8" };
76
+ const r = req({ "x-sb-remote-addr": "198.51.100.9", "x-sb-tls-version": "TLSv1.3" });
77
+ expect(H.__sbTrustedProxyHeader(r, "x-sb-tls-version")).toBeUndefined();
78
+ });
79
+
80
+ test("__sbTrustedProxyHeader: a trusted peer's value passes through, absent stays undefined", () => {
81
+ env = { SB_TRUSTED_PROXIES: "127.0.0.0/8" };
82
+ const r = req({ "x-sb-remote-addr": "127.0.0.1", "x-sb-http-protocol": "HTTP/2" });
83
+ expect(H.__sbTrustedProxyHeader(r, "x-sb-http-protocol")).toBe("HTTP/2");
84
+ expect(H.__sbTrustedProxyHeader(r, "x-sb-tls-cipher")).toBeUndefined();
85
+ });
@@ -1739,6 +1739,17 @@ function __sbClientIp(request) {
1739
1739
  }
1740
1740
  return peer;
1741
1741
  }
1742
+ // #128 — httpProtocol/tlsVersion/tlsCipher: only a trusted front proxy can set
1743
+ // these (an untrusted direct peer could otherwise fake any value), so this
1744
+ // reuses the same SB_TRUSTED_PROXIES chain #163 already resolves clientIp
1745
+ // against. Undefined when no trusted proxy is configured or it sent nothing —
1746
+ // omitted, not faked, matching how clientIp itself falls back to the raw peer.
1747
+ function __sbTrustedProxyHeader(request, name) {
1748
+ const peer = __sbNormalizeIp(request.headers.get("x-sb-remote-addr") || "");
1749
+ const trusted = __sbSplitList(__sbEnv("SB_TRUSTED_PROXIES"));
1750
+ if (!trusted.length || !__sbIpTrusted(peer, trusted)) return undefined;
1751
+ return request.headers.get(name) || undefined;
1752
+ }
1742
1753
 
1743
1754
  // #57 — ctx.waitUntil: work that must run before the turn completes but must
1744
1755
  // not block the response the handler already built. `waitUntil(p)` just
@@ -1824,9 +1835,18 @@ async function __sbQueueWithDrain(result, tasks) {
1824
1835
  globalThis.__sbEntry = function (handlers, request) {
1825
1836
  const trigger = request.headers.get("x-sb-trigger");
1826
1837
  if (!trigger) {
1827
- // #163 — expose the resolved client IP the Workers way, before the handler runs.
1838
+ // #163/#128 — expose the request metadata the edge already has, the
1839
+ // Workers way, before the handler runs. httpProtocol/tlsVersion/tlsCipher
1840
+ // are only set when a trusted front proxy forwarded them; colo, ASN and
1841
+ // geo fields stay absent everywhere (#128 explicitly rejects faking them).
1828
1842
  const __cf = request.cf || {};
1829
1843
  __cf.clientIp = __sbClientIp(request);
1844
+ const __httpProtocol = __sbTrustedProxyHeader(request, "x-sb-http-protocol");
1845
+ if (__httpProtocol) __cf.httpProtocol = __httpProtocol;
1846
+ const __tlsVersion = __sbTrustedProxyHeader(request, "x-sb-tls-version");
1847
+ if (__tlsVersion) __cf.tlsVersion = __tlsVersion;
1848
+ const __tlsCipher = __sbTrustedProxyHeader(request, "x-sb-tls-cipher");
1849
+ if (__tlsCipher) __cf.tlsCipher = __tlsCipher;
1830
1850
  request.cf = __cf;
1831
1851
  // #28 — per-invocation CPU time. One fetch turn per process (serial), so the
1832
1852
  // process CPU delta across the handler is this invocation's CPU.