Now that server.js is emptied out, it makes sense to eliminate the differences between the top-level .js files and everything else.
318 lines
9.3 KiB
JavaScript
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();
|
|
});
|
|
}
|