Skip to content

Commit bdcfd01

Browse files
committed
cleanup and document
1 parent 34ebc3e commit bdcfd01

File tree

3 files changed

+78
-31
lines changed

3 files changed

+78
-31
lines changed

README.md

+34-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
# git-http-mock-server
1+
# git-http-mock-server / git-ssh-mock-server
22
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fisomorphic-git%2Fgit-http-mock-server.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fisomorphic-git%2Fgit-http-mock-server?ref=badge_shield)
33

4-
5-
Clone and push to git repository test fixtures over HTTP.
4+
Clone and push to git repository test fixtures over HTTP or SSH.
65

76
## What it does
87

@@ -22,6 +21,12 @@ It also supports HTTP Basic Auth password protection of repos so you can test ho
2221

2322
Using `isomorphic-git` and testing things from browsers? Fear not, `git-http-mock-server` includes appropriate CORS headers.
2423

24+
`git-ssh-mock-server` is similar, but because authentication happens before the client can say which repo
25+
they are interested in, the authentication can't be customized per repository.
26+
By default it allows anonymous SSH access. You can disable anonymous access and activate password authentication by setting the `GIT_SSH_MOCK_SERVER_PASSWORD` evironment variable.
27+
(When password auth is activated, any username will work as long as the password matches the environment variable.)
28+
Alternatively, you can set the `GIT_SSH_MOCK_SERVER_PUBKEY` environment variable to true to disable anonymous access and activate Public Key authentication. What key to use is explained in detail later in this document.
29+
2530
## How to use
2631

2732
```sh
@@ -34,14 +39,16 @@ Now `cd` to a directory in which you have some bare git repos and run this serve
3439
> cd __fixtures__
3540
> ls
3641
test-repo1.git test-repo2.git imaginatively-named-repo.git
37-
> git-http-mock-server
42+
> git-http-mock-server &
43+
> git-ssh-mock-server &
3844
```
3945

4046
Now in another shell, clone and push away...
4147
```sh
4248
> git clone http://localhost:8174/test-repo1.git
4349
> git clone http://localhost:8174/test-repo2.git
4450
> git clone http://localhost:8174/imaginatively-named-repo.git
51+
> git clone ssh://localhost:2222/imaginatively-named-repo.git
4552
```
4653

4754
## Run in the background
@@ -64,8 +71,13 @@ Just be sure to run `start` and `stop` from the same working directory.
6471
- `GIT_HTTP_MOCK_SERVER_ROUTE` default is `/`
6572
- `GIT_HTTP_MOCK_SERVER_ROOT` default is `process.cwd()`
6673
- `GIT_HTTP_MOCK_SERVER_ALLOW_ORIGIN` default is `*` (used for CORS)
74+
- `GIT_SSH_MOCK_SERVER_PORT` default is 2222
75+
- `GIT_SSH_MOCK_SERVER_ROUTE` default is `/`
76+
- `GIT_SSH_MOCK_SERVER_ROOT` default is `process.cwd()`
77+
- `GIT_SSH_MOCK_SERVER_PASSWORD` activate Password Authentication and use this password (leave blank to allow anonymous SSH access.)
78+
- `GIT_SSH_MOCK_SERVER_PUBKEY` activate PubKey Authentication using the self-generated keypair (leave blank to allow anonymous SSH access.)
6779

68-
### .htpasswd support
80+
### .htpasswd support (http-only)
6981

7082
You can place an Apache-style `.htpasswd` file in a bare repo to protect it with Basic Authentication.
7183

@@ -80,13 +92,30 @@ testuser:$apr1$BRdvH4Mu$3HrpeyBrWiS88GcSPidgq/
8092
If you don't have `htpasswd` on your machine, you can use [htpasswd](https://npm.im/htpasswd) which is
8193
a cross-platform Node implementation of `htpasswd`.
8294

95+
### Public Key Auth support (ssh-only)
96+
97+
`git-ssh-mock-server` generates its own keypair using the system's native `ssh-keygen` the first time it's run,
98+
in order to create encrypted SSH connections.
99+
This key can be used to authenticate with the server as well!
100+
101+
1. Run `GIT_SSH_MOCK_SERVER_PUBKEY=true git-ssh-mock-server`
102+
2. Try cloning (e.g. `git clone ssh://localhost:2222/imaginatively-named-repo.git`). It shouldn't work.
103+
2. Run `git-ssh-mock-server exportKeys` which will copy the key files to `./id_rsa` and `./id_rsa.pub` in the working directory with the correct file permissions (`600`).
104+
3. Run `ssh-add ./id_rsa`
105+
4. Now try cloning. It works!
106+
5. To clear the key from the ssh-agent, use `ssh-add -d ./id_rsa`
107+
108+
You can use `GIT_SSH_MOCK_SERVER_PUBKEY` and `GIT_SSH_MOCK_SERVER_PASSWORD` together, but using either one disables anonymous SSH access.
109+
83110
## Dependencies
84111

85112
- [basic-auth](https://ghub.io/basic-auth): node.js basic auth parser
113+
- [buffer-equal-constant-time](https://ghub.io/buffer-equal-constant-time): Constant-time comparison of Buffers
86114
- [chalk](https://ghub.io/chalk): Terminal string styling done right
87115
- [fixturez](https://ghub.io/fixturez): Easily create and maintain test fixtures in the file system
88116
- [git-http-backend](https://ghub.io/git-http-backend): serve a git repository over http
89117
- [htpasswd-js](https://ghub.io/htpasswd-js): Pure JS htpasswd authentication
118+
- [ssh2](https://ghub.io/ssh2): SSH2 client and server modules written in pure JavaScript for node.js
90119

91120
originally inspired by '[git-http-server](https://github.com/bahamas10/node-git-http-server)'
92121

bin.js

100644100755
File mode changed.

ssh-server.js

100644100755
+44-26
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ const { spawn, spawnSync } = require('child_process')
33
var crypto = require('crypto')
44
var fs = require('fs')
55
var path = require('path')
6-
var inspect = require('util').inspect
76

87
var buffersEqual = require('buffer-equal-constant-time')
98
var fixturez = require('fixturez')
@@ -21,38 +20,55 @@ new Promise((resolve, reject) => {
2120
let pubKey = fs.readFileSync(path.join(__dirname, 'id_rsa.pub'))
2221
return resolve({key, pubKey})
2322
} 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})
23+
try {
24+
// Note: PEM is to workaround https://github.com/mscdex/ssh2/issues/746
25+
let proc = spawnSync('ssh-keygen', ['-m', 'PEM', '-C', '"git-ssh-mock-server@localhost"', '-N', '""', '-f', 'id_rsa'], {
26+
cwd: __dirname,
27+
shell: true
28+
})
29+
console.log(proc.stdout.toString('utf8'))
30+
let key = fs.readFileSync(path.join(__dirname, 'id_rsa'))
31+
let pubKey = fs.readFileSync(path.join(__dirname, 'id_rsa.pub'))
32+
return resolve({key, pubKey})
33+
} catch (err) {
34+
reject(err)
35+
}
3236
}
3337
})
3438
.then(keypair => {
39+
if (process.argv[2] === 'exportKeys') {
40+
fs.writeFileSync(path.join(process.cwd(), 'id_rsa'), keypair.key, { mode: 0o600, flag: 'wx' })
41+
fs.writeFileSync(path.join(process.cwd(), 'id_rsa.pub'), keypair.pubKey, { mode: 0o600, flag: 'wx' })
42+
process.exit()
43+
}
3544
var pubKey = ssh2.utils.genPublicKey(ssh2.utils.parseKey(keypair.pubKey))
3645
var f = fixturez(config.root, {root: process.cwd(), glob: config.glob})
3746

47+
const PASSWORD_BUFFER = Buffer.from(process.env.GIT_SSH_MOCK_SERVER_PASSWORD || '')
48+
3849
new ssh2.Server({ hostKeys: [keypair.key] }, function (client) {
3950
console.log('client connected')
40-
4151
client
4252
.on('authentication', function (ctx) {
4353
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-
) {
54+
ctx.method === 'none' &&
55+
!process.env.GIT_SSH_MOCK_SERVER_PASSWORD &&
56+
!process.env.GIT_SSH_MOCK_SERVER_PUBKEY
57+
) {
58+
ctx.accept()
59+
} else if (
60+
ctx.method === 'password' &&
61+
process.env.GIT_SSH_MOCK_SERVER_PASSWORD &&
62+
// After much thought... screw usernames.
63+
buffersEqual(Buffer.from(ctx.password || ''), PASSWORD_BUFFER)
64+
) {
65+
ctx.accept()
66+
} else if (
67+
ctx.method === 'publickey' &&
68+
ctx.key.algo === pubKey.fulltype &&
69+
process.env.GIT_SSH_MOCK_SERVER_PUBKEY &&
70+
buffersEqual(ctx.key.data, pubKey.public)
71+
) {
5672
if (ctx.signature) {
5773
var verifier = crypto.createVerify(ctx.sigAlgo)
5874
verifier.update(ctx.blob)
@@ -63,7 +79,9 @@ new Promise((resolve, reject) => {
6379
// the validity of the given public key
6480
ctx.accept()
6581
}
66-
} else ctx.reject()
82+
} else {
83+
ctx.reject()
84+
}
6785
})
6886
.on('ready', function () {
6987
console.log('client authenticated')
@@ -75,6 +93,7 @@ new Promise((resolve, reject) => {
7593
let [_, command, gitdir] = info.command.match(/^([a-z-]+) '(.*)'/)
7694
// Only allow these two commands to be executed
7795
if (command !== 'git-upload-pack' && command !== 'git-receive-pack') {
96+
console.log('invalid command:', command)
7897
return reject()
7998
}
8099
if (gitdir !== path.posix.normalize(gitdir)) {
@@ -111,11 +130,10 @@ new Promise((resolve, reject) => {
111130
})
112131
})
113132
.on('end', function () {
114-
console.log('Client disconnected')
133+
console.log('client disconnected')
115134
})
116135
}
117136
).listen(process.env.GIT_SSH_MOCK_SERVER_PORT || 2222, '127.0.0.1', function () {
118137
console.log('Listening on port ' + this.address().port)
119138
})
120-
121-
})
139+
})

0 commit comments

Comments
 (0)