Skip to content

Commit 34ebc3e

Browse files
committed
initial git-ssh-mock-server
1 parent 47c6e28 commit 34ebc3e

File tree

3 files changed

+130
-2
lines changed

3 files changed

+130
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,6 @@ typings/
5959

6060
# next.js build output
6161
.next
62+
id_rsa
63+
id_rsa.pub
64+
package-lock.json

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"description": "Clone and push to git repository test fixtures over HTTP.",
55
"main": "index.js",
66
"bin": {
7-
"git-http-mock-server": "daemon.js"
7+
"git-http-mock-server": "daemon.js",
8+
"git-ssh-mock-server": "ssh-server.js"
89
},
910
"scripts": {
1011
"test": "echo \"No tests\"",
@@ -29,14 +30,17 @@
2930
"homepage": "https://github.com/isomorphic-git/git-http-mock-server#readme",
3031
"dependencies": {
3132
"basic-auth": "^2.0.0",
33+
"buffer-equal-constant-time": "^1.0.1",
3234
"chalk": "^2.4.1",
3335
"daemonize-process": "^1.0.9",
3436
"fixturez": "^1.1.0",
3537
"git-http-backend": "^1.0.2",
3638
"htpasswd-js": "^1.0.2",
3739
"micro-cors": "^0.1.1",
3840
"minimisted": "^2.0.0",
39-
"tree-kill": "^1.2.0"
41+
"tree-kill": "^1.2.0",
42+
"ssh-keygen": "^0.4.2",
43+
"ssh2": "^0.6.1"
4044
},
4145
"devDependencies": {
4246
"semantic-release": "15.1.7",

ssh-server.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env node
2+
const { spawn, spawnSync } = require('child_process')
3+
var crypto = require('crypto')
4+
var fs = require('fs')
5+
var path = require('path')
6+
var inspect = require('util').inspect
7+
8+
var buffersEqual = require('buffer-equal-constant-time')
9+
var fixturez = require('fixturez')
10+
var ssh2 = require('ssh2')
11+
12+
var config = {
13+
root: path.resolve(process.cwd(), process.env.GIT_SSH_MOCK_SERVER_ROOT || '.'),
14+
glob: '*',
15+
route: process.env.GIT_SSH_MOCK_SERVER_ROUTE || '/'
16+
}
17+
18+
new Promise((resolve, reject) => {
19+
try {
20+
let key = fs.readFileSync(path.join(__dirname, 'id_rsa'))
21+
let pubKey = fs.readFileSync(path.join(__dirname, 'id_rsa.pub'))
22+
return resolve({key, pubKey})
23+
} catch (err) {
24+
let proc = spawnSync('ssh-keygen', ['-C', '"git-ssh-mock-server@localhost"', '-N', '""', '-f', 'id_rsa'], {
25+
cwd: __dirname,
26+
shell: true
27+
})
28+
console.log(proc.stdout.toString('utf8'))
29+
let key = fs.readFileSync(path.join(__dirname, 'id_rsa'))
30+
let pubKey = fs.readFileSync(path.join(__dirname, 'id_rsa.pub'))
31+
return resolve({key, pubKey})
32+
}
33+
})
34+
.then(keypair => {
35+
var pubKey = ssh2.utils.genPublicKey(ssh2.utils.parseKey(keypair.pubKey))
36+
var f = fixturez(config.root, {root: process.cwd(), glob: config.glob})
37+
38+
new ssh2.Server({ hostKeys: [keypair.key] }, function (client) {
39+
console.log('client connected')
40+
41+
client
42+
.on('authentication', function (ctx) {
43+
if (
44+
ctx.method === 'password' &&
45+
// Note: Don't do this in production code, see
46+
// https://www.brendanlong.com/timing-attacks-and-usernames.html
47+
// In node v6.0.0+, you can use `crypto.timingSafeEqual()` to safely
48+
// compare two values.
49+
ctx.username === 'foo' &&
50+
ctx.password === 'bar'
51+
) { ctx.accept() } else if (
52+
ctx.method === 'publickey' &&
53+
ctx.key.algo === pubKey.fulltype &&
54+
buffersEqual(ctx.key.data, pubKey.public)
55+
) {
56+
if (ctx.signature) {
57+
var verifier = crypto.createVerify(ctx.sigAlgo)
58+
verifier.update(ctx.blob)
59+
if (verifier.verify(pubKey.publicOrig, ctx.signature)) ctx.accept()
60+
else ctx.reject()
61+
} else {
62+
// if no signature present, that means the client is just checking
63+
// the validity of the given public key
64+
ctx.accept()
65+
}
66+
} else ctx.reject()
67+
})
68+
.on('ready', function () {
69+
console.log('client authenticated')
70+
71+
client.on('session', function (accept, reject) {
72+
var session = accept()
73+
session.once('exec', function (accept, reject, info) {
74+
console.log(info.command)
75+
let [_, command, gitdir] = info.command.match(/^([a-z-]+) '(.*)'/)
76+
// Only allow these two commands to be executed
77+
if (command !== 'git-upload-pack' && command !== 'git-receive-pack') {
78+
return reject()
79+
}
80+
if (gitdir !== path.posix.normalize(gitdir)) {
81+
// something fishy about this filepath
82+
console.log('suspicious file path:', gitdir)
83+
return reject()
84+
}
85+
86+
// Do copy-on-write trick for git push
87+
let fixtureName = path.posix.basename(gitdir)
88+
let fulldir
89+
if (command === 'git-upload-pack') {
90+
fulldir = f.find(fixtureName)
91+
} else if (command === 'git-receive-pack') {
92+
fulldir = f.copy(fixtureName)
93+
}
94+
95+
try {
96+
fs.accessSync(fulldir)
97+
} catch (err) {
98+
console.log(fulldir + ' does not exist.')
99+
return reject()
100+
}
101+
102+
var stream = accept()
103+
console.log('exec:', command, gitdir)
104+
console.log('actual:', command, fulldir)
105+
let proc = spawn(command, [fulldir])
106+
stream.exit(0) // always set a successful exit code
107+
stream.pipe(proc.stdin)
108+
proc.stdout.pipe(stream)
109+
proc.stderr.pipe(stream.stderr)
110+
})
111+
})
112+
})
113+
.on('end', function () {
114+
console.log('Client disconnected')
115+
})
116+
}
117+
).listen(process.env.GIT_SSH_MOCK_SERVER_PORT || 2222, '127.0.0.1', function () {
118+
console.log('Listening on port ' + this.address().port)
119+
})
120+
121+
})

0 commit comments

Comments
 (0)