Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework GitHub acceptor and move to its own module #2021

Merged
merged 14 commits into from
Nov 9, 2018
Merged
132 changes: 1 addition & 131 deletions lib/github-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -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.
Expand Down Expand Up @@ -214,6 +84,7 @@ function addGithubToken(token) {
if (githubUserTokens.indexOf(token) === -1) {
githubUserTokens.push(token)
}
emitter.emit('token-added', token)
}

function rmGithubToken(token) {
Expand Down Expand Up @@ -333,7 +204,6 @@ function githubRequest(request, url, query, cb) {

module.exports = {
request: githubRequest,
setRoutes,
serializeDebugInfo,
addGithubToken,
rmGithubToken,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"eslint-plugin-standard": "^4.0.0",
"fetch-ponyfill": "^6.0.0",
"fs-readfile-promise": "^3.0.1",
"got": "^9.2.2",
Copy link
Member Author

@paulmelnikow paulmelnikow Oct 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't use whatwg-fetch to test redirects, because they can't be disabled.

I don't have a strong preference between axios or got to supplant request and fetch. I had an easier time with got's documentation, so went with that for the moment. So far this is only used in test code.

"husky": "^1.1.2",
"icedfrisby": "2.0.0-alpha.2",
"icedfrisby-nock": "^1.0.0",
Expand Down
133 changes: 133 additions & 0 deletions services/github/auth/acceptor.js
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is difficult to test, and enabling #1939 should obviate the need for it, so I don't think it's worth writing a new test.

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) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test.

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) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test.

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) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint is obviated when #1939 is turned on; so I'm not going to take the time to write a new test for it.

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,
}
Loading