'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(undefined, { label: subject }) 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() }) }