hyperbrane 0.1.2 → 0.2.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 +67 -25
- package/dist/agent/agentClient.d.ts +5 -0
- package/dist/agent/agentClient.js +4 -0
- package/dist/agent/agentService.d.ts +9 -37
- package/dist/agent/agentService.js +62 -322
- package/dist/agent/agentStorage.d.ts +1 -20
- package/dist/agent/agentStorage.js +0 -30
- package/dist/agent/api.d.ts +80 -151
- package/dist/agent/api.js +1 -8
- package/dist/agent/client.d.ts +3 -1
- package/dist/agent/client.js +2 -3
- package/dist/agent/control.d.ts +23 -0
- package/dist/agent/control.js +479 -0
- package/dist/agent/hostTranscript.d.ts +0 -6
- package/dist/agent/hostTranscript.js +3 -14
- package/dist/agent/session/backend.d.ts +17 -32
- package/dist/agent/session/claude/home.d.ts +0 -1
- package/dist/agent/session/claude/home.js +0 -1
- package/dist/agent/session/claude/provision.d.ts +10 -0
- package/dist/agent/session/claude/provision.js +17 -0
- package/dist/agent/session/claude/transcript.d.ts +2 -7
- package/dist/agent/session/claude/transcript.js +68 -14
- package/dist/agent/session/client.d.ts +1 -10
- package/dist/agent/session/client.js +0 -3
- package/dist/agent/session/codex/provision.d.ts +14 -0
- package/dist/agent/session/codex/provision.js +25 -0
- package/dist/agent/session/fileTranscript.d.ts +6 -49
- package/dist/agent/session/fileTranscript.js +26 -82
- package/dist/agent/session/install.d.ts +5 -0
- package/dist/agent/session/install.js +35 -0
- package/dist/agent/session/sessionStorage.d.ts +4 -35
- package/dist/agent/session/sessionStorage.js +10 -16
- package/dist/agent/session/sharedConnection.d.ts +0 -8
- package/dist/agent/session/sharedConnection.js +0 -7
- package/dist/agent/session/storedTranscripts.d.ts +1 -6
- package/dist/agent/session/storedTranscripts.js +0 -3
- package/dist/agent/session/worker/jsonRpc.d.ts +2 -13
- package/dist/agent/session/worker/jsonRpc.js +2 -11
- package/dist/agent/session/worker/protocol.d.ts +26 -20
- package/dist/agent/session/worker/protocol.js +29 -18
- package/dist/agent/session/worker/workerParams.d.ts +0 -5
- package/dist/agent/session/worker/workerParams.js +14 -14
- package/dist/agent/session/workerConnection.d.ts +2 -13
- package/dist/agent/session/workerConnection.js +7 -12
- package/dist/agent/session/workerGatewayConnection.d.ts +18 -0
- package/dist/agent/session/workerGatewayConnection.js +86 -0
- package/dist/agent/vendor-types/manifest.json +87 -0
- package/dist/agent/workerBundle.d.ts +4 -19
- package/dist/agent/workerBundle.generated.js +1 -1
- package/dist/agent/workerBundle.js +3 -30
- package/dist/agent/workerReplacement.d.ts +3 -0
- package/dist/agent/workerReplacement.js +49 -0
- package/dist/cli/agent.js +31 -16
- package/dist/cli/command.d.ts +10 -11
- package/dist/cli/command.js +0 -4
- package/dist/cli/dimension.js +6 -6
- package/dist/cli/gateway.js +3 -6
- package/dist/cli/main.js +14 -31
- package/dist/cli/streamPrinter.d.ts +2 -10
- package/dist/cli/streamPrinter.js +16 -46
- package/dist/cli/table.d.ts +0 -3
- package/dist/cli/table.js +0 -4
- package/dist/dimension/api.d.ts +39 -27
- package/dist/dimension/api.js +28 -47
- package/dist/dimension/client.d.ts +5 -0
- package/dist/dimension/client.js +4 -0
- package/dist/dimension/control.d.ts +21 -0
- package/dist/dimension/control.js +328 -0
- package/dist/dimension/image.d.ts +0 -5
- package/dist/dimension/image.js +0 -8
- package/dist/dimension/imageFiles.generated.js +1 -1
- package/dist/dimension/paths.d.ts +5 -4
- package/dist/dimension/paths.js +15 -6
- package/dist/dimension/service.d.ts +8 -24
- package/dist/dimension/service.js +60 -228
- package/dist/dimension/transport-worker.d.ts +1 -0
- package/dist/dimension/transport-worker.js +151 -0
- package/dist/dimension/transportBundle.d.ts +2 -0
- package/dist/dimension/transportBundle.generated.d.ts +1 -0
- package/dist/dimension/transportBundle.generated.js +2 -0
- package/dist/dimension/transportBundle.js +4 -0
- package/dist/dimension/transportClient.d.ts +10 -0
- package/dist/dimension/transportClient.js +115 -0
- package/dist/dimension/transportProtocol.d.ts +53 -0
- package/dist/dimension/transportProtocol.js +8 -0
- package/dist/index.d.ts +5 -9
- package/dist/index.js +5 -19
- package/dist/lib/control.d.ts +8 -0
- package/dist/lib/control.js +62 -0
- package/dist/lib/docker.d.ts +0 -22
- package/dist/lib/docker.js +0 -10
- package/dist/lib/image.d.ts +0 -3
- package/dist/lib/image.js +0 -2
- package/dist/lib/jsonDirectory.d.ts +0 -13
- package/dist/lib/jsonDirectory.js +10 -15
- package/dist/lib/scriptBundle.d.ts +6 -0
- package/dist/lib/scriptBundle.js +28 -0
- package/dist/lib/sh.d.ts +2 -0
- package/dist/lib/sh.js +8 -0
- package/dist/lib/sleep.d.ts +0 -1
- package/dist/lib/sleep.js +0 -1
- package/dist/lib/streamFetch.d.ts +15 -0
- package/dist/lib/streamFetch.js +150 -0
- package/dist/lib/tar.d.ts +0 -1
- package/dist/lib/tar.js +5 -11
- package/dist/portal-gateway/api.d.ts +13 -44
- package/dist/portal-gateway/api.js +1 -3
- package/dist/portal-gateway/control.d.ts +42 -0
- package/dist/portal-gateway/control.js +657 -0
- package/dist/portal-gateway/gatewayClient.d.ts +5 -0
- package/dist/portal-gateway/gatewayClient.js +4 -0
- package/dist/portal-gateway/gatewayService.d.ts +8 -38
- package/dist/portal-gateway/gatewayService.js +68 -254
- package/dist/portal-gateway/image.d.ts +0 -1
- package/dist/portal-gateway/image.js +0 -2
- package/dist/portal-gateway/imageFiles.generated.js +2 -2
- package/dist/portal-gateway/portalClient.d.ts +5 -0
- package/dist/portal-gateway/portalClient.js +4 -0
- package/dist/portal-gateway/portalService.d.ts +5 -47
- package/dist/portal-gateway/portalService.js +19 -349
- package/dist/portal-gateway/transport-worker.d.ts +1 -0
- package/dist/portal-gateway/transport-worker.js +205 -0
- package/dist/portal-gateway/transportBundle.d.ts +2 -0
- package/dist/portal-gateway/transportBundle.generated.d.ts +1 -0
- package/dist/portal-gateway/transportBundle.generated.js +2 -0
- package/dist/portal-gateway/transportBundle.js +4 -0
- package/dist/portal-gateway/upstream.d.ts +0 -81
- package/dist/portal-gateway/upstream.js +0 -1
- package/dist/sandbox/api.d.ts +121 -187
- package/dist/sandbox/api.js +5 -7
- package/dist/sandbox/client.d.ts +2 -0
- package/dist/sandbox/client.js +4 -0
- package/dist/sandbox/data-endpoints/dataEndpoints.d.ts +13 -0
- package/dist/sandbox/data-endpoints/dataEndpoints.js +100 -0
- package/dist/sandbox/data-endpoints/firewalledPorts.d.ts +4 -0
- package/dist/sandbox/data-endpoints/firewalledPorts.js +28 -0
- package/dist/sandbox/lib/httpOverStream.d.ts +3 -0
- package/dist/sandbox/lib/httpOverStream.js +21 -0
- package/dist/sandbox/lib/remoteFileOperations.d.ts +10 -0
- package/dist/sandbox/lib/remoteFileOperations.js +70 -0
- package/dist/sandbox/lib/shellCommand.d.ts +3 -0
- package/dist/sandbox/lib/shellCommand.js +7 -0
- package/dist/sandbox/providers/boat/boatService.d.ts +2 -0
- package/dist/sandbox/providers/boat/boatService.js +321 -0
- package/dist/sandbox/providers/exedev/exeDevService.d.ts +2 -0
- package/dist/sandbox/providers/exedev/exeDevService.js +127 -0
- package/dist/sandbox/providers/exedev/exeDevToken.d.ts +1 -0
- package/dist/sandbox/providers/exedev/exeDevToken.js +54 -0
- package/dist/sandbox/providers/local/localService.d.ts +2 -0
- package/dist/sandbox/providers/local/localService.js +219 -0
- package/dist/sandbox/providers/ssh/sshService.d.ts +2 -0
- package/dist/sandbox/providers/ssh/sshService.js +82 -0
- package/dist/sandbox/providers/ssh/sshTransport.d.ts +12 -0
- package/dist/sandbox/providers/ssh/sshTransport.js +216 -0
- package/dist/sandbox/service.d.ts +2 -0
- package/dist/sandbox/service.js +18 -0
- package/dist/sandbox/storage/descriptors.d.ts +6 -0
- package/dist/sandbox/storage/descriptors.js +25 -0
- package/dist/sandbox/storage/sandboxStorage.d.ts +11 -0
- package/dist/sandbox/storage/sandboxStorage.js +42 -0
- package/dist/upstreams/anthropic.d.ts +0 -5
- package/dist/upstreams/anthropic.js +1 -8
- package/dist/upstreams/index.d.ts +2 -6
- package/dist/upstreams/index.js +0 -4
- package/dist/upstreams/openai.d.ts +0 -5
- package/dist/upstreams/openai.js +1 -13
- package/package.json +12 -4
- package/dist/agent/gatewaySignIns.d.ts +0 -5
- package/dist/agent/gatewaySignIns.js +0 -11
- package/dist/agent/session/codex/home.d.ts +0 -2
- package/dist/agent/session/codex/home.js +0 -4
- package/dist/agent/session/codex/messages.d.ts +0 -30
- package/dist/agent/session/codex/messages.js +0 -135
- package/dist/agent/session/codex/transcript.d.ts +0 -12
- package/dist/agent/session/codex/transcript.js +0 -22
- package/dist/agent/session/sdkMessages.d.ts +0 -15
- package/dist/agent/session/sdkMessages.js +0 -28
- package/dist/agent/session/workerSseConnection.d.ts +0 -31
- package/dist/agent/session/workerSseConnection.js +0 -76
- package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.d.ts +0 -10
- package/dist/agent/vendor-types/codex/app-server/AbsolutePathBuf.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.d.ts +0 -2
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputBody.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.d.ts +0 -22
- package/dist/agent/vendor-types/codex/app-server/FunctionCallOutputContentItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/ImageDetail.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/ImageDetail.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.d.ts +0 -5
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationFailure.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.d.ts +0 -11
- package/dist/agent/vendor-types/codex/app-server/ImageGenerationItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.d.ts +0 -23
- package/dist/agent/vendor-types/codex/app-server/LegacyAppPathString.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/MessagePhase.d.ts +0 -7
- package/dist/agent/vendor-types/codex/app-server/MessagePhase.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.d.ts +0 -4
- package/dist/agent/vendor-types/codex/app-server/ReasoningEffort.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/SleepItem.d.ts +0 -7
- package/dist/agent/vendor-types/codex/app-server/SleepItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/WebSearchItem.d.ts +0 -14
- package/dist/agent/vendor-types/codex/app-server/WebSearchItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.d.ts +0 -3
- package/dist/agent/vendor-types/codex/app-server/serde_json/JsonValue.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/AgentMessageDelivery.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.d.ts +0 -4
- package/dist/agent/vendor-types/codex/app-server/v2/AsyncUserInputQuestion.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.d.ts +0 -4
- package/dist/agent/vendor-types/codex/app-server/v2/ByteRange.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.d.ts +0 -5
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentState.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentTool.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/CollabAgentToolCallStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.d.ts +0 -19
- package/dist/agent/vendor-types/codex/app-server/v2/CommandAction.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionSource.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/CommandExecutionStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.d.ts +0 -10
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallOutputContentItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/DynamicToolCallStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.d.ts +0 -6
- package/dist/agent/vendor-types/codex/app-server/v2/FileUpdateChange.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.d.ts +0 -4
- package/dist/agent/vendor-types/codex/app-server/v2/HookPromptFragment.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppDisplayMode.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.d.ts +0 -8
- package/dist/agent/vendor-types/codex/app-server/v2/McpAppUi.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.d.ts +0 -7
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallAppContext.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.d.ts +0 -3
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallError.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.d.ts +0 -6
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallResult.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/McpToolCallStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.d.ts +0 -5
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitation.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.d.ts +0 -6
- package/dist/agent/vendor-types/codex/app-server/v2/MemoryCitationEntry.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/PatchApplyStatus.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.d.ts +0 -8
- package/dist/agent/vendor-types/codex/app-server/v2/PatchChangeKind.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.d.ts +0 -1
- package/dist/agent/vendor-types/codex/app-server/v2/SubAgentActivityKind.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/TextElement.d.ts +0 -11
- package/dist/agent/vendor-types/codex/app-server/v2/TextElement.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.d.ts +0 -215
- package/dist/agent/vendor-types/codex/app-server/v2/ThreadItem.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/UserInput.d.ts +0 -35
- package/dist/agent/vendor-types/codex/app-server/v2/UserInput.js +0 -2
- package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.d.ts +0 -14
- package/dist/agent/vendor-types/codex/app-server/v2/WebSearchAction.js +0 -2
- package/dist/cli/services.d.ts +0 -19
- package/dist/cli/services.js +0 -34
- package/dist/dimension/http.d.ts +0 -7
- package/dist/dimension/http.js +0 -77
- package/dist/dimension/storage.d.ts +0 -39
- package/dist/dimension/storage.js +0 -79
- package/dist/portal-gateway/fakes.d.ts +0 -3
- package/dist/portal-gateway/fakes.js +0 -11
- package/dist/portal-gateway/gatewayStorage.d.ts +0 -66
- package/dist/portal-gateway/gatewayStorage.js +0 -125
- package/dist/portal-gateway/portalStorage.d.ts +0 -34
- package/dist/portal-gateway/portalStorage.js +0 -44
- package/dist/sandbox/boatService.d.ts +0 -52
- package/dist/sandbox/boatService.js +0 -343
- package/dist/sandbox/containerService.d.ts +0 -15
- package/dist/sandbox/containerService.js +0 -184
- package/dist/sandbox/endpointProxy.d.ts +0 -90
- package/dist/sandbox/endpointProxy.js +0 -716
- package/dist/sandbox/exeDevAuth.d.ts +0 -16
- package/dist/sandbox/exeDevAuth.js +0 -49
- package/dist/sandbox/localService.d.ts +0 -19
- package/dist/sandbox/localService.js +0 -200
- package/dist/sandbox/sseConnection.d.ts +0 -41
- package/dist/sandbox/sseConnection.js +0 -166
- package/dist/sandbox/sshService.d.ts +0 -30
- package/dist/sandbox/sshService.js +0 -680
|
@@ -1,41 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import type { GatewayStorage } from "./gatewayStorage.ts";
|
|
6
|
-
import type { PortalStorage } from "./portalStorage.ts";
|
|
7
|
-
import type { PortalService } from "./portalService.ts";
|
|
1
|
+
import type { Sandbox, SandboxDataEndpoint } from "../sandbox/api.ts";
|
|
2
|
+
import { type GatewayClient } from "./api.ts";
|
|
3
|
+
import { GATEWAY_STATE_PATH, gatewayCaCertificatePath, gatewayContainerName } from "./control.ts";
|
|
4
|
+
export { GATEWAY_STATE_PATH, gatewayCaCertificatePath, gatewayContainerName };
|
|
8
5
|
/** @sealed */
|
|
9
|
-
export interface GatewayService {
|
|
10
|
-
createGateway(gateway: Gateway): Promise<Gateway>;
|
|
11
|
-
destroyGateway(gatewayName: string): Promise<void>;
|
|
12
|
-
listGateways(): Promise<Gateway[]>;
|
|
13
|
-
restartGateway(gatewayName: string): Promise<void>;
|
|
14
|
-
listCredentials(gatewayName: string): Promise<GatewayCredential[]>;
|
|
15
|
-
deleteCredential(gatewayName: string, credentialName: string): Promise<void>;
|
|
16
|
-
registerUpstream(gatewayName: string, upstream: Upstream): Promise<Upstream>;
|
|
17
|
-
listUpstreams(gatewayName: string): Promise<Upstream[]>;
|
|
18
|
-
removeUpstream(gatewayName: string, upstreamName: string): Promise<void>;
|
|
19
|
-
setApiKey(gatewayName: string, upstreamName: string, keyName: string, key: string): Promise<{
|
|
20
|
-
fake: string;
|
|
21
|
-
}>;
|
|
22
|
-
deleteApiKey(gatewayName: string, upstreamName: string, keyName: string): Promise<void>;
|
|
23
|
-
createTrafficRule(gatewayName: string, rule: TrafficRule): Promise<TrafficRule>;
|
|
24
|
-
updateTrafficRule(gatewayName: string, rule: TrafficRule): Promise<TrafficRule>;
|
|
25
|
-
deleteTrafficRule(gatewayName: string, ruleName: string): Promise<void>;
|
|
26
|
-
listTrafficRules(gatewayName: string): Promise<TrafficRule[]>;
|
|
27
|
-
trafficRuleRespond(gatewayName: string, ruleName: string, action: "ALLOW" | "DENY"): Promise<void>;
|
|
6
|
+
export interface GatewayService extends GatewayClient {
|
|
28
7
|
}
|
|
29
|
-
/** Where the gateway's state directory is mounted in its container. */
|
|
30
|
-
export declare const GATEWAY_STATE_PATH = "/state";
|
|
31
|
-
export declare function gatewayContainerName(gatewayName: string): string;
|
|
32
|
-
/** Host path of the CA that signs everything the gateway intercepts; dimensions behind it must trust it. */
|
|
33
|
-
export declare function gatewayCaCertificatePath(stateDirectory: string): string;
|
|
34
8
|
export declare function assertValidName(kind: string, name: string): void;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*/
|
|
39
|
-
export declare function createGatewayService(storage: GatewayStorage, portals: PortalStorage, docker: DockerClient, image: ManagedImage, host: Sandbox,
|
|
40
|
-
/** Re-wires the portals of a gateway whose container restarted or was replaced. */
|
|
41
|
-
restoring?: Pick<PortalService, "restorePortals">): GatewayService;
|
|
9
|
+
export declare function createGatewayService(host: Sandbox & SandboxDataEndpoint, { dataDirectory }?: {
|
|
10
|
+
dataDirectory?: string;
|
|
11
|
+
}): GatewayService;
|
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
import { isIPv4 } from "node:net";
|
|
2
|
-
import {
|
|
2
|
+
import { createDockerClient } from "../lib/docker.js";
|
|
3
|
+
import { createGatewayImage } from "./image.js";
|
|
3
4
|
import { InvalidNameError, checkName } from "../lib/jsonDirectory.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { HYPERBRANE_VERSION } from "../lib/version.js";
|
|
7
|
-
import { GatewayConflictError, GatewayInvalidError, GatewayNotFoundError, } from "./api.js";
|
|
8
|
-
import { issueKeyFake } from "./fakes.js";
|
|
5
|
+
import { GatewayInvalidError, GatewayNotFoundError } from "./api.js";
|
|
6
|
+
import { GATEWAY_STATE_PATH, gatewayCaCertificatePath, gatewayContainerName, gatewayControl } from "./control.js";
|
|
9
7
|
import { BUILTIN_UPSTREAMS } from "../upstreams/index.js";
|
|
10
|
-
|
|
11
|
-
export const GATEWAY_STATE_PATH = "/state";
|
|
12
|
-
const PROXY_PORT = 8080;
|
|
13
|
-
const DNS_PORT = 5353;
|
|
14
|
-
const READY_TIMEOUT_MS = 60_000;
|
|
15
|
-
// "hybgw-" can't collide with a dimension's "hyb-<name>": dimension names start with a letter or digit.
|
|
16
|
-
export function gatewayContainerName(gatewayName) {
|
|
17
|
-
return `hybgw-${gatewayName}`;
|
|
18
|
-
}
|
|
19
|
-
/** Host path of the CA that signs everything the gateway intercepts; dimensions behind it must trust it. */
|
|
20
|
-
export function gatewayCaCertificatePath(stateDirectory) {
|
|
21
|
-
return `${stateDirectory}/mitmproxy/mitmproxy-ca-cert.pem`;
|
|
22
|
-
}
|
|
8
|
+
export { GATEWAY_STATE_PATH, gatewayCaCertificatePath, gatewayContainerName };
|
|
23
9
|
export function assertValidName(kind, name) {
|
|
24
10
|
try {
|
|
25
11
|
checkName(name);
|
|
@@ -30,176 +16,51 @@ export function assertValidName(kind, name) {
|
|
|
30
16
|
throw error;
|
|
31
17
|
}
|
|
32
18
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
export function createGatewayService(
|
|
38
|
-
|
|
39
|
-
restoring) {
|
|
40
|
-
// Keeps no state between calls: the record is written first so a slow or crashed create stays
|
|
41
|
-
// visible, and every later step checks before it acts, so re-running converges.
|
|
19
|
+
const FAKE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
20
|
+
function endpointName(gatewayName) {
|
|
21
|
+
return `gateway-${gatewayName}`;
|
|
22
|
+
}
|
|
23
|
+
export function createGatewayService(host, { dataDirectory } = {}) {
|
|
24
|
+
const control = gatewayControl(host, () => createGatewayImage(createDockerClient(host)).ensure(), { dataDirectory });
|
|
42
25
|
async function createGateway(gateway) {
|
|
43
26
|
assertValidName("gateway", gateway.name);
|
|
44
27
|
const upstreams = (gateway.upstreams ?? BUILTIN_UPSTREAMS).map(validUpstream);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (existing)
|
|
48
|
-
assertOwnedContainer(existing, gateway.name);
|
|
49
|
-
const record = await storage.insertGateway({ name: gateway.name, state: "creating" });
|
|
50
|
-
const stateDirectory = await storage.ensureStateDirectory(gateway.name);
|
|
51
|
-
for (const upstream of upstreams)
|
|
52
|
-
await storage.saveUpstream(gateway.name, upstream);
|
|
53
|
-
const imageTag = await image.ensure();
|
|
54
|
-
await ensureContainer(gateway.name, stateDirectory, imageTag);
|
|
55
|
-
await docker.startContainer(name);
|
|
56
|
-
await awaitReady(gateway.name, stateDirectory);
|
|
57
|
-
if (record.state !== "ready")
|
|
58
|
-
await storage.saveGateway({ name: gateway.name, state: "ready" });
|
|
59
|
-
// A replaced container (an older image's) came without them; still wired, this is one look.
|
|
60
|
-
await restoring?.restorePortals({ gatewayName: gateway.name });
|
|
28
|
+
await control.createGateway(gateway.name, upstreams);
|
|
29
|
+
await control.startTransport(gateway.name, await host.openEndpoint(endpointName(gateway.name)), { restart: false });
|
|
61
30
|
return { name: gateway.name };
|
|
62
31
|
}
|
|
63
|
-
// A container from an older image is replaced: everything worth keeping (CA, credentials, rules)
|
|
64
|
-
// is in the mounted state directory. createGateway re-wires its portals once it's ready.
|
|
65
|
-
async function ensureContainer(gatewayName, stateDirectory, imageTag) {
|
|
66
|
-
const name = gatewayContainerName(gatewayName);
|
|
67
|
-
const existing = await docker.inspectContainer(name);
|
|
68
|
-
if (existing) {
|
|
69
|
-
assertOwnedContainer(existing, gatewayName);
|
|
70
|
-
if (existing.Config?.Image === imageTag)
|
|
71
|
-
return;
|
|
72
|
-
await docker.removeContainer(name);
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
await docker.createContainer(name, {
|
|
76
|
-
Image: imageTag,
|
|
77
|
-
Hostname: name,
|
|
78
|
-
Labels: {
|
|
79
|
-
hyperbrane: "true",
|
|
80
|
-
"hyperbrane.version": HYPERBRANE_VERSION,
|
|
81
|
-
"hyperbrane.gateway": gatewayName,
|
|
82
|
-
},
|
|
83
|
-
HostConfig: {
|
|
84
|
-
// A mount, not a copy: rules and credentials written by the service take effect on the next
|
|
85
|
-
// request, and the CA survives the container being recreated.
|
|
86
|
-
Binds: [`${stateDirectory}:${GATEWAY_STATE_PATH}`],
|
|
87
|
-
CapAdd: ["NET_ADMIN"],
|
|
88
|
-
NetworkMode: "bridge",
|
|
89
|
-
Init: true,
|
|
90
|
-
RestartPolicy: { Name: "unless-stopped" },
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
// Lost a race with another creator: fine as long as the winner is ours.
|
|
96
|
-
if (!(error instanceof DockerError && error.status === 409))
|
|
97
|
-
throw error;
|
|
98
|
-
assertOwnedContainer(await awaitContainer(name, error), gatewayName);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// Docker reserves the name before the winner's container becomes inspectable, so give it a moment.
|
|
102
|
-
async function awaitContainer(name, conflict) {
|
|
103
|
-
for (let attempt = 0; attempt < 50; attempt++) {
|
|
104
|
-
const container = await docker.inspectContainer(name);
|
|
105
|
-
if (container)
|
|
106
|
-
return container;
|
|
107
|
-
await sleep(100);
|
|
108
|
-
}
|
|
109
|
-
throw conflict;
|
|
110
|
-
}
|
|
111
|
-
// Ready = the proxy and its resolver are listening (so the redirect rules are in, and nothing
|
|
112
|
-
// through a portal is refused) and the CA exists (so portals can install it into dimensions).
|
|
113
|
-
async function awaitReady(gatewayName, stateDirectory) {
|
|
114
|
-
const name = gatewayContainerName(gatewayName);
|
|
115
|
-
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
116
|
-
while (Date.now() < deadline) {
|
|
117
|
-
if ((await proxyListening(name)) && (await exists(gatewayCaCertificatePath(stateDirectory))))
|
|
118
|
-
return;
|
|
119
|
-
await sleep(250);
|
|
120
|
-
}
|
|
121
|
-
const state = (await docker.inspectContainer(name))?.State;
|
|
122
|
-
throw new Error(`gateway ${gatewayName} did not become ready: container ${state?.Status ?? "missing"}, exit code ${state?.ExitCode ?? "-"}`);
|
|
123
|
-
}
|
|
124
|
-
async function proxyListening(name) {
|
|
125
|
-
try {
|
|
126
|
-
const result = await docker.exec(name, { cmd: ["ss", "-Hln", `( sport = :${PROXY_PORT} or sport = :${DNS_PORT} )`], user: "root" });
|
|
127
|
-
// Both: TCP for the proxy, UDP for the resolver.
|
|
128
|
-
return result.exitCode === 0 && /^tcp\b/m.test(result.stdout) && /^udp\b/m.test(result.stdout);
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
// Not running (yet, or restarting after a failed start).
|
|
132
|
-
if (error instanceof DockerError && error.status === 409)
|
|
133
|
-
return false;
|
|
134
|
-
throw error;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async function exists(path) {
|
|
138
|
-
try {
|
|
139
|
-
await host.stat(path);
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
if (error instanceof SandboxNotFoundError)
|
|
144
|
-
return false;
|
|
145
|
-
throw error;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// Container first: if this crashes before the record is removed, the gateway still shows up and
|
|
149
|
-
// the destroy can be retried. Its state (credentials included) goes with the record.
|
|
150
32
|
async function destroyGateway(gatewayName) {
|
|
151
33
|
assertValidName("gateway", gatewayName);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
throw new GatewayConflictError(`gateway ${gatewayName} still has portals: ${attached.map((portal) => portal.name).join(", ")}`);
|
|
155
|
-
}
|
|
156
|
-
const name = gatewayContainerName(gatewayName);
|
|
157
|
-
const existing = await docker.inspectContainer(name);
|
|
158
|
-
if (existing) {
|
|
159
|
-
assertOwnedContainer(existing, gatewayName);
|
|
160
|
-
await docker.removeContainer(name);
|
|
161
|
-
}
|
|
162
|
-
await storage.removeGateway(gatewayName);
|
|
34
|
+
await control.destroyGateway(gatewayName);
|
|
35
|
+
await host.closeEndpoint(endpointName(gatewayName));
|
|
163
36
|
}
|
|
164
37
|
async function restartGateway(gatewayName) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (!container)
|
|
169
|
-
throw new GatewayNotFoundError(`gateway ${gatewayName} has no container; create it again`);
|
|
170
|
-
assertOwnedContainer(container, gatewayName);
|
|
171
|
-
await docker.restartContainer(name);
|
|
172
|
-
await awaitReady(gatewayName, await storage.ensureStateDirectory(record.name));
|
|
173
|
-
// The restart took its end of every portal with its network namespace.
|
|
174
|
-
await restoring?.restorePortals({ gatewayName });
|
|
38
|
+
assertValidName("gateway", gatewayName);
|
|
39
|
+
await control.restartGateway(gatewayName);
|
|
40
|
+
await control.startTransport(gatewayName, await host.openEndpoint(endpointName(gatewayName)), { restart: true });
|
|
175
41
|
}
|
|
176
|
-
async function
|
|
177
|
-
|
|
42
|
+
async function endpoint(gatewayName, options) {
|
|
43
|
+
assertValidName("gateway", gatewayName);
|
|
44
|
+
const name = endpointName(gatewayName);
|
|
45
|
+
const found = (await host.listEndpoints()).find((candidate) => candidate.name === name);
|
|
46
|
+
if (!found)
|
|
47
|
+
throw new GatewayNotFoundError(`gateway ${gatewayName} has no data endpoint; create it again`);
|
|
48
|
+
return { url: found.url, credentials: await host.credentials(name, options) };
|
|
178
49
|
}
|
|
179
50
|
async function listCredentials(gatewayName) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (held.get(`${upstream.name}.oauth`))
|
|
186
|
-
credentials.push({ name: `${upstream.name}.oauth`, gatewayName, type: "oauth" });
|
|
187
|
-
const apiKeys = held.get(`${upstream.name}.api-keys`);
|
|
188
|
-
for (const [keyName, { fake }] of Object.entries(apiKeys?.keys ?? {})) {
|
|
189
|
-
credentials.push({ name: `${upstream.name}.key.${keyName}`, gatewayName, type: "apiKey", fake });
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return credentials;
|
|
51
|
+
assertValidName("gateway", gatewayName);
|
|
52
|
+
return (await control.listCredentials(gatewayName)).flatMap(({ upstream, oauth, apiKeys }) => [
|
|
53
|
+
...(oauth ? [{ name: `${upstream}.oauth`, gatewayName, type: "oauth" }] : []),
|
|
54
|
+
...Object.entries(apiKeys?.keys ?? {}).map(([keyName, { fake }]) => ({ name: `${upstream}.key.${keyName}`, gatewayName, type: "apiKey", fake })),
|
|
55
|
+
]);
|
|
193
56
|
}
|
|
194
|
-
// Storage holds the only copy of a secret, and the gateway re-reads credentials per request. Without
|
|
195
|
-
// its OAuth credential, the upstream's next sign-in goes to the upstream again (switching accounts).
|
|
196
57
|
async function deleteCredential(gatewayName, credentialName) {
|
|
197
58
|
assertValidName("gateway", gatewayName);
|
|
198
59
|
const keyAt = credentialName.indexOf(".key.");
|
|
199
60
|
if (credentialName.endsWith(".oauth")) {
|
|
200
61
|
const upstreamName = credentialName.slice(0, -".oauth".length);
|
|
201
62
|
assertValidName("upstream", upstreamName);
|
|
202
|
-
await
|
|
63
|
+
await control.removeOAuthCredential(gatewayName, upstreamName);
|
|
203
64
|
}
|
|
204
65
|
else if (keyAt > 0) {
|
|
205
66
|
await deleteApiKey(gatewayName, credentialName.slice(0, keyAt), credentialName.slice(keyAt + ".key.".length));
|
|
@@ -209,124 +70,84 @@ restoring) {
|
|
|
209
70
|
}
|
|
210
71
|
}
|
|
211
72
|
async function registerUpstream(gatewayName, upstream) {
|
|
212
|
-
|
|
73
|
+
assertValidName("gateway", gatewayName);
|
|
213
74
|
const definition = validUpstream(upstream);
|
|
214
|
-
await
|
|
75
|
+
await control.registerUpstream(gatewayName, definition);
|
|
215
76
|
return definition;
|
|
216
77
|
}
|
|
217
78
|
async function listUpstreams(gatewayName) {
|
|
218
|
-
|
|
219
|
-
return (await
|
|
79
|
+
assertValidName("gateway", gatewayName);
|
|
80
|
+
return (await control.listUpstreams(gatewayName)).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
220
81
|
}
|
|
221
82
|
async function removeUpstream(gatewayName, upstreamName) {
|
|
222
83
|
assertValidName("gateway", gatewayName);
|
|
223
84
|
assertValidName("upstream", upstreamName);
|
|
224
|
-
await
|
|
85
|
+
await control.removeUpstream(gatewayName, upstreamName);
|
|
225
86
|
}
|
|
226
|
-
// A key keeps its fake when replaced, so dimensions already holding it keep working.
|
|
227
87
|
async function setApiKey(gatewayName, upstreamName, keyName, key) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
throw new GatewayInvalidError(`upstream ${upstreamName} has no API key mode`);
|
|
88
|
+
assertValidName("gateway", gatewayName);
|
|
89
|
+
assertValidName("upstream", upstreamName);
|
|
231
90
|
assertValidName("API key", keyName);
|
|
232
91
|
if (typeof key !== "string" || key === "")
|
|
233
92
|
throw new GatewayInvalidError("the API key is empty");
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
await
|
|
237
|
-
return { fake };
|
|
93
|
+
const bytes = crypto.getRandomValues(new Uint8Array(key.length + 16));
|
|
94
|
+
const characters = Array.from(bytes, (byte) => FAKE_ALPHABET[byte % FAKE_ALPHABET.length]).join("");
|
|
95
|
+
return { fake: await control.setApiKey(gatewayName, upstreamName, keyName, key, characters) };
|
|
238
96
|
}
|
|
239
97
|
async function deleteApiKey(gatewayName, upstreamName, keyName) {
|
|
240
|
-
|
|
98
|
+
assertValidName("gateway", gatewayName);
|
|
99
|
+
assertValidName("upstream", upstreamName);
|
|
241
100
|
assertValidName("API key", keyName);
|
|
242
|
-
|
|
243
|
-
if (!stored?.keys[keyName])
|
|
244
|
-
return;
|
|
245
|
-
const { [keyName]: _removed, ...keys } = stored.keys;
|
|
246
|
-
if (Object.keys(keys).length === 0)
|
|
247
|
-
await storage.removeApiKeys(gatewayName, upstreamName);
|
|
248
|
-
else
|
|
249
|
-
await storage.saveApiKeys(gatewayName, upstreamName, { keys });
|
|
101
|
+
await control.deleteApiKey(gatewayName, upstreamName, keyName);
|
|
250
102
|
}
|
|
251
103
|
async function createTrafficRule(gatewayName, rule) {
|
|
252
|
-
|
|
104
|
+
assertValidName("gateway", gatewayName);
|
|
253
105
|
const requested = validTrafficRule(rule);
|
|
254
|
-
|
|
255
|
-
if (!sameTrafficRule(stored, requested)) {
|
|
256
|
-
throw new GatewayConflictError(`traffic rule ${rule.name} already exists with a different spec`);
|
|
257
|
-
}
|
|
106
|
+
await control.createTrafficRule(gatewayName, requested);
|
|
258
107
|
return requested;
|
|
259
108
|
}
|
|
260
109
|
async function updateTrafficRule(gatewayName, rule) {
|
|
261
|
-
|
|
110
|
+
assertValidName("gateway", gatewayName);
|
|
262
111
|
const requested = validTrafficRule(rule);
|
|
263
|
-
await
|
|
264
|
-
await storage.saveTrafficRule(gatewayName, requested);
|
|
112
|
+
await control.updateTrafficRule(gatewayName, requested);
|
|
265
113
|
return requested;
|
|
266
114
|
}
|
|
267
|
-
// Requests an ASK rule is holding carry on once it's gone.
|
|
268
115
|
async function deleteTrafficRule(gatewayName, ruleName) {
|
|
269
116
|
assertValidName("gateway", gatewayName);
|
|
270
117
|
assertValidName("traffic rule", ruleName);
|
|
271
|
-
await
|
|
118
|
+
await control.deleteTrafficRule(gatewayName, ruleName);
|
|
272
119
|
}
|
|
273
120
|
async function listTrafficRules(gatewayName) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return rules.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
121
|
+
assertValidName("gateway", gatewayName);
|
|
122
|
+
return (await control.listTrafficRules(gatewayName)).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
277
123
|
}
|
|
278
|
-
// Answers every request the rule is holding right now; later requests wait for a later answer.
|
|
279
124
|
async function trafficRuleRespond(gatewayName, ruleName, action) {
|
|
280
|
-
await requireGateway(gatewayName);
|
|
281
|
-
if (action !== "ALLOW" && action !== "DENY")
|
|
282
|
-
throw new GatewayInvalidError(`invalid response: ${action}`);
|
|
283
|
-
const rule = await requireTrafficRule(gatewayName, ruleName);
|
|
284
|
-
if (rule.action !== "ASK")
|
|
285
|
-
throw new GatewayInvalidError(`traffic rule ${ruleName} is not an ASK rule`);
|
|
286
|
-
await storage.saveTrafficRuleResponse(gatewayName, { name: ruleName, action, at: Date.now() });
|
|
287
|
-
}
|
|
288
|
-
async function requireGateway(gatewayName) {
|
|
289
125
|
assertValidName("gateway", gatewayName);
|
|
290
|
-
const record = await storage.readGateway(gatewayName);
|
|
291
|
-
if (!record)
|
|
292
|
-
throw new GatewayNotFoundError(`gateway ${gatewayName} does not exist`);
|
|
293
|
-
return record;
|
|
294
|
-
}
|
|
295
|
-
async function requireUpstream(gatewayName, upstreamName) {
|
|
296
|
-
await requireGateway(gatewayName);
|
|
297
|
-
assertValidName("upstream", upstreamName);
|
|
298
|
-
const upstream = await storage.readUpstream(gatewayName, upstreamName);
|
|
299
|
-
if (!upstream)
|
|
300
|
-
throw new GatewayNotFoundError(`upstream ${upstreamName} does not exist on gateway ${gatewayName}`);
|
|
301
|
-
return upstream;
|
|
302
|
-
}
|
|
303
|
-
async function requireTrafficRule(gatewayName, ruleName) {
|
|
304
126
|
assertValidName("traffic rule", ruleName);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return rule;
|
|
127
|
+
if (action !== "ALLOW" && action !== "DENY")
|
|
128
|
+
throw new GatewayInvalidError(`invalid response: ${action}`);
|
|
129
|
+
await control.trafficRuleRespond(gatewayName, ruleName, action);
|
|
309
130
|
}
|
|
310
131
|
return {
|
|
311
|
-
createGateway,
|
|
312
|
-
destroyGateway,
|
|
313
|
-
listGateways,
|
|
314
|
-
restartGateway,
|
|
132
|
+
createGateway: (gateway) => createGateway(gateway),
|
|
133
|
+
destroyGateway: (gatewayName) => destroyGateway(gatewayName),
|
|
134
|
+
listGateways: () => control.listGateways(),
|
|
135
|
+
restartGateway: (gatewayName) => restartGateway(gatewayName),
|
|
136
|
+
endpoint,
|
|
315
137
|
listCredentials,
|
|
316
|
-
deleteCredential,
|
|
317
|
-
registerUpstream,
|
|
138
|
+
deleteCredential: (gatewayName, credentialName) => deleteCredential(gatewayName, credentialName),
|
|
139
|
+
registerUpstream: (gatewayName, upstream) => registerUpstream(gatewayName, upstream),
|
|
318
140
|
listUpstreams,
|
|
319
|
-
removeUpstream,
|
|
320
|
-
setApiKey,
|
|
321
|
-
deleteApiKey,
|
|
322
|
-
createTrafficRule,
|
|
323
|
-
updateTrafficRule,
|
|
324
|
-
deleteTrafficRule,
|
|
141
|
+
removeUpstream: (gatewayName, upstreamName) => removeUpstream(gatewayName, upstreamName),
|
|
142
|
+
setApiKey: (gatewayName, upstreamName, keyName, key) => setApiKey(gatewayName, upstreamName, keyName, key),
|
|
143
|
+
deleteApiKey: (gatewayName, upstreamName, keyName) => deleteApiKey(gatewayName, upstreamName, keyName),
|
|
144
|
+
createTrafficRule: (gatewayName, rule) => createTrafficRule(gatewayName, rule),
|
|
145
|
+
updateTrafficRule: (gatewayName, rule) => updateTrafficRule(gatewayName, rule),
|
|
146
|
+
deleteTrafficRule: (gatewayName, ruleName) => deleteTrafficRule(gatewayName, ruleName),
|
|
325
147
|
listTrafficRules,
|
|
326
|
-
trafficRuleRespond,
|
|
148
|
+
trafficRuleRespond: (gatewayName, ruleName, action) => trafficRuleRespond(gatewayName, ruleName, action),
|
|
327
149
|
};
|
|
328
150
|
}
|
|
329
|
-
/** The checks the gateway relies on; the rest of the shape is the SDK client's (see upstream.ts). */
|
|
330
151
|
function validUpstream(upstream) {
|
|
331
152
|
assertValidName("upstream", upstream.name);
|
|
332
153
|
const { oauth, apiKey, forced } = upstream.modes ?? {};
|
|
@@ -349,7 +170,6 @@ function validUpstream(upstream) {
|
|
|
349
170
|
throw new GatewayInvalidError(`upstream ${upstream.name}: apiKey needs a fake shape`);
|
|
350
171
|
return upstream;
|
|
351
172
|
}
|
|
352
|
-
/** Returns just the rule's fields, so nothing unknown ends up in storage. */
|
|
353
173
|
function validTrafficRule(rule) {
|
|
354
174
|
assertValidName("traffic rule", rule.name);
|
|
355
175
|
if (typeof rule.urlMask !== "string" || rule.urlMask === "") {
|
|
@@ -368,7 +188,6 @@ function validTrafficRule(rule) {
|
|
|
368
188
|
return { name: rule.name, urlMask: rule.urlMask, action: rule.action, delayMs: rule.delayMs };
|
|
369
189
|
}
|
|
370
190
|
const TCP_SCHEME = "tcp://";
|
|
371
|
-
/** "tcp://<address>:<port>": address is "*", an IPv4 address or a CIDR; port is "*" or a number. */
|
|
372
191
|
function assertValidTcpMask(ruleName, urlMask) {
|
|
373
192
|
const target = urlMask.slice(TCP_SCHEME.length);
|
|
374
193
|
const separator = target.lastIndexOf(":");
|
|
@@ -385,8 +204,3 @@ function assertValidTcpMask(ruleName, urlMask) {
|
|
|
385
204
|
function sameTrafficRule(a, b) {
|
|
386
205
|
return a.urlMask === b.urlMask && a.action === b.action && a.delayMs === b.delayMs;
|
|
387
206
|
}
|
|
388
|
-
function assertOwnedContainer(container, gatewayName) {
|
|
389
|
-
if (container.Config?.Labels?.["hyperbrane.gateway"] !== gatewayName) {
|
|
390
|
-
throw new GatewayConflictError(`container ${gatewayContainerName(gatewayName)} exists but is not managed by hyperbrane`);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { DockerClient } from "../lib/docker.ts";
|
|
2
2
|
import { type ManagedImage } from "../lib/image.ts";
|
|
3
|
-
/** Bump whenever anything under ./image changes (and run `bun run embed:images`). */
|
|
4
3
|
export declare const GATEWAY_IMAGE_VERSION = "v17";
|
|
5
4
|
export declare const GATEWAY_IMAGE_REPOSITORY = "hyperbrane-gateway";
|
|
6
5
|
export declare function createGatewayImage(docker: DockerClient): ManagedImage;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { createManagedImage } from "../lib/image.js";
|
|
2
|
-
// The files under ./image, as strings (see dimension/image.ts).
|
|
3
2
|
import { IMAGE_FILES } from "./imageFiles.generated.js";
|
|
4
|
-
/** Bump whenever anything under ./image changes (and run `bun run embed:images`). */
|
|
5
3
|
export const GATEWAY_IMAGE_VERSION = "v17";
|
|
6
4
|
export const GATEWAY_IMAGE_REPOSITORY = "hyperbrane-gateway";
|
|
7
5
|
export function createGatewayImage(docker) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Generated by scripts/embed-image-files.ts from portal-gateway/image/ — do not edit; run `bun run embed:images`.
|
|
2
2
|
export const IMAGE_FILES = {
|
|
3
3
|
"Dockerfile": "FROM debian:13-slim\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends bash sudo git jq ca-certificates iproute2 nftables python3 python3-venv \\\n && python3 -m venv /opt/mitmproxy \\\n && /opt/mitmproxy/bin/pip install --no-cache-dir mitmproxy \\\n && ln -s /opt/mitmproxy/bin/mitmdump /usr/local/bin/mitmdump \\\n && mitmdump --version \\\n && rm -rf /var/lib/apt/lists/*\nCOPY entry.sh /usr/local/bin/hyb-gateway\nCOPY wire-portal.sh /usr/local/bin/hyb-wire-portal\nCOPY traffic_rules.py /opt/hyperbrane/traffic_rules.py\nCOPY upstreams.py /opt/hyperbrane/upstreams.py\nRUN chmod 0755 /usr/local/bin/hyb-gateway /usr/local/bin/hyb-wire-portal\nCMD [\"hyb-gateway\"]\n",
|
|
4
|
-
"entry.sh": "#!/bin/sh\
|
|
5
|
-
"wire-portal.sh": "#!/bin/sh\
|
|
4
|
+
"entry.sh": "#!/bin/sh\nset -eu\n\nSTATE=/state\nPROXY_PORT=8080\nDNS_PORT=5353\n\nnft -f - <<EOF\ntable ip hyperbrane\ndelete table ip hyperbrane\ntable ip hyperbrane {\n chain prerouting {\n type nat hook prerouting priority dstnat; policy accept;\n iifname \"hvg*\" meta l4proto tcp redirect to :$PROXY_PORT\n iifname \"hvg*\" udp dport 53 redirect to :$DNS_PORT\n }\n chain input {\n type filter hook input priority filter; policy accept;\n iifname \"hvg*\" tcp dport $PROXY_PORT accept\n iifname \"hvg*\" udp dport $DNS_PORT accept\n iifname \"hvg*\" drop\n }\n chain forward {\n type filter hook forward priority filter; policy accept;\n iifname \"hvg*\" drop\n }\n}\nEOF\n\nrules=\"$(nft list table ip hyperbrane)\"\ncase \"$rules\" in\n *\"redirect to :$PROXY_PORT\"*\"redirect to :$DNS_PORT\"*) ;;\n *) echo \"gateway: redirect rules are not installed\" >&2; exit 1 ;;\nesac\n\nowner_uid=\"$(stat -c %u \"$STATE\")\"\nowner_gid=\"$(stat -c %g \"$STATE\")\"\nexport HOME=/tmp\nexec setpriv --reuid=\"$owner_uid\" --regid=\"$owner_gid\" --clear-groups -- \\\n mitmdump \\\n --mode \"transparent@$PROXY_PORT\" \\\n --mode \"dns@$DNS_PORT\" \\\n --set \"confdir=$STATE/mitmproxy\" \\\n --set connection_strategy=eager \\\n -s /opt/hyperbrane/traffic_rules.py \\\n -s /opt/hyperbrane/upstreams.py\n",
|
|
5
|
+
"wire-portal.sh": "#!/bin/sh\nset -eu\n\nexec 9>\"/hostrun/hyperbrane-portal-$PORTAL.lock\"\nflock -w 30 9 || { echo \"timed out waiting for the wiring lock of $PORTAL\" >&2; exit 1; }\n\nmkdir -p /var/run/netns\nln -sf \"/proc/$DIMENSION_PID/ns/net\" /var/run/netns/dimension\nln -sf \"/proc/$GATEWAY_PID/ns/net\" /var/run/netns/gateway\ntrap 'rm -f /var/run/netns/dimension /var/run/netns/gateway' EXIT\n\nwired() {\n ip -n dimension addr show dev \"$DIMENSION_IF\" 2>/dev/null | grep -q \"inet $DIMENSION_IP/30\" \\\n && ip -n dimension route show default 2>/dev/null | grep -q \"via $GATEWAY_IP dev $DIMENSION_IF metric $ROUTE_METRIC\" \\\n && ip -n gateway addr show dev \"$GATEWAY_IF\" 2>/dev/null | grep -q \"inet $GATEWAY_IP/30\"\n}\nwired && exit 0\n\nip -n dimension link del \"$DIMENSION_IF\" 2>/dev/null || true\nip -n gateway link del \"$GATEWAY_IF\" 2>/dev/null || true\nip link del \"$DIMENSION_IF\" 2>/dev/null || true\nip link del \"$GATEWAY_IF\" 2>/dev/null || true\n\nip link add \"$DIMENSION_IF\" type veth peer name \"$GATEWAY_IF\"\nip link set \"$DIMENSION_IF\" netns dimension\nip link set \"$GATEWAY_IF\" netns gateway\nip -n dimension addr replace \"$DIMENSION_IP/30\" dev \"$DIMENSION_IF\"\nip -n dimension link set \"$DIMENSION_IF\" up\nip -n gateway addr replace \"$GATEWAY_IP/30\" dev \"$GATEWAY_IF\"\nip -n gateway link set \"$GATEWAY_IF\" up\nip -n dimension route replace default via \"$GATEWAY_IP\" dev \"$DIMENSION_IF\" metric \"$ROUTE_METRIC\"\n",
|
|
6
6
|
"traffic_rules.py": "# mitmproxy addon applying the gateway's traffic rules to everything arriving through a portal.\n#\n# Rules are the JSON files the gateway service writes to /state/traffic-rules, re-read every time, so\n# a change takes effect without a reload. A rule's urlMask is a glob, and its scheme says what it\n# applies to:\n# tcp://<address>:<port> every TCP connection, by its destination, before it is made upstream.\n# address is \"*\", an IPv4 address or a CIDR; port is \"*\" or a number.\n# Hostnames aren't known at that point, so rules don't use them.\n# anything else every HTTP(S) request, matched as a glob against host:port/path?query.\n# The scheme doesn't matter (http, https or none match the same), and a\n# mask without a port matches any port, one without a path any path:\n# \"github.com\" is the whole host, \"github.com:80/*\" only its plain HTTP.\n# Rules are checked in name order and the first match decides:\n# BLOCK refuse: a 403 for a request, a closed connection for TCP\n# DELAY hold for delayMs, then carry on\n# ASK hold until the service records an answer for the rule in /state/traffic-responses (only\n# answers given after the hold began count); DENY refuses, ALLOW carries on\n\nimport asyncio\nimport fnmatch\nimport ipaddress\nimport json\nimport os\nimport time\n\nfrom mitmproxy import http\nfrom mitmproxy.proxy import server_hooks\n\nSTATE = \"/state\"\nRULES = os.path.join(STATE, \"traffic-rules\")\nRESPONSES = os.path.join(STATE, \"traffic-responses\")\nASK_POLL_SECONDS = 0.2\nTCP_SCHEME = \"tcp://\"\n\n\ndef _load(path):\n try:\n with open(path) as f:\n return json.load(f)\n except (OSError, ValueError):\n return None\n\n\ndef _rules(tcp):\n try:\n files = sorted(f for f in os.listdir(RULES) if f.endswith(\".json\"))\n except OSError:\n return []\n rules = (_load(os.path.join(RULES, f)) for f in files)\n return [r for r in rules if r and r.get(\"urlMask\", \"\").startswith(TCP_SCHEME) == tcp]\n\n\ndef _request_target(request):\n \"\"\"host:port/path?query: the port always written out, the scheme left out.\"\"\"\n return f\"{request.pretty_host.lower()}:{request.port}{request.path}\"\n\n\ndef _url_mask(mask):\n \"\"\"The mask in _request_target's form: no scheme, and \"any\" for a port or path it leaves out.\"\"\"\n if \"://\" in mask:\n mask = mask.split(\"://\", 1)[1]\n host, slash, path = mask.partition(\"/\")\n if \":\" not in host:\n host += \":*\"\n return f\"{host.lower()}/{path if slash else '*'}\"\n\n\ndef _matching_url_rule(request):\n target = _request_target(request)\n for rule in _rules(tcp=False):\n if fnmatch.fnmatchcase(target, _url_mask(rule[\"urlMask\"])):\n return rule\n return None\n\n\ndef _tcp_mask_matches(mask, ip, port):\n address, _, mask_port = mask[len(TCP_SCHEME):].rpartition(\":\")\n if mask_port != \"*\" and mask_port != str(port):\n return False\n if address == \"*\":\n return True\n try:\n return ipaddress.ip_address(ip) in ipaddress.ip_network(address, strict=False)\n except ValueError:\n return False\n\n\ndef _matching_tcp_rule(ip, port):\n for rule in _rules(tcp=True):\n if _tcp_mask_matches(rule[\"urlMask\"], ip, port):\n return rule\n return None\n\n\nasync def _await_answer(name):\n \"\"\"The ALLOW/DENY given for the rule after now, or None once the rule is no longer an ASK.\"\"\"\n asked_at = time.time() * 1000\n while True:\n answer = _load(os.path.join(RESPONSES, f\"{name}.json\"))\n if answer and answer.get(\"at\", 0) >= asked_at:\n return answer.get(\"action\")\n rule = _load(os.path.join(RULES, f\"{name}.json\"))\n if not rule or rule.get(\"action\") != \"ASK\":\n return None\n # Never time.sleep: this runs on mitmproxy's event loop, shared by every connection.\n await asyncio.sleep(ASK_POLL_SECONDS)\n\n\nasync def _verdict(rule):\n \"\"\"Applies the rule's hold, if any; returns the refusal message, or None to carry on.\"\"\"\n action = rule.get(\"action\")\n if action == \"BLOCK\":\n return f\"blocked by gateway rule {rule['name']}\"\n if action == \"DELAY\":\n await asyncio.sleep(max(0, rule.get(\"delayMs\", 0)) / 1000)\n elif action == \"ASK\" and await _await_answer(rule[\"name\"]) == \"DENY\":\n return f\"denied by gateway rule {rule['name']}\"\n return None\n\n\nasync def request(flow: http.HTTPFlow):\n rule = _matching_url_rule(flow.request)\n refusal = rule and await _verdict(rule)\n if refusal:\n flow.response = http.Response.make(403, f\"{refusal}\\n\", {\"content-type\": \"text/plain\"})\n\n\nasync def server_connect(data: server_hooks.ServerConnectionHookData):\n if not data.server.address:\n return\n rule = _matching_tcp_rule(data.server.address[0], data.server.address[1])\n refusal = rule and await _verdict(rule)\n if refusal:\n data.server.error = refusal\n\n",
|
|
7
7
|
"upstreams.py": "# mitmproxy addon applying the gateway's upstreams (see portal-gateway/upstream.ts) to traffic from\n# portals. Definitions are the JSON files the gateway service writes to /state/upstreams; this addon\n# keeps each upstream's OAuth credential in /state/credentials/<upstream>.oauth.json and reads its\n# API keys from /state/credentials/<upstream>.api-keys.json. Everything is re-read per request, so a\n# change takes effect without a reload.\n#\n# Clients sign in from dimensions, through the proxy:\n# token endpoint without a usable credential, requests go through and the response is kept as\n# the credential, reaching the dimension with fakes in place of the tokens. With\n# one, the gateway answers from it: a sign-in (whatever code it carries) and a\n# refresh made with a fake refresh token. A client's refresh goes upstream: the\n# gateway replays it (encoding, fields, User-Agent) with the real refresh token,\n# one at a time, and refreshes arriving together share its result. Every client\n# holds the same fakes, so they all get the new tokens. A sign-in is answered from\n# the credential, refreshed first if expired by replaying the last client refresh.\n# An expired credential it can't refresh (no client refresh to replay yet, or\n# upstream refused) counts as none: a sign-in goes to the upstream again.\n# answers other sign-in steps (a device flow's poll) answered as the definition says, once\n# there is a credential\n# other requests a fake in the mode's `present` location is replaced by the real secret\n\nimport asyncio\nimport http.client as http_client\nimport json\nimport os\nimport secrets\nimport string\nimport time\nimport urllib.parse\n\nfrom mitmproxy import http\n\nSTATE = \"/state\"\nUPSTREAMS = os.path.join(STATE, \"upstreams\")\nCREDENTIALS = os.path.join(STATE, \"credentials\")\n# A token this close to expiry counts as expired.\nREFRESH_MARGIN_MS = 60_000\n# Refreshes arriving within this long of the last one get its tokens instead of going upstream again.\nREFRESH_SHARED_MS = 5_000\nFAKE_ALPHABET = string.ascii_letters + string.digits + \"-_\"\nTOKEN_FIELDS = ((\"access\", \"access_token\"), (\"refresh\", \"refresh_token\"), (\"id\", \"id_token\"))\n\n# One refresh per upstream at a time: concurrent ones would each spend the rotating refresh token.\n_refresh_locks: dict[str, asyncio.Lock] = {}\n\n\ndef _now_ms():\n return int(time.time() * 1000)\n\n\ndef _load(path):\n try:\n with open(path) as f:\n return json.load(f)\n except (OSError, ValueError):\n return None\n\n\ndef _save(path, data):\n tmp = f\"{path}.{secrets.token_hex(4)}.tmp\"\n with open(tmp, \"w\") as f:\n json.dump(data, f, indent=2)\n os.replace(tmp, path)\n\n\ndef _definitions():\n try:\n files = sorted(f for f in os.listdir(UPSTREAMS) if f.endswith(\".json\"))\n except OSError:\n return []\n definitions = (_load(os.path.join(UPSTREAMS, f)) for f in files)\n return [d for d in definitions if d and d.get(\"name\")]\n\n\ndef _oauth_path(name):\n return os.path.join(CREDENTIALS, f\"{name}.oauth.json\")\n\n\ndef _keys_path(name):\n return os.path.join(CREDENTIALS, f\"{name}.api-keys.json\")\n\n\ndef _base(url):\n return url.split(\"?\", 1)[0].split(\"#\", 1)[0]\n\n\ndef _hosts(mode):\n return {h.lower() for h in mode.get(\"hosts\", [])}\n\n\n# --- fakes -----------------------------------------------------------------------------------------\n\n\ndef _random(length):\n return \"\".join(secrets.choice(FAKE_ALPHABET) for _ in range(length))\n\n\ndef _issue_fake(shape, real):\n if shape.get(\"jwt\"):\n signature = real.rsplit(\".\", 1)[-1] if real.count(\".\") == 2 else \"\"\n return {\"jwtSignature\": _random(max(len(signature), 43))}\n prefix = shape.get(\"prefix\", \"\")\n return {\"value\": prefix + _random(max(len(real) - len(prefix), 16))}\n\n\ndef _fake_value(stored, real):\n if \"jwtSignature\" in stored:\n header_and_claims = real.rsplit(\".\", 1)[0] if real.count(\".\") == 2 else real\n return f\"{header_and_claims}.{stored['jwtSignature']}\"\n return stored[\"value\"]\n\n\ndef _is_fake(stored, candidate):\n if not stored or not candidate:\n return False\n if \"jwtSignature\" in stored:\n return candidate.count(\".\") == 2 and candidate.rsplit(\".\", 1)[1] == stored[\"jwtSignature\"]\n return candidate == stored[\"value\"]\n\n\n# --- presenting secrets ----------------------------------------------------------------------------\n\n\ndef _presented(request, present):\n if \"query\" in present:\n return request.query.get(present[\"query\"])\n value = request.headers.get(present[\"header\"])\n scheme = present.get(\"scheme\")\n if value and scheme:\n prefix = f\"{scheme} \"\n return value[len(prefix):] if value.lower().startswith(prefix.lower()) else None\n return value\n\n\ndef _present(request, present, secret):\n if \"query\" in present:\n request.query[present[\"query\"]] = secret\n elif present.get(\"scheme\"):\n request.headers[present[\"header\"]] = f\"{present['scheme']} {secret}\"\n else:\n request.headers[present[\"header\"]] = secret\n\n\ndef _force(request, force):\n if not force:\n return\n if force.get(\"target\"):\n target = urllib.parse.urlsplit(force[\"target\"])\n request.scheme = target.scheme or request.scheme\n request.host = target.hostname or request.host\n request.port = target.port or (443 if request.scheme == \"https\" else 80)\n if force.get(\"path\"):\n request.path = force[\"path\"].rstrip(\"/\") + request.path\n for key, value in force.get(\"query\", {}).items():\n request.query[key] = value\n for key, value in force.get(\"headers\", {}).items():\n request.headers[key] = value\n\n\ndef _swap_oauth(request, name, mode):\n credential = _load(_oauth_path(name))\n presented = _presented(request, mode[\"present\"])\n if credential and _is_fake(credential[\"fakes\"].get(\"access\"), presented):\n _present(request, mode[\"present\"], credential[\"access\"])\n return True\n return False\n\n\ndef _swap_api_key(request, name, mode):\n stored = _load(_keys_path(name)) or {}\n presented = _presented(request, mode[\"present\"])\n for key in stored.get(\"keys\", {}).values():\n if presented and presented == key[\"fake\"]:\n _present(request, mode[\"present\"], key[\"key\"])\n return True\n return False\n\n\n# --- the token endpoint ----------------------------------------------------------------------------\n\n\ndef _parse_form_or_json(text, content_type):\n if not text:\n return {}\n if \"json\" in (content_type or \"\"):\n try:\n value = json.loads(text)\n return value if isinstance(value, dict) else {}\n except ValueError:\n return {}\n return dict(urllib.parse.parse_qsl(text))\n\n\ndef _refresh_request(flow, fields):\n \"\"\"How a client refreshes, to replay with the real refresh token: everything but that token.\"\"\"\n return {\n \"contentType\": flow.request.headers.get(\"content-type\", \"\"),\n \"userAgent\": flow.request.headers.get(\"user-agent\"),\n \"fields\": {k: v for k, v in fields.items() if k != \"refresh_token\"},\n }\n\n\ndef _store_tokens(name, mode, body, refresh_request):\n \"\"\"Keeps a token response as the upstream's credential and returns it.\"\"\"\n path = _oauth_path(name)\n credential = dict(_load(path) or {})\n if refresh_request:\n credential[\"refreshRequest\"] = refresh_request\n now = _now_ms()\n for key, field in TOKEN_FIELDS:\n if body.get(field):\n credential[key] = body[field]\n if \"expires_in\" in body:\n credential[\"expiresAt\"] = now + int(body[\"expires_in\"]) * 1000\n if \"refresh_token_expires_in\" in body:\n credential[\"refreshExpiresAt\"] = now + int(body[\"refresh_token_expires_in\"]) * 1000\n if body.get(\"scope\"):\n credential[\"scopes\"] = body[\"scope\"].split()\n credential[\"lastRefresh\"] = now\n fakes = dict(credential.get(\"fakes\") or {})\n for key, _ in TOKEN_FIELDS:\n shape = mode[\"fakes\"].get(key)\n if shape and credential.get(key) and key not in fakes:\n fakes[key] = _issue_fake(shape, credential[key])\n credential[\"fakes\"] = fakes\n stripped = set((mode.get(\"strip\") or {}).get(\"fields\", []))\n credential[\"response\"] = {k: v for k, v in body.items() if k not in stripped}\n _save(path, credential)\n return credential\n\n\ndef _client_body(mode, credential):\n \"\"\"The token response the dimension gets: the last real one, with fakes for every token.\"\"\"\n body = dict(credential.get(\"response\") or {})\n fakes = credential[\"fakes\"]\n for key, field in TOKEN_FIELDS:\n if field not in body:\n continue\n if key in fakes and credential.get(key):\n body[field] = _fake_value(fakes[key], credential[key])\n else:\n del body[field] # a token with no fake shape never reaches a dimension\n now = _now_ms()\n if credential.get(\"expiresAt\") and \"expires_in\" in body:\n body[\"expires_in\"] = max(0, (credential[\"expiresAt\"] - now) // 1000)\n if credential.get(\"refreshExpiresAt\") and \"refresh_token_expires_in\" in body:\n body[\"refresh_token_expires_in\"] = max(0, (credential[\"refreshExpiresAt\"] - now) // 1000)\n return body\n\n\ndef _refresh_upstream(mode, credential):\n \"\"\"Replays the client's refresh request with the real refresh token; returns (status, parsed body\n or raw bytes). http.client, unlike urllib, adds no User-Agent of its own: a client that sent none\n stays that way.\"\"\"\n template = credential[\"refreshRequest\"]\n fields = dict(template[\"fields\"], refresh_token=credential[\"refresh\"])\n content_type = template[\"contentType\"] or \"application/x-www-form-urlencoded\"\n data = json.dumps(fields) if \"json\" in content_type else urllib.parse.urlencode(fields)\n headers = {\"content-type\": content_type, \"accept\": \"application/json\"}\n if template.get(\"userAgent\"):\n headers[\"user-agent\"] = template[\"userAgent\"]\n url = urllib.parse.urlsplit(mode[\"tokenUrl\"])\n connection_type = http_client.HTTPSConnection if url.scheme == \"https\" else http_client.HTTPConnection\n connection = connection_type(url.hostname, url.port, timeout=30)\n try:\n connection.request(\"POST\", url.path + (f\"?{url.query}\" if url.query else \"\"), body=data.encode(), headers=headers)\n response = connection.getresponse()\n raw = response.read()\n try:\n return response.status, json.loads(raw)\n except ValueError:\n return response.status, raw\n finally:\n connection.close()\n\n\nasync def _answer_from_credential(flow, name, mode, refresh_request):\n \"\"\"Answers a sign-in, or a refresh made with a fake (`refresh_request`), with the credential's\n fakes. A refresh is replayed upstream first unless another one just was; a sign-in only if the\n credential has expired. Returns False, answering nothing, when a sign-in can't be given valid\n tokens: expired, and no client refresh to replay yet or upstream refused it. A client's refresh\n gets upstream's refusal instead.\"\"\"\n lock = _refresh_locks.setdefault(name, asyncio.Lock())\n async with lock:\n credential = _load(_oauth_path(name))\n if refresh_request and credential.get(\"refreshRequest\") != refresh_request:\n credential[\"refreshRequest\"] = refresh_request\n _save(_oauth_path(name), credential)\n now = _now_ms()\n expired = not credential.get(\"expiresAt\") or credential[\"expiresAt\"] - now < REFRESH_MARGIN_MS\n just_refreshed = now - credential.get(\"lastRefresh\", 0) < REFRESH_SHARED_MS\n if expired or (refresh_request and not just_refreshed):\n if not (credential.get(\"refresh\") and credential.get(\"refreshRequest\")):\n if not refresh_request:\n return False\n # A client's refresh with nothing to refresh with: all there is to give it.\n flow.response = http.Response.make(\n 200, json.dumps(_client_body(mode, credential)), {\"content-type\": \"application/json\"}\n )\n return True\n status, body = await asyncio.to_thread(_refresh_upstream, mode, credential)\n if status != 200 or not isinstance(body, dict) or \"access_token\" not in body:\n if not refresh_request:\n return False\n content = body if isinstance(body, bytes) else json.dumps(body).encode()\n flow.response = http.Response.make(status, content, {\"content-type\": \"application/json\"})\n return True\n credential = _store_tokens(name, mode, body, None)\n flow.response = http.Response.make(\n 200, json.dumps(_client_body(mode, credential)), {\"content-type\": \"application/json\"}\n )\n return True\n\n\nasync def request(flow: http.HTTPFlow):\n if flow.response: # already answered, e.g. by a traffic rule\n return\n host = flow.request.pretty_host.lower()\n url = _base(flow.request.pretty_url)\n for definition in _definitions():\n name = definition[\"name\"]\n oauth = definition.get(\"modes\", {}).get(\"oauth\")\n api_key = definition.get(\"modes\", {}).get(\"apiKey\")\n forced = definition.get(\"modes\", {}).get(\"forced\")\n credential = _load(_oauth_path(name)) if oauth else None\n if oauth and url == _base(oauth[\"tokenUrl\"]):\n fields = _parse_form_or_json(flow.request.get_text(strict=False), flow.request.headers.get(\"content-type\"))\n refreshing = fields.get(\"grant_type\") == \"refresh_token\"\n refresh_request = _refresh_request(flow, fields) if refreshing else None\n # A refresh token the gateway didn't issue is a client that signed in before the upstream\n # was registered (or elsewhere): let it through, and keep what it gets.\n foreign_refresh = refreshing and not _is_fake(\n (credential or {}).get(\"fakes\", {}).get(\"refresh\"), fields.get(\"refresh_token\")\n )\n if credential and not foreign_refresh and await _answer_from_credential(flow, name, oauth, refresh_request):\n return\n flow.metadata[\"hyperbrane_token\"] = (name, refresh_request)\n return\n if credential:\n for answer in oauth.get(\"answers\", []):\n if url == _base(answer[\"url\"]):\n flow.response = http.Response.make(\n answer.get(\"status\", 200), json.dumps(answer[\"json\"]), {\"content-type\": \"application/json\"}\n )\n return\n for mode, swap in ((oauth, _swap_oauth), (api_key, _swap_api_key), (forced, None)):\n if mode and host in _hosts(mode):\n if swap:\n swap(flow.request, name, mode)\n _force(flow.request, mode.get(\"force\"))\n\n\ndef responseheaders(flow: http.HTTPFlow):\n # Only token responses are rewritten; everything else streams through as it arrives. mitmproxy\n # otherwise holds each response until its body is complete, so a model's streamed reply (SSE)\n # reached the dimension in one piece at the end of the turn.\n if not flow.metadata.get(\"hyperbrane_token\"):\n flow.response.stream = True\n\n\ndef response(flow: http.HTTPFlow):\n token = flow.metadata.get(\"hyperbrane_token\")\n if not token:\n return\n name, refresh_request = token\n definition = next((d for d in _definitions() if d[\"name\"] == name), None)\n oauth = (definition or {}).get(\"modes\", {}).get(\"oauth\")\n if not oauth:\n return\n body = _parse_form_or_json(flow.response.get_text(strict=False), \"json\")\n if flow.response.status_code == 200 and body.get(\"access_token\"):\n credential = _store_tokens(name, oauth, body, refresh_request)\n flow.response.text = json.dumps(_client_body(oauth, credential))\n for header in (oauth.get(\"strip\") or {}).get(\"headers\", []):\n flow.response.headers.pop(header, None)\n",
|
|
8
8
|
};
|