Skip to content

Commit 37967ae

Browse files
committed
Fix: Verify payload by signature
1 parent 0e2082e commit 37967ae

File tree

5 files changed

+142
-69
lines changed

5 files changed

+142
-69
lines changed

.env.example

+2-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
GH_TOKEN=
1+
GH_TOKEN=
2+
WEBHOOK_SECRET=

scripts/delete-hooks.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const fetch = require('node-fetch')
2+
const { ghAuthHeaders, getPublicRepos } = require('../utils')
3+
4+
async function main() {
5+
const repos = await getPublicRepos()
6+
let count = 0
7+
for (const repo of repos) {
8+
const hooks = await fetch(repo.hookUrl, { headers: ghAuthHeaders })
9+
.then(res => res.json())
10+
const id = hooks.find(({ config }) => config.url === 'https://git.mihir.ch/webhook/push')?.id
11+
if (id) {
12+
console.log(`[${++count}/${repos.length}] Deleting hook for ${repo.name}`)
13+
await fetch(repo.hookUrl + '/' + id, {
14+
method: 'DELETE',
15+
headers: ghAuthHeaders
16+
})
17+
}
18+
}
19+
}
20+
21+
main()

setup.js

+8-68
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,20 @@
11
require('dotenv').config()
22
const path = require('path')
3-
const { writeFile, mkdir } = require('fs/promises')
4-
const fetch = require('node-fetch')
5-
const git = require('simple-git')()
3+
const { mkdir } = require('fs/promises')
64
const config = require('./config.json')
7-
8-
const GH_API_BASE = 'https://api.github.com'
9-
10-
const ghAuthHeaders = {
11-
Authorization: `token ${process.env.GH_TOKEN}`
12-
}
5+
const {
6+
getPublicRepos,
7+
createMetaList,
8+
cloneToPath,
9+
updateDescription,
10+
createWebhook
11+
} = require('./utils')
1312

1413
async function createRepositoriesPath() {
1514
console.log('[INFO]', 'Creating repositories\' directory if it does not exist')
1615
await mkdir(config.repositoriesPath, { recursive: true })
1716
}
1817

19-
async function getPublicRepos() {
20-
const searchParams = new URLSearchParams()
21-
for (const [key, value] of Object.entries(config.githubRepoListParams)) {
22-
searchParams.set(key, value)
23-
}
24-
const response = await fetch(`${GH_API_BASE}/user/repos?${searchParams}`, { headers: ghAuthHeaders })
25-
const json = await response.json()
26-
return json
27-
}
28-
29-
function filterAndProcessRepos(repos) {
30-
return repos
31-
.filter(repo => !repo.fork)
32-
.map(({ full_name, description, ssh_url, hooks_url }) => ({
33-
name: full_name,
34-
description,
35-
hookUrl: hooks_url,
36-
cloneUrl: ssh_url
37-
}))
38-
}
39-
40-
async function createMetaList(repos) {
41-
console.log('[INFO]', 'Creating list of cloned directories')
42-
const list = JSON.stringify(repos.map(repo => repo.name))
43-
const filePath = path.join(config.repositoriesPath, 'repositories.json')
44-
await writeFile(filePath, list, 'utf-8')
45-
return repos
46-
}
47-
48-
async function cloneToPath(repo) {
49-
const remotePath = repo.cloneUrl
50-
const localPath = path.join(config.repositoriesPath, repo.name)
51-
await git.clone(remotePath, localPath)
52-
return localPath
53-
}
54-
55-
async function updateDescription(localPath, description) {
56-
const descriptionFilePath = path.join(localPath, '.git', 'description')
57-
await writeFile(descriptionFilePath, description || '', 'utf-8')
58-
}
59-
60-
async function createWebhook(webhookUrl) {
61-
const requestBody = {
62-
config: {
63-
url: config.webhookBase + '/push',
64-
content_type: 'json'
65-
}
66-
}
67-
await fetch(webhookUrl, {
68-
method: 'POST',
69-
headers: {
70-
'Content-Type': 'application/json',
71-
...ghAuthHeaders
72-
},
73-
body: JSON.stringify(requestBody)
74-
})
75-
}
76-
7718
async function main () {
7819
// Create repositories path if it doesn't exist
7920
await createRepositoriesPath()
@@ -84,7 +25,6 @@ async function main () {
8425

8526
console.log('[INFO] Getting repos from GitHub')
8627
await getPublicRepos()
87-
.then(filterAndProcessRepos)
8828
.then(repos => {
8929
totalProgressRequired = repos.length * 4
9030
return repos

utils.js

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
require('dotenv').config()
2+
const fetch = require('node-fetch')
3+
const config = require('./config.json')
4+
const git = require('simple-git')()
5+
const { writeFile } = require('fs/promises')
6+
7+
const GH_API_BASE = 'https://api.github.com'
8+
const ghAuthHeaders = {
9+
Authorization: `token ${process.env.GH_TOKEN}`
10+
}
11+
12+
function filterAndProcessRepos(repos) {
13+
return repos
14+
.filter(repo => !repo.fork)
15+
.map(({full_name, description, ssh_url, hooks_url}) => ({
16+
name: full_name,
17+
description,
18+
hookUrl: hooks_url,
19+
cloneUrl: ssh_url
20+
}))
21+
}
22+
23+
function getPublicRepoParams() {
24+
const searchParams = new URLSearchParams()
25+
for (const [key, value] of Object.entries(config.githubRepoListParams)) {
26+
searchParams.set(key, value)
27+
}
28+
return searchParams
29+
}
30+
31+
async function getPublicRepos() {
32+
const searchParams = getPublicRepoParams()
33+
const response = await fetch(`${GH_API_BASE}/user/repos?${searchParams}`, { headers: ghAuthHeaders })
34+
const json = await response.json()
35+
return filterAndProcessRepos(json)
36+
}
37+
38+
async function createMetaList(repos) {
39+
console.log('[INFO]', 'Creating list of cloned directories')
40+
const list = JSON.stringify(repos.map(repo => repo.name))
41+
const filePath = path.join(config.repositoriesPath, 'repositories.json')
42+
await writeFile(filePath, list, 'utf-8')
43+
return repos
44+
}
45+
46+
async function cloneToPath(repo, log = false) {
47+
if (log) console.log(`[INFO] Cloning %s`, repo.name)
48+
const remotePath = repo.cloneUrl
49+
const localPath = path.join(config.repositoriesPath, repo.name)
50+
await git.clone(remotePath, localPath)
51+
return localPath
52+
}
53+
54+
async function updateDescription(localPath, description, log = false) {
55+
if (log) console.log(`[INFO] Adding description for %s`, repo.name)
56+
const descriptionFilePath = path.join(localPath, '.git', 'description')
57+
await writeFile(descriptionFilePath, description || '', 'utf-8')
58+
}
59+
60+
async function createWebhook(webhookUrl, log = false) {
61+
if (log) console.log(`[INFO] Creating webhook for %s`, repo.name)
62+
const requestBody = {
63+
config: {
64+
url: config.webhookBase + '/push',
65+
content_type: 'json',
66+
secret: process.env.WEBHOOK_SECRET
67+
}
68+
}
69+
await fetch(webhookUrl, {
70+
method: 'POST',
71+
headers: {
72+
'Content-Type': 'application/json',
73+
...ghAuthHeaders
74+
},
75+
body: JSON.stringify(requestBody)
76+
})
77+
}
78+
79+
module.exports = {
80+
ghAuthHeaders,
81+
getPublicRepos,
82+
createMetaList,
83+
cloneToPath,
84+
updateDescription,
85+
createWebhook
86+
}

webhook-server.js

+25
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
11
require('dotenv').config()
2+
const crypto = require('crypto')
23
const polka = require('polka')
34
const { json } = require('body-parser')
45
const config = require('./config.json')
56
const git = require('simple-git')()
67

8+
function createComparisonSignature (body = '') {
9+
const hmac = crypto.createHmac('sha1', process.env.WEBHOOK_SECRET)
10+
const signature = hmac.update(JSON.stringify(body)).digest('hex')
11+
return `sha1=${signature}`
12+
}
13+
14+
function compareSignatures (receivedSignature = '', selfSignature) {
15+
const source = Buffer.from(receivedSignature)
16+
const comparison = Buffer.from(selfSignature)
17+
return receivedSignature.length === selfSignature.length &&
18+
crypto.timingSafeEqual(source, comparison)
19+
}
20+
21+
function verifyPayload (req, res, next) {
22+
const { headers, body } = req
23+
const receivedSignature = headers['x-hub-signature']
24+
const selfSignature = createComparisonSignature(body)
25+
if (!compareSignatures(receivedSignature, selfSignature)) {
26+
return res.writeHead(401).end('Signature mismatch!')
27+
}
28+
next()
29+
}
30+
731
async function pull(repo) {
832
const localPath = `${config.repositoriesPath}/${repo}`
933
await git.cwd(localPath).pull('origin', 'master')
@@ -23,6 +47,7 @@ const handlers = {
2347

2448
polka()
2549
.use(json())
50+
.use(verifyPayload)
2651
.post('/webhook/push', handlers.push)
2752
.post('/webhook/repo', handlers.repo)
2853
.listen(config.webhookPort)

0 commit comments

Comments
 (0)