Files
shields/server.js
Paul Melnikow 065dd570ad Move [StaticBadge] to own service & add test; also affects [gitter] (#2284)
This picks up @RedSparr0w's work in #1802.

1. The handler for the static badge is moved into its own service with a synchronous handler. Avoiding an async call may make the static badges slightly faster, though it may be worth profiling this if it turns out we want asynchronous static badges in the future. If it doesn't make a performance difference we could make this handler `async` like the others.
2. Most of the custom static-badge logic is in a BaseStaticBadge base class.
3. Rewrite the static Gitter badge to use BaseStaticBadge.
4. A bit of minor cleanup in related functions.
2018-11-16 19:21:48 -05:00

281 lines
7.8 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 PrometheusMetrics = require('./lib/sys/prometheus-metrics')
const sysMonitor = require('./lib/sys/monitor')
const log = require('./lib/log')
const makeBadge = require('./gh-badges/lib/make-badge')
const suggest = require('./lib/suggest')
const {
makeBadgeData: getBadgeData,
setBadgeColor,
} = require('./lib/badge-data')
const {
handleRequest: cache,
clearRequestCache,
} = require('./lib/request-handler')
const { clearRegularUpdateCache } = require('./lib/regular-update')
const { makeSend } = require('./lib/result-sender')
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 metrics = new PrometheusMetrics(config.metrics.prometheus)
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}`)
analytics.load()
analytics.scheduleAutosaving()
analytics.setRoutes(camp)
if (serverSecrets && serverSecrets.shieldsSecret) {
sysMonitor.setRoutes(camp)
}
githubConstellation.initialize(camp)
metrics.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,
profiling: config.profiling,
}
)
)
// 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)
}
})
},
})
)
// 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()
})
}