Rework GitHub acceptor and move to its own module (#2021)
Continue to merge the work from #1205.
This commit is contained in:
@@ -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
261
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
133
services/github/auth/acceptor.js
Normal file
133
services/github/auth/acceptor.js
Normal 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,
|
||||
}
|
||||
106
services/github/auth/acceptor.spec.js
Normal file
106
services/github/auth/acceptor.spec.js
Normal 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.'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user