Rework GitHub acceptor and move to its own module (#2021)

Continue to merge the work from #1205.
This commit is contained in:
Paul Melnikow
2018-11-09 15:14:01 -05:00
committed by GitHub
parent 02ec19fd22
commit 3eac8ebbfb
6 changed files with 464 additions and 177 deletions

View File

@@ -2,10 +2,7 @@
const { EventEmitter } = require('events')
const crypto = require('crypto')
const log = require('./log')
const secretIsValid = require('./sys/secret-is-valid')
const queryString = require('query-string')
const request = require('request')
const serverSecrets = require('./server-secrets')
const mapKeys = require('lodash.mapkeys')
@@ -25,133 +22,6 @@ if (serverSecrets && serverSecrets.gh_token) {
addGithubToken(serverSecrets.gh_token)
}
function setRoutes(server) {
const baseUrl = process.env.BASE_URL || 'https://img.shields.io'
server.route(/^\/github-auth$/, (data, match, end, ask) => {
if (!(serverSecrets && serverSecrets.gh_client_id)) {
return end('This server is missing GitHub client secrets.')
}
const query = queryString.stringify({
client_id: serverSecrets.gh_client_id,
redirect_uri: `${baseUrl}/github-auth/done`,
})
ask.res.statusCode = 302 // Found.
ask.res.setHeader(
'Location',
`https://github.com/login/oauth/authorize?${query}`
)
end('')
})
server.route(/^\/github-auth\/done$/, (data, match, end, ask) => {
if (
!(
serverSecrets &&
serverSecrets.gh_client_id &&
serverSecrets.gh_client_secret
)
) {
return end('This server is missing GitHub client secrets.')
}
if (!data.code) {
log(`GitHub OAuth data.code: ${JSON.stringify(data)}`)
return end('GitHub OAuth authentication failed to provide a code.')
}
const options = {
url: 'https://github.com/login/oauth/access_token',
headers: {
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
'User-Agent': 'Shields.io',
},
form: queryString.stringify({
client_id: serverSecrets.gh_client_id,
client_secret: serverSecrets.gh_client_secret,
code: data.code,
}),
method: 'POST',
}
request(options, (err, res, body) => {
if (err != null) {
return end('The connection to GitHub failed.')
}
let content
try {
content = queryString.parse(body)
} catch (e) {
return end('The GitHub OAuth token could not be parsed.')
}
const token = content.access_token
if (!token) {
return end('The GitHub OAuth process did not return a user token.')
}
ask.res.setHeader('Content-Type', 'text/html')
end(
'<p>Shields.io has received your app-specific GitHub user token. ' +
'You can revoke it by going to ' +
'<a href="https://github.com/settings/applications">GitHub</a>.</p>' +
'<p>Until you do, you have now increased the rate limit for GitHub ' +
'requests going through Shields.io. GitHub-related badges are ' +
'therefore more robust.</p>' +
'<p>Thanks for contributing to a smoother experience for ' +
'everyone!</p>' +
'<p><a href="/">Back to the website</a></p>'
)
sendTokenToAllServers(token).catch(e => {
console.error('GitHub user token transmission failed:', e)
})
})
})
server.route(/^\/github-auth\/add-token$/, (data, match, end, ask) => {
if (!secretIsValid(data.shieldsSecret)) {
// An unknown entity tries to connect. Let the connection linger for 10s.
return setTimeout(() => {
end('Invalid secret.')
}, 10000)
}
addGithubToken(data.token)
emitter.emit('token-added', data.token)
end('Thanks!')
})
}
function sendTokenToAllServers(token) {
const ips = serverSecrets.shieldsIps
return Promise.all(
ips.map(
ip =>
new Promise((resolve, reject) => {
const options = {
url: `https://${ip}/github-auth/add-token`,
method: 'POST',
form: {
shieldsSecret: serverSecrets.shieldsSecret,
token,
},
// We target servers by IP, and we use HTTPS. Assuming that
// 1. Internet routers aren't hacked, and
// 2. We don't unknowingly lose our IP to someone else,
// we're not leaking people's and our information.
// (If we did, it would have no impact, as we only ask for a token,
// no GitHub scope. The malicious entity would only be able to use
// our rate limit pool.)
// FIXME: use letsencrypt.
strictSSL: false,
}
request(options, (err, res, body) => {
if (err != null) {
return reject(err)
}
resolve()
})
})
)
)
}
// token: client token as a string.
// reqs: number of requests remaining.
// reset: timestamp when the number of remaining requests is reset.
@@ -214,6 +84,7 @@ function addGithubToken(token) {
if (githubUserTokens.indexOf(token) === -1) {
githubUserTokens.push(token)
}
emitter.emit('token-added', token)
}
function rmGithubToken(token) {
@@ -333,7 +204,6 @@ function githubRequest(request, url, query, cb) {
module.exports = {
request: githubRequest,
setRoutes,
serializeDebugInfo,
addGithubToken,
rmGithubToken,

261
package-lock.json generated
View File

@@ -1191,6 +1191,15 @@
"any-observable": "^0.3.0"
}
},
"@sindresorhus/is": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.12.0.tgz",
"integrity": "sha512-9ve22cGrAKlSRvi8Vb2JIjzcaaQg79531yQHnF+hi/kOpsSj3Om8AyR1wcHrgl0u7U3vYQ7gmF5erZzOp4+51Q==",
"dev": true,
"requires": {
"symbol-observable": "^1.2.0"
}
},
"@sinonjs/commons": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.0.tgz",
@@ -1234,6 +1243,15 @@
"integrity": "sha512-ZwTHAlC9akprWDinwEPD4kOuwaYZlyMwVJIANsKNC3QVp0AHB04m7RnB4eqeWfgmxw8MGTzS9uMaw93Z3QcZbw==",
"dev": true
},
"@szmarczak/http-timer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.1.tgz",
"integrity": "sha512-WljfOGkmSJe8SUkl+4TPvN2ec0dpUGVyfTBQLoXJUiILs+wBSc4Kvp2N3aAWE4VwwDSLGdmD3/bufS5BgZpVSQ==",
"dev": true,
"requires": {
"defer-to-connect": "^1.0.1"
}
},
"JSONSelect": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/JSONSelect/-/JSONSelect-0.4.0.tgz",
@@ -1278,8 +1296,7 @@
"acorn": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz",
"integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=",
"optional": true
"integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc="
},
"acorn-dynamic-import": {
"version": "2.0.2",
@@ -2291,6 +2308,42 @@
}
}
},
"cacheable-request": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-5.1.0.tgz",
"integrity": "sha512-UCdjX4N/QjymZGpKY7hW4VJsxsVJM+drIiCxPa9aTvFQN5sL2+kJCYyeys8f2W0dJ0sU6Et54Ovl0sAmCpHHsA==",
"dev": true,
"requires": {
"clone-response": "^1.0.2",
"get-stream": "^4.0.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^3.0.0",
"lowercase-keys": "^1.0.1",
"normalize-url": "^3.1.0",
"responselike": "^1.0.2"
},
"dependencies": {
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
"dev": true,
"requires": {
"pump": "^3.0.0"
}
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
}
}
},
"caller": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/caller/-/caller-1.0.1.tgz",
@@ -2835,6 +2888,15 @@
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz",
"integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk="
},
"clone-response": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
"integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
"dev": true,
"requires": {
"mimic-response": "^1.0.0"
}
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -3410,8 +3472,7 @@
"cssom": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz",
"integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=",
"optional": true
"integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs="
},
"cssstyle": {
"version": "0.2.37",
@@ -3575,6 +3636,15 @@
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"decompress-response": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
"dev": true,
"requires": {
"mimic-response": "^1.0.0"
}
},
"dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
@@ -3605,6 +3675,12 @@
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
},
"defer-to-connect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.0.1.tgz",
"integrity": "sha512-2e0FJesseUqQj671gvZWfUyxpnFx/5n4xleamlpCD3U6Fm5dh5qzmmLNxNhtmHF06+SYVHH8QU6FACffYTnj0Q==",
"dev": true
},
"define-properties": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz",
@@ -3800,6 +3876,12 @@
"resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
"integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw="
},
"duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
"dev": true
},
"duplexify": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz",
@@ -4187,7 +4269,7 @@
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"dev": true,
"requires": {
@@ -5512,8 +5594,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@@ -5534,14 +5615,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5556,20 +5635,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -5686,8 +5762,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@@ -5699,7 +5774,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -5714,7 +5788,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -5722,14 +5795,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -5748,7 +5819,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -5829,8 +5899,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -5842,7 +5911,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -5928,8 +5996,7 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -5965,7 +6032,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -5985,7 +6051,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -6029,14 +6094,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},
@@ -6226,6 +6289,46 @@
}
}
},
"got": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/got/-/got-9.3.2.tgz",
"integrity": "sha512-OyKOUg71IKvwb8Uj0KP6EN3+qVVvXmYsFznU1fnwUnKtDbZnwSlAi7muNlu4HhBfN9dImtlgg9e7H0g5qVdaeQ==",
"dev": true,
"requires": {
"@sindresorhus/is": "^0.12.0",
"@szmarczak/http-timer": "^1.1.0",
"cacheable-request": "^5.1.0",
"decompress-response": "^3.3.0",
"duplexer3": "^0.1.4",
"get-stream": "^4.1.0",
"lowercase-keys": "^1.0.1",
"mimic-response": "^1.0.1",
"p-cancelable": "^1.0.0",
"to-readable-stream": "^1.0.0",
"url-parse-lax": "^3.0.0"
},
"dependencies": {
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
"dev": true,
"requires": {
"pump": "^3.0.0"
}
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
}
}
},
"graceful-fs": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
@@ -6526,6 +6629,12 @@
}
}
},
"http-cache-semantics": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz",
"integrity": "sha512-NtexGRtaV5z3ZUX78W9UDTOJPBdpqms6RmwQXmOhHws7CuQK3cqIoQtnmeqi1VvVD6u6eMMRL0sKE9BCZXTDWQ==",
"dev": true
},
"http-deceiver": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
@@ -7533,6 +7642,12 @@
"yargs": "^11.0.0"
}
},
"json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
"integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=",
"dev": true
},
"json-loader": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz",
@@ -7687,6 +7802,15 @@
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.3.tgz",
"integrity": "sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g=="
},
"keyv": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
"integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
"dev": true,
"requires": {
"json-buffer": "3.0.0"
}
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@@ -8266,6 +8390,12 @@
"signal-exit": "^3.0.0"
}
},
"lowercase-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
"integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
"dev": true
},
"lru-cache": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz",
@@ -8618,6 +8748,12 @@
"integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=",
"dev": true
},
"mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"dev": true
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -10152,6 +10288,12 @@
"remove-trailing-separator": "^1.0.1"
}
},
"normalize-url": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz",
"integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==",
"dev": true
},
"npm-path": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.4.tgz",
@@ -10239,7 +10381,6 @@
"resolved": false,
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
"dev": true,
"optional": true,
"requires": {
"kind-of": "^3.0.2",
"longest": "^1.0.1",
@@ -10609,8 +10750,7 @@
"version": "1.1.6",
"resolved": false,
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"dev": true,
"optional": true
"dev": true
},
"is-builtin-module": {
"version": "1.0.0",
@@ -10706,7 +10846,6 @@
"resolved": false,
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"dev": true,
"optional": true,
"requires": {
"is-buffer": "^1.1.5"
}
@@ -10759,8 +10898,7 @@
"version": "1.0.1",
"resolved": false,
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
"dev": true,
"optional": true
"dev": true
},
"lru-cache": {
"version": "4.1.3",
@@ -11063,8 +11201,7 @@
"version": "1.6.1",
"resolved": false,
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
"dev": true,
"optional": true
"dev": true
},
"require-directory": {
"version": "2.1.1",
@@ -11752,6 +11889,12 @@
"integrity": "sha1-auIvresfhQ/7DPTCD/e4fl62UN8=",
"dev": true
},
"p-cancelable": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.0.0.tgz",
"integrity": "sha512-USgPoaC6tkTGlS831CxsVdmZmyb8tR1D+hStI84MyckLOzfJlYQUweomrwE3D8T7u5u5GVuW064LT501wHTYYA==",
"dev": true
},
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
@@ -12130,6 +12273,12 @@
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
"integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
},
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
"dev": true
},
"preserve": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
@@ -13120,6 +13269,15 @@
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"dev": true
},
"responselike": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
"integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
"dev": true,
"requires": {
"lowercase-keys": "^1.0.0"
}
},
"restore-cursor": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
@@ -14647,6 +14805,12 @@
"kind-of": "^3.0.2"
}
},
"to-readable-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
"integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
"dev": true
},
"to-regex": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
@@ -15103,6 +15267,15 @@
}
}
},
"url-parse-lax": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
"integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
"dev": true,
"requires": {
"prepend-http": "^2.0.0"
}
},
"url-template": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
@@ -15710,7 +15883,7 @@
},
"yargs": {
"version": "11.1.0",
"resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz",
"integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==",
"dev": true,
"requires": {

View File

@@ -157,6 +157,7 @@
"eslint-plugin-standard": "^4.0.0",
"fetch-ponyfill": "^6.0.0",
"fs-readfile-promise": "^3.0.1",
"got": "^9.2.2",
"husky": "^1.1.2",
"icedfrisby": "2.0.0-alpha.2",
"icedfrisby-nock": "^1.0.0",

View File

@@ -0,0 +1,133 @@
'use strict'
const queryString = require('query-string')
const request = require('request')
const log = require('../../../lib/log')
const githubAuth = require('../../../lib/github-auth')
const serverSecrets = require('../../../lib/server-secrets')
const secretIsValid = require('../../../lib/sys/secret-is-valid')
function sendTokenToAllServers(token) {
const { shieldsIps, shieldsSecret } = serverSecrets
return Promise.all(
shieldsIps.map(
ip =>
new Promise((resolve, reject) => {
const options = {
url: `https://${ip}/github-auth/add-token`,
method: 'POST',
form: {
shieldsSecret,
token,
},
// We target servers by IP, and we use HTTPS. Assuming that
// 1. Internet routers aren't hacked, and
// 2. We don't unknowingly lose our IP to someone else,
// we're not leaking people's and our information.
// (If we did, it would have no impact, as we only ask for a token,
// no GitHub scope. The malicious entity would only be able to use
// our rate limit pool.)
// FIXME: use letsencrypt.
strictSSL: false,
}
request(options, (err, res, body) => {
if (err != null) {
reject(err)
} else {
resolve()
}
})
})
)
)
}
function setRoutes(server) {
const baseUrl = process.env.BASE_URL || 'https://img.shields.io'
server.route(/^\/github-auth$/, (data, match, end, ask) => {
ask.res.statusCode = 302 // Found.
const query = queryString.stringify({
client_id: serverSecrets.gh_client_id,
redirect_uri: `${baseUrl}/github-auth/done`,
})
ask.res.setHeader(
'Location',
`https://github.com/login/oauth/authorize?${query}`
)
end('')
})
server.route(/^\/github-auth\/done$/, (data, match, end, ask) => {
if (!data.code) {
log(`GitHub OAuth data: ${JSON.stringify(data)}`)
return end('GitHub OAuth authentication failed to provide a code.')
}
const options = {
url: 'https://github.com/login/oauth/access_token',
method: 'POST',
headers: {
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
'User-Agent': 'Shields.io',
},
form: queryString.stringify({
client_id: serverSecrets.gh_client_id,
client_secret: serverSecrets.gh_client_secret,
code: data.code,
}),
}
request(options, (err, res, body) => {
if (err != null) {
return end('The connection to GitHub failed.')
}
let content
try {
content = queryString.parse(body)
} catch (e) {
return end('The GitHub OAuth token could not be parsed.')
}
const { access_token: token } = content
if (!token) {
return end('The GitHub OAuth process did not return a user token.')
}
ask.res.setHeader('Content-Type', 'text/html')
end(
'<p>Shields.io has received your app-specific GitHub user token. ' +
'You can revoke it by going to ' +
'<a href="https://github.com/settings/applications">GitHub</a>.</p>' +
'<p>Until you do, you have now increased the rate limit for GitHub ' +
'requests going through Shields.io. GitHub-related badges are ' +
'therefore more robust.</p>' +
'<p>Thanks for contributing to a smoother experience for ' +
'everyone!</p>' +
'<p><a href="/">Back to the website</a></p>'
)
sendTokenToAllServers(token).catch(e => {
console.error('GitHub user token transmission failed:', e)
})
})
})
server.route(/^\/github-auth\/add-token$/, (data, match, end, ask) => {
if (!secretIsValid(data.shieldsSecret)) {
// An unknown entity tries to connect. Let the connection linger for 10s.
setTimeout(() => {
end('Invalid secret.')
}, 10000)
return
}
githubAuth.addGithubToken(data.token)
end('Thanks!')
})
}
module.exports = {
sendTokenToAllServers,
setRoutes,
}

View File

@@ -0,0 +1,106 @@
'use strict'
const { expect } = require('chai')
const Camp = require('camp')
const got = require('got')
const queryString = require('query-string')
const nock = require('nock')
const config = require('../../../lib/test-config')
const serverSecrets = require('../../../lib/server-secrets')
const acceptor = require('./acceptor')
const baseUri = `http://127.0.0.1:${config.port}`
const fakeClientId = 'githubdabomb'
describe('Github token acceptor', function() {
// Frustratingly, potentially undefined properties can't reliably be stubbed
// with Sinon.
// https://github.com/sinonjs/sinon/pull/1557
before(function() {
serverSecrets.gh_client_id = fakeClientId
serverSecrets.shieldsIps = []
})
after(function() {
delete serverSecrets.gh_client_id
delete serverSecrets.shieldsIps
})
let camp
beforeEach(function(done) {
camp = Camp.start({ port: config.port, hostname: '::' })
camp.on('listening', () => done())
})
afterEach(function(done) {
if (camp) {
camp.close(() => done())
camp = null
}
})
beforeEach(function() {
acceptor.setRoutes(camp)
})
it('should start the OAuth process', async function() {
const res = await got(`${baseUri}/github-auth`, { followRedirect: false })
expect(res.statusCode).to.equal(302)
const qs = queryString.stringify({
client_id: fakeClientId,
redirect_uri: 'https://img.shields.io/github-auth/done',
})
const expectedLocationHeader = `https://github.com/login/oauth/authorize?${qs}`
expect(res.headers.location).to.equal(expectedLocationHeader)
})
describe('Finishing the OAuth process', function() {
context('no code is provided', function() {
it('should return an error', async function() {
const res = await got(`${baseUri}/github-auth/done`)
expect(res.body).to.equal(
'GitHub OAuth authentication failed to provide a code.'
)
})
})
const fakeCode = '123456789'
const fakeAccessToken = 'abcdef'
context('a code is provided', function() {
let scope
beforeEach(function() {
nock.enableNetConnect(/127\.0\.0\.1/)
scope = nock('https://github.com')
.post('/login/oauth/access_token')
.reply((url, requestBody) => {
expect(queryString.parse(requestBody).code).to.equal(fakeCode)
return queryString.stringify({ access_token: fakeAccessToken })
})
})
afterEach(function() {
if (scope) {
scope.done()
scope = null
}
})
afterEach(function() {
nock.enableNetConnect()
nock.cleanAll()
})
it('should finish the OAuth process', async function() {
const res = await got(`${baseUri}/github-auth/done`, {
form: true,
body: { code: fakeCode },
})
expect(res.body).to.startWith(
'<p>Shields.io has received your app-specific GitHub user token.'
)
})
})
})
})

View File

@@ -8,6 +8,7 @@ const RedisTokenPersistence = require('../../lib/redis-token-persistence')
const FsTokenPersistence = require('../../lib/fs-token-persistence')
const GithubApiProvider = require('./github-api-provider')
const { setRoutes: setAdminRoutes } = require('./auth/admin')
const { setRoutes: setAcceptorRoutes } = require('./auth/acceptor')
// Convenience class with all the stuff related to the Github API and its
// authorization tokens, to simplify server initialization.
@@ -53,13 +54,16 @@ class GithubConstellation {
log.error(e)
}
// Register for this event after `initialize()` finishes, so we don't
// catch `token-added` events for the initial tokens, which would be
// inefficient, though it wouldn't break anything.
githubAuth.emitter.on('token-added', this.persistence.noteTokenAdded)
githubAuth.emitter.on('token-removed', this.persistence.noteTokenRemoved)
setAdminRoutes(server)
if (serverSecrets && serverSecrets.gh_client_id) {
githubAuth.setRoutes(server)
if (serverSecrets.gh_client_id && serverSecrets.gh_client_secret) {
setAcceptorRoutes(server)
}
}