import { spawn } from "node:child_process"; import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; import path from "node:path"; import { Server, utils, type Connection } from "ssh2"; import { normalizeRepoName } from "./git"; import { findKeyByBlob, touchSshKey } from "./keys"; import { DATA_ROOT } from "./paths"; import { canWrite, findRepo, markPushed, repoAccess } from "./repos"; import { SSH_LISTEN_HOST, SSH_PORT } from "./ssh-config"; import { getUserById, type User } from "./users"; function hostKey() { const file = path.join(DATA_ROOT, "ssh_host_ed25519_key"); if (!existsSync(file)) { mkdirSync(DATA_ROOT, { recursive: true }); const { private: priv } = utils.generateKeyPairSync("ed25519"); writeFileSync(file, priv, { mode: 0o600 }); } return readFileSync(file); } // git sends e.g.: git-upload-pack '/alice/demo.git' const COMMAND_RE = /^git[ -](upload-pack|receive-pack) '?\/?([^'/\s]+)\/([^'/\s]+?)'?$/; function handleConnection(client: Connection) { let user: User | null = null; let keyId: number | null = null; client.on("authentication", (ctx) => { if (ctx.method !== "publickey") return ctx.reject(["publickey"]); const key = findKeyByBlob(ctx.key.data); if (!key) return ctx.reject(["publickey"]); // First the client asks whether a key is acceptable, then proves ownership with a signature. if (ctx.signature && ctx.blob) { const parsed = utils.parseKey(key.publicKey); if (parsed instanceof Error || parsed.verify(ctx.blob, ctx.signature, ctx.hashAlgo) !== true) { return ctx.reject(["publickey"]); } user = getUserById(key.userId); keyId = key.id; if (!user) return ctx.reject(["publickey"]); } ctx.accept(); }); client.on("ready", () => { if (keyId) touchSshKey(keyId); client.on("session", (acceptSession) => { const session = acceptSession(); const env: Record = {}; session.on("env", (accept, _reject, info) => { if (info.key === "GIT_PROTOCOL") env.GIT_PROTOCOL = info.val; accept?.(); }); session.on("pty", (accept) => accept?.()); session.on("shell", (accept) => { const channel = accept(); channel.write( `Hi ${user?.username}! You've successfully authenticated, but this server does not provide shell access.\r\n`, ); channel.exit(1); channel.end(); }); session.on("exec", (accept, _reject, info) => { const channel = accept(); const fail = (message: string) => { channel.stderr.write(`ERROR: ${message}\n`); channel.exit(1); channel.end(); }; const match = COMMAND_RE.exec(info.command.trim()); if (!match || !user) return fail("Only git clone, fetch and push are supported."); const [, service, owner, repoName] = match; const repo = findRepo(owner, normalizeRepoName(repoName)); const access = repo ? repoAccess(repo, user.id) : null; if (!repo || !access) return fail("Repository not found."); const pushing = service === "receive-pack"; if (pushing && !canWrite(access)) { return fail(`Permission to ${repo.owner}/${repo.name} denied to ${user.username}.`); } const child = spawn("git", [service, repo.dir], { env: { ...process.env, ...env } }); channel.pipe(child.stdin); child.stdout.pipe(channel, { end: false }); child.stderr.pipe(channel.stderr, { end: false }); child.on("close", (code) => { if (pushing && code === 0) markPushed(repo.id); channel.exit(code ?? 1); channel.end(); }); channel.on("close", () => child.kill()); }); }); }); client.on("error", () => { // Clients disconnecting mid-handshake are routine; ignore. }); } export function startSshServer() { const g = globalThis as unknown as { __sshServer?: Server }; if (g.__sshServer) return; const server = new Server({ hostKeys: [hostKey()] }, handleConnection); server.on("error", (err: NodeJS.ErrnoException) => { console.warn(`[ssh] could not start on port ${SSH_PORT}: ${err.code ?? err.message}`); }); server.listen(SSH_PORT, SSH_LISTEN_HOST, () => { console.log(`[ssh] git over SSH listening on ${SSH_LISTEN_HOST}:${SSH_PORT}`); }); g.__sshServer = server; }