import { execFile } from "node:child_process"; import { once } from "node:events"; import { createReadStream } from "node:fs"; import { stat } from "node:fs/promises"; import { createServer } from "node:http"; import path from "node:path"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); /** * Serve a bare repository over authenticated "dumb" HTTP. * * `git daemon` (story 01's default transport) has no authentication, so * it cannot exercise the ephemeral-credential-helper path. This helper * can: a plain static-file server over the bare repo (dumb git-HTTP * protocol — enabled by running `git update-server-info`) that rejects * every request lacking the expected HTTP Basic credential with a 401 + * `WWW-Authenticate`. A real `git clone` against it performs the real * credential dance: unauthenticated probe → 401 → git consults the * configured credential helper → retry with Basic auth. * * Every received Authorization header is recorded, so tests can assert * the exact credential git presented — i.e. that the PAT reached git * through the helper (and only through it). */ export interface AuthedGitHttpServer { /** Clone URL: `http://127.0.0.1:/repo.git` */ cloneUrl: string; /** Every Authorization header received, in order. */ authorizationHeaders: () => string[]; /** Count of requests that arrived with no Authorization header. */ unauthenticatedRequests: () => number; stop: () => Promise; } export async function serveBareRepoOverHttp( bareRepoPath: string, credentials: { username: string; password: string }, ): Promise { // Generate info/refs + objects/info/packs so the dumb protocol works. await execFileAsync("git", ["update-server-info"], { cwd: bareRepoPath }); const expected = "Basic " + Buffer.from(`${credentials.username}:${credentials.password}`).toString( "base64", ); const received: string[] = []; let unauthenticated = 0; const server = createServer((req, res) => { const auth = req.headers.authorization; if (auth === undefined) { unauthenticated += 1; } else { received.push(auth); } if (auth !== expected) { res.writeHead(401, { "WWW-Authenticate": 'Basic realm="veect-fixture"' }); res.end("authentication required"); return; } const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/"); const relative = urlPath.replace(/^\/repo\.git\/?/, ""); const file = path.resolve(bareRepoPath, relative); if (!file.startsWith(path.resolve(bareRepoPath) + path.sep)) { res.writeHead(403); res.end(); return; } void stat(file).then( (stats) => { if (!stats.isFile()) { res.writeHead(404); res.end(); return; } res.writeHead(200, { "content-type": "application/octet-stream" }); createReadStream(file).pipe(res); }, () => { res.writeHead(404); res.end(); }, ); }); server.listen(0, "127.0.0.1"); await once(server, "listening"); const address = server.address(); if (address === null || typeof address === "string") { server.close(); throw new Error("serveBareRepoOverHttp: could not determine a port"); } return { cloneUrl: `http://127.0.0.1:${address.port}/repo.git`, authorizationHeaders: () => [...received], unauthenticatedRequests: () => unauthenticated, stop: () => new Promise((resolve, reject) => { server.closeAllConnections(); server.close((error) => (error ? reject(error) : resolve())); }), }; }