diff --git a/lib/github-auth.js b/lib/github-auth.js index 36a576f3d5..b8ca019197 100644 --- a/lib/github-auth.js +++ b/lib/github-auth.js @@ -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( - '
Shields.io has received your app-specific GitHub user token. ' + - 'You can revoke it by going to ' + - 'GitHub.
' + - 'Until you do, you have now increased the rate limit for GitHub ' + - 'requests going through Shields.io. GitHub-related badges are ' + - 'therefore more robust.
' + - 'Thanks for contributing to a smoother experience for ' + - 'everyone!
' + - '' - ) - - 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, diff --git a/package-lock.json b/package-lock.json index 02a62cc81d..06b7e8a250 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 78548909d2..2b2938286f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/services/github/auth/acceptor.js b/services/github/auth/acceptor.js new file mode 100644 index 0000000000..0cc4236e76 --- /dev/null +++ b/services/github/auth/acceptor.js @@ -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( + 'Shields.io has received your app-specific GitHub user token. ' + + 'You can revoke it by going to ' + + 'GitHub.
' + + 'Until you do, you have now increased the rate limit for GitHub ' + + 'requests going through Shields.io. GitHub-related badges are ' + + 'therefore more robust.
' + + 'Thanks for contributing to a smoother experience for ' + + 'everyone!
' + + '' + ) + + 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, +} diff --git a/services/github/auth/acceptor.spec.js b/services/github/auth/acceptor.spec.js new file mode 100644 index 0000000000..888a8fcdca --- /dev/null +++ b/services/github/auth/acceptor.spec.js @@ -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( + 'Shields.io has received your app-specific GitHub user token.' + ) + }) + }) + }) +}) diff --git a/services/github/github-constellation.js b/services/github/github-constellation.js index 658975b31c..0534c6ea85 100644 --- a/services/github/github-constellation.js +++ b/services/github/github-constellation.js @@ -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) } }