Files
bare-operating-system/packages/bare-os-booter/lib/bare-openssh-sftp.js
T

246 lines
7.0 KiB
JavaScript

/**
* Minimal SFTP over Bare OS VFS (bare-ssh2 SFTP server).
* Paths are sandboxed under `homeLogical` and `/mnt` (logical).
*/
import { S_IFDIR, S_IFREG } from './vfs-posix-meta.js'
const S_IRUSR = 0o400
const S_IWUSR = 0o200
const S_IXUSR = 0o100
/**
* @param {import('events').EventEmitter} sftp
* @param {Record<string, unknown>} vfs
* @param {string} homeLogical absolute logical home (e.g. /home/guest)
* @param {(p: string) => string} resolveLogical
* @param {{ OPEN_MODE: Record<string, number>, STATUS_CODE: Record<string, number> }} sftpConsts
*/
export function attachBareOsSftp(
sftp,
vfs,
homeLogical,
resolveLogical,
sftpConsts
) {
const { OPEN_MODE, STATUS_CODE } = sftpConsts
/** @type {Map<number, { path: string, buf?: Uint8Array | null, pos: number, write?: boolean }>} */
const handles = new Map()
/** @type {Map<number, { path: string, entries?: string[], idx: number }>} */
const dirs = new Map()
let hid = 1
let did = 1
function ok(reqid) {
sftp.status(reqid, STATUS_CODE.OK)
}
/**
* @param {string} p
* @returns {string | null}
*/
function mapPath(p) {
const norm = String(p || '').replace(/\\/g, '/').replace(/\/+/g, '/')
let logical
if (norm === '.' || norm === '') logical = homeLogical
else if (norm.startsWith('/')) {
if (norm === homeLogical || norm.startsWith(homeLogical + '/')) logical = norm
else if (norm.startsWith('/mnt') || norm === '/mnt') logical = norm
else return null
} else {
logical = resolveLogical(`${homeLogical}/${norm}`.replace(/\/+/g, '/'))
}
const abs = norm.startsWith('/') ? resolveLogical(norm) : logical
if (!abs.startsWith(homeLogical) && !abs.startsWith('/mnt')) return null
return abs
}
async function statLike(reqid, p, isLstat) {
const abs = mapPath(p)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
const st = await vfs.lstat(abs)
if (!st) return sftp.status(reqid, STATUS_CODE.NO_SUCH_FILE)
const mode =
(st.isDirectory() ? S_IFDIR : S_IFREG) | S_IRUSR | S_IWUSR | S_IXUSR
sftp.attrs(reqid, {
mode,
uid: 1000,
gid: 1000,
size: st.size || 0,
atime: st.mtime?.getTime?.() || Date.now(),
mtime: st.mtime?.getTime?.() || Date.now()
})
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
}
sftp.on('REALPATH', (reqid, p) => {
const abs = mapPath(p) || homeLogical
const name = [
{
filename: abs,
longname: 'drwxr-xr-x 1 user user 0 Jan 1 1970 .',
attrs: {}
}
]
sftp.name(reqid, name)
})
sftp.on('STAT', (reqid, p) => {
void statLike(reqid, p, false)
})
sftp.on('LSTAT', (reqid, p) => {
void statLike(reqid, p, true)
})
sftp.on('OPENDIR', async (reqid, p) => {
const abs = mapPath(p)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
const list = await vfs.readdir(abs)
if (!Array.isArray(list)) return sftp.status(reqid, STATUS_CODE.FAILURE)
const h = Buffer.alloc(4)
const id = did++
h.writeUInt32BE(id, 0)
dirs.set(id, { path: abs, entries: list, idx: 0 })
sftp.handle(reqid, h)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
sftp.on('READDIR', async (reqid, handle) => {
const id = handle.readUInt32BE(0)
const d = dirs.get(id)
if (!d || !d.entries) return sftp.status(reqid, STATUS_CODE.FAILURE)
if (d.idx >= d.entries.length) return sftp.status(reqid, STATUS_CODE.EOF)
const chunk = d.entries.slice(d.idx, d.idx + 32)
d.idx += chunk.length
const names = []
for (const ent of chunk) {
names.push({
filename: ent,
longname: '-rw-r--r-- 1 user user 0 Jan 1 1970 ' + ent,
attrs: {}
})
}
sftp.name(reqid, names)
})
sftp.on('OPEN', async (reqid, filename, flags) => {
const abs = mapPath(filename)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
const wantRead = !!(flags & OPEN_MODE.READ)
const wantWrite = !!(flags & OPEN_MODE.WRITE) || !!(flags & OPEN_MODE.APPEND)
const wantCreat = !!(flags & OPEN_MODE.CREAT)
try {
let buf = null
if (!wantCreat) {
try {
buf = await vfs.readFile(abs)
} catch {
buf = null
}
}
if (buf == null && !wantCreat && wantRead) {
return sftp.status(reqid, STATUS_CODE.NO_SUCH_FILE)
}
if (buf == null && wantCreat) buf = new Uint8Array(0)
if (buf == null) buf = new Uint8Array(0)
const h = Buffer.alloc(4)
const id = hid++
h.writeUInt32BE(id, 0)
handles.set(id, {
path: abs,
buf,
pos: 0,
write: wantWrite || wantCreat
})
sftp.handle(reqid, h)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
sftp.on('READ', (reqid, handle, offset, length) => {
const id = handle.readUInt32BE(0)
const f = handles.get(id)
if (!f || !f.buf) return sftp.status(reqid, STATUS_CODE.FAILURE)
const end = Math.min(offset + length, f.buf.length)
if (offset >= f.buf.length) return sftp.status(reqid, STATUS_CODE.EOF)
const slice = f.buf.subarray(offset, end)
sftp.data(reqid, slice)
})
sftp.on('WRITE', async (reqid, handle, offset, data) => {
const id = handle.readUInt32BE(0)
const f = handles.get(id)
if (!f || !f.write) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
const u8 = data instanceof Uint8Array ? data : new Uint8Array(data)
const prev = f.buf ? new Uint8Array(f.buf) : new Uint8Array(0)
const need = offset + u8.length
const out = new Uint8Array(Math.max(prev.length, need))
out.set(prev)
out.set(u8, offset)
f.buf = out
await vfs.writeFile(f.path, out)
ok(reqid)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
sftp.on('CLOSE', (reqid, handle) => {
const id = handle.readUInt32BE(0)
handles.delete(id)
dirs.delete(id)
ok(reqid)
})
sftp.on('REMOVE', async (reqid, p) => {
const abs = mapPath(p)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
await vfs.unlink(abs)
ok(reqid)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
sftp.on('MKDIR', async (reqid, p) => {
const abs = mapPath(p)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
await vfs.mkdir(abs, { recursive: true })
ok(reqid)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
sftp.on('RMDIR', async (reqid, p) => {
const abs = mapPath(p)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
await vfs.rmdir(abs)
ok(reqid)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
sftp.on('RENAME', (reqid) => {
sftp.status(reqid, STATUS_CODE.OP_UNSUPPORTED)
})
sftp.on('READLINK', (reqid) => {
sftp.status(reqid, STATUS_CODE.OP_UNSUPPORTED)
})
sftp.on('SYMLINK', (reqid) => {
sftp.status(reqid, STATUS_CODE.OP_UNSUPPORTED)
})
}