Files
bare-operating-system/packages/bare-os-booter/lib/services/sshd-config-parse.js
T
2026-08-18 18:11:28 -04:00

130 lines
3.7 KiB
JavaScript

/**
* Minimal sshd_config subset parser (key value pairs and repeats).
* Lives under bare-os-booter so Pear bundles it with the booter (not bare-os-openssh).
*/
/** Default `AuthorizedKeysFile` when sshd_config omits it (OpenSSH-style relative → $HOME). */
export const SSHD_DEFAULT_AUTHORIZED_KEYS_FILE = '.ssh/authorized_keys'
/**
* Build the logical path bare-openssh uses before `vfs.resolveLogical` (must stay in sync).
* @param {string} [home]
* @param {string} [authKeysRel]
*/
export function logicalAuthorizedKeysPath(home, authKeysRel) {
const h = String(home || '/home/guest').replace(/\/+$/, '')
const rel = String(authKeysRel ?? SSHD_DEFAULT_AUTHORIZED_KEYS_FILE).trim()
if (rel.startsWith('/') || rel.startsWith('~/')) return rel
return `${h}/${rel.replace(/^\/+/, '')}`
}
/**
* @param {string} text
* @returns {{
* port: number,
* listenAddress: string,
* hostKeyPaths: string[],
* passwordAuthentication: boolean,
* pubkeyAuthentication: boolean,
* permitRootLogin: boolean,
* allowTcpForwarding: boolean,
* maxAuthTries: number,
* clientAliveInterval: number,
* authorizedKeysFile: string,
* subsystemSftp: string
* }}
*/
export function parseSshdConfig(text) {
const out = {
// >1024: Pear/Bare guests bind without root (port 22 is permission denied).
port: 2222,
listenAddress: '127.0.0.1',
hostKeyPaths: [] /** @type {string[]} */,
passwordAuthentication: true,
pubkeyAuthentication: true,
permitRootLogin: false,
allowTcpForwarding: false,
maxAuthTries: 6,
clientAliveInterval: 0,
authorizedKeysFile: SSHD_DEFAULT_AUTHORIZED_KEYS_FILE,
subsystemSftp: 'internal-sftp'
}
const lines = String(text || '').split(/\r?\n/)
for (const raw of lines) {
const line = raw.replace(/#.*$/, '').trim()
if (!line) continue
const m = /^([A-Za-z0-9]+)\s+(.+)$/.exec(line)
if (!m) continue
const key = m[1].toLowerCase()
let val = m[2].trim()
if (
(val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))
) {
val = val.slice(1, -1)
}
switch (key) {
case 'port': {
const n = Number.parseInt(val, 10)
if (Number.isFinite(n) && n > 0 && n < 65536) out.port = n
break
}
case 'listenaddress':
out.listenAddress = val
break
case 'hostkey':
out.hostKeyPaths.push(val)
break
case 'passwordauthentication':
out.passwordAuthentication = isYes(val)
break
case 'pubkeyauthentication':
out.pubkeyAuthentication = isYes(val)
break
case 'permitrootlogin':
out.permitRootLogin = val.toLowerCase() === 'yes' || val === 'without-password'
break
case 'allowtcpforwarding':
out.allowTcpForwarding = isYes(val)
break
case 'maxauthtries': {
const n = Number.parseInt(val, 10)
if (Number.isFinite(n) && n > 0) out.maxAuthTries = Math.min(n, 32)
break
}
case 'clientaliveinterval': {
const n = Number.parseInt(val, 10)
if (Number.isFinite(n) && n >= 0) out.clientAliveInterval = n
break
}
case 'authorizedkeysfile':
out.authorizedKeysFile = val
break
case 'subsystem':
if (val.toLowerCase().startsWith('sftp')) {
const parts = val.split(/\s+/)
out.subsystemSftp = parts.slice(1).join(' ') || 'internal-sftp'
}
break
default:
break
}
}
if (out.hostKeyPaths.length === 0) {
out.hostKeyPaths.push(
'~/.config/bare-os/ssh/host/ssh_host_ed25519_key'
)
}
return out
}
/** @param {string} v */
function isYes(v) {
const s = String(v).toLowerCase()
return s === 'yes' || s === 'true'
}