Files
shields/server.js
Paul Melnikow 102141123b Unify lint rules and clean lint (#2009)
Now that server.js is emptied out, it makes sense to eliminate the differences between the top-level .js files and everything else.
2018-09-01 11:08:17 -07:00

318 lines
9.3 KiB
JavaScript

'use strict';
const { DOMParser } = require('xmldom');
const jp = require('jsonpath');
const path = require('path');
const xpath = require('xpath');
const yaml = require('js-yaml');
const Raven = require('raven');
const serverSecrets = require('./lib/server-secrets');
Raven.config(process.env.SENTRY_DSN || serverSecrets.sentry_dsn).install();
Raven.disableConsoleAlerts();
const { loadServiceClasses } = require('./services');
const { checkErrorResponse } = require('./lib/error-helper');
const analytics = require('./lib/analytics');
const config = require('./lib/server-config');
const GithubConstellation = require('./services/github/github-constellation');
const sysMonitor = require('./lib/sys/monitor');
const log = require('./lib/log');
const { makeMakeBadgeFn } = require('./lib/make-badge');
const { QuickTextMeasurer } = require('./lib/text-measurer');
const suggest = require('./lib/suggest');
const {
makeColorB,
makeLabel: getLabel,
makeBadgeData: getBadgeData,
setBadgeColor,
} = require('./lib/badge-data');
const {
makeHandleRequestFn,
clearRequestCache,
} = require('./lib/request-handler');
const { clearRegularUpdateCache } = require('./lib/regular-update');
const { makeSend } = require('./lib/result-sender');
const { escapeFormat } = require('./lib/path-helpers');
const serverStartTime = new Date((new Date()).toGMTString());
const camp = require('camp').start({
documentRoot: path.join(__dirname, 'public'),
port: config.bind.port,
hostname: config.bind.address,
secure: config.ssl.isSecure,
cert: config.ssl.cert,
key: config.ssl.key,
});
const githubConstellation = new GithubConstellation({
persistence: config.persistence,
service: config.services.github,
});
const { apiProvider: githubApiProvider } = githubConstellation;
function reset() {
clearRequestCache();
clearRegularUpdateCache();
}
async function stop() {
await githubConstellation.stop();
analytics.cancelAutosaving();
return new Promise(resolve => {
camp.close(resolve);
});
}
module.exports = {
camp,
reset,
stop,
};
log(`Server is starting up: ${config.baseUri}`);
let measurer;
try {
measurer = new QuickTextMeasurer(config.font.path, config.font.fallbackPath);
} catch (e) {
console.log(`Unable to load fallback font. Using Helvetica-Bold instead.`);
measurer = new QuickTextMeasurer('Helvetica');
}
const makeBadge = makeMakeBadgeFn(measurer);
const cache = makeHandleRequestFn(makeBadge);
analytics.load();
analytics.scheduleAutosaving();
analytics.setRoutes(camp);
if (serverSecrets && serverSecrets.shieldsSecret) {
sysMonitor.setRoutes(camp);
}
githubConstellation.initialize(camp);
suggest.setRoutes(config.cors.allowedOrigin, githubApiProvider, camp);
camp.notfound(/\.(svg|png|gif|jpg|json)/, (query, match, end, request) => {
const format = match[1];
const badgeData = getBadgeData("404", query);
badgeData.text[1] = 'badge not found';
badgeData.colorscheme = 'red';
// Add format to badge data.
badgeData.format = format;
const svg = makeBadge(badgeData);
makeSend(format, request.res, end)(svg);
});
camp.notfound(/.*/, (query, match, end, request) => {
end(null, { template: '404.html' });
});
// Vendors.
loadServiceClasses().forEach(
serviceClass => serviceClass.register(
{ camp, handleRequest: cache, githubApiProvider },
{ handleInternalErrors: config.handleInternalErrors }));
// User defined sources - JSON response
camp.route(/^\/badge\/dynamic\/(json|xml|yaml)\.(svg|png|gif|jpg|json)$/,
cache({
queryParams: ['uri', 'url', 'query', 'prefix', 'suffix'],
handler: function(query, match, sendBadge, request) {
const type = match[1];
const format = match[2];
const prefix = query.prefix || '';
const suffix = query.suffix || '';
const pathExpression = query.query;
let requestOptions = {};
const badgeData = getBadgeData('custom badge', query);
if (!query.uri && !query.url || !query.query){
setBadgeColor(badgeData, 'red');
badgeData.text[1] = !query.query ? 'no query specified' : 'no url specified';
sendBadge(format, badgeData);
return;
}
let url
try {
url = encodeURI(decodeURIComponent(query.url || query.uri));
} catch(e){
setBadgeColor(badgeData, 'red');
badgeData.text[1] = 'malformed url';
sendBadge(format, badgeData);
return;
}
switch (type) {
case 'json':
requestOptions = {
headers: {
Accept: 'application/json',
},
json: true,
};
break;
case 'xml':
requestOptions = {
headers: {
Accept: 'application/xml, text/xml',
},
};
break;
case 'yaml':
requestOptions = {
headers: {
Accept: 'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain',
},
};
break;
}
request(url, requestOptions, (err, res, data) => {
try {
if (checkErrorResponse(badgeData, err, res, { 404: 'resource not found' })) {
return;
}
badgeData.colorscheme = 'brightgreen';
let innerText = [];
switch (type){
case 'json':
data = (typeof data === 'object' ? data : JSON.parse(data));
data = jp.query(data, pathExpression);
if (!data.length) {
throw Error('no result');
}
innerText = data;
break;
case 'xml':
data = new DOMParser().parseFromString(data);
data = xpath.select(pathExpression, data);
if (!data.length) {
throw Error('no result');
}
data.forEach((i,v)=>{
innerText.push(pathExpression.indexOf('@') + 1 ? i.value : i.firstChild.data);
});
break;
case 'yaml':
data = yaml.safeLoad(data);
data = jp.query(data, pathExpression);
if (!data.length) {
throw Error('no result');
}
innerText = data;
break;
}
badgeData.text[1] = (prefix || '') + innerText.join(', ') + (suffix || '');
} catch (e) {
setBadgeColor(badgeData, 'lightgrey');
badgeData.text[1] = e.message;
} finally {
sendBadge(format, badgeData);
}
});
},
}));
// Any badge.
camp.route(/^\/(:|badge\/)(([^-]|--)*?)-?(([^-]|--)*)-(([^-]|--)+)\.(svg|png|gif|jpg)$/,
(data, match, end, ask) => {
const subject = escapeFormat(match[2]);
const status = escapeFormat(match[4]);
const color = escapeFormat(match[6]);
const format = match[8];
analytics.noteRequest(data, match);
// Cache management - the badge is constant.
const cacheDuration = (3600*24*1)|0; // 1 day.
ask.res.setHeader('Cache-Control', 'max-age=' + cacheDuration);
if (+(new Date(ask.req.headers['if-modified-since'])) >= +serverStartTime) {
ask.res.statusCode = 304;
ask.res.end(); // not modified.
return;
}
ask.res.setHeader('Last-Modified', serverStartTime.toGMTString());
// Badge creation.
try {
const badgeData = getBadgeData(subject, data);
badgeData.text[0] = getLabel(subject, data);
badgeData.text[1] = status;
badgeData.colorB = makeColorB(color, data);
badgeData.template = data.style;
if (config.profiling.makeBadge) {
console.time('makeBadge total');
}
const svg = makeBadge(badgeData);
if (config.profiling.makeBadge) {
console.timeEnd('makeBadge total');
}
makeSend(format, ask.res, end)(svg);
} catch(e) {
log.error(e.stack);
const svg = makeBadge({ text: ['error', 'bad badge'], colorscheme: 'red' });
makeSend(format, ask.res, end)(svg);
}
});
// Production cache debugging.
let bitFlip = false;
camp.route(/^\/flip\.svg$/, (data, match, end, ask) => {
const cacheSecs = 60;
ask.res.setHeader('Cache-Control', 'max-age=' + cacheSecs);
const reqTime = new Date();
const date = (new Date(+reqTime + cacheSecs * 1000)).toGMTString();
ask.res.setHeader('Expires', date);
const badgeData = getBadgeData('flip', data);
bitFlip = !bitFlip;
badgeData.text[1] = bitFlip? 'on': 'off';
badgeData.colorscheme = bitFlip? 'brightgreen': 'red';
const svg = makeBadge(badgeData);
makeSend('svg', ask.res, end)(svg);
});
// Any badge, old version.
camp.route(/^\/([^/]+)\/(.+).png$/,
(data, match, end, ask) => {
const subject = match[1];
const status = match[2];
const color = data.color;
// Cache management - the badge is constant.
const cacheDuration = (3600*24*1)|0; // 1 day.
ask.res.setHeader('Cache-Control', 'max-age=' + cacheDuration);
if (+(new Date(ask.req.headers['if-modified-since'])) >= +serverStartTime) {
ask.res.statusCode = 304;
ask.res.end(); // not modified.
return;
}
ask.res.setHeader('Last-Modified', serverStartTime.toGMTString());
// Badge creation.
try {
const badgeData = { text: [subject, status] };
badgeData.colorscheme = color;
const svg = makeBadge(badgeData);
makeSend('png', ask.res, end)(svg);
} catch(e) {
const svg = makeBadge({ text: ['error', 'bad badge'], colorscheme: 'red' });
makeSend('png', ask.res, end)(svg);
}
});
if (config.redirectUri) {
camp.route(/^\/$/, (data, match, end, ask) => {
ask.res.statusCode = 302;
ask.res.setHeader('Location', config.redirectUri);
ask.res.end();
});
}