130 lines
3.7 KiB
JavaScript
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'
|
|
}
|