@floegence/flowersec-core 0.27.0 → 2.0.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/README.md +91 -22
- package/THIRD_PARTY_NOTICES.md +141 -0
- package/dist/browser/connectSession.d.ts +13 -0
- package/dist/browser/connectSession.js +40 -0
- package/dist/browser/index.d.ts +3 -10
- package/dist/browser/index.js +2 -5
- package/dist/browser/runtimeCapability.d.ts +13 -0
- package/dist/browser/runtimeCapability.js +27 -0
- package/dist/browser/sessionRuntime.d.ts +2 -0
- package/dist/browser/sessionRuntime.js +21 -0
- package/dist/browser/webTransportClient.d.ts +8 -0
- package/dist/browser/webTransportClient.js +55 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +126 -0
- package/dist/cliSpendMarker.d.ts +1 -0
- package/dist/cliSpendMarker.js +22 -0
- package/dist/connectionController.d.ts +54 -0
- package/dist/connectionController.js +438 -0
- package/dist/connector/adapters/webSocketCandidate.d.ts +7 -0
- package/dist/connector/adapters/webSocketCandidate.js +116 -0
- package/dist/connector/adapters/webTransportCandidate.d.ts +5 -0
- package/dist/connector/adapters/webTransportCandidate.js +73 -0
- package/dist/connector/admissionCommit.d.ts +31 -0
- package/dist/connector/admissionCommit.js +239 -0
- package/dist/connector/sessionAcceptor.d.ts +16 -0
- package/dist/connector/sessionAcceptor.js +114 -0
- package/dist/connector/sessionConfig.d.ts +3 -0
- package/dist/connector/sessionConfig.js +30 -0
- package/dist/connector/sessionConnector.d.ts +45 -0
- package/dist/connector/sessionConnector.js +603 -0
- package/dist/defaults.d.ts +7 -7
- package/dist/defaults.js +7 -7
- package/dist/facade.d.ts +12 -27
- package/dist/facade.js +6 -24
- package/dist/node/connectSession.d.ts +21 -0
- package/dist/node/connectSession.js +62 -0
- package/dist/node/index.d.ts +3 -11
- package/dist/node/index.js +2 -8
- package/dist/node/runtimeCapability.d.ts +7 -0
- package/dist/node/runtimeCapability.js +10 -0
- package/dist/node/sessionRuntime.d.ts +2 -0
- package/dist/node/sessionRuntime.js +13 -0
- package/dist/node/webTransportClient.d.ts +9 -0
- package/dist/node/webTransportClient.js +56 -0
- package/dist/node/webTransportServer.d.ts +22 -0
- package/dist/node/webTransportServer.js +100 -0
- package/dist/node/wsFactory.d.ts +3 -1
- package/dist/node/wsFactory.js +4 -3
- package/dist/proxy/controllerGuard.d.ts +4 -4
- package/dist/proxy/controllerGuard.js +83 -177
- package/dist/proxy/disableUpstreamServiceWorkerRegister.d.ts +3 -1
- package/dist/proxy/disableUpstreamServiceWorkerRegister.js +19 -17
- package/dist/proxy/index.d.ts +16 -15
- package/dist/proxy/index.js +8 -14
- package/dist/proxy/integration.d.ts +23 -71
- package/dist/proxy/integration.js +37 -340
- package/dist/proxy/registerServiceWorker.d.ts +1 -10
- package/dist/proxy/registerServiceWorker.js +43 -105
- package/dist/proxy/runtime.d.ts +3 -63
- package/dist/proxy/runtime.js +349 -462
- package/dist/proxy/scope.d.ts +6 -0
- package/dist/proxy/scope.js +131 -0
- package/dist/proxy/serviceWorker.d.ts +5 -16
- package/dist/proxy/serviceWorker.js +230 -617
- package/dist/proxy/stream.d.ts +9 -0
- package/dist/proxy/stream.js +40 -0
- package/dist/proxy/types.d.ts +78 -30
- package/dist/proxy/windowBridge.d.ts +61 -0
- package/dist/proxy/windowBridge.js +344 -0
- package/dist/proxy/wsPatch.d.ts +4 -14
- package/dist/proxy/wsPatch.js +173 -268
- package/dist/public/artifact.d.ts +11 -0
- package/dist/public/artifact.js +44 -0
- package/dist/public/artifactLease.d.ts +10 -0
- package/dist/public/artifactLease.js +48 -0
- package/dist/public/connectError.d.ts +6 -0
- package/dist/public/connectError.js +9 -0
- package/dist/public/contract.d.ts +83 -0
- package/dist/public/contract.js +17 -0
- package/dist/public/streamMetadata.d.ts +10 -0
- package/dist/public/streamMetadata.js +40 -0
- package/dist/rpc/caller.d.ts +1 -1
- package/dist/rpc/client.d.ts +1 -4
- package/dist/rpc/client.js +0 -21
- package/dist/rpc/rpcProxy.d.ts +1 -1
- package/dist/rpc/server.d.ts +1 -1
- package/dist/rpc/validate.d.ts +1 -1
- package/dist/rpc/wire.d.ts +15 -0
- package/dist/runtime/errors.d.ts +6 -0
- package/dist/runtime/errors.js +10 -0
- package/dist/transport/webSocketAdapter.d.ts +24 -0
- package/dist/transport/webSocketAdapter.js +234 -0
- package/dist/transport/webTransportAdapter.d.ts +27 -0
- package/dist/transport/webTransportAdapter.js +519 -0
- package/dist/utils/errors.d.ts +2 -20
- package/dist/utils/errors.js +73 -20
- package/dist/v2/admissionError.d.ts +4 -0
- package/dist/v2/admissionError.js +8 -0
- package/dist/v2/artifact.d.ts +128 -0
- package/dist/v2/artifact.js +1126 -0
- package/dist/v2/artifactLease.d.ts +1 -0
- package/dist/v2/artifactLease.js +7 -0
- package/dist/v2/capability.d.ts +29 -0
- package/dist/v2/capability.js +190 -0
- package/dist/v2/carrier.d.ts +79 -0
- package/dist/v2/carrier.js +405 -0
- package/dist/v2/contract.d.ts +37 -0
- package/dist/v2/contract.js +1 -0
- package/dist/v2/handshake.d.ts +80 -0
- package/dist/v2/handshake.js +470 -0
- package/dist/v2/opaqueArtifact.d.ts +1 -0
- package/dist/v2/opaqueArtifact.js +2 -0
- package/dist/v2/protocol.d.ts +99 -0
- package/dist/v2/protocol.js +739 -0
- package/dist/v2/publicSession.d.ts +1 -0
- package/dist/v2/publicSession.js +179 -0
- package/dist/v2/retryDisposition.d.ts +16 -0
- package/dist/v2/retryDisposition.js +30 -0
- package/dist/v2/session.d.ts +277 -0
- package/dist/v2/session.js +1879 -0
- package/dist/v2/streamLifetimeLedger.d.ts +31 -0
- package/dist/v2/streamLifetimeLedger.js +131 -0
- package/dist/v2/streamMetadata.d.ts +1 -0
- package/dist/v2/streamMetadata.js +7 -0
- package/dist/v2/unreliableMessage.d.ts +43 -0
- package/dist/v2/unreliableMessage.js +252 -0
- package/dist/vendor/tr46.d.ts +10 -0
- package/dist/vendor/tr46.js +501 -0
- package/dist/ws-client/binaryTransport.d.ts +0 -3
- package/dist/ws-client/binaryTransport.js +15 -39
- package/dist/yamux/errors.d.ts +0 -4
- package/dist/yamux/errors.js +0 -9
- package/dist/yamux/session.d.ts +0 -5
- package/dist/yamux/session.js +4 -70
- package/dist/yamux/stream.d.ts +1 -0
- package/dist/yamux/stream.js +4 -0
- package/package.json +29 -81
- package/sbom/cyclonedx.json +2764 -0
- package/sbom/spdx.json +1947 -0
- package/YAMUX_ALIGNMENT.md +0 -127
- package/dist/_examples/flowersec/demo/v1.facade.gen.d.ts +0 -12
- package/dist/_examples/flowersec/demo/v1.facade.gen.js +0 -15
- package/dist/_examples/flowersec/demo/v1.gen.d.ts +0 -16
- package/dist/_examples/flowersec/demo/v1.gen.js +0 -86
- package/dist/_examples/flowersec/demo/v1.rpc.gen.d.ts +0 -11
- package/dist/_examples/flowersec/demo/v1.rpc.gen.js +0 -22
- package/dist/browser/connect.d.ts +0 -13
- package/dist/browser/connect.js +0 -31
- package/dist/browser/reconnectConfig.d.ts +0 -15
- package/dist/browser/reconnectConfig.js +0 -20
- package/dist/client-connect/common.d.ts +0 -26
- package/dist/client-connect/common.js +0 -167
- package/dist/client-connect/connectCore.d.ts +0 -61
- package/dist/client-connect/connectCore.js +0 -572
- package/dist/client-connect/contract.d.ts +0 -8
- package/dist/client-connect/contract.js +0 -51
- package/dist/client-connect/termination.d.ts +0 -6
- package/dist/client-connect/termination.js +0 -7
- package/dist/client-connect/transportSecurity.d.ts +0 -27
- package/dist/client-connect/transportSecurity.js +0 -199
- package/dist/client-connect/tunnelAttachCloseReason.d.ts +0 -3
- package/dist/client-connect/tunnelAttachCloseReason.js +0 -23
- package/dist/client.d.ts +0 -23
- package/dist/connect/artifact.d.ts +0 -36
- package/dist/connect/artifact.js +0 -213
- package/dist/connect/resolveArtifact.d.ts +0 -22
- package/dist/connect/resolveArtifact.js +0 -87
- package/dist/controlplane/channelInit.d.ts +0 -27
- package/dist/controlplane/channelInit.js +0 -116
- package/dist/controlplane/http.d.ts +0 -16
- package/dist/controlplane/http.js +0 -59
- package/dist/controlplane/index.d.ts +0 -6
- package/dist/controlplane/index.js +0 -5
- package/dist/controlplane/issuer.d.ts +0 -18
- package/dist/controlplane/issuer.js +0 -113
- package/dist/controlplane/request.d.ts +0 -51
- package/dist/controlplane/request.js +0 -273
- package/dist/controlplane/token.d.ts +0 -33
- package/dist/controlplane/token.js +0 -119
- package/dist/direct-client/connect.d.ts +0 -9
- package/dist/direct-client/connect.js +0 -90
- package/dist/direct-client/index.d.ts +0 -1
- package/dist/direct-client/index.js +0 -1
- package/dist/e2ee/constants.d.ts +0 -9
- package/dist/e2ee/constants.js +0 -18
- package/dist/e2ee/errors.d.ts +0 -5
- package/dist/e2ee/errors.js +0 -8
- package/dist/e2ee/framing.d.ts +0 -12
- package/dist/e2ee/framing.js +0 -57
- package/dist/e2ee/handshake.d.ts +0 -88
- package/dist/e2ee/handshake.js +0 -353
- package/dist/e2ee/index.d.ts +0 -7
- package/dist/e2ee/index.js +0 -7
- package/dist/e2ee/kdf.d.ts +0 -15
- package/dist/e2ee/kdf.js +0 -39
- package/dist/e2ee/record.d.ts +0 -11
- package/dist/e2ee/record.js +0 -80
- package/dist/e2ee/secureChannel.d.ts +0 -93
- package/dist/e2ee/secureChannel.js +0 -342
- package/dist/e2ee/transcript.d.ts +0 -23
- package/dist/e2ee/transcript.js +0 -31
- package/dist/endpoint/index.d.ts +0 -84
- package/dist/endpoint/index.js +0 -557
- package/dist/endpoint/node.d.ts +0 -9
- package/dist/endpoint/node.js +0 -51
- package/dist/gen/flowersec/controlplane/v1.gen.d.ts +0 -36
- package/dist/gen/flowersec/controlplane/v1.gen.js +0 -135
- package/dist/gen/flowersec/direct/v1.gen.d.ts +0 -21
- package/dist/gen/flowersec/direct/v1.gen.js +0 -101
- package/dist/gen/flowersec/e2ee/v1.gen.d.ts +0 -68
- package/dist/gen/flowersec/e2ee/v1.gen.js +0 -194
- package/dist/gen/flowersec/rpc/v1.gen.d.ts +0 -30
- package/dist/gen/flowersec/rpc/v1.gen.js +0 -107
- package/dist/gen/flowersec/tunnel/v1.gen.d.ts +0 -23
- package/dist/gen/flowersec/tunnel/v1.gen.js +0 -104
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -22
- package/dist/node/connect.d.ts +0 -10
- package/dist/node/connect.js +0 -29
- package/dist/node/reconnectConfig.d.ts +0 -16
- package/dist/node/reconnectConfig.js +0 -20
- package/dist/observability/index.d.ts +0 -1
- package/dist/observability/index.js +0 -1
- package/dist/observability/observer.d.ts +0 -52
- package/dist/observability/observer.js +0 -314
- package/dist/proxy/appWindow.d.ts +0 -37
- package/dist/proxy/appWindow.js +0 -273
- package/dist/proxy/bootstrap.d.ts +0 -35
- package/dist/proxy/bootstrap.js +0 -119
- package/dist/proxy/constants.d.ts +0 -7
- package/dist/proxy/constants.js +0 -8
- package/dist/proxy/controllerWindow.d.ts +0 -12
- package/dist/proxy/controllerWindow.js +0 -384
- package/dist/proxy/cookieJar.d.ts +0 -7
- package/dist/proxy/cookieJar.js +0 -136
- package/dist/proxy/headerPolicy.d.ts +0 -14
- package/dist/proxy/headerPolicy.js +0 -134
- package/dist/proxy/portStream.d.ts +0 -5
- package/dist/proxy/portStream.js +0 -256
- package/dist/proxy/preset.d.ts +0 -29
- package/dist/proxy/preset.js +0 -111
- package/dist/proxy/runtimeScope.d.ts +0 -57
- package/dist/proxy/runtimeScope.js +0 -218
- package/dist/proxy/server.d.ts +0 -28
- package/dist/proxy/server.js +0 -833
- package/dist/proxy/windowBridgeProtocol.d.ts +0 -65
- package/dist/proxy/windowBridgeProtocol.js +0 -11
- package/dist/reconnect/artifactControlplane.d.ts +0 -16
- package/dist/reconnect/artifactControlplane.js +0 -35
- package/dist/reconnect/index.d.ts +0 -36
- package/dist/reconnect/index.js +0 -388
- package/dist/rpc-proxy/index.d.ts +0 -1
- package/dist/rpc-proxy/index.js +0 -1
- package/dist/rpc-proxy/rpcProxy.d.ts +0 -1
- package/dist/rpc-proxy/rpcProxy.js +0 -1
- package/dist/streamhello/index.d.ts +0 -1
- package/dist/streamhello/index.js +0 -1
- package/dist/streamhello/streamHello.d.ts +0 -3
- package/dist/streamhello/streamHello.js +0 -13
- package/dist/streamio/index.d.ts +0 -32
- package/dist/streamio/index.js +0 -87
- package/dist/tunnel-client/connect.d.ts +0 -9
- package/dist/tunnel-client/connect.js +0 -147
- package/dist/tunnel-client/index.d.ts +0 -1
- package/dist/tunnel-client/index.js +0 -1
- package/dist/ws/index.d.ts +0 -1
- package/dist/ws/index.js +0 -1
- /package/dist/{client.js → rpc/wire.js} +0 -0
package/dist/proxy/runtime.js
CHANGED
|
@@ -1,502 +1,408 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { SDK_DEFAULTS } from "../defaults.js";
|
|
2
|
+
import { readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
|
|
3
3
|
import { base64urlEncode } from "../utils/base64url.js";
|
|
4
4
|
import { readU32be, u32be } from "../utils/bin.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
for (let i = 0; i < b.length; i++)
|
|
19
|
-
b[i] = Math.floor(Math.random() * 256);
|
|
20
|
-
}
|
|
21
|
-
return base64urlEncode(b);
|
|
22
|
-
}
|
|
23
|
-
function pathOnly(path) {
|
|
24
|
-
const p = path.trim();
|
|
25
|
-
if (!p.startsWith("/"))
|
|
26
|
-
throw new Error("path must start with /");
|
|
27
|
-
if (p.startsWith("//"))
|
|
28
|
-
throw new Error("path must not start with //");
|
|
29
|
-
if (/[ \t\r\n]/.test(p))
|
|
30
|
-
throw new Error("path contains whitespace");
|
|
31
|
-
if (p.includes("://"))
|
|
32
|
-
throw new Error("path must not include scheme/host");
|
|
33
|
-
return p;
|
|
5
|
+
import { SessionError } from "../public/contract.js";
|
|
6
|
+
import { createStreamMetadata } from "../public/streamMetadata.js";
|
|
7
|
+
import { ProxyByteReader, writeAll } from "./stream.js";
|
|
8
|
+
const PROXY_HTTP_STREAM_KIND = "flowersec-proxy/http1";
|
|
9
|
+
const PROXY_WEBSOCKET_STREAM_KIND = "flowersec-proxy/ws";
|
|
10
|
+
const PROXY_WIRE_VERSION = 1;
|
|
11
|
+
const DEFAULT_MAX_WS_BUFFERED_AMOUNT_BYTES = 4 * 1024 * 1024;
|
|
12
|
+
const DEFAULT_MAX_CONCURRENT_HTTP_STREAMS = 24;
|
|
13
|
+
const DEFAULT_MAX_QUEUED_HTTP_REQUESTS = 128;
|
|
14
|
+
const DEFAULT_MAX_QUEUED_HTTP_BODY_BYTES = 64 * 1024 * 1024;
|
|
15
|
+
class ProxyPolicyError extends Error {
|
|
34
16
|
}
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
|
|
17
|
+
function randomID() {
|
|
18
|
+
const bytes = new Uint8Array(18);
|
|
19
|
+
if (globalThis.crypto?.getRandomValues !== undefined)
|
|
20
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
21
|
+
else
|
|
22
|
+
for (let index = 0; index < bytes.length; index++)
|
|
23
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
24
|
+
return base64urlEncode(bytes);
|
|
38
25
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
26
|
+
function positiveLimit(name, input, fallback) {
|
|
27
|
+
if (input === undefined || input === 0)
|
|
28
|
+
return fallback;
|
|
29
|
+
if (!Number.isSafeInteger(input) || input < 0)
|
|
30
|
+
throw new TypeError(`${name} must be a non-negative safe integer`);
|
|
31
|
+
return input;
|
|
41
32
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
function strictlyPositiveLimit(name, input, fallback) {
|
|
34
|
+
if (input === undefined)
|
|
35
|
+
return fallback;
|
|
36
|
+
if (!Number.isSafeInteger(input) || input <= 0)
|
|
37
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
38
|
+
return input;
|
|
47
39
|
}
|
|
48
|
-
function
|
|
49
|
-
if (
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return undefined;
|
|
54
|
-
let parsed;
|
|
55
|
-
try {
|
|
56
|
-
parsed = new URL(externalOrigin);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
throw new Error("external_origin must be an http(s) origin");
|
|
40
|
+
function normalizeTimeout(input) {
|
|
41
|
+
if (input === undefined)
|
|
42
|
+
return 0;
|
|
43
|
+
if (!Number.isSafeInteger(input) || input < 0 || input > SDK_DEFAULTS.proxy.maxTimeoutMs) {
|
|
44
|
+
throw new TypeError("timeoutMs must be within the proxy timeout contract");
|
|
60
45
|
}
|
|
61
|
-
|
|
62
|
-
throw new Error("external_origin must be an http(s) origin");
|
|
63
|
-
}
|
|
64
|
-
if (parsed.username !== "" || parsed.password !== "" || (parsed.pathname !== "" && parsed.pathname !== "/") || parsed.search !== "" || parsed.hash !== "") {
|
|
65
|
-
throw new Error("external_origin must be an origin without credentials, path, query, or fragment");
|
|
66
|
-
}
|
|
67
|
-
return parsed.origin;
|
|
46
|
+
return input;
|
|
68
47
|
}
|
|
69
|
-
function
|
|
70
|
-
if (input
|
|
71
|
-
|
|
72
|
-
const s = String(input);
|
|
73
|
-
if (s === "")
|
|
74
|
-
return undefined;
|
|
75
|
-
if (s.trim() !== s || /[\s\u0000-\u001f\u007f]/.test(s)) {
|
|
76
|
-
throw new Error(`${name} must not contain whitespace or control characters`);
|
|
48
|
+
function normalizePath(input) {
|
|
49
|
+
if (input !== input.trim() || !input.startsWith("/") || input.startsWith("//") || /[\u0000-\u0020]/u.test(input) || input.includes("://")) {
|
|
50
|
+
throw new TypeError("proxy path must be an origin-relative path");
|
|
77
51
|
}
|
|
78
|
-
return
|
|
52
|
+
return input;
|
|
53
|
+
}
|
|
54
|
+
function pathName(path) {
|
|
55
|
+
const query = path.indexOf("?");
|
|
56
|
+
return query < 0 ? path : path.slice(0, query);
|
|
79
57
|
}
|
|
80
|
-
function
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
if (!out.includes(s))
|
|
89
|
-
out.push(s);
|
|
58
|
+
function normalizePrefixes(name, values) {
|
|
59
|
+
const result = [];
|
|
60
|
+
for (const raw of values ?? []) {
|
|
61
|
+
const value = normalizePath(raw);
|
|
62
|
+
if (value.includes("?"))
|
|
63
|
+
throw new TypeError(`${name} must not include a query`);
|
|
64
|
+
if (!result.includes(value))
|
|
65
|
+
result.push(value);
|
|
90
66
|
}
|
|
91
|
-
return
|
|
67
|
+
return Object.freeze(result);
|
|
92
68
|
}
|
|
93
69
|
function normalizePathPolicy(policy) {
|
|
94
70
|
return {
|
|
95
|
-
allowedPathPrefixes:
|
|
96
|
-
deniedPathPrefixes:
|
|
97
|
-
allowedWebSocketPathPrefixes:
|
|
98
|
-
deniedWebSocketPathPrefixes:
|
|
71
|
+
allowedPathPrefixes: normalizePrefixes("allowedPathPrefixes", policy?.allowedPathPrefixes),
|
|
72
|
+
deniedPathPrefixes: normalizePrefixes("deniedPathPrefixes", policy?.deniedPathPrefixes),
|
|
73
|
+
allowedWebSocketPathPrefixes: normalizePrefixes("allowedWebSocketPathPrefixes", policy?.allowedWebSocketPathPrefixes),
|
|
74
|
+
deniedWebSocketPathPrefixes: normalizePrefixes("deniedWebSocketPathPrefixes", policy?.deniedWebSocketPathPrefixes),
|
|
99
75
|
};
|
|
100
76
|
}
|
|
101
|
-
function
|
|
102
|
-
const
|
|
103
|
-
const denied = kind === "websocket"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
77
|
+
function enforcePathPolicy(kind, path, policy) {
|
|
78
|
+
const candidate = pathName(path);
|
|
79
|
+
const denied = kind === "websocket"
|
|
80
|
+
? [...policy.deniedPathPrefixes, ...policy.deniedWebSocketPathPrefixes]
|
|
81
|
+
: policy.deniedPathPrefixes;
|
|
82
|
+
if (denied.some((prefix) => candidate.startsWith(prefix)))
|
|
83
|
+
throw new ProxyPolicyError("proxy path denied");
|
|
108
84
|
const allowed = kind === "websocket" && policy.allowedWebSocketPathPrefixes.length > 0
|
|
109
85
|
? policy.allowedWebSocketPathPrefixes
|
|
110
86
|
: policy.allowedPathPrefixes;
|
|
111
|
-
if (allowed.length
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
87
|
+
if (allowed.length > 0 && !allowed.some((prefix) => candidate.startsWith(prefix))) {
|
|
88
|
+
throw new ProxyPolicyError("proxy path not allowed");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function normalizeOrigin(input) {
|
|
92
|
+
if (input === undefined || input === "")
|
|
93
|
+
return undefined;
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = new URL(input);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new TypeError("externalOrigin must be an HTTP origin");
|
|
100
|
+
}
|
|
101
|
+
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || parsed.origin !== input || parsed.username !== "" || parsed.password !== "") {
|
|
102
|
+
throw new TypeError("externalOrigin must be an HTTP origin");
|
|
116
103
|
}
|
|
117
|
-
|
|
104
|
+
return input;
|
|
118
105
|
}
|
|
119
|
-
function
|
|
120
|
-
if (
|
|
121
|
-
return
|
|
122
|
-
if (
|
|
123
|
-
throw new
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
throw new Error(`${name} must be a safe integer`);
|
|
127
|
-
if (n < 0)
|
|
128
|
-
throw new Error(`${name} must be >= 0`);
|
|
129
|
-
if (n === 0)
|
|
130
|
-
return defaultValue;
|
|
131
|
-
return n;
|
|
106
|
+
function normalizeToken(input) {
|
|
107
|
+
if (input === undefined || input === "")
|
|
108
|
+
return undefined;
|
|
109
|
+
if (input !== input.trim() || /[\u0000-\u0020\u007f]/u.test(input) || input.length > 256) {
|
|
110
|
+
throw new TypeError("runtimeRegistrationToken is invalid");
|
|
111
|
+
}
|
|
112
|
+
return input;
|
|
132
113
|
}
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
114
|
+
const BASE_REQUEST_HEADERS = new Set(["accept", "accept-language", "content-type", "if-match", "if-none-match", "range"]);
|
|
115
|
+
const BASE_RESPONSE_HEADERS = new Set(["accept-ranges", "cache-control", "content-disposition", "content-language", "content-range", "content-type", "etag", "expires", "last-modified", "location"]);
|
|
116
|
+
const FORBIDDEN_HEADERS = new Set(["authorization", "connection", "cookie", "host", "keep-alive", "proxy-authorization", "set-cookie", "transfer-encoding", "upgrade"]);
|
|
117
|
+
function normalizeHeaderNames(values) {
|
|
118
|
+
const result = new Set();
|
|
119
|
+
for (const raw of values ?? []) {
|
|
120
|
+
const value = raw.toLowerCase().trim();
|
|
121
|
+
if (!/^[!#$%&'*+\-.^_`|~0-9a-z]+$/u.test(value) || FORBIDDEN_HEADERS.has(value)) {
|
|
122
|
+
throw new TypeError("proxy header allowlist contains a forbidden name");
|
|
123
|
+
}
|
|
124
|
+
result.add(value);
|
|
142
125
|
}
|
|
143
|
-
return
|
|
126
|
+
return result;
|
|
144
127
|
}
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
128
|
+
function filterHeaders(input, base, extra) {
|
|
129
|
+
const result = [];
|
|
130
|
+
for (const entry of input) {
|
|
131
|
+
if (typeof entry?.name !== "string" || typeof entry?.value !== "string")
|
|
132
|
+
continue;
|
|
133
|
+
const name = entry.name.toLowerCase().trim();
|
|
134
|
+
if ((!base.has(name) && !extra.has(name)) || FORBIDDEN_HEADERS.has(name) || /[\r\n]/u.test(entry.value))
|
|
135
|
+
continue;
|
|
136
|
+
result.push(Object.freeze({ name, value: entry.value }));
|
|
150
137
|
}
|
|
151
|
-
return
|
|
138
|
+
return result;
|
|
152
139
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
140
|
+
function parseRuntimeRequest(raw) {
|
|
141
|
+
const headers = Array.isArray(raw.headers) ? raw.headers.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.name === "string" && typeof entry.value === "string") : [];
|
|
142
|
+
const body = raw.body instanceof ArrayBuffer ? raw.body : undefined;
|
|
143
|
+
return {
|
|
144
|
+
id: typeof raw.id === "string" ? raw.id : "",
|
|
145
|
+
method: typeof raw.method === "string" ? raw.method : "GET",
|
|
146
|
+
path: typeof raw.path === "string" ? raw.path : "",
|
|
147
|
+
headers,
|
|
148
|
+
...(typeof raw.external_origin === "string" ? { externalOrigin: raw.external_origin } : {}),
|
|
149
|
+
...(raw.response_flow_control === "chunk_credit_v2" ? { responseFlowControl: "chunk_credit_v2" } : {}),
|
|
150
|
+
...(body === undefined ? {} : { body }),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
class StreamAdmission {
|
|
154
|
+
concurrent;
|
|
155
|
+
queued;
|
|
156
|
+
queuedBodyBytes;
|
|
158
157
|
active = 0;
|
|
159
|
-
|
|
160
|
-
pendingBodyBytes = 0;
|
|
158
|
+
queuedBytes = 0;
|
|
161
159
|
closed = false;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
this.
|
|
165
|
-
this.
|
|
166
|
-
this.
|
|
160
|
+
queue = [];
|
|
161
|
+
constructor(concurrent, queued, queuedBodyBytes) {
|
|
162
|
+
this.concurrent = concurrent;
|
|
163
|
+
this.queued = queued;
|
|
164
|
+
this.queuedBodyBytes = queuedBodyBytes;
|
|
167
165
|
}
|
|
168
|
-
acquire(
|
|
166
|
+
acquire(bytes, signal) {
|
|
169
167
|
if (this.closed)
|
|
170
|
-
return Promise.reject(
|
|
171
|
-
if (signal?.aborted)
|
|
172
|
-
return Promise.reject(
|
|
173
|
-
if (this.active < this.
|
|
168
|
+
return Promise.reject(new SessionError("closed"));
|
|
169
|
+
if (signal?.aborted === true)
|
|
170
|
+
return Promise.reject(new SessionError("canceled"));
|
|
171
|
+
if (this.active < this.concurrent && this.queue.length === 0) {
|
|
174
172
|
this.active++;
|
|
175
|
-
return Promise.resolve(this.
|
|
176
|
-
}
|
|
177
|
-
if (this.pending.length >= this.maxQueued) {
|
|
178
|
-
return Promise.reject(new FlowersecError({
|
|
179
|
-
path: this.path,
|
|
180
|
-
stage: "yamux",
|
|
181
|
-
code: "resource_exhausted",
|
|
182
|
-
message: "proxy runtime HTTP request queue is full",
|
|
183
|
-
}));
|
|
173
|
+
return Promise.resolve(this.releaseFunction());
|
|
184
174
|
}
|
|
185
|
-
if (this.
|
|
186
|
-
return Promise.reject(new
|
|
187
|
-
path: this.path,
|
|
188
|
-
stage: "yamux",
|
|
189
|
-
code: "resource_exhausted",
|
|
190
|
-
message: "proxy runtime HTTP request body queue is full",
|
|
191
|
-
}));
|
|
175
|
+
if (this.queue.length >= this.queued || this.queuedBytes + bytes > this.queuedBodyBytes) {
|
|
176
|
+
return Promise.reject(new SessionError("resource_exhausted"));
|
|
192
177
|
}
|
|
193
178
|
return new Promise((resolve, reject) => {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
resolve,
|
|
197
|
-
reject,
|
|
179
|
+
const entry = {
|
|
180
|
+
bytes,
|
|
198
181
|
...(signal === undefined ? {} : { signal }),
|
|
182
|
+
resolve,
|
|
183
|
+
reject: (error) => reject(error),
|
|
199
184
|
};
|
|
200
|
-
|
|
201
|
-
const index = this.
|
|
185
|
+
const onAbort = () => {
|
|
186
|
+
const index = this.queue.indexOf(entry);
|
|
202
187
|
if (index < 0)
|
|
203
188
|
return;
|
|
204
|
-
this.
|
|
205
|
-
this.
|
|
206
|
-
|
|
207
|
-
reject(this.abortedError());
|
|
189
|
+
this.queue.splice(index, 1);
|
|
190
|
+
this.queuedBytes -= bytes;
|
|
191
|
+
reject(new SessionError("canceled"));
|
|
208
192
|
};
|
|
209
|
-
signal?.addEventListener("abort",
|
|
210
|
-
this.
|
|
211
|
-
this.
|
|
193
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
194
|
+
this.queue.push(entry);
|
|
195
|
+
this.queuedBytes += bytes;
|
|
212
196
|
});
|
|
213
197
|
}
|
|
214
198
|
close() {
|
|
215
199
|
if (this.closed)
|
|
216
200
|
return;
|
|
217
201
|
this.closed = true;
|
|
218
|
-
for (const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
waiter.reject(this.closedError());
|
|
222
|
-
}
|
|
202
|
+
for (const entry of this.queue.splice(0))
|
|
203
|
+
entry.reject(new SessionError("closed"));
|
|
204
|
+
this.queuedBytes = 0;
|
|
223
205
|
}
|
|
224
|
-
|
|
225
|
-
if (this.closed)
|
|
226
|
-
throw this.closedError();
|
|
227
|
-
}
|
|
228
|
-
createRelease() {
|
|
206
|
+
releaseFunction() {
|
|
229
207
|
let released = false;
|
|
230
208
|
return () => {
|
|
231
209
|
if (released)
|
|
232
210
|
return;
|
|
233
211
|
released = true;
|
|
234
|
-
this.active
|
|
212
|
+
this.active--;
|
|
235
213
|
this.drain();
|
|
236
214
|
};
|
|
237
215
|
}
|
|
238
216
|
drain() {
|
|
239
|
-
while (!this.closed && this.active < this.
|
|
240
|
-
const
|
|
241
|
-
this.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
waiter.reject(this.abortedError());
|
|
217
|
+
while (!this.closed && this.active < this.concurrent && this.queue.length > 0) {
|
|
218
|
+
const entry = this.queue.shift();
|
|
219
|
+
this.queuedBytes -= entry.bytes;
|
|
220
|
+
if (entry.signal?.aborted === true) {
|
|
221
|
+
entry.reject(new SessionError("canceled"));
|
|
245
222
|
continue;
|
|
246
223
|
}
|
|
247
224
|
this.active++;
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
cleanupWaiter(waiter) {
|
|
252
|
-
if (waiter.onAbort != null) {
|
|
253
|
-
waiter.signal?.removeEventListener("abort", waiter.onAbort);
|
|
225
|
+
entry.resolve(this.releaseFunction());
|
|
254
226
|
}
|
|
255
227
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
message: "proxy runtime is disposed",
|
|
265
|
-
});
|
|
228
|
+
}
|
|
229
|
+
async function writeChunks(stream, body, chunkBytes, maxBodyBytes, signal) {
|
|
230
|
+
if (body.length > maxBodyBytes)
|
|
231
|
+
throw new SessionError("resource_exhausted");
|
|
232
|
+
for (let offset = 0; offset < body.length; offset += chunkBytes) {
|
|
233
|
+
const chunk = body.subarray(offset, Math.min(body.length, offset + chunkBytes));
|
|
234
|
+
await writeAll(stream, u32be(chunk.length), { signal });
|
|
235
|
+
await writeAll(stream, chunk, { signal });
|
|
266
236
|
}
|
|
237
|
+
await writeAll(stream, u32be(0), { signal });
|
|
267
238
|
}
|
|
268
|
-
async function
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
await
|
|
278
|
-
off = end;
|
|
239
|
+
async function* readChunks(reader, maxChunkBytes, maxBodyBytes) {
|
|
240
|
+
let total = 0;
|
|
241
|
+
while (true) {
|
|
242
|
+
const length = readU32be(await reader.readExactly(4), 0);
|
|
243
|
+
if (length === 0)
|
|
244
|
+
return;
|
|
245
|
+
if (length > maxChunkBytes || total + length > maxBodyBytes)
|
|
246
|
+
throw new SessionError("resource_exhausted");
|
|
247
|
+
total += length;
|
|
248
|
+
yield await reader.readExactly(length);
|
|
279
249
|
}
|
|
280
|
-
await stream.write(u32be(0));
|
|
281
250
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const n = readU32be(lenBuf, 0);
|
|
288
|
-
if (n === 0)
|
|
289
|
-
return;
|
|
290
|
-
if (maxChunkBytes > 0 && n > maxChunkBytes)
|
|
291
|
-
throw new Error("response chunk too large");
|
|
292
|
-
total += n;
|
|
293
|
-
if (maxBodyBytes > 0 && total > maxBodyBytes)
|
|
294
|
-
throw new Error("response body too large");
|
|
295
|
-
const payload = await reader.readExactly(n);
|
|
296
|
-
yield payload;
|
|
297
|
-
}
|
|
251
|
+
function publicFailure(error) {
|
|
252
|
+
if (error instanceof ProxyPolicyError)
|
|
253
|
+
return { status: 403, code: "policy_denied", message: "proxy request denied" };
|
|
254
|
+
if (error instanceof SessionError && (error.code === "resource_exhausted" || error.code === "closed" || error.code === "going_away")) {
|
|
255
|
+
return { status: 503, code: error.code, message: "proxy service unavailable" };
|
|
298
256
|
}
|
|
299
|
-
|
|
257
|
+
if (error instanceof SessionError && error.code === "canceled")
|
|
258
|
+
return { status: 499, code: "canceled", message: "proxy request canceled" };
|
|
259
|
+
return { status: 502, code: "operation_failed", message: "proxy request failed" };
|
|
300
260
|
}
|
|
301
|
-
export function createProxyRuntime(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
const timeoutMs =
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const data = ev.data;
|
|
330
|
-
if (data == null || typeof data !== "object")
|
|
331
|
-
return;
|
|
332
|
-
if (data.type !== "flowersec-proxy:fetch")
|
|
261
|
+
export function createProxyRuntime(options) {
|
|
262
|
+
if (options.session === null || typeof options.session !== "object" || typeof options.session.openStream !== "function") {
|
|
263
|
+
throw new TypeError("proxy runtime requires a Session");
|
|
264
|
+
}
|
|
265
|
+
const maxJsonFrameBytes = positiveLimit("maxJsonFrameBytes", options.maxJsonFrameBytes, SDK_DEFAULTS.proxy.maxJsonFrameBytes);
|
|
266
|
+
const maxChunkBytes = positiveLimit("maxChunkBytes", options.maxChunkBytes, SDK_DEFAULTS.proxy.maxChunkBytes);
|
|
267
|
+
const maxBodyBytes = positiveLimit("maxBodyBytes", options.maxBodyBytes, SDK_DEFAULTS.proxy.maxBodyBytes);
|
|
268
|
+
const maxWsFrameBytes = positiveLimit("maxWsFrameBytes", options.maxWsFrameBytes, SDK_DEFAULTS.proxy.maxWsFrameBytes);
|
|
269
|
+
const maxWsBufferedAmountBytes = positiveLimit("maxWsBufferedAmountBytes", options.maxWsBufferedAmountBytes, DEFAULT_MAX_WS_BUFFERED_AMOUNT_BYTES);
|
|
270
|
+
const maxConcurrentHttpStreams = strictlyPositiveLimit("maxConcurrentHttpStreams", options.maxConcurrentHttpStreams, DEFAULT_MAX_CONCURRENT_HTTP_STREAMS);
|
|
271
|
+
const maxQueuedHttpRequests = positiveLimit("maxQueuedHttpRequests", options.maxQueuedHttpRequests, DEFAULT_MAX_QUEUED_HTTP_REQUESTS);
|
|
272
|
+
const maxQueuedHttpBodyBytes = positiveLimit("maxQueuedHttpBodyBytes", options.maxQueuedHttpBodyBytes, DEFAULT_MAX_QUEUED_HTTP_BODY_BYTES);
|
|
273
|
+
const timeoutMs = normalizeTimeout(options.timeoutMs);
|
|
274
|
+
const pathPolicy = normalizePathPolicy(options.pathPolicy);
|
|
275
|
+
const externalOrigin = normalizeOrigin(options.externalOrigin);
|
|
276
|
+
const runtimeRegistrationToken = normalizeToken(options.runtimeRegistrationToken);
|
|
277
|
+
const requestExtra = normalizeHeaderNames(options.extraRequestHeaders);
|
|
278
|
+
const responseExtra = normalizeHeaderNames(options.extraResponseHeaders);
|
|
279
|
+
const webSocketExtra = normalizeHeaderNames(options.extraWebSocketHeaders);
|
|
280
|
+
const admission = new StreamAdmission(maxConcurrentHttpStreams, maxQueuedHttpRequests, maxQueuedHttpBodyBytes);
|
|
281
|
+
let disposed = false;
|
|
282
|
+
const register = () => void ensureServiceWorkerRuntimeRegistered({
|
|
283
|
+
timeoutMs: 2_000,
|
|
284
|
+
...(runtimeRegistrationToken === undefined ? {} : { runtimeRegistrationToken }),
|
|
285
|
+
}).catch(() => undefined);
|
|
286
|
+
const serviceWorker = globalThis.navigator?.serviceWorker;
|
|
287
|
+
const onMessage = (event) => {
|
|
288
|
+
if (event.data === null || typeof event.data !== "object" || event.data.type !== "flowersec-proxy:fetch")
|
|
333
289
|
return;
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
if (!port)
|
|
290
|
+
const port = event.ports?.[0];
|
|
291
|
+
if (port === undefined)
|
|
337
292
|
return;
|
|
338
|
-
dispatchFetch(
|
|
293
|
+
dispatchFetch(parseRuntimeRequest(event.data.req), port);
|
|
339
294
|
};
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
let
|
|
347
|
-
let
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
responseCreditResolve = null;
|
|
354
|
-
resolve?.();
|
|
355
|
-
};
|
|
356
|
-
port.onmessage = (ev) => {
|
|
357
|
-
const m = ev.data;
|
|
358
|
-
if (!m || typeof m !== "object")
|
|
359
|
-
return;
|
|
360
|
-
if (m.type === "flowersec-proxy:abort") {
|
|
361
|
-
responseCreditAvailable = false;
|
|
362
|
-
ac.abort("aborted");
|
|
363
|
-
wakeResponseCreditWaiter();
|
|
364
|
-
return;
|
|
295
|
+
serviceWorker?.addEventListener("message", onMessage);
|
|
296
|
+
serviceWorker?.addEventListener("controllerchange", register);
|
|
297
|
+
register();
|
|
298
|
+
function dispatchFetch(request, port) {
|
|
299
|
+
const controller = new AbortController();
|
|
300
|
+
let stream;
|
|
301
|
+
let release;
|
|
302
|
+
let credit = request.responseFlowControl !== "chunk_credit_v2";
|
|
303
|
+
let creditWake;
|
|
304
|
+
port.onmessage = (event) => {
|
|
305
|
+
if (event.data?.type === "flowersec-proxy:abort") {
|
|
306
|
+
controller.abort();
|
|
307
|
+
creditWake?.();
|
|
365
308
|
}
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
309
|
+
else if (event.data?.type === "flowersec-proxy:response_credit") {
|
|
310
|
+
credit = true;
|
|
311
|
+
creditWake?.();
|
|
369
312
|
}
|
|
370
313
|
};
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (ac.signal.aborted)
|
|
378
|
-
throw new AbortError("aborted");
|
|
379
|
-
await new Promise((resolve) => {
|
|
380
|
-
responseCreditResolve = resolve;
|
|
381
|
-
});
|
|
314
|
+
const waitForCredit = async () => {
|
|
315
|
+
while (!credit) {
|
|
316
|
+
if (controller.signal.aborted)
|
|
317
|
+
throw new SessionError("canceled");
|
|
318
|
+
await new Promise((resolve) => { creditWake = resolve; });
|
|
319
|
+
creditWake = undefined;
|
|
382
320
|
}
|
|
383
|
-
|
|
384
|
-
throw new AbortError("aborted");
|
|
385
|
-
responseCreditAvailable = false;
|
|
321
|
+
credit = request.responseFlowControl !== "chunk_credit_v2";
|
|
386
322
|
};
|
|
387
323
|
void (async () => {
|
|
388
324
|
try {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
const body =
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const reader =
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
await writeJsonFrame(stream, {
|
|
404
|
-
v:
|
|
325
|
+
if (disposed)
|
|
326
|
+
throw new SessionError("closed");
|
|
327
|
+
const path = normalizePath(request.path);
|
|
328
|
+
enforcePathPolicy("http", path, pathPolicy);
|
|
329
|
+
const body = request.body === undefined ? new Uint8Array() : new Uint8Array(request.body);
|
|
330
|
+
release = await admission.acquire(body.length, controller.signal);
|
|
331
|
+
stream = await options.session.openStream(PROXY_HTTP_STREAM_KIND, {
|
|
332
|
+
signal: controller.signal,
|
|
333
|
+
metadata: createStreamMetadata({ protocol: "flowersec.proxy.http", version: 2 }),
|
|
334
|
+
});
|
|
335
|
+
const reader = new ProxyByteReader(stream, { signal: controller.signal });
|
|
336
|
+
const requestID = request.id.trim() === "" ? randomID() : request.id;
|
|
337
|
+
const headers = filterHeaders(request.headers, BASE_REQUEST_HEADERS, requestExtra);
|
|
338
|
+
const requestOrigin = externalOrigin ?? normalizeOrigin(request.externalOrigin);
|
|
339
|
+
await writeJsonFrame({ write: async (data) => await writeAll(stream, data, { signal: controller.signal }) }, {
|
|
340
|
+
v: PROXY_WIRE_VERSION,
|
|
405
341
|
request_id: requestID,
|
|
406
|
-
method:
|
|
342
|
+
method: request.method.toUpperCase(),
|
|
407
343
|
path,
|
|
408
|
-
headers
|
|
409
|
-
...(
|
|
410
|
-
timeout_ms: timeoutMs
|
|
344
|
+
headers,
|
|
345
|
+
...(requestOrigin === undefined ? {} : { external_origin: requestOrigin }),
|
|
346
|
+
...(timeoutMs === 0 ? {} : { timeout_ms: timeoutMs }),
|
|
411
347
|
});
|
|
412
|
-
await
|
|
413
|
-
const
|
|
414
|
-
if (
|
|
415
|
-
throw new Error("invalid
|
|
416
|
-
}
|
|
417
|
-
if (!respMeta.ok) {
|
|
418
|
-
const msg = respMeta.error?.message ?? "upstream error";
|
|
419
|
-
port.postMessage({
|
|
420
|
-
type: "flowersec-proxy:response_error",
|
|
421
|
-
status: 502,
|
|
422
|
-
...(respMeta.error?.code === undefined ? {} : { code: respMeta.error.code }),
|
|
423
|
-
message: msg,
|
|
424
|
-
});
|
|
425
|
-
await stream.reset(new Error(msg));
|
|
426
|
-
stream = null;
|
|
427
|
-
return;
|
|
348
|
+
await writeChunks(stream, body, maxChunkBytes, maxBodyBytes, controller.signal);
|
|
349
|
+
const response = await readJsonFrame(reader, maxJsonFrameBytes);
|
|
350
|
+
if (response.v !== PROXY_WIRE_VERSION || response.request_id !== requestID || response.ok !== true || !Number.isInteger(response.status)) {
|
|
351
|
+
throw new Error("invalid proxy response");
|
|
428
352
|
}
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
for await (const chunk of chunks) {
|
|
436
|
-
await waitForResponseCredit();
|
|
437
|
-
if (ac.signal.aborted)
|
|
438
|
-
throw new AbortError("aborted");
|
|
439
|
-
// Always transfer an ArrayBuffer (SharedArrayBuffer is not transferable).
|
|
440
|
-
const ab = chunk.slice().buffer;
|
|
441
|
-
port.postMessage({ type: "flowersec-proxy:response_chunk", data: ab }, [ab]);
|
|
353
|
+
const headersOut = filterHeaders(response.headers ?? [], BASE_RESPONSE_HEADERS, responseExtra);
|
|
354
|
+
port.postMessage({ type: "flowersec-proxy:response_meta", status: response.status, headers: headersOut });
|
|
355
|
+
for await (const chunk of readChunks(reader, maxChunkBytes, maxBodyBytes)) {
|
|
356
|
+
await waitForCredit();
|
|
357
|
+
const data = chunk.slice().buffer;
|
|
358
|
+
port.postMessage({ type: "flowersec-proxy:response_chunk", data }, [data]);
|
|
442
359
|
}
|
|
443
360
|
port.postMessage({ type: "flowersec-proxy:response_end" });
|
|
444
361
|
await stream.close();
|
|
445
|
-
stream =
|
|
362
|
+
stream = undefined;
|
|
446
363
|
}
|
|
447
|
-
catch (
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
? e.status
|
|
452
|
-
: code === "resource_exhausted" || code === "not_connected"
|
|
453
|
-
? 503
|
|
454
|
-
: 502;
|
|
455
|
-
port.postMessage({
|
|
456
|
-
type: "flowersec-proxy:response_error",
|
|
457
|
-
status,
|
|
458
|
-
...(code === undefined ? {} : { code }),
|
|
459
|
-
message: msg,
|
|
460
|
-
});
|
|
461
|
-
await stream?.reset(new Error(msg));
|
|
364
|
+
catch (error) {
|
|
365
|
+
const failure = publicFailure(error);
|
|
366
|
+
port.postMessage({ type: "flowersec-proxy:response_error", ...failure });
|
|
367
|
+
await stream?.reset().catch(() => undefined);
|
|
462
368
|
}
|
|
463
369
|
finally {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
try {
|
|
467
|
-
port.close();
|
|
468
|
-
}
|
|
469
|
-
catch {
|
|
470
|
-
// Best-effort.
|
|
471
|
-
}
|
|
370
|
+
release?.();
|
|
371
|
+
port.close();
|
|
472
372
|
}
|
|
473
373
|
})();
|
|
474
|
-
}
|
|
475
|
-
async function openWebSocketStream(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
await
|
|
494
|
-
|
|
374
|
+
}
|
|
375
|
+
async function openWebSocketStream(input, openOptions = {}) {
|
|
376
|
+
if (disposed)
|
|
377
|
+
throw new SessionError("closed");
|
|
378
|
+
const path = normalizePath(input);
|
|
379
|
+
enforcePathPolicy("websocket", path, pathPolicy);
|
|
380
|
+
const stream = await options.session.openStream(PROXY_WEBSOCKET_STREAM_KIND, {
|
|
381
|
+
...(openOptions.signal === undefined ? {} : { signal: openOptions.signal }),
|
|
382
|
+
metadata: createStreamMetadata({ protocol: "flowersec.proxy.websocket", version: 2 }),
|
|
383
|
+
});
|
|
384
|
+
try {
|
|
385
|
+
const protocols = (openOptions.protocols ?? []).filter((value) => value.trim() !== "" && value === value.trim());
|
|
386
|
+
const headers = filterHeaders(protocols.length === 0 ? [] : [{ name: "sec-websocket-protocol", value: protocols.join(", ") }], new Set(["sec-websocket-protocol"]), webSocketExtra);
|
|
387
|
+
await writeJsonFrame({ write: async (data) => await writeAll(stream, data, openOptions.signal === undefined ? {} : { signal: openOptions.signal }) }, {
|
|
388
|
+
v: PROXY_WIRE_VERSION,
|
|
389
|
+
conn_id: randomID(),
|
|
390
|
+
path,
|
|
391
|
+
headers,
|
|
392
|
+
});
|
|
393
|
+
const response = await readJsonFrame(new ProxyByteReader(stream, openOptions.signal === undefined ? {} : { signal: openOptions.signal }), maxJsonFrameBytes);
|
|
394
|
+
if (response.v !== PROXY_WIRE_VERSION || response.ok !== true || (response.protocol !== undefined && typeof response.protocol !== "string")) {
|
|
395
|
+
throw new Error("proxy WebSocket open failed");
|
|
396
|
+
}
|
|
397
|
+
return Object.freeze({ stream, protocol: response.protocol ?? "" });
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
await stream.reset().catch(() => undefined);
|
|
401
|
+
throw error instanceof SessionError ? error : new SessionError("operation_failed");
|
|
495
402
|
}
|
|
496
|
-
return { stream, protocol: resp.protocol ?? "" };
|
|
497
403
|
}
|
|
498
|
-
return {
|
|
499
|
-
limits: {
|
|
404
|
+
return Object.freeze({
|
|
405
|
+
limits: Object.freeze({
|
|
500
406
|
maxJsonFrameBytes,
|
|
501
407
|
maxChunkBytes,
|
|
502
408
|
maxBodyBytes,
|
|
@@ -505,74 +411,55 @@ export function createProxyRuntime(opts) {
|
|
|
505
411
|
maxConcurrentHttpStreams,
|
|
506
412
|
maxQueuedHttpRequests,
|
|
507
413
|
maxQueuedHttpBodyBytes,
|
|
508
|
-
},
|
|
414
|
+
}),
|
|
509
415
|
dispatchFetch,
|
|
510
416
|
openWebSocketStream,
|
|
511
417
|
dispose: () => {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
418
|
+
if (disposed)
|
|
419
|
+
return;
|
|
420
|
+
disposed = true;
|
|
421
|
+
admission.close();
|
|
422
|
+
serviceWorker?.removeEventListener("message", onMessage);
|
|
423
|
+
serviceWorker?.removeEventListener("controllerchange", register);
|
|
424
|
+
},
|
|
425
|
+
});
|
|
517
426
|
}
|
|
518
|
-
export async function ensureServiceWorkerRuntimeRegistered(
|
|
519
|
-
const
|
|
520
|
-
if (
|
|
427
|
+
export async function ensureServiceWorkerRuntimeRegistered(options = {}) {
|
|
428
|
+
const controller = globalThis.navigator?.serviceWorker?.controller;
|
|
429
|
+
if (controller === null || controller === undefined || typeof controller.postMessage !== "function")
|
|
521
430
|
return;
|
|
522
|
-
const timeoutMs =
|
|
523
|
-
|
|
524
|
-
|
|
431
|
+
const timeoutMs = options.timeoutMs ?? 2_000;
|
|
432
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 60_000)
|
|
433
|
+
throw new TypeError("invalid runtime registration timeout");
|
|
434
|
+
const token = normalizeToken(options.runtimeRegistrationToken);
|
|
435
|
+
const channel = new MessageChannel();
|
|
525
436
|
await new Promise((resolve, reject) => {
|
|
526
|
-
let
|
|
527
|
-
let timer = null;
|
|
437
|
+
let settled = false;
|
|
528
438
|
const finish = (error) => {
|
|
529
|
-
if (
|
|
439
|
+
if (settled)
|
|
530
440
|
return;
|
|
531
|
-
|
|
532
|
-
if (timer
|
|
441
|
+
settled = true;
|
|
442
|
+
if (timer !== undefined)
|
|
533
443
|
clearTimeout(timer);
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
try {
|
|
537
|
-
ch.port1.close();
|
|
538
|
-
}
|
|
539
|
-
catch {
|
|
540
|
-
// Best-effort.
|
|
541
|
-
}
|
|
542
|
-
if (error == null) {
|
|
543
|
-
resolve();
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
444
|
+
channel.port1.close();
|
|
445
|
+
error === undefined ? resolve() : reject(error);
|
|
547
446
|
};
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (data == null || typeof data !== "object")
|
|
447
|
+
channel.port1.onmessage = (event) => {
|
|
448
|
+
if (event.data?.type !== "flowersec-proxy:register-runtime-ack")
|
|
551
449
|
return;
|
|
552
|
-
|
|
553
|
-
return;
|
|
554
|
-
if (data.ok !== true) {
|
|
555
|
-
finish(new Error("service worker runtime registration was rejected"));
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
finish();
|
|
450
|
+
finish(event.data.ok === true ? undefined : new Error("service worker runtime registration rejected"));
|
|
559
451
|
};
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
};
|
|
563
|
-
if (timeoutMs > 0) {
|
|
564
|
-
timer = setTimeout(() => {
|
|
565
|
-
finish(new Error("service worker runtime registration timed out"));
|
|
566
|
-
}, timeoutMs);
|
|
567
|
-
}
|
|
452
|
+
channel.port1.onmessageerror = () => finish(new Error("service worker runtime registration failed"));
|
|
453
|
+
const timer = timeoutMs === 0 ? undefined : setTimeout(() => finish(new Error("service worker runtime registration timed out")), timeoutMs);
|
|
568
454
|
try {
|
|
569
|
-
|
|
455
|
+
controller.postMessage({
|
|
570
456
|
type: "flowersec-proxy:register-runtime",
|
|
571
|
-
|
|
572
|
-
|
|
457
|
+
version: 2,
|
|
458
|
+
...(token === undefined ? {} : { token }),
|
|
459
|
+
}, [channel.port2]);
|
|
573
460
|
}
|
|
574
|
-
catch
|
|
575
|
-
finish(
|
|
461
|
+
catch {
|
|
462
|
+
finish(new Error("service worker runtime registration failed"));
|
|
576
463
|
}
|
|
577
464
|
});
|
|
578
465
|
}
|