1// Stands in for fcgiwrap in scenarios, which may run where it isn't2// installed: an HTTP server that runs git http-backend as a CGI script for3// each request. Caddy reaches it with a reverse_proxy whose header_up lines4// carry what the forge's Caddyfile passes fcgiwrap as env (X-Cgi-<NAME>:5// <value>; test/scenarios/private-web-clone.sh rewrites the one into the6// other), so the routing under test is Caddy's own.7//8// node test/git-cgi.ts <port>9import http from 'node:http';10import path from 'node:path';11import { execFileSync, spawn } from 'node:child_process';1213const backend = path.join(execFileSync('git', ['--exec-path']).toString().trim(), 'git-http-backend');14const keep = ['PATH', 'HOME', 'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_NOSYSTEM'];1516http.createServer((req, res) => {17 const url = new URL(req.url ?? '/', 'http://x');18 const env: Record<string, string> = { REQUEST_METHOD: req.method ?? 'GET', QUERY_STRING: url.search.slice(1), REMOTE_ADDR: '127.0.0.1', CONTENT_TYPE: req.headers['content-type'] ?? '' };19 for (const k of keep) if (process.env[k]) env[k] = process.env[k]!;20 if (req.headers['content-length']) env.CONTENT_LENGTH = req.headers['content-length'];21 if (req.headers['content-encoding']) env.HTTP_CONTENT_ENCODING = req.headers['content-encoding'];22 if (req.headers['git-protocol']) env.GIT_PROTOCOL = String(req.headers['git-protocol']);23 for (const [k, v] of Object.entries(req.headers)) if (k.startsWith('x-cgi-')) env[k.slice(6).toUpperCase()] = String(v);24 const cgi = spawn(backend, [], { env, stdio: ['pipe', 'pipe', 'inherit'] });25 req.pipe(cgi.stdin);26 // CGI output: header lines (Status: among them), a blank line, the body.27 let head = Buffer.alloc(0);28 let sent = false;29 cgi.stdout.on('data', (d: Buffer) => {30 if (sent) return void res.write(d);31 head = Buffer.concat([head, d]);32 const end = head.indexOf('\r\n\r\n') >= 0 ? head.indexOf('\r\n\r\n') + 4 : head.indexOf('\n\n') >= 0 ? head.indexOf('\n\n') + 2 : -1;33 if (end < 0) return;34 let status = 200;35 const headers: Record<string, string> = {};36 for (const line of head.subarray(0, end).toString('latin1').split(/\r?\n/)) {37 const i = line.indexOf(':');38 if (i <= 0) continue;39 const k = line.slice(0, i).trim();40 const v = line.slice(i + 1).trim();41 if (k.toLowerCase() === 'status') status = Number(v.split(' ')[0]);42 else headers[k] = v;43 }44 res.writeHead(status, headers);45 sent = true;46 res.write(head.subarray(end));47 });48 cgi.on('close', () => (sent ? res.end() : res.writeHead(502).end()));49}).listen(Number(process.argv[2]), '127.0.0.1', () => console.log('git-cgi up'));