mirror your GitHub repos to tangled.org automatically
1import { spawn } from 'node:child_process'
2import { afterEach, beforeEach, describe, expect, it } from 'vitest'
3import { Server, utils as sshUtils } from 'ssh2'
4import { ReceivePackSession } from '../../server/utils/git-wire/receive-pack'
5import { ssh2ReceivePackFactory } from '../../server/utils/git-wire/receive-pack'
6import { ZERO_SHA } from '../../server/utils/git-wire/refs'
7import { generateKeypair, pkcs8ToOpenSshPrivate } from '../../server/utils/ssh-keypair'
8import { fakeGithubFetch, GitFixture } from '../utils/git-wire'
9import { fetchPack } from '../../server/utils/git-wire/upload-pack'
10
11async function* fromBuffer(b: Buffer): AsyncGenerator<Buffer> {
12 yield b
13}
14
15async function drain(gen: AsyncGenerator<Buffer>): Promise<Buffer> {
16 const parts: Buffer[] = []
17 for await (const c of gen) parts.push(c)
18 return Buffer.concat(parts)
19}
20
21/**
22 * An in-process ssh2 server that authorises one public key and runs the real
23 * `git-receive-pack` against the given bare repo on exec. Mirrors the knot's
24 * `git@host: git-receive-pack '<path>'` surface so the ssh2 factory is exercised
25 * end to end.
26 */
27function startKnotServer(authorizedPubKey: string, repoFor: (path: string) => string | null) {
28 const hostKey = sshUtils.generateKeyPairSync('ed25519').private
29 const parsed = sshUtils.parseKey(authorizedPubKey)
30 if (parsed instanceof Error) throw parsed
31 const allowed = Array.isArray(parsed) ? parsed[0]! : parsed
32 const allowedSSH = allowed.getPublicSSH()
33
34 const server = new Server({ hostKeys: [hostKey] }, client => {
35 client.on('authentication', ctx => {
36 if (ctx.method === 'publickey' && ctx.key.algo === allowed.type && ctx.key.data.equals(allowedSSH)) {
37 ctx.accept()
38 return
39 }
40 ctx.reject()
41 })
42 client.on('ready', () => {
43 client.on('session', accept => {
44 accept().once('exec', (acceptExec, _reject, info) => {
45 const match = info.command.match(/^git-receive-pack '(.+)'$/)
46 const repo = match ? repoFor(match[1]!) : null
47 const stream = acceptExec()
48 if (!repo) {
49 stream.stderr.write('repository not found\n')
50 stream.exit(128)
51 stream.end()
52 return
53 }
54 const child = spawn('git-receive-pack', [repo], { stdio: ['pipe', 'pipe', 'pipe'] })
55 stream.pipe(child.stdin)
56 child.stdout.pipe(stream)
57 child.stderr.on('data', (d: Buffer) => stream.stderr.write(d))
58 child.on('close', code => { stream.exit(code ?? 0); stream.end() })
59 })
60 })
61 })
62 })
63
64 return new Promise<{ port: number, close: () => void }>(resolve => {
65 server.listen(0, '127.0.0.1', () => {
66 resolve({ port: (server.address() as { port: number }).port, close: () => server.close() })
67 })
68 })
69}
70
71describe('ssh2ReceivePackFactory (against an in-process ssh2 knot)', () => {
72 let fx: GitFixture
73 let realFetch: typeof globalThis.fetch
74 let knotServer: { port: number, close: () => void } | null = null
75
76 beforeEach(() => {
77 fx = new GitFixture()
78 realFetch = globalThis.fetch
79 })
80
81 afterEach(() => {
82 globalThis.fetch = realFetch
83 knotServer?.close()
84 knotServer = null
85 fx.cleanup()
86 })
87
88 async function packFor(ghBare: string, want: string, haves: string[]): Promise<Buffer> {
89 globalThis.fetch = fakeGithubFetch(new Map([['owner/repo', ghBare]])) as unknown as typeof globalThis.fetch
90 const { pack } = await fetchPack({ repoFullName: 'owner/repo', token: 't', want, haves, maxBytes: 1 << 30 })
91 return drain(pack)
92 }
93
94 it('pushes a ref to the knot over a real ssh2 connection', async () => {
95 const gh = fx.initBare('gh.git')
96 const work = fx.initWork('work')
97 const sha = fx.commit(work, 'a.txt', 'hello')
98 fx.pushTo(work, gh, 'HEAD:refs/heads/main')
99 const knot = fx.initBare('knot.git')
100
101 const key = generateKeypair('synchub.to/1')
102 knotServer = await startKnotServer(key.publicKeyOpenSsh, p => (p === '/repo-did' ? knot : null))
103
104 const factory = ssh2ReceivePackFactory({
105 host: '127.0.0.1',
106 port: knotServer.port,
107 repoPath: '/repo-did',
108 // Mirror production: the worker hands ssh2 the OpenSSH-format key that
109 // `loadSshKeyForInstall` derives from the stored PKCS#8 PEM.
110 privateKey: pkcs8ToOpenSshPrivate(key.privateKeyPem, 'synchub.to/1'),
111 })
112
113 const session = await ReceivePackSession.open(factory)
114 await session.push([{ ref: 'refs/heads/main', old: ZERO_SHA, next: sha }], fromBuffer(await packFor(gh, sha, [])))
115
116 expect(fx.revParse(knot, 'refs/heads/main')).toBe(sha)
117 })
118
119 it('surfaces a connection failure as a caught WireError, not an uncaught throw', async () => {
120 const key = generateKeypair('synchub.to/1')
121 // Nothing listening on this port: connect() emits 'error' (ECONNREFUSED).
122 const factory = ssh2ReceivePackFactory({
123 host: '127.0.0.1',
124 port: 1,
125 repoPath: '/repo-did',
126 privateKey: pkcs8ToOpenSshPrivate(key.privateKeyPem, 'synchub.to/1'),
127 })
128 await expect(ReceivePackSession.open(factory)).rejects.toThrow(/ssh error|ECONNREFUSED|advertisement|end of stream/)
129 })
130})