was-teaching-server 0.9.1
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 +1432 -0
- package/LICENSE +661 -0
- package/README.md +305 -0
- package/common/css/materialize.1.0.0.min.css +13 -0
- package/dist/auth-header-hooks.d.ts +84 -0
- package/dist/auth-header-hooks.d.ts.map +1 -0
- package/dist/auth-header-hooks.js +136 -0
- package/dist/auth-header-hooks.js.map +1 -0
- package/dist/authorize.d.ts +45 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +76 -0
- package/dist/authorize.js.map +1 -0
- package/dist/backends/filesystem.d.ts +1179 -0
- package/dist/backends/filesystem.d.ts.map +1 -0
- package/dist/backends/filesystem.js +2727 -0
- package/dist/backends/filesystem.js.map +1 -0
- package/dist/backends/postgres.d.ts +833 -0
- package/dist/backends/postgres.d.ts.map +1 -0
- package/dist/backends/postgres.js +2164 -0
- package/dist/backends/postgres.js.map +1 -0
- package/dist/backends/postgresSchema.d.ts +36 -0
- package/dist/backends/postgresSchema.d.ts.map +1 -0
- package/dist/backends/postgresSchema.js +203 -0
- package/dist/backends/postgresSchema.js.map +1 -0
- package/dist/config.default.d.ts +360 -0
- package/dist/config.default.d.ts.map +1 -0
- package/dist/config.default.js +493 -0
- package/dist/config.default.js.map +1 -0
- package/dist/corsProxy.d.ts +45 -0
- package/dist/corsProxy.d.ts.map +1 -0
- package/dist/corsProxy.js +278 -0
- package/dist/corsProxy.js.map +1 -0
- package/dist/digest.d.ts +45 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +174 -0
- package/dist/digest.js.map +1 -0
- package/dist/errors.d.ts +654 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +960 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/atomicFile.d.ts +61 -0
- package/dist/lib/atomicFile.d.ts.map +1 -0
- package/dist/lib/atomicFile.js +150 -0
- package/dist/lib/atomicFile.js.map +1 -0
- package/dist/lib/backendRegistry.d.ts +59 -0
- package/dist/lib/backendRegistry.d.ts.map +1 -0
- package/dist/lib/backendRegistry.js +92 -0
- package/dist/lib/backendRegistry.js.map +1 -0
- package/dist/lib/backendUsage.d.ts +28 -0
- package/dist/lib/backendUsage.d.ts.map +1 -0
- package/dist/lib/backendUsage.js +55 -0
- package/dist/lib/backendUsage.js.map +1 -0
- package/dist/lib/backends.d.ts +142 -0
- package/dist/lib/backends.d.ts.map +1 -0
- package/dist/lib/backends.js +272 -0
- package/dist/lib/backends.js.map +1 -0
- package/dist/lib/blindedIndex.d.ts +156 -0
- package/dist/lib/blindedIndex.d.ts.map +1 -0
- package/dist/lib/blindedIndex.js +338 -0
- package/dist/lib/blindedIndex.js.map +1 -0
- package/dist/lib/collectionDescription.d.ts +33 -0
- package/dist/lib/collectionDescription.d.ts.map +1 -0
- package/dist/lib/collectionDescription.js +21 -0
- package/dist/lib/collectionDescription.js.map +1 -0
- package/dist/lib/cursor.d.ts +18 -0
- package/dist/lib/cursor.d.ts.map +1 -0
- package/dist/lib/cursor.js +50 -0
- package/dist/lib/cursor.js.map +1 -0
- package/dist/lib/edvEnvelope.d.ts +50 -0
- package/dist/lib/edvEnvelope.d.ts.map +1 -0
- package/dist/lib/edvEnvelope.js +106 -0
- package/dist/lib/edvEnvelope.js.map +1 -0
- package/dist/lib/encryption.d.ts +192 -0
- package/dist/lib/encryption.d.ts.map +1 -0
- package/dist/lib/encryption.js +383 -0
- package/dist/lib/encryption.js.map +1 -0
- package/dist/lib/etag.d.ts +33 -0
- package/dist/lib/etag.d.ts.map +1 -0
- package/dist/lib/etag.js +36 -0
- package/dist/lib/etag.js.map +1 -0
- package/dist/lib/exportManifest.d.ts +29 -0
- package/dist/lib/exportManifest.d.ts.map +1 -0
- package/dist/lib/exportManifest.js +77 -0
- package/dist/lib/exportManifest.js.map +1 -0
- package/dist/lib/importTar.d.ts +81 -0
- package/dist/lib/importTar.d.ts.map +1 -0
- package/dist/lib/importTar.js +301 -0
- package/dist/lib/importTar.js.map +1 -0
- package/dist/lib/isJson.d.ts +16 -0
- package/dist/lib/isJson.d.ts.map +1 -0
- package/dist/lib/isJson.js +17 -0
- package/dist/lib/isJson.js.map +1 -0
- package/dist/lib/keyEpoch.d.ts +38 -0
- package/dist/lib/keyEpoch.d.ts.map +1 -0
- package/dist/lib/keyEpoch.js +75 -0
- package/dist/lib/keyEpoch.js.map +1 -0
- package/dist/lib/keyedMutex.d.ts +28 -0
- package/dist/lib/keyedMutex.d.ts.map +1 -0
- package/dist/lib/keyedMutex.js +45 -0
- package/dist/lib/keyedMutex.js.map +1 -0
- package/dist/lib/kmsModule.d.ts +62 -0
- package/dist/lib/kmsModule.d.ts.map +1 -0
- package/dist/lib/kmsModule.js +327 -0
- package/dist/lib/kmsModule.js.map +1 -0
- package/dist/lib/kmsRecordCipher.d.ts +82 -0
- package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
- package/dist/lib/kmsRecordCipher.js +226 -0
- package/dist/lib/kmsRecordCipher.js.map +1 -0
- package/dist/lib/pagination.d.ts +19 -0
- package/dist/lib/pagination.d.ts.map +1 -0
- package/dist/lib/pagination.js +21 -0
- package/dist/lib/pagination.js.map +1 -0
- package/dist/lib/paths.d.ts +273 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +270 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/preconditions.d.ts +65 -0
- package/dist/lib/preconditions.d.ts.map +1 -0
- package/dist/lib/preconditions.js +112 -0
- package/dist/lib/preconditions.js.map +1 -0
- package/dist/lib/resourceFileName.d.ts +41 -0
- package/dist/lib/resourceFileName.d.ts.map +1 -0
- package/dist/lib/resourceFileName.js +58 -0
- package/dist/lib/resourceFileName.js.map +1 -0
- package/dist/lib/revocations.d.ts +49 -0
- package/dist/lib/revocations.d.ts.map +1 -0
- package/dist/lib/revocations.js +86 -0
- package/dist/lib/revocations.js.map +1 -0
- package/dist/lib/validateDid.d.ts +19 -0
- package/dist/lib/validateDid.d.ts.map +1 -0
- package/dist/lib/validateDid.js +35 -0
- package/dist/lib/validateDid.js.map +1 -0
- package/dist/lib/validateId.d.ts +66 -0
- package/dist/lib/validateId.d.ts.map +1 -0
- package/dist/lib/validateId.js +126 -0
- package/dist/lib/validateId.js.map +1 -0
- package/dist/plugin.d.ts +115 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +134 -0
- package/dist/plugin.js.map +1 -0
- package/dist/policy.d.ts +78 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +115 -0
- package/dist/policy.js.map +1 -0
- package/dist/provisioning.d.ts +38 -0
- package/dist/provisioning.d.ts.map +1 -0
- package/dist/provisioning.js +72 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/requests/BackendRequest.d.ts +61 -0
- package/dist/requests/BackendRequest.d.ts.map +1 -0
- package/dist/requests/BackendRequest.js +152 -0
- package/dist/requests/BackendRequest.js.map +1 -0
- package/dist/requests/CollectionRequest.d.ts +234 -0
- package/dist/requests/CollectionRequest.d.ts.map +1 -0
- package/dist/requests/CollectionRequest.js +705 -0
- package/dist/requests/CollectionRequest.js.map +1 -0
- package/dist/requests/KeyRequest.d.ts +115 -0
- package/dist/requests/KeyRequest.d.ts.map +1 -0
- package/dist/requests/KeyRequest.js +468 -0
- package/dist/requests/KeyRequest.js.map +1 -0
- package/dist/requests/KeystoreRequest.d.ts +99 -0
- package/dist/requests/KeystoreRequest.d.ts.map +1 -0
- package/dist/requests/KeystoreRequest.js +247 -0
- package/dist/requests/KeystoreRequest.js.map +1 -0
- package/dist/requests/PolicyRequest.d.ts +53 -0
- package/dist/requests/PolicyRequest.d.ts.map +1 -0
- package/dist/requests/PolicyRequest.js +124 -0
- package/dist/requests/PolicyRequest.js.map +1 -0
- package/dist/requests/ResourceRequest.d.ts +134 -0
- package/dist/requests/ResourceRequest.d.ts.map +1 -0
- package/dist/requests/ResourceRequest.js +548 -0
- package/dist/requests/ResourceRequest.js.map +1 -0
- package/dist/requests/RevocationRequest.d.ts +67 -0
- package/dist/requests/RevocationRequest.d.ts.map +1 -0
- package/dist/requests/RevocationRequest.js +187 -0
- package/dist/requests/RevocationRequest.js.map +1 -0
- package/dist/requests/SpaceRequest.d.ts +204 -0
- package/dist/requests/SpaceRequest.d.ts.map +1 -0
- package/dist/requests/SpaceRequest.js +520 -0
- package/dist/requests/SpaceRequest.js.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.js +159 -0
- package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
- package/dist/requests/collectionContext.d.ts +21 -0
- package/dist/requests/collectionContext.d.ts.map +1 -0
- package/dist/requests/collectionContext.js +30 -0
- package/dist/requests/collectionContext.js.map +1 -0
- package/dist/requests/controllerConsent.d.ts +58 -0
- package/dist/requests/controllerConsent.d.ts.map +1 -0
- package/dist/requests/controllerConsent.js +73 -0
- package/dist/requests/controllerConsent.js.map +1 -0
- package/dist/requests/keystoreContext.d.ts +68 -0
- package/dist/requests/keystoreContext.d.ts.map +1 -0
- package/dist/requests/keystoreContext.js +87 -0
- package/dist/requests/keystoreContext.js.map +1 -0
- package/dist/requests/resourceInput.d.ts +19 -0
- package/dist/requests/resourceInput.d.ts.map +1 -0
- package/dist/requests/resourceInput.js +102 -0
- package/dist/requests/resourceInput.js.map +1 -0
- package/dist/requests/spaceContext.d.ts +127 -0
- package/dist/requests/spaceContext.d.ts.map +1 -0
- package/dist/requests/spaceContext.js +201 -0
- package/dist/requests/spaceContext.js.map +1 -0
- package/dist/routes.d.ts +55 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +306 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +60 -0
- package/dist/server.js.map +1 -0
- package/dist/start.d.ts +7 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +61 -0
- package/dist/start.js.map +1 -0
- package/dist/storage.d.ts +29 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +47 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +915 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/views/home.hbs +9 -0
- package/dist/views/templates/main.hbs +12 -0
- package/dist/zcap.d.ts +267 -0
- package/dist/zcap.d.ts.map +1 -0
- package/dist/zcap.js +478 -0
- package/dist/zcap.js.map +1 -0
- package/package.json +109 -0
- package/src/auth-header-hooks.ts +163 -0
- package/src/authorize.ts +117 -0
- package/src/backends/filesystem.ts +3626 -0
- package/src/backends/postgres.ts +3073 -0
- package/src/backends/postgresSchema.ts +231 -0
- package/src/config.default.ts +657 -0
- package/src/corsProxy.ts +326 -0
- package/src/declarations.d.ts +28 -0
- package/src/digest.ts +223 -0
- package/src/errors.ts +1122 -0
- package/src/index.ts +21 -0
- package/src/lib/atomicFile.ts +176 -0
- package/src/lib/backendRegistry.ts +139 -0
- package/src/lib/backendUsage.ts +79 -0
- package/src/lib/backends.ts +351 -0
- package/src/lib/blindedIndex.ts +466 -0
- package/src/lib/collectionDescription.ts +43 -0
- package/src/lib/cursor.ts +52 -0
- package/src/lib/edvEnvelope.ts +120 -0
- package/src/lib/encryption.ts +472 -0
- package/src/lib/etag.ts +40 -0
- package/src/lib/exportManifest.ts +115 -0
- package/src/lib/importTar.ts +381 -0
- package/src/lib/isJson.ts +18 -0
- package/src/lib/keyEpoch.ts +89 -0
- package/src/lib/keyedMutex.ts +45 -0
- package/src/lib/kmsModule.ts +451 -0
- package/src/lib/kmsRecordCipher.ts +292 -0
- package/src/lib/pagination.ts +22 -0
- package/src/lib/paths.ts +382 -0
- package/src/lib/preconditions.ts +145 -0
- package/src/lib/resourceFileName.ts +69 -0
- package/src/lib/revocations.ts +116 -0
- package/src/lib/validateDid.ts +41 -0
- package/src/lib/validateId.ts +148 -0
- package/src/plugin.ts +288 -0
- package/src/policy.ts +176 -0
- package/src/provisioning.ts +98 -0
- package/src/requests/BackendRequest.ts +219 -0
- package/src/requests/CollectionRequest.ts +899 -0
- package/src/requests/KeyRequest.ts +617 -0
- package/src/requests/KeystoreRequest.ts +325 -0
- package/src/requests/PolicyRequest.ts +169 -0
- package/src/requests/ResourceRequest.ts +649 -0
- package/src/requests/RevocationRequest.ts +267 -0
- package/src/requests/SpaceRequest.ts +686 -0
- package/src/requests/SpacesRepositoryRequest.ts +194 -0
- package/src/requests/collectionContext.ts +41 -0
- package/src/requests/controllerConsent.ts +108 -0
- package/src/requests/keystoreContext.ts +124 -0
- package/src/requests/resourceInput.ts +118 -0
- package/src/requests/spaceContext.ts +311 -0
- package/src/routes.ts +420 -0
- package/src/server.ts +69 -0
- package/src/start.ts +71 -0
- package/src/storage.ts +60 -0
- package/src/types.ts +1016 -0
- package/src/views/home.hbs +9 -0
- package/src/views/templates/main.hbs +12 -0
- package/src/zcap.ts +657 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { lookup as dnsLookup } from 'node:dns/promises';
|
|
3
|
+
import { fetch, Agent } from 'undici';
|
|
4
|
+
const PROXY_TIMEOUT_MS = 10_000;
|
|
5
|
+
const MAX_REDIRECTS = 5;
|
|
6
|
+
/**
|
|
7
|
+
* Upper bound on the response body the proxy will buffer and relay. Caps memory
|
|
8
|
+
* use per proxied request and refuses to relay an oversized upstream response
|
|
9
|
+
* (enforced both from a declared `content-length` and while streaming).
|
|
10
|
+
*/
|
|
11
|
+
const PROXY_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
12
|
+
/**
|
|
13
|
+
* True for an IPv4 address in a private, loopback, link-local, or otherwise
|
|
14
|
+
* non-public range -- the SSRF-sensitive destinations the proxy must refuse
|
|
15
|
+
* (RFC 1918 private space, `127.0.0.0/8` loopback, `169.254.0.0/16` link-local
|
|
16
|
+
* -- which covers the `169.254.169.254` cloud-metadata endpoint -- CGNAT, and
|
|
17
|
+
* multicast/reserved). A syntactically invalid address is treated as blocked.
|
|
18
|
+
* @param ip {string}
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function isBlockedIpv4(ip) {
|
|
22
|
+
const parts = ip.split('.').map(Number);
|
|
23
|
+
if (parts.length !== 4 ||
|
|
24
|
+
parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const [a, b] = parts;
|
|
28
|
+
return (a === 0 || // 0.0.0.0/8 "this network"
|
|
29
|
+
a === 10 || // private
|
|
30
|
+
a === 127 || // loopback
|
|
31
|
+
(a === 169 && b === 254) || // link-local (incl. cloud metadata)
|
|
32
|
+
(a === 172 && b >= 16 && b <= 31) || // private
|
|
33
|
+
(a === 192 && b === 168) || // private
|
|
34
|
+
(a === 100 && b >= 64 && b <= 127) || // CGNAT (RFC 6598)
|
|
35
|
+
(a === 192 && b === 0) || // 192.0.0.0/24 IETF protocol assignments
|
|
36
|
+
a >= 224 // multicast / reserved
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* True for an IPv6 address in a loopback, unspecified, unique-local (`fc00::/7`),
|
|
41
|
+
* or link-local (`fe80::/10`) range, or an IPv4-mapped/embedded address whose
|
|
42
|
+
* IPv4 form is blocked. A syntactically invalid address is treated as blocked.
|
|
43
|
+
* @param ip {string}
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isBlockedIpv6(ip) {
|
|
47
|
+
const lower = ip.toLowerCase();
|
|
48
|
+
const mapped = lower.match(/(?:^|:)(\d+\.\d+\.\d+\.\d+)$/);
|
|
49
|
+
if (mapped) {
|
|
50
|
+
return isBlockedIpv4(mapped[1]);
|
|
51
|
+
}
|
|
52
|
+
return (lower === '::' ||
|
|
53
|
+
lower === '::1' ||
|
|
54
|
+
lower.startsWith('fe8') || // fe80::/10 link-local
|
|
55
|
+
lower.startsWith('fe9') ||
|
|
56
|
+
lower.startsWith('fea') ||
|
|
57
|
+
lower.startsWith('feb') ||
|
|
58
|
+
lower.startsWith('fc') || // fc00::/7 unique-local
|
|
59
|
+
lower.startsWith('fd'));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* True for an IP the proxy must not reach. Unparseable input is blocked
|
|
63
|
+
* defensively.
|
|
64
|
+
* @param ip {string}
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
function isBlockedIp(ip) {
|
|
68
|
+
const family = net.isIP(ip);
|
|
69
|
+
if (family === 4) {
|
|
70
|
+
return isBlockedIpv4(ip);
|
|
71
|
+
}
|
|
72
|
+
if (family === 6) {
|
|
73
|
+
return isBlockedIpv6(ip);
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parses and validates a single proxy target: `http`/`https` scheme only, and
|
|
79
|
+
* the host must not resolve to a blocked (private / loopback / link-local)
|
|
80
|
+
* address. Runs once for the client-supplied URL and again for every redirect
|
|
81
|
+
* hop, so an allowed public host cannot bounce the proxy to an internal one.
|
|
82
|
+
* Returns the exact addresses it validated so the caller can pin the upstream
|
|
83
|
+
* connection to them (closing the DNS-rebinding TOCTOU).
|
|
84
|
+
* @param target {string}
|
|
85
|
+
* @returns {Promise<{url: URL, addresses: {address: string, family: number}[]} | {status: number, error: string}>}
|
|
86
|
+
* the parsed URL plus its validated addresses when allowed, or the HTTP
|
|
87
|
+
* status + message to reject with.
|
|
88
|
+
*/
|
|
89
|
+
async function checkProxyTarget(target) {
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = new URL(target);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return { status: 400, error: 'Invalid url query parameter' };
|
|
96
|
+
}
|
|
97
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
98
|
+
return {
|
|
99
|
+
status: 400,
|
|
100
|
+
error: 'Only http and https URLs may be proxied.'
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Resolve the host and refuse private / loopback / link-local destinations
|
|
104
|
+
// (SSRF). `dns.lookup` returns the literal itself for an IP host, so
|
|
105
|
+
// IP-literal targets are covered too.
|
|
106
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, '');
|
|
107
|
+
let addresses;
|
|
108
|
+
try {
|
|
109
|
+
addresses = await dnsLookup(hostname, { all: true });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return { status: 502, error: 'Unable to resolve proxied host.' };
|
|
113
|
+
}
|
|
114
|
+
if (addresses.length === 0 ||
|
|
115
|
+
addresses.some(({ address }) => isBlockedIp(address))) {
|
|
116
|
+
return { status: 403, error: 'Proxying to this host is not allowed.' };
|
|
117
|
+
}
|
|
118
|
+
return { url: parsed, addresses };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Builds the undici `connect.lookup` function that pins each upstream socket to
|
|
122
|
+
* the exact addresses `checkProxyTarget` already validated, keyed by hostname.
|
|
123
|
+
* The `fetch` still connects using the original hostname (so TLS certificate
|
|
124
|
+
* validation and SNI keep working) but resolves it only via this map, so a
|
|
125
|
+
* rebinding attacker cannot swap in a fresh, private address between validation
|
|
126
|
+
* and connection. A hostname absent from the map is refused (defense in depth:
|
|
127
|
+
* the agent must never resolve an unpinned host).
|
|
128
|
+
* @param pins {Map<string, {address: string, family: number}[]>} validated
|
|
129
|
+
* addresses keyed by lower-cased hostname.
|
|
130
|
+
* @returns {LookupFunction} an undici/`net`-compatible lookup callback.
|
|
131
|
+
*/
|
|
132
|
+
export function createPinnedLookup(pins) {
|
|
133
|
+
return function pinnedLookup(hostname, options, callback) {
|
|
134
|
+
const entries = pins.get(hostname.toLowerCase());
|
|
135
|
+
if (!entries || entries.length === 0) {
|
|
136
|
+
callback(new Error(`Refusing to resolve unpinned host: ${hostname}`));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (options.all) {
|
|
140
|
+
callback(null, entries);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const first = entries[0];
|
|
144
|
+
callback(null, first.address, first.family);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Registers a server-side CORS proxy at `/api/cors`.
|
|
149
|
+
*
|
|
150
|
+
* Example:
|
|
151
|
+
* `/api/cors?url=https%3A%2F%2Fregistry.dcconsortium.org%2F.well-known%2Fopenid-federation`
|
|
152
|
+
*
|
|
153
|
+
* SSRF guard: only `http`/`https` URLs are proxied, and the target host is
|
|
154
|
+
* resolved and rejected when it maps to a private, loopback, or link-local
|
|
155
|
+
* address (e.g. `http://169.254.169.254/...` cloud-metadata, or an internal
|
|
156
|
+
* service). Redirects are followed manually (up to `MAX_REDIRECTS` hops) and
|
|
157
|
+
* every hop is re-validated the same way, so a public host cannot 3xx the
|
|
158
|
+
* proxy into an internal one. The addresses each hop validated are then pinned
|
|
159
|
+
* for the actual upstream connection (via a per-request undici `Agent` whose
|
|
160
|
+
* `connect.lookup` resolves only from that pin map), so a DNS-rebinding
|
|
161
|
+
* attacker cannot pass validation with a public IP and have the subsequent
|
|
162
|
+
* `fetch` re-resolve to a private one (closing the TOCTOU). The relayed
|
|
163
|
+
* response body is capped at `PROXY_MAX_RESPONSE_BYTES`, both from a declared
|
|
164
|
+
* `content-length` and while streaming. This is an unauthenticated open
|
|
165
|
+
* endpoint; adding a lightweight auth gate (a shared secret header, an
|
|
166
|
+
* allowlist of caller origins, or the server's own capability-invocation
|
|
167
|
+
* check) is a reasonable follow-up.
|
|
168
|
+
*
|
|
169
|
+
* @param app - Fastify instance
|
|
170
|
+
* @param _options - Fastify plugin options
|
|
171
|
+
*/
|
|
172
|
+
export async function initCorsProxyRoutes(app, _options) {
|
|
173
|
+
app.get('/api/cors', async (request, reply) => {
|
|
174
|
+
const target = request.query.url;
|
|
175
|
+
if (!target) {
|
|
176
|
+
return reply.code(400).send({ error: 'Missing url query parameter' });
|
|
177
|
+
}
|
|
178
|
+
const checked = await checkProxyTarget(target);
|
|
179
|
+
if (!('url' in checked)) {
|
|
180
|
+
return reply.code(checked.status).send({ error: checked.error });
|
|
181
|
+
}
|
|
182
|
+
let current = checked.url;
|
|
183
|
+
const headers = {};
|
|
184
|
+
if (typeof request.headers.accept === 'string') {
|
|
185
|
+
headers.accept = request.headers.accept;
|
|
186
|
+
}
|
|
187
|
+
// One undici Agent per proxied request, resolving each hop only via the
|
|
188
|
+
// addresses that hop already validated. The Agent must stay alive until
|
|
189
|
+
// the body is fully read, so it is torn down once in the finally below.
|
|
190
|
+
const pins = new Map();
|
|
191
|
+
const pinKey = (url) => url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
192
|
+
pins.set(pinKey(current), checked.addresses);
|
|
193
|
+
const agent = new Agent({ connect: { lookup: createPinnedLookup(pins) } });
|
|
194
|
+
try {
|
|
195
|
+
for (let hop = 0;; hop++) {
|
|
196
|
+
const upstream = await fetch(current.href, {
|
|
197
|
+
headers,
|
|
198
|
+
redirect: 'manual',
|
|
199
|
+
signal: AbortSignal.timeout(PROXY_TIMEOUT_MS),
|
|
200
|
+
dispatcher: agent
|
|
201
|
+
});
|
|
202
|
+
const location = upstream.headers.get('location');
|
|
203
|
+
if (upstream.status >= 300 && upstream.status < 400 && location) {
|
|
204
|
+
if (hop >= MAX_REDIRECTS) {
|
|
205
|
+
return reply
|
|
206
|
+
.code(502)
|
|
207
|
+
.send({ error: 'Too many proxied redirects.' });
|
|
208
|
+
}
|
|
209
|
+
let next;
|
|
210
|
+
try {
|
|
211
|
+
next = new URL(location, current);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return reply
|
|
215
|
+
.code(502)
|
|
216
|
+
.send({ error: 'Invalid redirect from proxied URL.' });
|
|
217
|
+
}
|
|
218
|
+
const nextChecked = await checkProxyTarget(next.toString());
|
|
219
|
+
if (!('url' in nextChecked)) {
|
|
220
|
+
return reply
|
|
221
|
+
.code(nextChecked.status)
|
|
222
|
+
.send({ error: nextChecked.error });
|
|
223
|
+
}
|
|
224
|
+
current = nextChecked.url;
|
|
225
|
+
pins.set(pinKey(current), nextChecked.addresses);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// Refuse an upstream that declares a body larger than the cap before
|
|
229
|
+
// reading any of it.
|
|
230
|
+
const declaredLength = Number(upstream.headers.get('content-length'));
|
|
231
|
+
if (Number.isFinite(declaredLength) &&
|
|
232
|
+
declaredLength > PROXY_MAX_RESPONSE_BYTES) {
|
|
233
|
+
return reply
|
|
234
|
+
.code(502)
|
|
235
|
+
.send({ error: 'Proxied response too large.' });
|
|
236
|
+
}
|
|
237
|
+
// Read incrementally so an undeclared (or lying) oversized body is
|
|
238
|
+
// stopped mid-stream rather than fully buffered. Upstream headers
|
|
239
|
+
// are copied only after the body is fully read, so a mid-stream
|
|
240
|
+
// rejection does not carry the upstream's content-type /
|
|
241
|
+
// content-length on the JSON error response.
|
|
242
|
+
const chunks = [];
|
|
243
|
+
if (upstream.body) {
|
|
244
|
+
const reader = upstream.body.getReader();
|
|
245
|
+
let total = 0;
|
|
246
|
+
for (;;) {
|
|
247
|
+
const { done, value } = await reader.read();
|
|
248
|
+
if (done) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
total += value.byteLength;
|
|
252
|
+
if (total > PROXY_MAX_RESPONSE_BYTES) {
|
|
253
|
+
await reader.cancel();
|
|
254
|
+
return reply
|
|
255
|
+
.code(502)
|
|
256
|
+
.send({ error: 'Proxied response too large.' });
|
|
257
|
+
}
|
|
258
|
+
chunks.push(Buffer.from(value));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
upstream.headers.forEach((value, name) => reply.header(name, value));
|
|
262
|
+
return reply.code(upstream.status).send(Buffer.concat(chunks));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
request.log.warn({ error, target }, 'CORS proxy fetch failed');
|
|
267
|
+
return reply.code(502).send({ error: 'Unable to fetch proxied URL' });
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
// destroy(), not close(): early-exit paths (blocked redirect, too
|
|
271
|
+
// many redirects, oversized content-length) leave upstream bodies
|
|
272
|
+
// unconsumed, and close() would wait on them. Everything the reply
|
|
273
|
+
// sends is already buffered, so tearing down the sockets is safe.
|
|
274
|
+
await agent.destroy();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=corsProxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"corsProxy.js","sourceRoot":"","sources":["../src/corsProxy.ts"],"names":[],"mappings":"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"}
|
package/dist/digest.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Body Integrity (spec "Request Body Integrity (Digest Header)"): binds
|
|
3
|
+
* a request body to its HTTP Signature via the `Digest` header. Two hooks,
|
|
4
|
+
* installed by every route group alongside the auth hooks:
|
|
5
|
+
*
|
|
6
|
+
* - `captureRawBody` (preParsing) tees the exact body bytes onto
|
|
7
|
+
* `request.rawBody` for JSON/text bodies, so the digest can be recomputed
|
|
8
|
+
* against what the client actually signed. Re-serializing the parsed body is
|
|
9
|
+
* not guaranteed byte-identical, so we keep the raw bytes instead. Streamed
|
|
10
|
+
* bodies (multipart uploads, tar imports, raw blobs) are not buffered; instead
|
|
11
|
+
* they pass through a hashing transform that verifies the `Digest`
|
|
12
|
+
* incrementally at end-of-stream, so large/binary uploads keep streaming yet
|
|
13
|
+
* are still bound to the signature.
|
|
14
|
+
* - `verifyBodyDigest` (preValidation) enforces, for any request carrying a
|
|
15
|
+
* `Content-Type`: that the signature covers the `digest` header (MUST), that a
|
|
16
|
+
* `Digest` header is present, and -- when the raw body was captured --
|
|
17
|
+
* independently recomputes the body digest and compares it (SHOULD). A
|
|
18
|
+
* missing, malformed, uncovered, or non-matching digest is rejected with
|
|
19
|
+
* `invalid-authorization-header` (400).
|
|
20
|
+
*/
|
|
21
|
+
import type { FastifyReply, FastifyRequest } from 'fastify';
|
|
22
|
+
import { type Readable } from 'node:stream';
|
|
23
|
+
/**
|
|
24
|
+
* preParsing hook: for JSON/text bodies, tees the incoming payload into
|
|
25
|
+
* `request.rawBody` while passing the same bytes through to Fastify's content-
|
|
26
|
+
* type parser (so `verifyBodyDigest` can recompute the digest). Other (streamed)
|
|
27
|
+
* bodies are not buffered; when the request is signed with a `Digest` they pass
|
|
28
|
+
* through a `DigestVerifyStream` that verifies that digest incrementally, so a
|
|
29
|
+
* swapped binary/large body under a valid signed `Digest` is still rejected.
|
|
30
|
+
* @param request {import('fastify').FastifyRequest}
|
|
31
|
+
* @param reply {import('fastify').FastifyReply}
|
|
32
|
+
* @param payload {Readable} the raw request body stream
|
|
33
|
+
* @returns {Promise<Readable>} the stream Fastify should parse
|
|
34
|
+
*/
|
|
35
|
+
export declare function captureRawBody(request: FastifyRequest, _reply: FastifyReply, payload: Readable): Promise<Readable>;
|
|
36
|
+
/**
|
|
37
|
+
* preValidation hook: enforces the `Digest` header binding for any request that
|
|
38
|
+
* carries a `Content-Type`. Bodyless requests (no `Content-Type`) and anonymous
|
|
39
|
+
* reads (no parsed `request.zcap`) are passed through.
|
|
40
|
+
* @param request {import('fastify').FastifyRequest}
|
|
41
|
+
* @param reply {import('fastify').FastifyReply}
|
|
42
|
+
* @returns {Promise<void>}
|
|
43
|
+
*/
|
|
44
|
+
export declare function verifyBodyDigest(request: FastifyRequest, _reply: FastifyReply): Promise<void>;
|
|
45
|
+
//# sourceMappingURL=digest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE3D,OAAO,EAA0B,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAA;AAyFnE;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,QAAQ,CAAC,CA0BnB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CAwDf"}
|
package/dist/digest.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { verifyHeaderValue } from '@interop/http-digest-header';
|
|
2
|
+
import { PassThrough, Transform } from 'node:stream';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { isJson } from './lib/isJson.js';
|
|
5
|
+
import { InvalidDigestError } from './errors.js';
|
|
6
|
+
/**
|
|
7
|
+
* True for the body shapes `verifyBodyDigest` recomputes: JSON
|
|
8
|
+
* (`application/json` / `+json`) and text. These reach handlers fully parsed
|
|
9
|
+
* (object / string), so buffering their bytes is cheap; streamed bodies
|
|
10
|
+
* (multipart, `application/x-tar`) are deliberately excluded.
|
|
11
|
+
* @param contentType {string | undefined}
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
function isBufferableBody(contentType) {
|
|
15
|
+
return (isJson({ contentType }) ||
|
|
16
|
+
(typeof contentType === 'string' && contentType.startsWith('text/')));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Compares an incrementally-computed SHA-256 digest against a `Digest` header
|
|
20
|
+
* value, mirroring `@interop/http-digest-header`'s encodings without buffering
|
|
21
|
+
* the body: the multihash form (`mh=u<base64url(0x12 0x20 <digest>)>`, what WAS
|
|
22
|
+
* clients send) and the RFC 9651 `SHA-256=<base64>` form (colons stripped).
|
|
23
|
+
* @param options {object}
|
|
24
|
+
* @param options.sha256Digest {Buffer} the computed 32-byte SHA-256 digest
|
|
25
|
+
* @param options.headerValue {string} the request's `Digest` header value
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
function digestMatches({ sha256Digest, headerValue }) {
|
|
29
|
+
const [key = '', rawValue] = headerValue.split(/=(.+)/);
|
|
30
|
+
if (key === 'mh') {
|
|
31
|
+
const multihash = Buffer.concat([Buffer.from([0x12, 0x20]), sha256Digest]);
|
|
32
|
+
return rawValue === `u${multihash.toString('base64url')}`;
|
|
33
|
+
}
|
|
34
|
+
if (key.replace('-', '').toLowerCase() === 'sha256') {
|
|
35
|
+
const expected = (rawValue ?? '').replace(/^:(.*):$/, '$1');
|
|
36
|
+
return expected === sha256Digest.toString('base64');
|
|
37
|
+
}
|
|
38
|
+
// Only SHA-256 is supported (as in the library); anything else cannot match.
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A pass-through Transform that hashes the streamed bytes (SHA-256) and, at
|
|
43
|
+
* end-of-stream, verifies the accumulated digest against the request's `Digest`
|
|
44
|
+
* header -- erroring the stream (which fails the write) on a mismatch. This
|
|
45
|
+
* binds a streamed/binary body to its signed `Digest` without buffering it. A
|
|
46
|
+
* mismatch surfaces mid-write; consumers that persist the stream remove any
|
|
47
|
+
* partial output on the resulting error (see the filesystem backend's blob
|
|
48
|
+
* write).
|
|
49
|
+
*/
|
|
50
|
+
class DigestVerifyStream extends Transform {
|
|
51
|
+
_hash = createHash('sha256');
|
|
52
|
+
_digestHeader;
|
|
53
|
+
constructor(digestHeader) {
|
|
54
|
+
super();
|
|
55
|
+
this._digestHeader = digestHeader;
|
|
56
|
+
}
|
|
57
|
+
_transform(chunk, _encoding, callback) {
|
|
58
|
+
this._hash.update(chunk);
|
|
59
|
+
callback(null, chunk);
|
|
60
|
+
}
|
|
61
|
+
_flush(callback) {
|
|
62
|
+
const matched = digestMatches({
|
|
63
|
+
sha256Digest: this._hash.digest(),
|
|
64
|
+
headerValue: this._digestHeader
|
|
65
|
+
});
|
|
66
|
+
callback(matched
|
|
67
|
+
? null
|
|
68
|
+
: new InvalidDigestError({
|
|
69
|
+
detail: 'The `Digest` header does not match the request body.'
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* preParsing hook: for JSON/text bodies, tees the incoming payload into
|
|
75
|
+
* `request.rawBody` while passing the same bytes through to Fastify's content-
|
|
76
|
+
* type parser (so `verifyBodyDigest` can recompute the digest). Other (streamed)
|
|
77
|
+
* bodies are not buffered; when the request is signed with a `Digest` they pass
|
|
78
|
+
* through a `DigestVerifyStream` that verifies that digest incrementally, so a
|
|
79
|
+
* swapped binary/large body under a valid signed `Digest` is still rejected.
|
|
80
|
+
* @param request {import('fastify').FastifyRequest}
|
|
81
|
+
* @param reply {import('fastify').FastifyReply}
|
|
82
|
+
* @param payload {Readable} the raw request body stream
|
|
83
|
+
* @returns {Promise<Readable>} the stream Fastify should parse
|
|
84
|
+
*/
|
|
85
|
+
export async function captureRawBody(request, _reply, payload) {
|
|
86
|
+
if (!isBufferableBody(request.headers['content-type'])) {
|
|
87
|
+
// Streamed (non-buffered) body. Bind it to the signature by hashing it as it
|
|
88
|
+
// flows; an unsigned/anonymous body (no `Digest`) has nothing to verify.
|
|
89
|
+
// `parseAuthHeaders` (onRequest) has already run, so `request.zcap` is set
|
|
90
|
+
// for a signed request.
|
|
91
|
+
const digestHeader = request.zcap?.digest;
|
|
92
|
+
if (!digestHeader) {
|
|
93
|
+
return payload;
|
|
94
|
+
}
|
|
95
|
+
const verify = new DigestVerifyStream(digestHeader);
|
|
96
|
+
payload.on('error', err => verify.destroy(err));
|
|
97
|
+
return payload.pipe(verify);
|
|
98
|
+
}
|
|
99
|
+
const chunks = [];
|
|
100
|
+
const passthrough = new PassThrough();
|
|
101
|
+
payload.on('data', chunk => {
|
|
102
|
+
chunks.push(chunk);
|
|
103
|
+
passthrough.write(chunk);
|
|
104
|
+
});
|
|
105
|
+
payload.on('end', () => {
|
|
106
|
+
request.rawBody = Buffer.concat(chunks);
|
|
107
|
+
passthrough.end();
|
|
108
|
+
});
|
|
109
|
+
payload.on('error', err => passthrough.destroy(err));
|
|
110
|
+
return passthrough;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* preValidation hook: enforces the `Digest` header binding for any request that
|
|
114
|
+
* carries a `Content-Type`. Bodyless requests (no `Content-Type`) and anonymous
|
|
115
|
+
* reads (no parsed `request.zcap`) are passed through.
|
|
116
|
+
* @param request {import('fastify').FastifyRequest}
|
|
117
|
+
* @param reply {import('fastify').FastifyReply}
|
|
118
|
+
* @returns {Promise<void>}
|
|
119
|
+
*/
|
|
120
|
+
export async function verifyBodyDigest(request, _reply) {
|
|
121
|
+
// A provisioning-authorized request (onboarding token) is not signed, so
|
|
122
|
+
// there is no signature to cover a `Digest` header -- nothing to bind.
|
|
123
|
+
if (request.provisioningAuthorized) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const contentType = request.headers['content-type'];
|
|
127
|
+
// Bodyless requests carry no Content-Type and no Digest -- nothing to bind.
|
|
128
|
+
if (!contentType) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// No parsed auth headers: writes require auth (the auth hooks 401 first), so a
|
|
132
|
+
// bodied request reaching here without `zcap` is a safe method that happens to
|
|
133
|
+
// carry a Content-Type; leave it to the handler's policy decision.
|
|
134
|
+
const { zcap } = request;
|
|
135
|
+
if (!zcap) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// MUST: `digest` is among the signature's covered (signed) headers. Guard the
|
|
139
|
+
// `headers` param itself: an `Authorization` header that omits `headers="..."`
|
|
140
|
+
// leaves it undefined, and splitting that would be a 500 rather than the
|
|
141
|
+
// intended `invalid-authorization-header` (400).
|
|
142
|
+
if (!zcap.headers || !zcap.headers.split(/\s+/).includes('digest')) {
|
|
143
|
+
throw new InvalidDigestError({
|
|
144
|
+
detail: 'The request signature must cover the `digest` header when the ' +
|
|
145
|
+
'request carries a body.'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// The `Digest` header itself MUST be present.
|
|
149
|
+
if (!zcap.digest) {
|
|
150
|
+
throw new InvalidDigestError({
|
|
151
|
+
detail: 'A `Digest` header is required when the request carries a body.'
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// SHOULD: independently recompute and compare, when the raw body was captured.
|
|
155
|
+
// Streamed bodies (multipart, tar import, raw blobs) are not buffered here;
|
|
156
|
+
// `captureRawBody` has already wrapped them in a `DigestVerifyStream` that
|
|
157
|
+
// verifies the digest incrementally as the body is consumed.
|
|
158
|
+
if (request.rawBody === undefined) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const { verified, error } = await verifyHeaderValue({
|
|
162
|
+
data: request.rawBody,
|
|
163
|
+
headerValue: zcap.digest
|
|
164
|
+
});
|
|
165
|
+
if (!verified) {
|
|
166
|
+
throw new InvalidDigestError({
|
|
167
|
+
detail: error
|
|
168
|
+
? 'The `Digest` header is malformed or uses an unsupported algorithm.'
|
|
169
|
+
: 'The `Digest` header does not match the request body.',
|
|
170
|
+
cause: error
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=digest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.js","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"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"}
|