/** * 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' }