Skip to content
Snippets Groups Projects
Select Git revision
  • 51897b3c7ecbf0deaccd50c5c33c3a41ee6a8800
  • master default protected
  • gh-pages
  • dependabot/npm_and_yarn/neostandard-0.12.2
  • dependabot/npm_and_yarn/eslint-plugin-prettier-5.5.1
  • dependabot/npm_and_yarn/eslint-9.30.1
  • dependabot/npm_and_yarn/cypress-14.5.1
  • dependabot/npm_and_yarn/eslint-plugin-import-2.32.0
  • dependabot/npm_and_yarn/eslint-plugin-jsdoc-51.3.4
  • dependabot/npm_and_yarn/mocha-11.7.1
  • dependabot/npm_and_yarn/typescript-eslint/parser-8.36.0
  • dependabot/npm_and_yarn/nock-14.0.5
  • dependabot/npm_and_yarn/react-19.1.0
  • dependabot/npm_and_yarn/react-dom-19.1.0
  • server-2025-02-01-6100669a
  • server-2024-11-01-87cba042
  • server-2024-10-01-6875b7c8
  • dependabot/npm_and_yarn/path-to-regexp-8.2.0
  • server-2024-09-01-3d52575c
  • daily-tests-gha2
  • daily-tests-gha
  • server-2025-07-01
  • 5.0.2
  • 5.0.1
  • 5.0.0
  • server-2025-06-01
  • server-2025-05-01
  • server-2025-04-03
  • server-2025-03-02
  • server-2025-03-01
  • server-2025-02-02
  • server-2025-01-01
  • server-2024-12-01
  • server-2024-11-02
  • 4.1.0
  • server-2024-09-25
  • server-2024-09-02
  • server-2024-08-01
  • server-2024-07-01
  • 4.0.0
  • server-2024-06-01
41 results

server.js

Blame
  • user avatar
    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
    51897b3c
    History
    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()
      })
    }