Skip to content

Commit 587a3a4

Browse files
committed
wip verifies commits with GitHub and emojis, pretty sweet
1 parent 8a05ad7 commit 587a3a4

File tree

7 files changed

+74
-45
lines changed

7 files changed

+74
-45
lines changed

demo.git/logs/HEAD

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
0000000000000000000000000000000000000000 71c666705d861b556d73f2badddaca0cfcf5e930 William Hilton <[email protected]> 1574741587 -0500 commit (initial): Initial commit
2+
0000000000000000000000000000000000000000 b1f9a9a2689e95d8aaf9d2ed5513fe78f4c53901 William Hilton <[email protected]> 1575084779 -0500 commit (initial): Initial commit

demo.git/logs/refs/heads/master

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
0000000000000000000000000000000000000000 71c666705d861b556d73f2badddaca0cfcf5e930 William Hilton <[email protected]> 1574741587 -0500 commit (initial): Initial commit
2+
0000000000000000000000000000000000000000 b1f9a9a2689e95d8aaf9d2ed5513fe78f4c53901 William Hilton <[email protected]> 1575084779 -0500 commit (initial): Initial commit

demo.git/objects/71/c666705d861b556d73f2badddaca0cfcf5e930

-3
This file was deleted.
Binary file not shown.

demo.git/refs/heads/master

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
71c666705d861b556d73f2badddaca0cfcf5e930
1+
b1f9a9a2689e95d8aaf9d2ed5513fe78f4c53901

lookup.js

+22-21
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,25 @@ module.exports = (username) =>
99
return json.map(data => data.raw_key)
1010
})
1111

12-
async function username2keys(username) {
13-
return new Promise((resolve, reject) => {
14-
get.concat({
15-
url: `https://api.github.com/users/${username}/gpg_keys`,
16-
json: true,
17-
headers: {
18-
'user-agent': 'GitHub PGP KeyFinder'
19-
}
20-
}, (err, res, data) => {
21-
if (err) return reject(err)
22-
return resolve(data.map(i => i.raw_key))
23-
})
24-
})
12+
async function usernames2keys(usernames) {
13+
const all = await Promise.all(
14+
usernames.map(username => new Promise((resolve, reject) => {
15+
get.concat({
16+
url: `https://api.github.com/users/${username}/gpg_keys`,
17+
json: true,
18+
headers: {
19+
'user-agent': 'GitHub PGP KeyFinder'
20+
}
21+
}, (err, res, data) => {
22+
if (err) return reject(err)
23+
return resolve(data.map(i => i.raw_key))
24+
})
25+
}))
26+
)
27+
return all.reduce((a, b) => a.concat(b)).filter(Boolean)
2528
}
2629

27-
async function email2username(email) {
30+
async function email2usernames(email) {
2831
return new Promise((resolve, reject) => {
2932
get.concat({
3033
url: `https://api.github.com/search/users?q=${email}+in:email`,
@@ -35,11 +38,9 @@ async function email2username(email) {
3538
}, (err, res, data) => {
3639
if (err) return reject(err)
3740
if (data.total_count === 0) {
38-
return reject(new Error(`No GitHub user publicly associated with ${email}`))
39-
} else if (data.total_count > 1) {
40-
return reject(new Error(`Multiple GitHub users found for ${email}: ${JSON.stringify(data.items.map(i => i.login))}`))
41-
} else if (data.total_count === 1) {
42-
return resolve(data.items[0].login)
41+
return reject(new Error(`Could not find the GitHub user publicly associated with the email address "${email}"`))
42+
} else if (data.total_count > 0) {
43+
return resolve(data.items.map(i => i.login))
4344
} else {
4445
return reject('Unexpected value for data.total_count returned by GitHub API')
4546
}
@@ -49,8 +50,8 @@ async function email2username(email) {
4950

5051
async function lookup(email) {
5152
if (cache[email]) return cache[email]
52-
const username = await email2username(email)
53-
const keys = await username2keys(username)
53+
const usernames = await email2usernames(email)
54+
const keys = await usernames2keys(usernames)
5455
cache[email] = keys
5556
return cache[email]
5657
}

middleware.js

+49-20
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const fp = require('fs').promises
33
const path = require('path')
44
const url = require('url')
55
const EventEmitter = require('events').EventEmitter
6-
const { indexPack, plugins, readObject, verify } = require('isomorphic-git')
6+
const { E, indexPack, plugins, readObject, verify } = require('isomorphic-git')
77
const { serveInfoRefs, serveReceivePack, parseReceivePackRequest } = require('isomorphic-git/dist/for-node/isomorphic-git/internal-apis.js')
88
const { pgp } = require('@isomorphic-git/pgp-plugin')
99

@@ -21,6 +21,10 @@ function pad (str) {
2121
return (str + ' ').slice(0, 7)
2222
}
2323

24+
function abbr (oid) {
25+
return oid.slice(0, 7)
26+
}
27+
2428
const sleep = ms => new Promise(cb => setTimeout(cb, ms))
2529

2630
function log(req, res) {
@@ -86,37 +90,43 @@ function factory (config) {
8690

8791
// send HTTP response headers
8892
const { headers } = await serveReceivePack({ type: 'service', service })
89-
for (const header in headers) {
90-
res.setHeader(header, headers[header])
91-
}
92-
res.statusCode = 200
93+
res.writeHead(200, headers)
9394

9495
// index packfile
9596
res.write(await serveReceivePack({ type: 'print', message: 'Indexing packfile...' }))
97+
console.log('Indexing packfile...')
98+
await sleep(1)
9699
let currentPhase = null
97100
const listener = async ({ phase, loaded, total, lengthComputable }) => {
98101
let np = phase !== currentPhase ? '\n' : '\r'
99102
currentPhase = phase
100103
res.write(await serveReceivePack({ type: 'print', message: `${np}${phase} ${loaded}/${total}` }))
104+
res.flush()
101105
}
102-
let problem = false
103106
let oids
104107
try {
105108
ee.on(`${last20}:progress`, listener)
106109
oids = await indexPack({ fs, gitdir, dir, filepath, emitterPrefix: `${last20}:` })
107-
res.write(await serveReceivePack({ type: 'print', message: '\nIndexing a success!' }))
110+
await sleep(1)
111+
res.write(await serveReceivePack({ type: 'print', message: '\nIndexing completed' }))
108112
res.write(await serveReceivePack({ type: 'unpack', unpack: 'ok' }))
109113
} catch (e) {
110-
problem = true
111114
res.write(await serveReceivePack({ type: 'print', message: '\nOh dear!' }))
112115
res.write(await serveReceivePack({ type: 'unpack', unpack: e.message }))
116+
117+
for (const update of updates) {
118+
res.write(await serveReceivePack({ type: 'ng', ref: update.fullRef, message: 'Could not index pack' }))
119+
}
120+
throw e
113121
} finally {
114122
ee.removeListener(`${last20}:progress`, listener)
115123
}
124+
await sleep(1)
116125

117126
// Move packfile and index into repo
118127
await fp.rename(path.join(dir, filepath), path.join(gitdir, 'objects', 'pack', filepath))
119128
await fp.rename(path.join(dir, filepath.replace(/\.pack$/, '.idx')), path.join(gitdir, 'objects', 'pack', filepath.replace(/\.pack$/, '.idx')))
129+
await fp.rmdir(path.join(dir))
120130

121131
// Verify objects (ideally we'd do this _before_ moving it into the repo... but I think we'd need a custom 'fs' implementation with overlays)
122132
res.write(await serveReceivePack({ type: 'print', message: '\nVerifying objects...\n' }))
@@ -128,44 +138,63 @@ function factory (config) {
128138
const { type, object } = await readObject({ gitdir, oid })
129139
if (type === 'commit' || type === 'tag') {
130140
const email = type === 'commit' ? object.author.email : object.tagger.email
131-
res.write(await serveReceivePack({ type: 'print', message: `\nVerifying ${type} ${oid} by ${email}\n` }))
132-
const keys = await lookup(email)
141+
res.write(await serveReceivePack({ type: 'print', message: `\nVerifying ${type} ${abbr(oid)} by ${email}: ` }))
142+
let keys
143+
try {
144+
keys = await lookup(email)
145+
} catch (e) {
146+
res.write(await serveReceivePack({ type: 'print', message: `no keys found 👎\n` }))
147+
throw e
148+
}
149+
if (keys.length === 0) {
150+
res.write(await serveReceivePack({ type: 'print', message: `no keys found 👎\n` }))
151+
throw new Error(`\nSignature verification failed for ${type} ${abbr(oid)}. No PGP keys could be found for ${email}.\n`)
152+
}
133153
let ok = false
134154
for (const key of keys) {
135155
const result = await verify({ gitdir, ref: oid, publicKeys: key })
136156
if (result === false) {
137157
demote(email, key)
138158
} else {
139-
res.write(await serveReceivePack({ type: 'print', message: `\nSigned by ${result[0]}\n` }))
159+
res.write(await serveReceivePack({ type: 'print', message: `signed with ${result[0]} 👍\n` }))
140160
ok = true
141161
break
142162
}
143163
}
144164
if (!ok) {
145-
res.write(await serveReceivePack({ type: 'error', message: `\nNo valid signature for ${type} ${oid}\n` }))
146-
throw new Error('NO SIGNATURE')
165+
res.write(await serveReceivePack({ type: 'print', message: `no keys matched 👎\n` }))
166+
throw new Error(`\nSignature verification failed for ${type} ${abbr(oid)}. It was not signed with a key publicly associated with the email address "${email}".
167+
168+
Learn how you can associate your GPG key with your email account using GitHub here:
169+
https://help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account
170+
`)
147171
}
148172
}
149173
// await sleep(1)
150174
}
151175

176+
res.write(await serveReceivePack({ type: 'print', message: `\nVerification complete` }))
177+
152178
// refs
153179
for (const update of updates) {
154-
if (!problem) {
155-
res.write(await serveReceivePack({ type: 'ok', ref: update.fullRef }))
156-
} else {
157-
res.write(await serveReceivePack({ type: 'ng', ref: update.fullRef, message: 'Could not index pack' }))
158-
}
180+
res.write(await serveReceivePack({ type: 'ok', ref: update.fullRef }))
159181
}
160182

161183
// gratuitous banner
162184
res.write(await serveReceivePack({ type: 'print', message: '\n' + require('./logo.js') }))
163185
} catch (e) {
164186
if (e.message === 'Client is done') {
165187
res.statusCode = 200
188+
} else if (e.code && e.code === E.NoSignatureError) {
189+
res.write(await serveReceivePack({ type: 'print', message: `no signature 👎\n` }))
190+
res.write(await serveReceivePack({ type: 'error', message: e.message + `
191+
192+
This server's policy is to only accept GPG-signed commits.
193+
Learn how you can create a GPG key and configure git to sign commits here:
194+
https://help.github.com/en/github/authenticating-to-github/managing-commit-signature-verification
195+
` }))
166196
} else {
167-
console.log(e)
168-
res.statusCode = 500
197+
res.write(await serveReceivePack({ type: 'error', message: e.message }))
169198
}
170199
} finally {
171200
// fin

0 commit comments

Comments
 (0)