mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
229 lines
8.1 KiB
JavaScript
229 lines
8.1 KiB
JavaScript
import { spawn } from 'node:child_process'
|
|
import { createHash } from 'node:crypto'
|
|
import { createReadStream } from 'node:fs'
|
|
import { lstat, mkdir, realpath, rm } from 'node:fs/promises'
|
|
import { basename, dirname, resolve } from 'node:path'
|
|
import { pipeline } from 'node:stream/promises'
|
|
import { pathToFileURL } from 'node:url'
|
|
|
|
import { extract } from 'tar'
|
|
|
|
import { inspectSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs'
|
|
import { assertSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs'
|
|
import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs'
|
|
import { readSshRelayRuntimeNativeSigningIdentity } from './ssh-relay-runtime-native-signing-plan.mjs'
|
|
import { extractSshRelayRuntimeZip } from './ssh-relay-runtime-zip.mjs'
|
|
import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs'
|
|
|
|
const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024
|
|
const EXTRACTION_TIMEOUT_MS = 5 * 60_000
|
|
const MAX_DIAGNOSTIC_BYTES = 64 * 1024
|
|
|
|
function expectedArchiveName(identity) {
|
|
const extension = identity.os === 'win32' ? 'zip' : 'tar.xz'
|
|
return `orca-ssh-relay-runtime-v1-${identity.tupleId}-${identity.contentId.slice('sha256:'.length)}.${extension}`
|
|
}
|
|
|
|
function sourceIdentity(identity) {
|
|
const { archive: _archive, ...candidate } = identity
|
|
return candidate
|
|
}
|
|
|
|
function assertIdentityArchive(identity, archivePath) {
|
|
assertSshRelayRuntimeClosureEntries(identity)
|
|
const candidate = sourceIdentity(identity)
|
|
if (
|
|
computeSshRelayRuntimeContentId(candidate) !== identity.contentId ||
|
|
identity.archive?.name !== expectedArchiveName(identity) ||
|
|
basename(archivePath) !== identity.archive.name ||
|
|
!Number.isSafeInteger(identity.archive.size) ||
|
|
identity.archive.size <= 0 ||
|
|
identity.archive.size > MAX_ARCHIVE_BYTES ||
|
|
identity.archive.expandedSize !== identity.expandedSize ||
|
|
identity.archive.fileCount !== identity.fileCount ||
|
|
!/^sha256:[0-9a-f]{64}$/u.test(identity.archive.sha256 ?? '')
|
|
) {
|
|
throw new Error('Runtime archive extraction identity or archive reference is inconsistent')
|
|
}
|
|
return candidate
|
|
}
|
|
|
|
function sameFileState(left, right) {
|
|
return (
|
|
left.dev === right.dev &&
|
|
left.ino === right.ino &&
|
|
left.size === right.size &&
|
|
left.mtimeNs === right.mtimeNs &&
|
|
left.ctimeNs === right.ctimeNs
|
|
)
|
|
}
|
|
|
|
async function describeArchive(path, maximumBytes, signal) {
|
|
signal.throwIfAborted()
|
|
const before = await lstat(path, { bigint: true })
|
|
if (
|
|
!before.isFile() ||
|
|
before.isSymbolicLink() ||
|
|
before.size <= 0n ||
|
|
before.size > BigInt(maximumBytes)
|
|
) {
|
|
throw new Error('Runtime archive extraction input must be one bounded regular file')
|
|
}
|
|
const hash = createHash('sha256')
|
|
let size = 0
|
|
for await (const chunk of createReadStream(path, { signal })) {
|
|
size += chunk.length
|
|
if (size > maximumBytes) {
|
|
throw new Error('Runtime archive extraction input exceeds its size bound')
|
|
}
|
|
hash.update(chunk)
|
|
}
|
|
const after = await lstat(path, { bigint: true })
|
|
if (!sameFileState(before, after) || BigInt(size) !== before.size) {
|
|
throw new Error('Runtime archive extraction input changed while hashing')
|
|
}
|
|
return { size, sha256: `sha256:${hash.digest('hex')}`, state: after }
|
|
}
|
|
|
|
async function assertOutputAbsent(outputDirectory) {
|
|
try {
|
|
await lstat(outputDirectory)
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
return
|
|
}
|
|
throw error
|
|
}
|
|
throw new Error('Runtime archive extraction requires an exclusive output directory')
|
|
}
|
|
|
|
async function extractTarXz(archivePath, outputDirectory, signal) {
|
|
const child = spawn('xz', ['--decompress', '--stdout', '--single-stream', '--', archivePath], {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
windowsHide: true,
|
|
signal
|
|
})
|
|
let stderr = ''
|
|
child.stderr.setEncoding('utf8')
|
|
child.stderr.on('data', (chunk) => {
|
|
stderr = `${stderr}${chunk}`.slice(-MAX_DIAGNOSTIC_BYTES)
|
|
})
|
|
const completion = new Promise((resolvePromise, reject) => {
|
|
child.once('error', reject)
|
|
child.once('close', (code, closeSignal) => {
|
|
if (code === 0) {
|
|
resolvePromise()
|
|
} else {
|
|
reject(new Error(`xz extraction failed (${code ?? closeSignal ?? 'unknown'}): ${stderr}`))
|
|
}
|
|
})
|
|
})
|
|
const unpack = extract({
|
|
cwd: outputDirectory,
|
|
strict: true,
|
|
preserveOwner: false,
|
|
noChmod: false,
|
|
unlink: false
|
|
})
|
|
try {
|
|
await Promise.all([pipeline(child.stdout, unpack, { signal }), completion])
|
|
} catch (error) {
|
|
if (child.exitCode === null && child.signalCode === null) {
|
|
child.kill()
|
|
}
|
|
await completion.catch(() => {})
|
|
throw error
|
|
}
|
|
}
|
|
|
|
export async function extractSshRelayRuntimeArchive({
|
|
archivePath,
|
|
outputDirectory,
|
|
identity,
|
|
signal
|
|
}) {
|
|
const effectiveSignal = signal
|
|
? AbortSignal.any([signal, AbortSignal.timeout(EXTRACTION_TIMEOUT_MS)])
|
|
: AbortSignal.timeout(EXTRACTION_TIMEOUT_MS)
|
|
effectiveSignal.throwIfAborted()
|
|
const absoluteArchive = resolve(archivePath)
|
|
const absoluteOutput = resolve(outputDirectory)
|
|
const finalIdentity = assertIdentityArchive(identity, absoluteArchive)
|
|
const physicalParent = await realpath(dirname(absoluteOutput))
|
|
const physicalOutput = resolve(physicalParent, basename(absoluteOutput))
|
|
await assertOutputAbsent(physicalOutput)
|
|
const before = await describeArchive(absoluteArchive, identity.archive.size, effectiveSignal)
|
|
if (before.size !== identity.archive.size || before.sha256 !== identity.archive.sha256) {
|
|
throw new Error('Runtime archive extraction input size or digest mismatch')
|
|
}
|
|
await inspectSshRelayRuntimeArchive(absoluteArchive, finalIdentity, { signal: effectiveSignal })
|
|
|
|
let outputCreated = false
|
|
try {
|
|
await mkdir(physicalOutput)
|
|
outputCreated = true
|
|
await (identity.os === 'win32'
|
|
? extractSshRelayRuntimeZip({
|
|
archivePath: absoluteArchive,
|
|
outputDirectory: physicalOutput,
|
|
identity: finalIdentity,
|
|
signal: effectiveSignal
|
|
})
|
|
: extractTarXz(absoluteArchive, physicalOutput, effectiveSignal))
|
|
const tree = await verifyRuntimeTree(physicalOutput, finalIdentity, { signal: effectiveSignal })
|
|
const after = await describeArchive(absoluteArchive, identity.archive.size, effectiveSignal)
|
|
if (!sameFileState(before.state, after.state) || before.sha256 !== after.sha256) {
|
|
throw new Error('Runtime archive extraction input changed during extraction')
|
|
}
|
|
return { tupleId: identity.tupleId, runtimeRoot: physicalOutput, tree }
|
|
} catch (error) {
|
|
if (outputCreated) {
|
|
// Why: only a completely verified reconstruction may cross the native-signing boundary.
|
|
await rm(physicalOutput, { recursive: true, force: true })
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const ARGUMENT_FIELDS = new Map([
|
|
['--identity', 'identityPath'],
|
|
['--archive', 'archivePath'],
|
|
['--output-directory', 'outputDirectory']
|
|
])
|
|
|
|
export function parseSshRelayRuntimeArchiveExtractionArguments(argv) {
|
|
const result = {}
|
|
for (let index = 0; index < argv.length; index += 2) {
|
|
const flag = argv[index]
|
|
const field = ARGUMENT_FIELDS.get(flag)
|
|
const value = argv[index + 1]
|
|
if (!field || !value || value.startsWith('--') || result[field]) {
|
|
throw new Error(`Invalid runtime archive extraction argument: ${flag}`)
|
|
}
|
|
result[field] = resolve(value)
|
|
}
|
|
if (Object.keys(result).length !== ARGUMENT_FIELDS.size) {
|
|
throw new Error('Runtime archive extraction requires identity, archive, and output directory')
|
|
}
|
|
return result
|
|
}
|
|
|
|
async function main() {
|
|
const options = parseSshRelayRuntimeArchiveExtractionArguments(process.argv.slice(2))
|
|
const identity = await readSshRelayRuntimeNativeSigningIdentity(options.identityPath)
|
|
const result = await extractSshRelayRuntimeArchive({ ...options, identity })
|
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
}
|
|
|
|
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
|
main().catch((error) => {
|
|
process.stderr.write(`SSH relay runtime archive extraction failed: ${error.stack ?? error}\n`)
|
|
process.exitCode = 1
|
|
})
|
|
}
|
|
|
|
export const SSH_RELAY_RUNTIME_ARCHIVE_EXTRACTION_LIMITS = Object.freeze({
|
|
maximumArchiveBytes: MAX_ARCHIVE_BYTES,
|
|
timeoutMs: EXTRACTION_TIMEOUT_MS
|
|
})
|