194 lines
6.2 KiB
JavaScript
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 './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-<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 */
|
|
}
|
|
}
|
|
}
|
|
}
|