hyperbrane 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -23
- 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 +64 -323
- package/dist/agent/agentStorage.d.ts +1 -20
- package/dist/agent/agentStorage.js +0 -30
- package/dist/agent/api.d.ts +105 -152
- 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/dimensionAgentClient.d.ts +8 -0
- package/dist/agent/dimensionAgentClient.js +99 -0
- package/dist/agent/hostTranscript.d.ts +0 -6
- package/dist/agent/hostTranscript.js +3 -14
- package/dist/agent/session/backend.d.ts +18 -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 +3 -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 +30 -21
- 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 +24 -17
- 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 +32 -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 +42 -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 +22 -0
- package/dist/dimension/control.js +346 -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 +85 -227
- 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 +7 -9
- package/dist/index.js +7 -19
- package/dist/lib/control.d.ts +11 -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 +34 -18
- 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/lib/unixSocket.d.ts +2 -0
- package/dist/lib/unixSocket.js +28 -0
- package/dist/portal-gateway/api.d.ts +14 -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 +82 -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/restore.d.ts +9 -0
- package/dist/restore.js +10 -0
- package/dist/sandbox/api.d.ts +126 -187
- package/dist/sandbox/api.js +5 -7
- package/dist/sandbox/client.d.ts +3 -0
- package/dist/sandbox/client.js +7 -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/lib/sshKey.d.ts +6 -0
- package/dist/sandbox/lib/sshKey.js +75 -0
- package/dist/sandbox/providers/boat/boatService.d.ts +3 -0
- package/dist/sandbox/providers/boat/boatService.js +373 -0
- package/dist/sandbox/providers/exedev/exeDevService.d.ts +2 -0
- package/dist/sandbox/providers/exedev/exeDevService.js +129 -0
- package/dist/sandbox/providers/exedev/exeDevToken.d.ts +1 -0
- package/dist/sandbox/providers/exedev/exeDevToken.js +50 -0
- package/dist/sandbox/providers/local/localService.d.ts +2 -0
- package/dist/sandbox/providers/local/localService.js +197 -0
- package/dist/sandbox/providers/ssh/sshService.d.ts +2 -0
- package/dist/sandbox/providers/ssh/sshService.js +83 -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 +3 -0
- package/dist/sandbox/service.js +26 -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/dist/vendor-types/claude/agent-sdk/bridge.d.ts +20 -4
- package/dist/vendor-types/claude/agent-sdk/browser-sdk.d.ts +24 -0
- package/dist/vendor-types/claude/agent-sdk/core.d.ts +2 -0
- package/dist/vendor-types/claude/agent-sdk/sdk-tools.d.ts +75 -6
- package/dist/vendor-types/claude/agent-sdk/sdk.d.ts +569 -100
- package/package.json +13 -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,680 +0,0 @@
|
|
|
1
|
-
import { posix } from "node:path";
|
|
2
|
-
import { Client } from "ssh2";
|
|
3
|
-
import { SandboxNotFoundError, } from "./api.js";
|
|
4
|
-
import { createProxyEndpoints } from "./endpointProxy.js";
|
|
5
|
-
import { socketConnection } from "./localService.js";
|
|
6
|
-
/** SFTP status codes (draft-ietf-secsh-filexfer-02). */
|
|
7
|
-
const SFTP_NO_SUCH_FILE = 2;
|
|
8
|
-
const SFTP_EOF = 1;
|
|
9
|
-
/**
|
|
10
|
-
* Commands running at once on the connection. sshd refuses sessions past its MaxSessions (10 by
|
|
11
|
-
* default) per connection, and counts a closed one until it has cleaned up after it, so this stays
|
|
12
|
-
* well below; the services fan out wider, and the rest wait their turn.
|
|
13
|
-
*/
|
|
14
|
-
const MAX_CHANNELS = 4;
|
|
15
|
-
/** Attempts at a command sshd refused to start (it was busy cleaning up sessions). */
|
|
16
|
-
const ATTEMPTS = 8;
|
|
17
|
-
export function createSshSandbox(host, options) {
|
|
18
|
-
const { user, port = 22, privateKey, passphrase, hostVerifier, readyTimeout = 15000, endpoints } = options;
|
|
19
|
-
let connecting = null;
|
|
20
|
-
function connection() {
|
|
21
|
-
if (connecting)
|
|
22
|
-
return connecting;
|
|
23
|
-
const opened = new Promise((resolve, reject) => {
|
|
24
|
-
const client = new Client();
|
|
25
|
-
let ready = false;
|
|
26
|
-
client
|
|
27
|
-
.on("ready", () => {
|
|
28
|
-
ready = true;
|
|
29
|
-
// Small writes (keystrokes, short replies) go out at once.
|
|
30
|
-
client.setNoDelay(true);
|
|
31
|
-
resolve(client);
|
|
32
|
-
})
|
|
33
|
-
.on("error", (error) => {
|
|
34
|
-
if (!ready)
|
|
35
|
-
reject(new Error(`ssh to ${user}@${host}:${port} failed: ${error.message}`));
|
|
36
|
-
})
|
|
37
|
-
.on("close", () => {
|
|
38
|
-
if (connecting === opened)
|
|
39
|
-
connecting = null;
|
|
40
|
-
if (!ready)
|
|
41
|
-
reject(new Error(`ssh to ${user}@${host}:${port} closed before it was ready`));
|
|
42
|
-
})
|
|
43
|
-
// strictVendor off: OpenSSH extensions (stream-local forwarding) are tried on any server, not
|
|
44
|
-
// only on one that announces itself as OpenSSH (a proxy in front of it may not).
|
|
45
|
-
.connect({ host, port, username: user, privateKey, passphrase, readyTimeout, keepaliveInterval: 20000, hostVerifier, strictVendor: false });
|
|
46
|
-
});
|
|
47
|
-
connecting = opened;
|
|
48
|
-
opened.catch(() => {
|
|
49
|
-
if (connecting === opened)
|
|
50
|
-
connecting = null;
|
|
51
|
-
});
|
|
52
|
-
return opened;
|
|
53
|
-
}
|
|
54
|
-
function remotePath(path) {
|
|
55
|
-
return posix.resolve("/", path);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* SSH hands the remote side a single command line for the login shell to parse, so every word is
|
|
59
|
-
* quoted. Environment goes through env(1), since servers drop most variables set over the protocol.
|
|
60
|
-
*/
|
|
61
|
-
function commandLine(cmd, env = {}, cwd) {
|
|
62
|
-
const words = [...Object.entries(env).map(([name, value]) => `${name}=${value}`), ...cmd];
|
|
63
|
-
const run = `exec env ${words.map(quote).join(" ")}`;
|
|
64
|
-
return cwd === undefined ? run : `cd ${quote(cwd)} && ${run}`;
|
|
65
|
-
}
|
|
66
|
-
let running = 0;
|
|
67
|
-
const waiting = [];
|
|
68
|
-
/** Waits for one of the MAX_CHANNELS slots; the returned function gives it back. */
|
|
69
|
-
async function slot() {
|
|
70
|
-
if (running >= MAX_CHANNELS)
|
|
71
|
-
await new Promise((resolve) => waiting.push(resolve));
|
|
72
|
-
else
|
|
73
|
-
running += 1;
|
|
74
|
-
let released = false;
|
|
75
|
-
return () => {
|
|
76
|
-
if (released)
|
|
77
|
-
return;
|
|
78
|
-
released = true;
|
|
79
|
-
const next = waiting.shift();
|
|
80
|
-
if (next)
|
|
81
|
-
next(); // the slot passes straight on
|
|
82
|
-
else
|
|
83
|
-
running -= 1;
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
const backoff = (attempt) => new Promise((resolve) => setTimeout(resolve, 50 * 2 ** attempt));
|
|
87
|
-
/**
|
|
88
|
-
* Starts `command` on its own channel, feeding it `stdin` (or nothing) and closing its input. The
|
|
89
|
-
* channel holds a slot until it closes. A channel sshd refuses to open is asked for again.
|
|
90
|
-
*/
|
|
91
|
-
async function exec(command, stdin) {
|
|
92
|
-
for (let attempt = 1;; attempt++) {
|
|
93
|
-
const release = await slot();
|
|
94
|
-
try {
|
|
95
|
-
const client = await connection();
|
|
96
|
-
const channel = await new Promise((resolve, reject) => client.exec(command, (error, opened) => (error ? reject(error) : resolve(opened))));
|
|
97
|
-
channel.on("close", release);
|
|
98
|
-
if (stdin !== "keep open")
|
|
99
|
-
channel.end(stdin === undefined ? undefined : typeof stdin === "string" ? stdin : Buffer.from(stdin));
|
|
100
|
-
return channel;
|
|
101
|
-
}
|
|
102
|
-
catch (error) {
|
|
103
|
-
release();
|
|
104
|
-
if (attempt >= ATTEMPTS || !/Channel open failure/.test(error.message))
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
await backoff(attempt);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* The exit code once the channel closes (-1 if the command died of a signal), or null if sshd
|
|
112
|
-
* closed it without running the command: no exit status (not even a signal) and no output.
|
|
113
|
-
*/
|
|
114
|
-
function exited(channel) {
|
|
115
|
-
return new Promise((resolve) => {
|
|
116
|
-
let code = null;
|
|
117
|
-
channel.on("exit", (exitCode) => (code = exitCode ?? -1));
|
|
118
|
-
channel.on("close", () => resolve(code));
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
async function runBytes(command, stdin) {
|
|
122
|
-
for (let attempt = 1;; attempt++) {
|
|
123
|
-
const channel = await exec(command, stdin);
|
|
124
|
-
const out = [];
|
|
125
|
-
const err = [];
|
|
126
|
-
channel.on("data", (chunk) => out.push(chunk));
|
|
127
|
-
channel.stderr.on("data", (chunk) => err.push(chunk));
|
|
128
|
-
const exitCode = await exited(channel);
|
|
129
|
-
const output = { stdout: Buffer.concat(out), stderr: Buffer.concat(err).toString() };
|
|
130
|
-
if (exitCode !== null)
|
|
131
|
-
return { exitCode, ...output };
|
|
132
|
-
if (attempt >= ATTEMPTS || out.length > 0 || err.length > 0)
|
|
133
|
-
return { exitCode: -1, ...output };
|
|
134
|
-
await backoff(attempt);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async function run(command, stdin) {
|
|
138
|
-
const { exitCode, stdout, stderr } = await runBytes(command, stdin);
|
|
139
|
-
return { exitCode, stdout: stdout.toString(), stderr };
|
|
140
|
-
}
|
|
141
|
-
async function sh(script, env = {}, stdin) {
|
|
142
|
-
return run(commandLine(["sh", "-c", script], env), stdin);
|
|
143
|
-
}
|
|
144
|
-
let sftpOpening = null;
|
|
145
|
-
/** The connection's SFTP channel, opened on first use (and again after it closes). */
|
|
146
|
-
function sftp() {
|
|
147
|
-
if (sftpOpening)
|
|
148
|
-
return sftpOpening;
|
|
149
|
-
const opening = connection().then((client) => new Promise((resolve, reject) => client.sftp((error, channel) => {
|
|
150
|
-
if (error)
|
|
151
|
-
return reject(error);
|
|
152
|
-
channel.on("close", () => {
|
|
153
|
-
if (sftpOpening === opening)
|
|
154
|
-
sftpOpening = null;
|
|
155
|
-
});
|
|
156
|
-
resolve(channel);
|
|
157
|
-
})));
|
|
158
|
-
sftpOpening = opening;
|
|
159
|
-
opening.catch(() => {
|
|
160
|
-
if (sftpOpening === opening)
|
|
161
|
-
sftpOpening = null;
|
|
162
|
-
});
|
|
163
|
-
return opening;
|
|
164
|
-
}
|
|
165
|
-
const missing = (error) => error.code === SFTP_NO_SUCH_FILE;
|
|
166
|
-
/** Runs one SFTP request; a missing path becomes SandboxNotFoundError. */
|
|
167
|
-
async function sftpCall(what, path, request) {
|
|
168
|
-
const channel = await sftp();
|
|
169
|
-
return new Promise((resolve, reject) => request(channel, (error, value) => {
|
|
170
|
-
if (!error)
|
|
171
|
-
return resolve(value);
|
|
172
|
-
if (missing(error))
|
|
173
|
-
return reject(new SandboxNotFoundError(`Not found: "${path}"`));
|
|
174
|
-
reject(new Error(`${what} ${path} on ${host} failed: ${error.message}`));
|
|
175
|
-
}));
|
|
176
|
-
}
|
|
177
|
-
function statOf(full) {
|
|
178
|
-
return sftpCall("stat", full, (channel, done) => channel.stat(full, done));
|
|
179
|
-
}
|
|
180
|
-
async function sftpReadFile(path) {
|
|
181
|
-
const full = remotePath(path);
|
|
182
|
-
const data = await sftpCall("read", full, (channel, done) => channel.readFile(full, done));
|
|
183
|
-
return new Uint8Array(data);
|
|
184
|
-
}
|
|
185
|
-
async function sftpReadFileRange(path, start, end) {
|
|
186
|
-
const full = remotePath(path);
|
|
187
|
-
const channel = await sftp();
|
|
188
|
-
const handle = await sftpCall("read", full, (sftpChannel, done) => sftpChannel.open(full, "r", done));
|
|
189
|
-
try {
|
|
190
|
-
const buffer = Buffer.alloc(Math.max(0, end - start));
|
|
191
|
-
let filled = 0;
|
|
192
|
-
while (filled < buffer.length) {
|
|
193
|
-
const read = await new Promise((resolve, reject) => channel.read(handle, buffer, filled, buffer.length - filled, start + filled, (error, bytesRead) => {
|
|
194
|
-
// The end of the file comes as an EOF status rather than a short read.
|
|
195
|
-
if (error && error.code !== SFTP_EOF)
|
|
196
|
-
return reject(new Error(`read ${full} on ${host} failed: ${error.message}`));
|
|
197
|
-
resolve(error ? 0 : bytesRead);
|
|
198
|
-
}));
|
|
199
|
-
if (read === 0)
|
|
200
|
-
break;
|
|
201
|
-
filled += read;
|
|
202
|
-
}
|
|
203
|
-
return new Uint8Array(buffer.subarray(0, filled));
|
|
204
|
-
}
|
|
205
|
-
finally {
|
|
206
|
-
channel.close(handle, () => { });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
/** Writes `data` to a unique hidden file next to `full` (creating its directory) and returns its path. */
|
|
210
|
-
async function writeTemp(full, data) {
|
|
211
|
-
await createDirectory(posix.dirname(full));
|
|
212
|
-
const tmp = posix.join(posix.dirname(full), `.${posix.basename(full)}.${crypto.randomUUID()}.tmp`);
|
|
213
|
-
const bytes = typeof data === "string" ? Buffer.from(data) : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
214
|
-
await sftpCall("write", full, (channel, done) => channel.writeFile(tmp, bytes, done));
|
|
215
|
-
return tmp;
|
|
216
|
-
}
|
|
217
|
-
const unlinkQuietly = (path) => sftpCall("remove", path, (channel, done) => channel.unlink(path, done)).catch(() => { });
|
|
218
|
-
// Written in full beside the target, then renamed over it (posix-rename replaces an existing file).
|
|
219
|
-
async function sftpWriteFile(path, data) {
|
|
220
|
-
const full = remotePath(path);
|
|
221
|
-
const tmp = await writeTemp(full, data);
|
|
222
|
-
try {
|
|
223
|
-
await sftpCall("write", full, (channel, done) => channel.ext_openssh_rename(tmp, full, done));
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
await unlinkQuietly(tmp);
|
|
227
|
-
throw error;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
// A hard link refuses to replace an existing file, and publishes the fully written temp file.
|
|
231
|
-
async function sftpWriteFileExclusive(path, data) {
|
|
232
|
-
const full = remotePath(path);
|
|
233
|
-
const tmp = await writeTemp(full, data);
|
|
234
|
-
try {
|
|
235
|
-
await sftpCall("write", full, (channel, done) => channel.ext_openssh_hardlink(tmp, full, done));
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
catch (error) {
|
|
239
|
-
if (await statOf(full).then(() => true, () => false))
|
|
240
|
-
return false;
|
|
241
|
-
throw error;
|
|
242
|
-
}
|
|
243
|
-
finally {
|
|
244
|
-
await unlinkQuietly(tmp);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
async function sftpRemoveFile(path) {
|
|
248
|
-
const full = remotePath(path);
|
|
249
|
-
await sftpCall("remove", full, (channel, done) => channel.unlink(full, done)).catch((error) => {
|
|
250
|
-
if (!(error instanceof SandboxNotFoundError))
|
|
251
|
-
throw error;
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
// mkdir -p: each missing directory from the top; one that appears meanwhile is fine.
|
|
255
|
-
async function sftpCreateDirectory(path) {
|
|
256
|
-
const full = remotePath(path);
|
|
257
|
-
const parts = full.split("/").filter(Boolean);
|
|
258
|
-
for (let depth = parts.length; depth >= 0; depth--) {
|
|
259
|
-
const dir = `/${parts.slice(0, depth).join("/")}`;
|
|
260
|
-
const info = await statOf(dir).catch((error) => {
|
|
261
|
-
if (error instanceof SandboxNotFoundError)
|
|
262
|
-
return null;
|
|
263
|
-
throw error;
|
|
264
|
-
});
|
|
265
|
-
if (!info)
|
|
266
|
-
continue;
|
|
267
|
-
if (!info.isDirectory())
|
|
268
|
-
throw new Error(`mkdir ${full} on ${host} failed: ${dir} is not a directory`);
|
|
269
|
-
for (let next = depth + 1; next <= parts.length; next++) {
|
|
270
|
-
const create = `/${parts.slice(0, next).join("/")}`;
|
|
271
|
-
await sftpCall("mkdir", create, (channel, done) => channel.mkdir(create, done)).catch(async (error) => {
|
|
272
|
-
if (!(await statOf(create).then((s) => s.isDirectory(), () => false)))
|
|
273
|
-
throw error;
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
/** Throws SandboxNotFoundError for a missing path, and an Error for any other failure. */
|
|
280
|
-
function check(result, what, path) {
|
|
281
|
-
if (result.exitCode !== 0)
|
|
282
|
-
throw new Error(`${what} ${path} on ${host} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);
|
|
283
|
-
}
|
|
284
|
-
async function removeDirectory(path) {
|
|
285
|
-
const full = remotePath(path);
|
|
286
|
-
check(await sh('rm -rf -- "$P"', { P: full }), "remove", full);
|
|
287
|
-
}
|
|
288
|
-
async function sftpListDirectory(path) {
|
|
289
|
-
const full = remotePath(path);
|
|
290
|
-
const entries = await sftpCall("list", full, (channel, done) => channel.readdir(full, done));
|
|
291
|
-
return entries.map((entry) => entry.filename).filter((name) => name !== "." && name !== "..");
|
|
292
|
-
}
|
|
293
|
-
async function sftpStat(path) {
|
|
294
|
-
const info = await statOf(remotePath(path));
|
|
295
|
-
const type = info.isDirectory() ? "directory" : info.isFile() ? "file" : "other";
|
|
296
|
-
return { type, uid: info.uid, gid: info.gid, size: info.size };
|
|
297
|
-
}
|
|
298
|
-
// SFTP starts in the user's home directory. Asked once: storage paths start from it on every call.
|
|
299
|
-
let home = null;
|
|
300
|
-
function sftpHomeDirectory() {
|
|
301
|
-
return sftpCall("home of", user, (channel, done) => channel.realpath(".", done));
|
|
302
|
-
}
|
|
303
|
-
// Without SFTP (a server, or a proxy in front of one, that doesn't offer it), files go through
|
|
304
|
-
// shell commands instead; slower, and subject to sshd's session limits, but they work anywhere.
|
|
305
|
-
let sftpSupport = null;
|
|
306
|
-
function sftpAvailable() {
|
|
307
|
-
sftpSupport ??= sftp().then(() => true, (error) => {
|
|
308
|
-
if (/subsystem/i.test(error.message))
|
|
309
|
-
return false;
|
|
310
|
-
sftpSupport = null; // the connection failed, not SFTP: ask again next time
|
|
311
|
-
throw error;
|
|
312
|
-
});
|
|
313
|
-
return sftpSupport;
|
|
314
|
-
}
|
|
315
|
-
/** What the shell scripts below exit with for a missing path, and for an existing one. */
|
|
316
|
-
const NOT_FOUND = 44;
|
|
317
|
-
const EXISTS = 45;
|
|
318
|
-
function checkShell(result, what, path) {
|
|
319
|
-
if (result.exitCode === NOT_FOUND)
|
|
320
|
-
throw new SandboxNotFoundError(`Not found: "${path}"`);
|
|
321
|
-
check(result, what, path);
|
|
322
|
-
}
|
|
323
|
-
const shell = {
|
|
324
|
-
async readFile(full) {
|
|
325
|
-
const result = await runBytes(commandLine(["sh", "-c", `[ -e "$P" ] || exit ${NOT_FOUND}; cat -- "$P"`], { P: full }));
|
|
326
|
-
checkShell(result, "read", full);
|
|
327
|
-
return new Uint8Array(result.stdout);
|
|
328
|
-
},
|
|
329
|
-
async readFileRange(full, start, end) {
|
|
330
|
-
const script = `[ -e "$P" ] || exit ${NOT_FOUND}; tail -c +$(($S + 1)) -- "$P" | head -c "$L"`;
|
|
331
|
-
const result = await runBytes(commandLine(["sh", "-c", script], { P: full, S: String(start), L: String(Math.max(0, end - start)) }));
|
|
332
|
-
checkShell(result, "read", full);
|
|
333
|
-
return new Uint8Array(result.stdout);
|
|
334
|
-
},
|
|
335
|
-
/** Streams `data` into a temp file beside `full`, then runs `publish` ($T the temp file, $F the target). */
|
|
336
|
-
writeVia(full, data, publish) {
|
|
337
|
-
const tmp = posix.join(posix.dirname(full), `.${posix.basename(full)}.${crypto.randomUUID()}.tmp`);
|
|
338
|
-
const script = `mkdir -p -- "$D" && cat > "$T" || { rm -f -- "$T"; exit 1; }\n${publish}`;
|
|
339
|
-
return sh(script, { D: posix.dirname(full), T: tmp, F: full }, data);
|
|
340
|
-
},
|
|
341
|
-
async writeFile(full, data) {
|
|
342
|
-
checkShell(await shell.writeVia(full, data, 'mv -f -- "$T" "$F" || { rm -f -- "$T"; exit 1; }'), "write", full);
|
|
343
|
-
},
|
|
344
|
-
async writeFileExclusive(full, data) {
|
|
345
|
-
const result = await shell.writeVia(full, data, `if ln -- "$T" "$F" 2>/dev/null; then rm -f -- "$T"; exit 0; fi; rm -f -- "$T"; [ -e "$F" ] && exit ${EXISTS}; exit 1`);
|
|
346
|
-
if (result.exitCode === EXISTS)
|
|
347
|
-
return false;
|
|
348
|
-
checkShell(result, "write", full);
|
|
349
|
-
return true;
|
|
350
|
-
},
|
|
351
|
-
async removeFile(full) {
|
|
352
|
-
checkShell(await sh('rm -f -- "$P"', { P: full }), "remove", full);
|
|
353
|
-
},
|
|
354
|
-
async createDirectory(full) {
|
|
355
|
-
checkShell(await sh('mkdir -p -- "$P"', { P: full }), "mkdir", full);
|
|
356
|
-
},
|
|
357
|
-
async listDirectory(full) {
|
|
358
|
-
const result = await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; ls -A1 -- "$P"`, { P: full });
|
|
359
|
-
checkShell(result, "list", full);
|
|
360
|
-
return result.stdout.split("\n").filter(Boolean);
|
|
361
|
-
},
|
|
362
|
-
async stat(full) {
|
|
363
|
-
const result = await sh(`[ -e "$P" ] || exit ${NOT_FOUND}; stat -c '%u:%g:%s:%F' -- "$P"`, { P: full });
|
|
364
|
-
checkShell(result, "stat", full);
|
|
365
|
-
const [uid = "", gid = "", size = "", kind = ""] = result.stdout.trim().split(":");
|
|
366
|
-
const type = kind === "directory" ? "directory" : kind.startsWith("regular") ? "file" : "other";
|
|
367
|
-
return { type, uid: Number(uid), gid: Number(gid), size: Number(size) };
|
|
368
|
-
},
|
|
369
|
-
// sshd always sets HOME for the session.
|
|
370
|
-
async homeDirectory() {
|
|
371
|
-
const result = await sh('printf %s "$HOME"');
|
|
372
|
-
check(result, "home of", user);
|
|
373
|
-
return result.stdout || "/";
|
|
374
|
-
},
|
|
375
|
-
};
|
|
376
|
-
const readFile = async (path) => ((await sftpAvailable()) ? sftpReadFile(path) : shell.readFile(remotePath(path)));
|
|
377
|
-
const readFileRange = async (path, start, end) => (await sftpAvailable()) ? sftpReadFileRange(path, start, end) : shell.readFileRange(remotePath(path), start, end);
|
|
378
|
-
const writeFile = async (path, data) => (await sftpAvailable()) ? sftpWriteFile(path, data) : shell.writeFile(remotePath(path), data);
|
|
379
|
-
const writeFileExclusive = async (path, data) => (await sftpAvailable()) ? sftpWriteFileExclusive(path, data) : shell.writeFileExclusive(remotePath(path), data);
|
|
380
|
-
const removeFile = async (path) => ((await sftpAvailable()) ? sftpRemoveFile(path) : shell.removeFile(remotePath(path)));
|
|
381
|
-
const createDirectory = async (path) => (await sftpAvailable()) ? sftpCreateDirectory(path) : shell.createDirectory(remotePath(path));
|
|
382
|
-
const listDirectory = async (path) => ((await sftpAvailable()) ? sftpListDirectory(path) : shell.listDirectory(remotePath(path)));
|
|
383
|
-
const stat = async (path) => ((await sftpAvailable()) ? sftpStat(path) : shell.stat(remotePath(path)));
|
|
384
|
-
const homeDirectory = () => {
|
|
385
|
-
home ??= sftpAvailable().then((available) => (available ? sftpHomeDirectory() : shell.homeDirectory()));
|
|
386
|
-
home.catch(() => (home = null));
|
|
387
|
-
return home;
|
|
388
|
-
};
|
|
389
|
-
async function runCommand(cmd, { cwd, env = {}, stdin } = {}) {
|
|
390
|
-
return run(commandLine(cmd, env, cwd === undefined ? undefined : remotePath(cwd)), stdin);
|
|
391
|
-
}
|
|
392
|
-
/** A stream to the remote unix socket at `full`, through stream-local forwarding. */
|
|
393
|
-
// Whether the server does stream-local forwarding: unknown until a socket has been reached one way
|
|
394
|
-
// or the other. Servers that don't (or a proxy in front of one) get the relay instead.
|
|
395
|
-
let forwarding = "unknown";
|
|
396
|
-
/**
|
|
397
|
-
* A stream to the remote unix socket at `full`: forwarded by sshd where it can, else through a
|
|
398
|
-
* relay command on the server.
|
|
399
|
-
*/
|
|
400
|
-
async function forward(full) {
|
|
401
|
-
if (forwarding === "unsupported")
|
|
402
|
-
return relay(full);
|
|
403
|
-
const client = await connection();
|
|
404
|
-
try {
|
|
405
|
-
const stream = await new Promise((resolve, reject) => client.openssh_forwardOutStreamLocal(full, (error, opened) => (error ? reject(error) : resolve(opened))));
|
|
406
|
-
forwarding = "works";
|
|
407
|
-
return stream;
|
|
408
|
-
}
|
|
409
|
-
catch (error) {
|
|
410
|
-
const failure = new Error(`connecting to ${full} on ${host} failed: ${error.message}`);
|
|
411
|
-
if (forwarding === "works")
|
|
412
|
-
throw failure;
|
|
413
|
-
// The socket may just be missing (sshd says so in the same words); if the relay reaches it,
|
|
414
|
-
// it was the forwarding.
|
|
415
|
-
const stream = await relay(full).catch(() => {
|
|
416
|
-
throw failure;
|
|
417
|
-
});
|
|
418
|
-
forwarding = "unsupported";
|
|
419
|
-
return stream;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* A stream to the remote unix socket at `full` through a command on the server that connects to it
|
|
424
|
-
* and copies between it and the channel (python3, which servers mostly have). Resolves once the
|
|
425
|
-
* relay has connected; rejects with why it couldn't.
|
|
426
|
-
*/
|
|
427
|
-
async function relay(full) {
|
|
428
|
-
const channel = await exec(commandLine(["python3", "-c", RELAY, full]), "keep open");
|
|
429
|
-
return new Promise((resolve, reject) => {
|
|
430
|
-
let errors = "";
|
|
431
|
-
const failed = () => {
|
|
432
|
-
reject(new Error(`connecting to ${full} on ${host} failed: ${errors.trim() || "the relay exited"}`));
|
|
433
|
-
channel.close();
|
|
434
|
-
};
|
|
435
|
-
// Its first line on stderr says whether it connected.
|
|
436
|
-
const onError = (chunk) => {
|
|
437
|
-
errors += chunk.toString();
|
|
438
|
-
if (!errors.includes("\n"))
|
|
439
|
-
return;
|
|
440
|
-
channel.stderr.off("data", onError);
|
|
441
|
-
channel.off("exit", failed);
|
|
442
|
-
channel.off("close", failed);
|
|
443
|
-
if (errors.startsWith("connected\n"))
|
|
444
|
-
resolve(channel);
|
|
445
|
-
else
|
|
446
|
-
failed();
|
|
447
|
-
};
|
|
448
|
-
channel.stderr.on("data", onError);
|
|
449
|
-
// With its input still open, the channel doesn't close when the relay exits; the exit says it.
|
|
450
|
-
channel.on("exit", failed);
|
|
451
|
-
channel.on("close", failed);
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* HTTP/1.1 over the remote unix socket `init.unix`, one forwarded stream per request (closed when
|
|
456
|
-
* the response ends). The response streams as it arrives; aborting `signal` closes the stream.
|
|
457
|
-
*/
|
|
458
|
-
async function fetchRemote(url, init = {}) {
|
|
459
|
-
if (init.signal?.aborted)
|
|
460
|
-
throw init.signal.reason;
|
|
461
|
-
if (init.unix === undefined)
|
|
462
|
-
throw new Error("the SSH sandbox fetches only over unix sockets (init.unix)");
|
|
463
|
-
const target = new URL(url);
|
|
464
|
-
const stream = await forward(remotePath(init.unix));
|
|
465
|
-
const body = init.body === undefined ? undefined : typeof init.body === "string" ? Buffer.from(init.body) : Buffer.from(init.body);
|
|
466
|
-
const head = [`${init.method ?? "GET"} ${target.pathname}${target.search} HTTP/1.1`, `Host: ${target.host}`, "Connection: close"];
|
|
467
|
-
for (const [name, value] of Object.entries(init.headers ?? {}))
|
|
468
|
-
head.push(`${name}: ${value}`);
|
|
469
|
-
if (body)
|
|
470
|
-
head.push(`Content-Length: ${body.length}`);
|
|
471
|
-
stream.write(`${head.join("\r\n")}\r\n\r\n`);
|
|
472
|
-
if (body)
|
|
473
|
-
stream.write(body);
|
|
474
|
-
let idle;
|
|
475
|
-
let fail = () => { };
|
|
476
|
-
const touch = () => {
|
|
477
|
-
if (typeof init.timeout !== "number")
|
|
478
|
-
return;
|
|
479
|
-
clearTimeout(idle);
|
|
480
|
-
idle = setTimeout(() => fail(new Error(`fetch ${url} on ${host}: no data for ${init.timeout}ms`)), init.timeout);
|
|
481
|
-
};
|
|
482
|
-
const abort = () => fail(init.signal.reason instanceof Error ? init.signal.reason : new Error("aborted"));
|
|
483
|
-
init.signal?.addEventListener("abort", abort, { once: true });
|
|
484
|
-
const finish = () => {
|
|
485
|
-
clearTimeout(idle);
|
|
486
|
-
init.signal?.removeEventListener("abort", abort);
|
|
487
|
-
stream.destroy();
|
|
488
|
-
};
|
|
489
|
-
touch();
|
|
490
|
-
return new Promise((resolve, reject) => {
|
|
491
|
-
let buffer = Buffer.alloc(0);
|
|
492
|
-
let responded = false;
|
|
493
|
-
let controller = null;
|
|
494
|
-
// The body as framed: by Content-Length, chunked, or until the stream closes.
|
|
495
|
-
let framing = { kind: "close" };
|
|
496
|
-
let done = false;
|
|
497
|
-
const end = () => {
|
|
498
|
-
if (done)
|
|
499
|
-
return;
|
|
500
|
-
done = true;
|
|
501
|
-
finish();
|
|
502
|
-
try {
|
|
503
|
-
controller?.close();
|
|
504
|
-
}
|
|
505
|
-
catch {
|
|
506
|
-
/* cancelled */
|
|
507
|
-
}
|
|
508
|
-
};
|
|
509
|
-
fail = (error) => {
|
|
510
|
-
if (done)
|
|
511
|
-
return;
|
|
512
|
-
done = true;
|
|
513
|
-
finish();
|
|
514
|
-
if (!responded)
|
|
515
|
-
reject(error);
|
|
516
|
-
else
|
|
517
|
-
controller?.error(error);
|
|
518
|
-
};
|
|
519
|
-
/** Hands on what the buffer holds of the body; true once the body is complete. */
|
|
520
|
-
const pumpBody = () => {
|
|
521
|
-
if (framing.kind === "close") {
|
|
522
|
-
if (buffer.length)
|
|
523
|
-
controller?.enqueue(new Uint8Array(buffer));
|
|
524
|
-
buffer = Buffer.alloc(0);
|
|
525
|
-
return false;
|
|
526
|
-
}
|
|
527
|
-
if (framing.kind === "length") {
|
|
528
|
-
const take = buffer.subarray(0, framing.left);
|
|
529
|
-
if (take.length)
|
|
530
|
-
controller?.enqueue(new Uint8Array(take));
|
|
531
|
-
framing.left -= take.length;
|
|
532
|
-
buffer = buffer.subarray(take.length);
|
|
533
|
-
return framing.left === 0;
|
|
534
|
-
}
|
|
535
|
-
for (;;) {
|
|
536
|
-
const lineEnd = buffer.indexOf("\r\n");
|
|
537
|
-
if (lineEnd < 0)
|
|
538
|
-
return false;
|
|
539
|
-
const size = parseInt(buffer.subarray(0, lineEnd).toString(), 16);
|
|
540
|
-
if (Number.isNaN(size))
|
|
541
|
-
throw new Error("malformed chunked body");
|
|
542
|
-
if (size === 0)
|
|
543
|
-
return true;
|
|
544
|
-
if (buffer.length < lineEnd + 2 + size + 2)
|
|
545
|
-
return false;
|
|
546
|
-
controller?.enqueue(new Uint8Array(buffer.subarray(lineEnd + 2, lineEnd + 2 + size)));
|
|
547
|
-
buffer = buffer.subarray(lineEnd + 2 + size + 2);
|
|
548
|
-
}
|
|
549
|
-
};
|
|
550
|
-
const onData = (chunk) => {
|
|
551
|
-
if (done)
|
|
552
|
-
return;
|
|
553
|
-
touch();
|
|
554
|
-
buffer = buffer.length ? Buffer.concat([buffer, chunk]) : chunk;
|
|
555
|
-
try {
|
|
556
|
-
if (!responded) {
|
|
557
|
-
const headEnd = buffer.indexOf("\r\n\r\n");
|
|
558
|
-
if (headEnd < 0)
|
|
559
|
-
return;
|
|
560
|
-
const [statusLine = "", ...lines] = buffer.subarray(0, headEnd).toString("latin1").split("\r\n");
|
|
561
|
-
buffer = buffer.subarray(headEnd + 4);
|
|
562
|
-
const status = Number(statusLine.split(" ")[1]);
|
|
563
|
-
if (status >= 100 && status < 200)
|
|
564
|
-
return onData(Buffer.alloc(0)); // interim response
|
|
565
|
-
const headers = new Headers();
|
|
566
|
-
for (const line of lines) {
|
|
567
|
-
const colon = line.indexOf(":");
|
|
568
|
-
if (colon > 0)
|
|
569
|
-
headers.append(line.slice(0, colon).trim(), line.slice(colon + 1).trim());
|
|
570
|
-
}
|
|
571
|
-
responded = true;
|
|
572
|
-
// A null-body status can't be given a body, even an empty one.
|
|
573
|
-
if (status === 204 || status === 304 || init.method === "HEAD") {
|
|
574
|
-
resolve(new Response(null, { status, headers }));
|
|
575
|
-
return end();
|
|
576
|
-
}
|
|
577
|
-
if (headers.get("transfer-encoding")?.toLowerCase().includes("chunked"))
|
|
578
|
-
framing = { kind: "chunked" };
|
|
579
|
-
else if (headers.has("content-length"))
|
|
580
|
-
framing = { kind: "length", left: Number(headers.get("content-length")) };
|
|
581
|
-
const responseBody = new ReadableStream({
|
|
582
|
-
start(c) {
|
|
583
|
-
controller = c;
|
|
584
|
-
},
|
|
585
|
-
cancel() {
|
|
586
|
-
done = true;
|
|
587
|
-
finish();
|
|
588
|
-
},
|
|
589
|
-
});
|
|
590
|
-
resolve(new Response(responseBody, { status, headers }));
|
|
591
|
-
}
|
|
592
|
-
if (pumpBody())
|
|
593
|
-
end();
|
|
594
|
-
}
|
|
595
|
-
catch (error) {
|
|
596
|
-
fail(error);
|
|
597
|
-
}
|
|
598
|
-
};
|
|
599
|
-
stream.on("data", onData);
|
|
600
|
-
stream.on("error", (error) => fail(new Error(`fetch ${url} on ${host} failed: ${error.message}`)));
|
|
601
|
-
// The server is done sending at EOF. That may be all it does: docker only half-closes a hijacked
|
|
602
|
-
// connection (exec output) and waits for the client to close it.
|
|
603
|
-
const ended = () => {
|
|
604
|
-
if (!responded)
|
|
605
|
-
return fail(new Error(`fetch ${url} on ${host} failed: the socket closed before a response`));
|
|
606
|
-
if (framing.kind === "close")
|
|
607
|
-
end();
|
|
608
|
-
else
|
|
609
|
-
fail(new Error(`fetch ${url} on ${host} failed: the response was cut short`));
|
|
610
|
-
};
|
|
611
|
-
stream.on("end", ended);
|
|
612
|
-
stream.on("close", ended);
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
async function connect(path) {
|
|
616
|
-
const full = remotePath(path);
|
|
617
|
-
return socketConnection(await forward(full), full, { opened: true });
|
|
618
|
-
}
|
|
619
|
-
function close() {
|
|
620
|
-
const pending = connecting;
|
|
621
|
-
connecting = null;
|
|
622
|
-
sftpOpening = null;
|
|
623
|
-
void pending?.then((client) => client.end()).catch(() => { });
|
|
624
|
-
}
|
|
625
|
-
const sandbox = {
|
|
626
|
-
readFile,
|
|
627
|
-
readFileRange,
|
|
628
|
-
writeFile,
|
|
629
|
-
writeFileExclusive,
|
|
630
|
-
removeFile,
|
|
631
|
-
createDirectory,
|
|
632
|
-
removeDirectory,
|
|
633
|
-
listDirectory,
|
|
634
|
-
stat,
|
|
635
|
-
homeDirectory,
|
|
636
|
-
runCommand,
|
|
637
|
-
fetch: fetchRemote,
|
|
638
|
-
connect,
|
|
639
|
-
close,
|
|
640
|
-
};
|
|
641
|
-
if (!endpoints)
|
|
642
|
-
return sandbox;
|
|
643
|
-
return { ...sandbox, ...createProxyEndpoints({ runCommand }, endpoints, host) };
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* relay()'s command: connects to the unix socket in argv[1], says "connected" on stderr, then copies
|
|
647
|
-
* stdin to the socket and the socket to stdout until the socket closes (its end of a half-close
|
|
648
|
-
* passed on).
|
|
649
|
-
*/
|
|
650
|
-
const RELAY = `
|
|
651
|
-
import os, socket, sys, threading
|
|
652
|
-
s = socket.socket(socket.AF_UNIX)
|
|
653
|
-
try:
|
|
654
|
-
s.connect(sys.argv[1])
|
|
655
|
-
except OSError as e:
|
|
656
|
-
sys.stderr.write(f"{e.strerror}\\n")
|
|
657
|
-
sys.exit(1)
|
|
658
|
-
sys.stderr.write("connected\\n")
|
|
659
|
-
sys.stderr.flush()
|
|
660
|
-
def up():
|
|
661
|
-
while True:
|
|
662
|
-
data = os.read(0, 65536)
|
|
663
|
-
if not data:
|
|
664
|
-
break
|
|
665
|
-
s.sendall(data)
|
|
666
|
-
try:
|
|
667
|
-
s.shutdown(socket.SHUT_WR)
|
|
668
|
-
except OSError:
|
|
669
|
-
pass
|
|
670
|
-
threading.Thread(target=up, daemon=True).start()
|
|
671
|
-
while True:
|
|
672
|
-
data = s.recv(65536)
|
|
673
|
-
if not data:
|
|
674
|
-
break
|
|
675
|
-
os.write(1, data)
|
|
676
|
-
`;
|
|
677
|
-
/** Quotes a word for a POSIX shell. */
|
|
678
|
-
function quote(word) {
|
|
679
|
-
return `'${word.replaceAll("'", `'\\''`)}'`;
|
|
680
|
-
}
|