Squash of Megavolt's 627979a17..86963d281 on origin/megavolt/row199-on-190 (base00d4cc1f6, row 190's landing), the 37 manifest paths, build.patch 05383075, manifest 35f900fd (Duckworth approve round 1, comment 29779, rev 2540). Mutant batches 1 and 2 in agents/megavolt/work/queue-199/BUILD.md: batch 2 26 of 28 killed, M17 and M27 equivalent. docs/TOOLS.md is a 3-way merge with row 200 (9e1085816): row 199's hunk applies unchanged (patch-id d88379111e2e) and no test reads the file. Gate on9e1085816plus the patch: 15 package suites 2735 pass 0 fail, 10 scripts/test-*.sh suites 429 pass 0 fail (plus 156 node tests in the queue suite). Refactor moved to76134dfdesince, a brief touching no manifest path. No live change: the stack runs from copy4429a104funtil the 10-24 re-pin window.
96 lines
4.5 KiB
JavaScript
96 lines
4.5 KiB
JavaScript
// Row 199 (#1697, design T3): a fake Gitea for publish, on 127.0.0.1 over TLS with a scratch CA,
|
|
// serving bare repositories under `<dir>/repos/<user>/<repo>.git` through git http-backend. Every
|
|
// request needs Basic auth with the user and token it was made with, or with any pair in `users`,
|
|
// as a private repository on Gitea does. It records each request's method, path and user, never the password. `redirect`
|
|
// makes every request a 302 to `redirect`. A repository's own hooks run on a push, so a test can
|
|
// make the fork echo something back as `remote:` lines. Nothing here leaves 127.0.0.1.
|
|
import { spawn, spawnSync } from "node:child_process";
|
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { createServer } from "node:https";
|
|
import { join } from "node:path";
|
|
|
|
// A CA and a server certificate for IP 127.0.0.1, made once per directory with openssl.
|
|
export function scratchCa(dir) {
|
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
const ssl = (...args) => {
|
|
const r = spawnSync("openssl", args, { cwd: dir, encoding: "utf8" });
|
|
if (r.status !== 0) throw new Error(`openssl ${args[0]}: ${r.stderr}`);
|
|
};
|
|
ssl("req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:P-256", "-nodes", "-keyout", "ca.key", "-out", "ca.pem", "-days", "2", "-subj", "/CN=mosaic test CA");
|
|
ssl("req", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:P-256", "-nodes", "-keyout", "server.key", "-out", "server.csr", "-subj", "/CN=127.0.0.1");
|
|
writeFileSync(join(dir, "san.ext"), "subjectAltName=IP:127.0.0.1\n");
|
|
ssl("x509", "-req", "-in", "server.csr", "-CA", "ca.pem", "-CAkey", "ca.key", "-CAcreateserial", "-out", "server.pem", "-days", "2", "-extfile", "san.ext");
|
|
return { ca: join(dir, "ca.pem"), key: readFileSync(join(dir, "server.key")), cert: readFileSync(join(dir, "server.pem")) };
|
|
}
|
|
|
|
export async function fakeGitea({ dir, tls, user, token, users = { [user]: token }, redirect = null }) {
|
|
const root = join(dir, "repos");
|
|
mkdirSync(root, { recursive: true });
|
|
const requests = [];
|
|
const server = createServer({ key: tls.key, cert: tls.cert }, (req, res) => {
|
|
const url = new URL(req.url, "https://127.0.0.1");
|
|
const auth = /^Basic (.+)$/.exec(req.headers.authorization ?? "");
|
|
const [who, pass] = auth ? Buffer.from(auth[1], "base64").toString().split(":") : [null, null];
|
|
requests.push({ method: req.method, path: url.pathname, user: who });
|
|
if (redirect) {
|
|
res.writeHead(302, { location: redirect + req.url });
|
|
return res.end();
|
|
}
|
|
if (!who || !Object.hasOwn(users, who) || pass !== users[who]) {
|
|
res.writeHead(401, { "www-authenticate": 'Basic realm="Gitea"' });
|
|
return res.end();
|
|
}
|
|
// /<owner>/<repo>/info/refs and the like, on <root>/<owner>/<repo>.git.
|
|
const m = /^\/([^/]+)\/([^/]+?)(?:\.git)?(\/.*)$/.exec(url.pathname);
|
|
if (!m) {
|
|
res.writeHead(404);
|
|
return res.end();
|
|
}
|
|
const cgi = spawn("git", ["http-backend"], {
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
GIT_PROJECT_ROOT: root,
|
|
GIT_HTTP_EXPORT_ALL: "1",
|
|
GIT_CONFIG_NOSYSTEM: "1",
|
|
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
HOME: "/nonexistent",
|
|
PATH_INFO: `/${m[1]}/${m[2]}.git${m[3]}`,
|
|
REQUEST_METHOD: req.method,
|
|
QUERY_STRING: url.search.slice(1),
|
|
CONTENT_TYPE: req.headers["content-type"] ?? "",
|
|
HTTP_CONTENT_ENCODING: req.headers["content-encoding"] ?? "",
|
|
GIT_PROTOCOL: req.headers["git-protocol"] ?? "",
|
|
REMOTE_USER: who,
|
|
REMOTE_ADDR: "127.0.0.1",
|
|
},
|
|
stdio: ["pipe", "pipe", "ignore"],
|
|
});
|
|
req.pipe(cgi.stdin);
|
|
const chunks = [];
|
|
cgi.stdout.on("data", (c) => chunks.push(c));
|
|
cgi.on("close", () => {
|
|
const out = Buffer.concat(chunks);
|
|
const split = out.indexOf("\r\n\r\n");
|
|
const head = out.subarray(0, split).toString().split("\r\n");
|
|
const headers = {};
|
|
let status = 200;
|
|
for (const line of head) {
|
|
const [k, ...v] = line.split(":");
|
|
if (k.toLowerCase() === "status") status = Number.parseInt(v.join(":").trim(), 10);
|
|
else headers[k] = v.join(":").trim();
|
|
}
|
|
res.writeHead(status, headers);
|
|
res.end(out.subarray(split + 4));
|
|
});
|
|
});
|
|
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
const port = server.address().port;
|
|
return {
|
|
port,
|
|
origin: `https://127.0.0.1:${port}`,
|
|
requests,
|
|
repo: (owner, name) => join(root, owner, `${name}.git`),
|
|
close: () => new Promise((r) => server.close(r)),
|
|
};
|
|
}
|