From 1af1f497bb411b9b8aaafb1e5e02212da999a3d4 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Sat, 25 Nov 2017 18:27:02 -0500 Subject: [PATCH] Allow configuring allowed CORS origins for suggest (#1282) - Support single-server testing and a local dev server (like Next) that is on a different port from the shields server - Refactor config schema With this change, the suggestions work locally in #1273. --- doc/self-hosting.md | 13 ++++++++-- lib/server-config.js | 58 ++++++++++++++++++++++++++++++++------------ lib/suggest.js | 27 +++++++++++++-------- server.js | 21 ++++++++-------- 4 files changed, 80 insertions(+), 39 deletions(-) diff --git a/doc/self-hosting.md b/doc/self-hosting.md index b3267ae237..e0a8452516 100644 --- a/doc/self-hosting.md +++ b/doc/self-hosting.md @@ -139,8 +139,17 @@ If you want to host the frontend on a separate server, such as cloud storage or a CDN, you can do that. Just copy the built `index.html` there. To help out users, you can make the Shields server redirect the server root. -Set the `FRONTEND_REDIRECT_URL` environment variable: +Set the `REDIRECT_URI` environment variable: ```sh -FRONTEND_REDIRECT_URL=http://my-custom-shields.s3.amazonaws.com/ +REDIRECT_URI=http://my-custom-shields.s3.amazonaws.com/ ``` + +If you want to use server suggestions, you should also set `ALLOWED_ORIGIN`: + +```sh +ALLOWED_ORIGIN=http://my-custom-shields.s3.amazonaws.com,https://my-custom-shields.s3.amazonaws.com +``` + +This should be a comma-separated list of allowed origin headers. They should +not have paths or trailing slashes. diff --git a/lib/server-config.js b/lib/server-config.js index b298711ebf..de41ef279b 100644 --- a/lib/server-config.js +++ b/lib/server-config.js @@ -4,25 +4,51 @@ // should be injected into other components needing it. const url = require('url'); +const envFlag = require('node-env-flag'); -const secureServer = !!process.env.HTTPS; -const serverPort = +process.env.PORT || +process.argv[2] || (secureServer ? 443 : 80); -const bindAddress = process.env.BIND_ADDRESS || process.argv[3] || '::'; +function envArray(envVar, defaultValue, delimiter) { + delimiter = delimiter || ','; + if (envVar) { + return envVar.split(delimiter); + } else { + return defaultValue; + } +} + +const isSecure = envFlag(process.env.HTTPS, false); +const port = +process.env.PORT || +process.argv[2] || (isSecure ? 443 : 80); +const address = process.env.BIND_ADDRESS || process.argv[3] || '::'; +const baseUri = url.format({ + protocol: isSecure ? 'https' : 'http', + hostname: address, + port, + pathname: '/', +}); + +// The base URI provides a suitable value for development. Production should +// configure this. +const allowedOrigin = envArray(process.env.ALLOWED_ORIGIN, baseUri.replace(/\/$/, ''), ','); const config = { - secureServer, - secureServerKey: process.env.HTTPS_KEY, - secureServerCert: process.env.HTTPS_CRT, - serverPort, - bindAddress, - githubApiUrl: process.env.GITHUB_URL || 'https://api.github.com', - frontendUri: url.format({ - protocol: secureServer ? 'https' : 'http', - hostname: bindAddress, - port: serverPort, - pathname: '/', - }), - frontendRedirectUrl: process.env.FRONTEND_REDIRECT_URL || process.env.INFOSITE, + bind: { + port, + address, + }, + ssl: { + isSecure, + key: process.env.HTTPS_KEY, + cert: process.env.HTTPS_CRT, + }, + baseUri, + redirectUri: process.env.REDIRECT_URI || process.env.INFOSITE, + cors: { + allowedOrigin, + }, + services: { + github: { + baseUri: process.env.GITHUB_URL || 'https://api.github.com', + }, + }, }; module.exports = config; diff --git a/lib/suggest.js b/lib/suggest.js index 28565519d1..8983dc8fc8 100644 --- a/lib/suggest.js +++ b/lib/suggest.js @@ -12,15 +12,20 @@ const githubApiUrl = process.env.GITHUB_URL || 'https://api.github.com'; // - link: target as a string URL. // - badge: shields image URL. // - name: string -function suggest (data, end, ask) { - const origin = ask.req.headers['origin']; - if (/^https?:\/\/shields\.io$/.test(origin)) { - ask.res.setHeader('Access-Control-Allow-Origin', origin); - } else { - ask.res.setHeader('Access-Control-Allow-Origin', 'null'); - end({err: 'Disallowed'}); - return; +function suggest (allowedOrigin, data, end, ask) { + // Same-host requests may be made in development or in single-server + // testing. These are legitimate, but do not have an origin header. + const origin = ask.req.headers.origin; + if (origin) { + if (allowedOrigin.includes(origin)) { + ask.res.setHeader('Access-Control-Allow-Origin', origin); + } else { + ask.res.setHeader('Access-Control-Allow-Origin', 'null'); + end({err: 'Disallowed'}); + return; + } } + let url; try { url = nodeUrl.parse(data.url); @@ -122,8 +127,10 @@ function githubLicense (user, repo) { }); } -function setRoutes (camp) { - camp.ajax.on('suggest/v1', suggest); +function setRoutes (allowedOrigin, camp) { + camp.ajax.on( + 'suggest/v1', + (data, end, ask) => suggest(allowedOrigin, data, end, ask)); } module.exports = { diff --git a/server.js b/server.js index 5b0cddcd40..b953b970a6 100644 --- a/server.js +++ b/server.js @@ -95,15 +95,15 @@ const { } = require('./lib/github-provider'); const serverStartTime = new Date((new Date()).toGMTString()); -const { githubApiUrl } = config; +const githubApiUrl = config.services.github.baseUri; const camp = require('camp').start({ documentRoot: path.join(__dirname, 'public'), - port: config.serverPort, - hostname: config.bindAddress, - secure: config.secureServer, - cert: config.secureServerCert, - key: config.secureServerKey, + port: config.bind.port, + hostname: config.bind.address, + secure: config.ssl.isSecure, + cert: config.ssl.cert, + key: config.ssl.key, }); function reset() { @@ -123,8 +123,7 @@ module.exports = { stop }; -log('Server is starting up'); -log(config.frontendUri); +log(`Server is starting up: ${config.baseUri}`); analytics.load(); analytics.scheduleAutosaving(); @@ -135,7 +134,7 @@ if (serverSecrets && serverSecrets.gh_client_id) { githubAuth.setRoutes(camp); } -suggest.setRoutes(camp); +suggest.setRoutes(config.cors.allowedOrigin, camp); camp.notfound(/\.(svg|png|gif|jpg|json)/, function(query, match, end, request) { var format = match[1]; @@ -7518,10 +7517,10 @@ function(data, match, end, ask) { } }); -if (config.frontendRedirectUrl) { +if (config.redirectUri) { camp.route(/^\/$/, (data, match, end, ask) => { ask.res.statusCode = 302; - ask.res.setHeader('Location', config.frontendRedirectUrl); + ask.res.setHeader('Location', config.redirectUri); ask.res.end(); }); }