OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / handlers / bootstrap.js

94 lines · javascript ref:
/**
 * HTTP bootstrap handler — serves the first-install artifacts for a fresh
 * machine. pear-git-v3 IS the deploy service, so it owns this.
 *
 * Layout under ${PEAR_GIT_STORAGE}/bootstrap/ (or opts.bootstrapDir):
 *
 *   install.sh                                 (symlink → pgh tree's scripts/)
 *   <app>-bootstrap-bare-<os>-<arch>.tar.gz    (tar of the local app store tree)
 *
 * The bootstrap dir (<store>/bootstrap) is produced + owned by pgh-sidecar
 * (pgh/bootstrap-artifacts): on install/OTA it TARS the local pgh + pgh-sidecar
 * app trees (the apps ARE the self-contained bootstrap trees) into that dir and
 * symlinks install.sh from the pgh tree (#66). So this handler stays a plain
 * static file server — no reconcile, no overlay/manifest logic, no network in
 * the bootstrap path: it serves whatever the sidecar has produced locally.
 *
 * URLs:
 *   GET /bootstrap/install.sh
 *   GET /bootstrap/<app>-bootstrap-bare-<os>-<arch>.tar.gz
 *
 * Missing files return 404. No directory listing, no traversal — paths are
 * normalized + scoped to the bootstrap dir.
 */

import { createReadStream, statSync, existsSync } from '#node:fs'
import path from '#node:path'
import { pipeFile } from '../web/pipe-file.js'

const TYPES = {
  '.sh': 'text/x-shellscript; charset=utf-8',
  '.tar.gz': 'application/gzip',
  '.tgz': 'application/gzip',
  '.txt': 'text/plain; charset=utf-8',
  '.sha256': 'text/plain; charset=utf-8'
}

function contentType (p) {
  if (p.endsWith('.tar.gz')) return TYPES['.tar.gz']
  return TYPES[path.extname(p)] || 'application/octet-stream'
}

/**
 * Returns true if the handler consumed the request. URL is prefix-stripped by
 * the caller (router.js). `bootstrapDir` may be a symlink to the materialized
 * store tree — serving follows it transparently.
 */
export async function handleBootstrap (req, res, url, bootstrapDir) {
  if (!url.startsWith('/bootstrap/') && url !== '/bootstrap') return false
  if (req.method !== 'GET' && req.method !== 'HEAD') {
    res.writeHead(405, { 'Content-Type': 'text/plain; charset=utf-8' })
    res.end('method not allowed')
    return true
  }
  const rel = url === '/bootstrap' ? '' : url.slice('/bootstrap/'.length)
  if (!bootstrapDir) {
    res.writeHead(503, { 'Content-Type': 'text/plain; charset=utf-8' })
    res.end('bootstrap dir not configured on this daemon')
    return true
  }
  // Normalize + scope (lexical) to the bootstrap dir; refuse `..` escapes. The
  // dir itself may be a symlink to the store tree — that's resolved at read.
  const base = path.resolve(bootstrapDir)
  const abs = path.resolve(bootstrapDir, rel)
  if (!abs.startsWith(base + path.sep) && abs !== base) {
    res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
    res.end('forbidden')
    return true
  }
  if (!existsSync(abs)) {
    res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
    res.end('bootstrap artifact not found: ' + rel + '\n\nExpected layout:\n  install.sh\n  <app>-bootstrap-bare-<os>-<arch>.tar.gz\n')
    return true
  }
  const st = statSync(abs)
  if (st.isDirectory()) {
    res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
    res.end('directory listing disabled')
    return true
  }
  if (req.method === 'HEAD') {
    res.writeHead(200, { 'Content-Type': contentType(abs), 'Content-Length': st.size, 'Cache-Control': 'no-cache' })
    res.end()
    return true
  }
  // Content-Length omitted on the streamed body — bare-http1 emits a duplicate
  // Content-Length if it's set in writeHead AND a stream is piped; let the
  // response chunk. (HEAD above still advertises the size.)
  res.writeHead(200, { 'Content-Type': contentType(abs), 'Cache-Control': 'no-cache' })
  // pipeFile, not a bare .pipe(): these are the largest bodies the forge
  // serves (multi-hundred-MB bootstrap tarballs), so an aborted download is
  // routine — and unguarded it would kill the daemon (see lib/web/pipe-file.js).
  return pipeFile(createReadStream(abs), res, 'bootstrap ' + rel)
}