UNPKG

the-moby-effect

Version:
144 lines 6.14 kB
import * as Effect from "effect/Effect"; import * as Function from "effect/Function"; import * as Layer from "effect/Layer"; import * as Socket from "effect/unstable/socket/Socket"; import * as internalAgnostic from "./agnostic.js"; import * as internalConnection from "./connection.js"; /** @internal */ export const makeNodeSshAgent = (httpLazy, ssh2Lazy, connectionOptions) => new class extends httpLazy.Agent { // The ssh client that will be connecting to the server sshClient; // How to connect to the remote server and where the moby socket is located. sshConfig; // Internal state to know if we are connected or not sshConnection = "unopened"; openFailedError = null; constructor(ssh2ConnectConfig, agentOptions) { super(agentOptions); this.sshConfig = ssh2ConnectConfig; this.sshClient = new ssh2Lazy.Client(); } /** * When the agent is destroyed, we also want to close the ssh connection * if it is still open. * * @since 1.0.0 * @see https://nodejs.org/api/http.html#agentdestroy */ destroy() { this.sshClient.end(); this.sshClient.destroy(); super.destroy(); } /** * When creating a new connection, first start by trying to connect to * the remote server over ssh. Then, if that was successful, we send a * forwardOurStreamLocal request which is an OpenSSH extension that * opens a connection to the unix domain socket at socketPath on the * remote server and forwards traffic. * * @since 1.0.0 * @see https://nodejs.org/api/http.html#agentcreateconnectionoptions-callback */ createConnection(_options, callback) { const onError = error => { /** * Indicates 'client-socket' for socket-level errors and * 'client-ssh' for SSH disconnection messages. If the error is * at the client-ssh level, we want to close the ssh connection * and this agent cannot be used anymore. */ // if (error.level === "client-ssh") this.destroy(); callback(error, undefined); }; // No connection yet, start connecting if (this.sshConnection === "unopened") { this.sshConnection = "connecting"; const unableToOpen = error => { this.sshConnection = "open-failed"; this.openFailedError = error; onError(error); }; this.sshClient.once("ready", () => { this.sshConnection = "ready"; this.sshClient.off("error", unableToOpen); this.createConnection(_options, callback); }).once("error", unableToOpen).connect(this.sshConfig); } // Already tried to connect but failed else if (this.sshConnection === "open-failed") { callback(this.openFailedError, undefined); } // Another connection attempt is already in progress, wait for it else if (this.sshConnection === "connecting") { this.sshClient.once("ready", () => { this.sshClient.off("error", onError); this.createConnection(_options, callback); }).once("error", onError); } // We are connected to the ssh server, now forward our stream local else if (this.sshConnection === "ready") { this.sshClient.openssh_forwardOutStreamLocal(this.sshConfig.remoteSocketPath, (error, stream) => { this.sshClient.off("error", onError); if (error) return callback(error, void 0);else return callback(null, stream); }).once("error", onError); } // Should never happen else { return Function.absurd(this.sshConnection); } } }(connectionOptions); /** @internal */ export const getNodeAgent = connectionOptions => Function.pipe(Effect.all({ httpLazy: Effect.promise(() => import("node:http")), httpsLazy: Effect.promise(() => import("node:https")) }, { concurrency: 2 }), Effect.flatMap(({ httpLazy, httpsLazy }) => { const AcquireNodeHttpAgent = internalConnection.MobyConnectionOptions.$match({ http: options => Effect.succeed(new httpLazy.Agent({ host: options.host, port: options.port })), socket: options => Effect.succeed(new httpLazy.Agent({ socketPath: options.socketPath })), ssh: options => Effect.map(Effect.promise(() => import("ssh2")), ssh2Lazy => makeNodeSshAgent(httpLazy, ssh2Lazy, options)), https: options => Effect.succeed(new httpsLazy.Agent({ ca: options.ca, key: options.key, cert: options.cert, host: options.host, port: options.port, passphrase: options.passphrase })) }); const releaseNodeHttpAgent = agent => Effect.sync(() => agent.destroy()); const resource = Effect.acquireRelease(AcquireNodeHttpAgent(connectionOptions), releaseNodeHttpAgent); return Effect.map(resource, agent => ({ http: agent, https: agent })); })); /** @internal */ export const getWebsocketConstructor = connectionOptions => Layer.effect(Socket.WebSocketConstructor, Effect.gen(function* () { const ws = yield* Effect.promise(() => import("ws")); const nodeHttpClientLazy = yield* Effect.promise(() => import("@effect/platform-node/NodeHttpClient")); const agent = yield* nodeHttpClientLazy.HttpAgent; return (url, protocols) => new ws.WebSocket(`ws://0.0.0.0${internalAgnostic.makeVersionPath(connectionOptions)}${url}`, protocols, { agent: agent.http }); })); /** @internal */ export const makeNodeHttpClientLayer = connectionOptions => { const httpAgentLive = Function.pipe(Effect.promise(() => import("@effect/platform-node/NodeHttpClient")), Effect.map(nodeHttpClientLazy => Layer.effect(nodeHttpClientLazy.HttpAgent, getNodeAgent(connectionOptions))), Layer.unwrap); const websocketConstructorLive = getWebsocketConstructor(connectionOptions); const nodeHttpClientLive = Function.pipe(Effect.promise(() => import("@effect/platform-node/NodeHttpClient")), Effect.map(nodeHttpClientLazy => nodeHttpClientLazy.layerNodeHttpNoAgent), Layer.unwrap); const agnosticHttpClientLayer = internalAgnostic.makeAgnosticHttpClientLayer(connectionOptions); return agnosticHttpClientLayer.pipe(Layer.merge(websocketConstructorLive), Layer.provide(nodeHttpClientLive), Layer.provide(httpAgentLive)); }; //# sourceMappingURL=node.js.map