| 1 | import * as NodeDnsPromises from "node:dns/promises"; |
| 2 | import * as NodeNet from "node:net"; |
| 3 | import type { SshDeviceHostConfig } from "@t3tools/contracts"; |
| 4 | import * as NodeOS from "node:os"; |
| 5 | import * as Context from "effect/Context"; |
| 6 | import { runSshCommand } from "@t3tools/ssh/command"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | |
| 9 | export const LocalDeviceHostAddresses = Context.Reference<ReadonlySet<string>>( |
| 10 | "LocalDeviceHostAddresses", |
| 11 | { |
| 12 | defaultValue: () => |
| 13 | new Set( |
| 14 | Object.values(NodeOS.networkInterfaces()).flatMap( |
| 15 | (entries) => entries?.map((entry) => entry.address) ?? [], |
| 16 | ), |
| 17 | ), |
| 18 | }, |
| 19 | ); |
| 20 | |
| 21 | /** Resolve aliases on the owning environment without opening an SSH connection. */ |
| 22 | export const isLocalSshDeviceHost = Effect.fn("isLocalSshDeviceHost")(function* ( |
| 23 | host: SshDeviceHostConfig, |
| 24 | ) { |
| 25 | const result = yield* runSshCommand( |
| 26 | { alias: host.target, hostname: host.target, username: null, port: host.port ?? null }, |
| 27 | { |
| 28 | preHostArgs: ["-G", ...(host.identityFile ? ["-i", host.identityFile] : [])], |
| 29 | timeoutMs: 5000, |
| 30 | }, |
| 31 | ).pipe(Effect.result); |
| 32 | if (result._tag === "Failure") return false; |
| 33 | const config = new Map( |
| 34 | result.success.stdout.split("\n").map((line) => { |
| 35 | const separator = line.indexOf(" "); |
| 36 | return [line.slice(0, separator), line.slice(separator + 1).trim()]; |
| 37 | }), |
| 38 | ); |
| 39 | // A local forwarded port or a proxy can lead to a different machine. |
| 40 | if ( |
| 41 | config.get("port") !== "22" || |
| 42 | ["proxycommand", "proxyjump"].some((key) => config.has(key) && config.get(key) !== "none") |
| 43 | ) |
| 44 | return false; |
| 45 | const hostname = config.get("hostname")?.replace(/^\[|\]$/g, ""); |
| 46 | if (!hostname) return false; |
| 47 | const addresses = NodeNet.isIP(hostname) |
| 48 | ? [hostname] |
| 49 | : yield* Effect.tryPromise(() => NodeDnsPromises.lookup(hostname, { all: true })).pipe( |
| 50 | Effect.map((entries) => entries.map((entry) => entry.address)), |
| 51 | Effect.timeout("2 seconds"), |
| 52 | Effect.orElseSucceed(() => [] as string[]), |
| 53 | ); |
| 54 | const localAddresses = yield* LocalDeviceHostAddresses; |
| 55 | return ( |
| 56 | addresses.length > 0 && |
| 57 | addresses.every( |
| 58 | (address) => localAddresses.has(address) || address === "::1" || address.startsWith("127."), |
| 59 | ) |
| 60 | ); |
| 61 | }); |
| 62 | |
| 63 | export const remoteSshDeviceHosts = Effect.fn("remoteSshDeviceHosts")(function* ( |
| 64 | hosts: ReadonlyArray<SshDeviceHostConfig>, |
| 65 | ) { |
| 66 | return yield* Effect.filter( |
| 67 | hosts, |
| 68 | (host) => isLocalSshDeviceHost(host).pipe(Effect.map((local) => !local)), |
| 69 | { concurrency: 4 }, |
| 70 | ); |
| 71 | }); |