/** * 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 './bare-holesail-env.js' import { bareHolesailManagedEnsureServerSeedPersisted, bareHolesailManagedGenerateSeedHex, bareHolesailManagedReadState, bareHolesailManagedRuntimeRunning, bareHolesailManagedServiceIsRunning, bareHolesailManagedStartOne, bareHolesailManagedStopOne, bareHolesailManagedWriteState } from './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-`), while selecting * one existing row to carry forward for key/seed reuse and listing stale ids to prune. * @param {{ connections: Record }} 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} 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} ctx * @param {Record} 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} */ (env) ) const { id, sourceId, staleIds } = bareOsSshResolveManagedIdentity(state, p) /** @type {Record} */ 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} */ (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} */ (env), id ) if ( bareHolesailManagedServiceIsRunning() && !bareHolesailManagedRuntimeRunning(id) ) { try { await bareHolesailManagedStartOne( ctx, /** @type {Record} */ (env), id ) } catch { /* may already run */ } } return } } /** @type {Record} */ const next = { ...want } if (existing && typeof existing === 'object') { const ex = /** @type {Record} */ (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} */ (env), state ) await bareHolesailManagedEnsureServerSeedPersisted( ctx, /** @type {Record} */ (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} */ (env), id ) } catch (e) { try { ctx.console?.error?.( `[bare-os-ssh] holesail start ${id}: ${(e && /** @type {{ message?: string }} */ (e).message) || String(e)}` ) } catch { /* ignore */ } } } }