Files
bare-operating-system/packages/bare-os-booter/lib/services/bare-os-ssh-holesail.js
T
2026-08-18 18:11:34 -04:00

194 lines
6.2 KiB
JavaScript

/**
* Idempotent managed Holesail entry for the stock bare-openssh listen address (see `bare-openssh.js`).
* Same persistence rules as **`bare-os-www-holesail.js`** (`~/.holesail/state.json`).
*/
import { bareHolesailEnvTruthy } from '../p2p/bare-holesail-env.js'
import {
bareHolesailManagedEnsureServerSeedPersisted,
bareHolesailManagedGenerateSeedHex,
bareHolesailManagedReadState,
bareHolesailManagedRuntimeRunning,
bareHolesailManagedServiceIsRunning,
bareHolesailManagedStartOne,
bareHolesailManagedStopOne,
bareHolesailManagedWriteState
} from '../p2p/bare-holesail-managed.js'
export const BARE_OS_SSH_HOLESAIL_CONNECTION_ID_PREFIX = 'bare-ssh-'
/**
* @param {number} port
*/
export function bareOsSshHolesailConnectionId(port) {
return `${BARE_OS_SSH_HOLESAIL_CONNECTION_ID_PREFIX}${port}`
}
/**
* Resolve canonical SSH connection id (always `bare-ssh-<live_port>`), while selecting
* one existing row to carry forward for key/seed reuse and listing stale ids to prune.
* @param {{ connections: Record<string, unknown> }} state
* @param {number} port
*/
function bareOsSshResolveManagedIdentity(state, port) {
const exactId = bareOsSshHolesailConnectionId(port)
const allIds = Object.keys(state.connections).filter((id) =>
id.startsWith(BARE_OS_SSH_HOLESAIL_CONNECTION_ID_PREFIX)
)
if (allIds.includes(exactId)) {
return { id: exactId, sourceId: exactId, staleIds: allIds.filter((id) => id !== exactId) }
}
if (allIds.length === 0) return { id: exactId, sourceId: exactId, staleIds: [] }
const idsSorted = allIds.sort()
const sourceId = idsSorted[0]
return { id: exactId, sourceId, staleIds: idsSorted }
}
/**
* Default on (set **`BARE_OS_SSH_HOLESAIL=0`** or **`false`** to disable).
* @param {Record<string, string | undefined>} env
*/
export function bareOsSshHolesailAutoEnabled(env) {
const v = env.BARE_OS_SSH_HOLESAIL
if (v === '0' || v === 'false') return false
return true
}
/**
* Normalize bind host for stable comparison (matches openssh listen string).
* @param {string} host
*/
function normalizeWantHost(host) {
const h = String(host ?? '').trim()
return h || '127.0.0.1'
}
/**
* Persist a managed server tunnel for SSH `port` / `host` and start it if bare-holesail is already active.
* Call after **`sshServer.listen`** succeeds with the **actual** bound port and host.
* @param {Record<string, unknown>} ctx
* @param {Record<string, string | undefined>} env
* @param {number} port
* @param {string} [host]
*/
export async function ensureBareOsSshHolesailTunnel(ctx, env, port, host) {
if (!bareOsSshHolesailAutoEnabled(env)) return
const p =
Number.isFinite(port) && port > 0 && port < 65536
? Math.floor(port)
: 2222
const wantHost = normalizeWantHost(host)
const vfs = ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') {
return
}
if (!ctx.b4a || typeof ctx.b4a.from !== 'function') return
const { path, state } = await bareHolesailManagedReadState(
ctx,
/** @type {Record<string, string | undefined>} */ (env)
)
const { id, sourceId, staleIds } = bareOsSshResolveManagedIdentity(state, p)
/** @type {Record<string, unknown>} */
const want = { server: true, port: p, host: wantHost, enabled: true }
const existing = state.connections[sourceId]
let shouldRestartRuntime = false
if (existing && typeof existing === 'object') {
const ex = /** @type {Record<string, unknown>} */ (existing)
const curPort = Number.parseInt(String(ex.port ?? ''), 10)
const server = bareHolesailEnvTruthy(ex.server)
const enabled =
ex.enabled === undefined || ex.enabled === null || String(ex.enabled) === ''
? true
: bareHolesailEnvTruthy(ex.enabled)
const rawHost = String(ex.host ?? '').trim()
const hostOk = rawHost === wantHost
shouldRestartRuntime = !(curPort === p && hostOk && server && enabled)
if (server && curPort === p && enabled && hostOk) {
await bareHolesailManagedEnsureServerSeedPersisted(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
if (
bareHolesailManagedServiceIsRunning() &&
!bareHolesailManagedRuntimeRunning(id)
) {
try {
await bareHolesailManagedStartOne(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
} catch {
/* may already run */
}
}
return
}
}
/** @type {Record<string, unknown>} */
const next = { ...want }
if (existing && typeof existing === 'object') {
const ex = /** @type {Record<string, unknown>} */ (existing)
const k = String(ex.key ?? '').trim()
if (k) next.key = k
const s = String(ex.seed ?? '').trim()
if (s) next.seed = s
}
const legacyHsOnly =
/^hs:\/\//i.test(String(next.key ?? '').trim()) && !String(next.seed ?? '').trim()
if (!legacyHsOnly && !String(next.seed ?? '').trim()) {
next.seed = bareHolesailManagedGenerateSeedHex()
}
state.connections[id] = next
for (const staleId of staleIds) delete state.connections[staleId]
await bareHolesailManagedWriteState(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
state
)
await bareHolesailManagedEnsureServerSeedPersisted(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
void path
if (shouldRestartRuntime && bareHolesailManagedRuntimeRunning(id)) {
try {
await bareHolesailManagedStopOne(ctx, id)
} catch {
/* ignore */
}
}
for (const staleId of staleIds) {
if (bareHolesailManagedRuntimeRunning(staleId)) {
try {
await bareHolesailManagedStopOne(ctx, staleId)
} catch {
/* ignore */
}
}
}
if (
bareHolesailManagedServiceIsRunning() &&
!bareHolesailManagedRuntimeRunning(id)
) {
try {
await bareHolesailManagedStartOne(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
} catch (e) {
try {
ctx.console?.error?.(
`[bare-os-ssh] holesail start ${id}: ${(e && /** @type {{ message?: string }} */ (e).message) || String(e)}`
)
} catch {
/* ignore */
}
}
}
}