import { execFileSync, spawn, spawnSync } from "node:child_process"; import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; import { createServer as createTLSServer, type Server as TLSServer } from "node:https"; import { connect } from "node:net"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { pipeline, type Duplex, type Readable, type Writable } from "node:stream"; export const fixtureGitProvider = "fixture-source-control"; export const fixtureGitHost = "bitbucket.example.test"; export const fixtureGitPath = "/scm/TEAM/fixture"; export const fixtureGitURL = `https://${fixtureGitHost}${fixtureGitPath}`; export const fixtureGitUser = "fixture-user"; export const fixtureGitSecret = "fixture-credential-secret"; const fixturePluginID = "kandev-plugin-e2e"; const fixturePluginPackage = path.resolve( __dirname, "../../../backend/.build/kandev-plugin-e2e-1.0.0.tar.gz", ); type GitHTTPFixture = { checkoutPath: string; proxyURL: string; profileGitEnv: Array<{ key: string; value: string }>; requestPaths: () => string[]; requestHosts: () => string[]; pushed: () => boolean; close: () => Promise; }; /** * A real HTTPS Git server which insists on Basic auth, reached through a * bridge-visible CONNECT proxy. The remote URL remains the fixture provider's * exact HTTPS identity, so the credential helper receives the host/path that * its broker lease was issued for rather than a test-server substitute. */ export async function startPluginGitFixture(root: string): Promise { const repoRoot = path.join(root, "credential-git"); const bareRepo = path.join(repoRoot, fixtureGitPath); const checkoutPath = path.join(root, "credential-git-checkout"); fs.mkdirSync(path.dirname(bareRepo), { recursive: true }); fs.mkdirSync(checkoutPath, { recursive: true }); execFileSync("git", ["init", "--bare", "-b", "main", bareRepo]); execFileSync("git", ["init", "-b", "main"], { cwd: checkoutPath }); fs.writeFileSync(path.join(checkoutPath, "README.md"), "credential fixture\n"); execFileSync("git", ["add", "."], { cwd: checkoutPath }); execFileSync( "git", ["-c", "user.name=E2E", "-c", "user.email=e2e@test.local", "commit", "-m", "initial"], { cwd: checkoutPath }, ); execFileSync("git", ["remote", "add", "origin", bareRepo], { cwd: checkoutPath }); execFileSync("git", ["push", "origin", "main"], { cwd: checkoutPath }); execFileSync("git", ["remote", "set-url", "origin", fixtureGitURL], { cwd: checkoutPath }); const certDir = fs.mkdtempSync(path.join(os.tmpdir(), "kandev-e2e-git-cert-")); const keyPath = path.join(certDir, "key.pem"); const certPath = path.join(certDir, "cert.pem"); execFileSync( "openssl", [ "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", keyPath, "-out", certPath, "-subj", `/CN=${fixtureGitHost}`, "-days", "1", ], { stdio: process.env.E2E_DEBUG ? "inherit" : "ignore" }, ); const requestPaths: string[] = []; const requestHosts: string[] = []; const gitServer = createTLSServer( { key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) }, (req, res) => { requestPaths.push(new URL(req.url ?? "/", "https://fixture").pathname); requestHosts.push(req.headers.host ?? ""); if (!requestHasFixtureAuth(req)) { res.writeHead(401, { "www-authenticate": 'Basic realm="fixture"' }).end(); return; } serveGitHTTP(req, res, repoRoot); }, ); const gitPort = await listen(gitServer); const proxy = createFixtureConnectProxy(gitPort); const proxyPort = await listen(proxy); const gateway = dockerBridgeGateway(); const proxyURL = `http://${gateway}:${proxyPort}`; return { checkoutPath, proxyURL, profileGitEnv: [ { key: "HTTPS_PROXY", value: proxyURL }, { key: "GIT_SSL_NO_VERIFY", value: "true" }, ], requestPaths: () => [...requestPaths], requestHosts: () => [...requestHosts], pushed: () => execFileSync("git", ["--git-dir", bareRepo, "log", "-1", "--format=%s"], { encoding: "utf8", }).trim() === "credential fixture push", close: async () => { await closeServer(proxy); await closeServer(gitServer); fs.rmSync(certDir, { recursive: true, force: true }); }, }; } export function credentialBrokerPublicURL(backendPort: number): string | undefined { const raw = process.env.KANDEV_E2E_CREDENTIAL_BROKER_PUBLIC_BASE_URL; return raw?.trim().replace("{port}", String(backendPort)) || undefined; } export async function installFixturePlugin(baseURL: string): Promise { if (!fs.existsSync(fixturePluginPackage)) { throw new Error(`fixture plugin package missing: ${fixturePluginPackage}`); } const response = await fetch(`${baseURL}/api/plugins/install`, { method: "POST", body: (() => { const form = new FormData(); form.append( "package", new Blob([fs.readFileSync(fixturePluginPackage)], { type: "application/gzip" }), path.basename(fixturePluginPackage), ); return form; })(), }); if (!response.ok) throw new Error(`fixture plugin install failed (${response.status})`); } export async function uninstallFixturePlugin(baseURL: string): Promise { await fetch(`${baseURL}/api/plugins/${fixturePluginID}`, { method: "DELETE" }); } export async function revokeFixtureConnection( baseURL: string, workspaceId: string, ): Promise { return fetch(`${baseURL}/api/plugins/${fixturePluginID}/actions/connection-status`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ workspaceId, body: { revoke: true } }), }); } export function assertExactFixtureTransport(fixture: GitHTTPFixture): void { const paths = fixture.requestPaths(); const hosts = fixture.requestHosts(); if (paths.length === 0 || paths.some((requestPath) => !requestPath.startsWith(fixtureGitPath))) { throw new Error("Git did not request the exact fixture repository path"); } if (hosts.some((host) => host !== fixtureGitHost)) { throw new Error("Git did not preserve the exact fixture repository host"); } } export function runDockerGit( containerID: string, command: string, ): Promise<{ output: string; status: number }> { return runProcess("docker", ["exec", containerID, "sh", "-lc", command]); } export function runRemoteGit( agentctlPID: number, command: string, targetName: string, ): Promise<{ output: string; status: number; }> { return runProcess("docker", [ "exec", "--user", "kandev", targetName, "sh", "-lc", `xargs -0 sh -c 'env "$@" sh -lc "$0"' ${shellQuote(command)} < /proc/${agentctlPID}/environ`, ]); } function runProcess(command: string, args: string[]): Promise<{ output: string; status: number }> { return new Promise((resolve, reject) => { const child = spawn(command, args); const chunks: Buffer[] = []; child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk)); child.stderr.on("data", (chunk: Buffer) => chunks.push(chunk)); child.once("error", reject); child.once("close", (status) => { resolve({ output: Buffer.concat(chunks).toString(), status: status ?? 1 }); }); }); } function requestHasFixtureAuth(req: IncomingMessage): boolean { const expected = `Basic ${Buffer.from(`${fixtureGitUser}:${fixtureGitSecret}`).toString("base64")}`; return req.headers.authorization === expected; } function serveGitHTTP(req: IncomingMessage, res: ServerResponse, root: string): void { const parsed = new URL(req.url ?? "/", "https://fixture"); const child = spawn("git", ["http-backend"], { env: { ...process.env, GIT_PROJECT_ROOT: root, GIT_HTTP_EXPORT_ALL: "1", PATH_INFO: parsed.pathname, QUERY_STRING: parsed.search.slice(1), REQUEST_METHOD: req.method ?? "GET", CONTENT_TYPE: req.headers["content-type"] ?? "", CONTENT_LENGTH: req.headers["content-length"] ?? "", REMOTE_USER: fixtureGitUser, }, }); const chunks: Buffer[] = []; let failed = false; child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk)); child.once("error", () => { failed = true; res.writeHead(500).end(); }); child.once("close", () => { if (!failed) writeCGIResponse(res, Buffer.concat(chunks)); }); pipeFixtureGitRequest(req, child.stdin, () => { if (failed) return; failed = true; child.kill(); if (!res.destroyed) res.destroy(); }); } /** Pipes an HTTP request into git-http-backend with deterministic EPIPE handling. */ export function pipeFixtureGitRequest( request: Readable, childInput: Writable, onError: (error: Error) => void, ): void { pipeline(request, childInput, (error) => { if (error) onError(error); }); } function writeCGIResponse(res: ServerResponse, body: Buffer): void { const splitAt = body.indexOf("\r\n\r\n"); if (splitAt < 0) { res.writeHead(500).end(); return; } let status = 200; const headers: Record = {}; for (const line of body.subarray(0, splitAt).toString().split("\r\n")) { const [name, value] = line.split(/:\s*/, 2); if (name.toLowerCase() === "status") status = Number(value?.split(" ")[0]) || 500; else if (name && value) headers[name] = value; } res.writeHead(status, headers).end(body.subarray(splitAt + 4)); } function createFixtureConnectProxy(gitPort: number): Server { const proxy = createServer((_, res) => res.writeHead(405).end()); proxy.on("connect", (_req, client, head) => { const upstream = connect(gitPort, "127.0.0.1", () => { client.write("HTTP/1.1 200 Connection Established\r\n\r\n"); if (head.length > 0) upstream.write(head); bridgeFixtureConnectStreams(client, upstream); }); bindFixtureConnectErrors(client, upstream); }); return proxy; } export function bindFixtureConnectErrors(client: Duplex, upstream: Duplex): void { const destroyBoth = () => destroyFixtureConnectStreams(client, upstream); client.once("error", destroyBoth); upstream.once("error", destroyBoth); } export function bridgeFixtureConnectStreams(client: Duplex, upstream: Duplex): void { const destroyBoth = () => destroyFixtureConnectStreams(client, upstream); pipeline(client, upstream, destroyBoth); pipeline(upstream, client, destroyBoth); } function destroyFixtureConnectStreams(client: Duplex, upstream: Duplex): void { client.destroy(); upstream.destroy(); } function dockerBridgeGateway(): string { const result = spawnSync( "docker", ["network", "inspect", "bridge", "-f", "{{(index .IPAM.Config 0).Gateway}}"], { encoding: "utf8" }, ); const gateway = result.status === 0 ? result.stdout.trim() : ""; if (!gateway) throw new Error("could not determine Docker bridge gateway"); return gateway; } function listen(server: Server | TLSServer): Promise { return new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "0.0.0.0", () => { server.off("error", reject); const address = server.address(); if (!address || typeof address === "string") { reject(new Error("fixture server did not receive a TCP port")); return; } resolve(address.port); }); }); } function closeServer(server: Server | TLSServer): Promise { return new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())), ); } function shellQuote(value: string): string { return `'${value.replaceAll("'", "'\\''")}'`; }