|
|
|
@@ -6,8 +6,14 @@ import { dirname } from '#host-path'
|
|
|
|
|
*
|
|
|
|
|
* State path: **`BARE_OS_HOLESAIL_STATE`** override, else **`~/.holesail/state.json`** (resolves under **`$HOME`**).
|
|
|
|
|
* Managed Holesail starts only after unlock; legacy **`/.bare/holesail/**`** files may be merged on first read.
|
|
|
|
|
*
|
|
|
|
|
* **Server identity** — Stock rows persist a **`seed`** (64 hex chars from **`bare-crypto` `randomBytes(32)`**).
|
|
|
|
|
* That value is passed to upstream **`Holesail`** as constructor **`key`** (non-`hs://` form) so each login
|
|
|
|
|
* mints the **same** `hs://…` URL; after **`ready()`** the canonical URL is stored in **`key`** for sharing.
|
|
|
|
|
* Legacy rows with only an **`hs://…`** `key` and no **`seed`** keep using the URL string until replaced.
|
|
|
|
|
* Upstream holesail is AGPL-3.0.
|
|
|
|
|
*/
|
|
|
|
|
import { randomBytes } from 'bare-crypto'
|
|
|
|
|
import { bareHolesailEnvTruthy } from './bare-holesail-env.js'
|
|
|
|
|
import { loadHolesailConstructor } from './bare-holesail-loader.js'
|
|
|
|
|
import { appendVarLog, BARE_OS_VAR_LOG_DIR } from './bare-os-var-log.js'
|
|
|
|
@@ -95,6 +101,23 @@ export function bareHolesailManagedValidateId(id) {
|
|
|
|
|
return { ok: true, id: s }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 32 bytes hex — upstream holesail expects `key` length ≥ 32 chars for servers. */
|
|
|
|
|
const HOLESAIL_SEED_HEX_LEN = 64
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* New server tunnel secret (persist in **`state.connections[id].seed`**; passed to **`Holesail({ key })`**).
|
|
|
|
|
*/
|
|
|
|
|
export function bareHolesailManagedGenerateSeedHex() {
|
|
|
|
|
return randomBytes(HOLESAIL_SEED_HEX_LEN / 2).toString('hex')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} s
|
|
|
|
|
*/
|
|
|
|
|
export function bareHolesailManagedSeedLooksValid(s) {
|
|
|
|
|
return /^[0-9a-f]{64}$/i.test(String(s || '').trim())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Record<string, unknown>} raw
|
|
|
|
|
* @returns {{ ok: true, entry: Record<string, unknown> } | { ok: false, err: string }}
|
|
|
|
@@ -114,8 +137,15 @@ export function bareHolesailManagedNormalizeEntry(raw) {
|
|
|
|
|
/** @type {Record<string, unknown>} */
|
|
|
|
|
const entry = { server, client }
|
|
|
|
|
const key = String(raw.key ?? '').trim()
|
|
|
|
|
const seed = String(raw.seed ?? '').trim()
|
|
|
|
|
if (client && !key) return { ok: false, err: 'client requires key' }
|
|
|
|
|
|
|
|
|
|
if (seed) {
|
|
|
|
|
if (!bareHolesailManagedSeedLooksValid(seed))
|
|
|
|
|
return { ok: false, err: 'invalid seed (expect 64 hex chars)' }
|
|
|
|
|
entry.seed = seed.toLowerCase()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (key) entry.key = key
|
|
|
|
|
|
|
|
|
|
if (raw.secure !== undefined && raw.secure !== null && String(raw.secure) !== '')
|
|
|
|
@@ -186,10 +216,12 @@ export function bareHolesailManagedLogLevel(conn, env) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructor options for `new Holesail(opts)` — now passes the **full key** (`hs://0000...` or `hs://s000...`)
|
|
|
|
|
* exactly as stored in state.json. secure is explicitly derived from the key prefix when not set in the row.
|
|
|
|
|
* Constructor options for `new Holesail(opts)`.
|
|
|
|
|
*
|
|
|
|
|
* This matches the working standalone script that reuses the exact hs:// key.
|
|
|
|
|
* **Server** — Prefer persisted **`seed`** (hex) as **`opts.key`** so restarts reproduce the same **`hs://…`** URL.
|
|
|
|
|
* If only a legacy **`hs://…`** share string exists (**no seed**), pass that string as **`opts.key`**.
|
|
|
|
|
*
|
|
|
|
|
* **Client** — Passthrough **`key`** (typically **`hs://…`**).
|
|
|
|
|
*
|
|
|
|
|
* @param {Record<string, unknown>} conn one `state.connections[id]` object
|
|
|
|
|
* @param {Record<string, string | undefined>} env
|
|
|
|
@@ -197,15 +229,17 @@ export function bareHolesailManagedLogLevel(conn, env) {
|
|
|
|
|
*/
|
|
|
|
|
export function bareHolesailManagedNewHolesailOpts(conn, env) {
|
|
|
|
|
const c = /** @type {Record<string, unknown>} */ (conn)
|
|
|
|
|
const seedStr = c.seed != null ? String(c.seed).trim() : ''
|
|
|
|
|
const keyFull = c.key != null ? String(c.key).trim() : ''
|
|
|
|
|
|
|
|
|
|
const server = bareHolesailEnvTruthy(c.server)
|
|
|
|
|
const client = bareHolesailEnvTruthy(c.client)
|
|
|
|
|
|
|
|
|
|
// Derive secure from key prefix if not explicitly set in the row
|
|
|
|
|
let secure = false
|
|
|
|
|
if (c.secure !== undefined && c.secure !== null && String(c.secure) !== '') {
|
|
|
|
|
secure = bareHolesailEnvTruthy(c.secure)
|
|
|
|
|
} else if (seedStr) {
|
|
|
|
|
secure = false
|
|
|
|
|
} else if (keyFull) {
|
|
|
|
|
secure = /^hs:\/\/s/i.test(keyFull)
|
|
|
|
|
}
|
|
|
|
@@ -216,14 +250,23 @@ export function bareHolesailManagedNewHolesailOpts(conn, env) {
|
|
|
|
|
|
|
|
|
|
const log = bareHolesailManagedLogLevel(conn, env)
|
|
|
|
|
|
|
|
|
|
/** @type {string} */
|
|
|
|
|
let keyOpt = ''
|
|
|
|
|
if (server) {
|
|
|
|
|
if (seedStr) keyOpt = seedStr.toLowerCase()
|
|
|
|
|
else keyOpt = keyFull
|
|
|
|
|
} else {
|
|
|
|
|
keyOpt = keyFull
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @type {Record<string, unknown>} */
|
|
|
|
|
const opts = {
|
|
|
|
|
server,
|
|
|
|
|
client,
|
|
|
|
|
port: c.port,
|
|
|
|
|
host: c.host != null ? String(c.host) : undefined,
|
|
|
|
|
key: keyFull, // ← Full hs://... key (critical fix)
|
|
|
|
|
secure, // ← Explicit secure flag
|
|
|
|
|
key: keyOpt,
|
|
|
|
|
secure,
|
|
|
|
|
udp,
|
|
|
|
|
log
|
|
|
|
|
}
|
|
|
|
@@ -264,8 +307,12 @@ function mergeCoercedHolesailStates(a, b) {
|
|
|
|
|
const n = /** @type {Record<string, unknown>} */ (raw)
|
|
|
|
|
const ck = String(c.key ?? '').trim()
|
|
|
|
|
const nk = String(n.key ?? '').trim()
|
|
|
|
|
const cseed = String(c.seed ?? '').trim()
|
|
|
|
|
const nseed = String(n.seed ?? '').trim()
|
|
|
|
|
if (nk && !ck) conns[id] = { ...c, ...n, key: nk }
|
|
|
|
|
else if (ck && !nk) conns[id] = { ...n, ...c, key: ck }
|
|
|
|
|
else if (nseed && !cseed) conns[id] = { ...c, ...n, seed: nseed }
|
|
|
|
|
else if (cseed && !nseed) conns[id] = { ...n, ...c, seed: cseed }
|
|
|
|
|
else conns[id] = { ...c, ...n }
|
|
|
|
|
}
|
|
|
|
|
return { version: 1, connections: conns }
|
|
|
|
@@ -372,6 +419,50 @@ export async function bareHolesailManagedWriteState(ctx, env, state) {
|
|
|
|
|
await vfs.writeFile(path, ctx.b4a.from(body))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Ensure a managed **server** row has a **`seed`** before `new Holesail`, unless it is a legacy row that
|
|
|
|
|
* only stores an **`hs://…`** URL in **`key`** (no deterministic seed possible).
|
|
|
|
|
*
|
|
|
|
|
* Idempotent; persists **`state.json`** when a new seed is minted or hex-only **`key`** is migrated into **`seed`**.
|
|
|
|
|
*
|
|
|
|
|
* @param {Record<string, unknown>} ctx
|
|
|
|
|
* @param {Record<string, string | undefined>} env
|
|
|
|
|
* @param {string} id
|
|
|
|
|
*/
|
|
|
|
|
export async function bareHolesailManagedEnsureServerSeedPersisted(ctx, env, id) {
|
|
|
|
|
const { state } = await bareHolesailManagedReadState(ctx, env)
|
|
|
|
|
const raw = state.connections[id]
|
|
|
|
|
if (!raw || typeof raw !== 'object') return
|
|
|
|
|
const row = /** @type {Record<string, unknown>} */ (raw)
|
|
|
|
|
if (!bareHolesailEnvTruthy(row.server)) return
|
|
|
|
|
|
|
|
|
|
let seed = String(row.seed ?? '').trim()
|
|
|
|
|
if (seed && !bareHolesailManagedSeedLooksValid(seed)) seed = ''
|
|
|
|
|
|
|
|
|
|
const key = String(row.key ?? '').trim()
|
|
|
|
|
|
|
|
|
|
if (seed) {
|
|
|
|
|
row.seed = seed.toLowerCase()
|
|
|
|
|
state.connections[id] = row
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (/^hs:\/\//i.test(key)) return
|
|
|
|
|
|
|
|
|
|
if (bareHolesailManagedSeedLooksValid(key)) {
|
|
|
|
|
row.seed = key.toLowerCase()
|
|
|
|
|
state.connections[id] = row
|
|
|
|
|
await bareHolesailManagedWriteState(ctx, env, state)
|
|
|
|
|
logLine(ctx, `managed: migrated hex key → seed for ${id}`)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
row.seed = bareHolesailManagedGenerateSeedHex()
|
|
|
|
|
state.connections[id] = row
|
|
|
|
|
await bareHolesailManagedWriteState(ctx, env, state)
|
|
|
|
|
logLine(ctx, `managed: persisted new server seed for ${id}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Record<string, unknown>} ctx
|
|
|
|
|
* @param {Record<string, string | undefined>} env
|
|
|
|
@@ -420,6 +511,8 @@ export async function bareHolesailManagedSyncPersistedServerKey(ctx, env, id) {
|
|
|
|
|
* @param {string} id
|
|
|
|
|
*/
|
|
|
|
|
async function startManagedInstance(ctx, env, id) {
|
|
|
|
|
await bareHolesailManagedEnsureServerSeedPersisted(ctx, env, id)
|
|
|
|
|
|
|
|
|
|
const { state } = await bareHolesailManagedReadState(ctx, env)
|
|
|
|
|
const raw = state.connections[id]
|
|
|
|
|
if (!raw || typeof raw !== 'object') return
|
|
|
|
@@ -440,19 +533,35 @@ async function startManagedInstance(ctx, env, id) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const keyStr = String(conn.key ?? '').trim()
|
|
|
|
|
const seedStr = String(conn.seed ?? '').trim()
|
|
|
|
|
if (client && !keyStr) {
|
|
|
|
|
logLine(ctx, `managed: skip ${id}: client requires key`)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (managedInstances.has(id)) {
|
|
|
|
|
if (server && keyStr) {
|
|
|
|
|
const live = bareHolesailManagedRuntimeUrl(id).trim()
|
|
|
|
|
if (bareHolesailManagedPersistedUrlMatchesLive(keyStr, live)) return
|
|
|
|
|
await stopManagedInstance(ctx, id)
|
|
|
|
|
} else {
|
|
|
|
|
if (!server) return
|
|
|
|
|
|
|
|
|
|
const live = bareHolesailManagedRuntimeUrl(id).trim()
|
|
|
|
|
const rec = managedInstances.get(id)
|
|
|
|
|
const prevRow =
|
|
|
|
|
rec?.entry && typeof rec.entry === 'object'
|
|
|
|
|
? /** @type {Record<string, unknown>} */ (rec.entry)
|
|
|
|
|
: null
|
|
|
|
|
const prevSeed = prevRow ? String(prevRow.seed ?? '').trim() : ''
|
|
|
|
|
|
|
|
|
|
if (seedStr && prevSeed && seedStr.toLowerCase() === prevSeed.toLowerCase() && live)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
keyStr &&
|
|
|
|
|
live &&
|
|
|
|
|
bareHolesailManagedPersistedUrlMatchesLive(keyStr, live) &&
|
|
|
|
|
(!seedStr || seedStr.toLowerCase() === prevSeed.toLowerCase())
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
await stopManagedInstance(ctx, id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const Holesail = await loadHolesailConstructor(ctx)
|
|
|
|
|