Select Git revision
Paul Melnikow
authored and
GitHub
committed
This simplifies and further optimizes text-width computation by computing the entire width table in advance, and serializing it in the style of QuickTextMeasurer (#1390). This entirely removes the need for PDFKit at runtime. This has the advantage of fixing #1305 – more generally: producing the same result everywhere – without having to deploy a copy of Verdana. The lifting is delegated to these three libraries, which are housed in a monorepo: https://github.com/metabolize/anafanafo I'd be happy to move it into the badges org if folks want to collaborate on maintaining them. QuickTextMeasurer took kerning pairs into account, whereas this implementation does not. I was thinking kerning would be a necessary refinement, though this seems to work well enough. I dropped in a binary-search package to traverse the data structure, in part to conserve space. This causes a moderate performance regression, though there is ample room for improving on that: https://github.com/badges/shields/pull/2311#issuecomment-439182704
server.js 9.30 KiB
'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 {
makeColorB,
makeLabel: getLabel,
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 { 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 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 }
)
)
// 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()
})
}