Skip to content
Snippets Groups Projects
Select Git revision
  • 549b12529fa9edca7f9bd73d4695c84dd23c8c37
  • main default protected
  • renovate/main-ghcr.io-renovatebot-base-image-10.x
  • renovate/main-ghcr.io-containerbase-devcontainer-13.x
  • next
  • revert-31645-feat/rename-gradle-wrapper-validation-action
  • renovate/main-redis-5.x
  • fix/36615b-branch-reuse-no-cache
  • chore/punycode
  • fix/36615-branch-reuse-bug
  • refactor/pin-new-value
  • feat/36219--git-x509-signing
  • feat/structured-logger
  • hotfix/39.264.1
  • feat/skip-dangling
  • gh-readonly-queue/next/pr-36034-7a061c4ca1024a19e2c295d773d9642625d1c2be
  • hotfix/39.238.3
  • refactor/gitlab-auto-approve
  • feat/template-strings
  • gh-readonly-queue/next/pr-35654-137d934242c784e0c45d4b957362214f0eade1d7
  • fix/32307-global-extends-merging
  • 41.31.1
  • 41.31.0
  • 41.30.5
  • 41.30.4
  • 41.30.3
  • 41.30.2
  • 41.30.1
  • 41.30.0
  • 41.29.1
  • 41.29.0
  • 41.28.2
  • 41.28.1
  • 41.28.0
  • 41.27.1
  • 41.27.0
  • 41.26.2
  • 41.26.1
  • 41.26.0
  • 41.25.1
  • 41.25.0
41 results

manager-npm.js

Blame
  • core_server_server.js.html 20.21 KiB
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <title>JSDoc: Source: core/server/server.js</title>
    
        <script src="scripts/prettify/prettify.js"> </script>
        <script src="scripts/prettify/lang-css.js"> </script>
        <!--[if lt IE 9]>
          <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
        <![endif]-->
        <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
        <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
    </head>
    
    <body>
    
    <div id="main">
    
        <h1 class="page-title">Source: core/server/server.js</h1>
    
        
    
    
    
        
        <section>
            <article>
                <pre class="prettyprint source linenums"><code>'use strict'
    /**
     * @module
     */
    
    const path = require('path')
    const url = require('url')
    const { URL } = url
    const cloudflareMiddleware = require('cloudflare-middleware')
    const bytes = require('bytes')
    const Camp = require('@shields_io/camp')
    const originalJoi = require('joi')
    const makeBadge = require('../../badge-maker/lib/make-badge')
    const GithubConstellation = require('../../services/github/github-constellation')
    const suggest = require('../../services/suggest')
    const { loadServiceClasses } = require('../base-service/loader')
    const { makeSend } = require('../base-service/legacy-result-sender')
    const { handleRequest } = require('../base-service/legacy-request-handler')
    const { clearRegularUpdateCache } = require('../legacy/regular-update')
    const { rasterRedirectUrl } = require('../badge-urls/make-badge-url')
    const log = require('./log')
    const sysMonitor = require('./monitor')
    const PrometheusMetrics = require('./prometheus-metrics')
    const InfluxMetrics = require('./influx-metrics')
    
    const Joi = originalJoi
      .extend(base => ({
        type: 'arrayFromString',
        base: base.array(),
        coerce: (value, state, options) => ({
          value: typeof value === 'string' ? value.split(' ') : value,
        }),
      }))
      .extend(base => ({
        type: 'string',
        base: base.string(),
        messages: {
          'string.origin':
            'needs to be an origin string, e.g. https://host.domain with optional port and no trailing slash',
        },
        rules: {
          origin: {
            validate(value, helpers) {
              let origin
              try {
                ;({ origin } = new URL(value))
              } catch (e) {}
              if (origin !== undefined &amp;&amp; origin === value) {
                return value
              } else {
                return helpers.error('string.origin')
              }
            },
          },
        },
      }))
    
    const optionalUrl = Joi.string().uri({ scheme: ['http', 'https'] })
    const requiredUrl = optionalUrl.required()
    const origins = Joi.arrayFromString().items(Joi.string().origin())
    const defaultService = Joi.object({ authorizedOrigins: origins }).default({
      authorizedOrigins: [],
    })
    
    const publicConfigSchema = Joi.object({
      bind: {
        port: Joi.alternatives().try(
          Joi.number().port(),
          Joi.string().pattern(/^\\\\\.\\pipe\\.+$/)
        ),
        address: Joi.alternatives().try(
          Joi.string().ip().required(),
          Joi.string().hostname().required()
        ),
      },
      metrics: {
        prometheus: {
          enabled: Joi.boolean().required(),
          endpointEnabled: Joi.boolean().required(),
        },
        influx: {
          enabled: Joi.boolean().required(),
          url: Joi.string()
            .uri()
            .when('enabled', { is: true, then: Joi.required() }),
          timeoutMilliseconds: Joi.number()
            .integer()
            .min(1)
            .when('enabled', { is: true, then: Joi.required() }),
          intervalSeconds: Joi.number().integer().min(1).when('enabled', {
            is: true,
            then: Joi.required(),
          }),
          instanceIdFrom: Joi.string()
            .equal('hostname', 'env-var', 'random')
            .when('enabled', { is: true, then: Joi.required() }),
          instanceIdEnvVarName: Joi.string().when('instanceIdFrom', {
            is: 'env-var',
            then: Joi.required(),
          }),
          envLabel: Joi.string().when('enabled', {
            is: true,
            then: Joi.required(),
          }),
          hostnameAliases: Joi.object(),
        },
      },
      ssl: {
        isSecure: Joi.boolean().required(),
        key: Joi.string(),
        cert: Joi.string(),
      },
      redirectUrl: optionalUrl,
      rasterUrl: optionalUrl,
      cors: {
        allowedOrigin: Joi.array().items(optionalUrl).required(),
      },
      services: Joi.object({
        bitbucketServer: defaultService,
        drone: defaultService,
        github: {
          baseUri: requiredUrl,
          debug: {
            enabled: Joi.boolean().required(),
            intervalSeconds: Joi.number().integer().min(1).required(),
          },
        },
        jira: defaultService,
        jenkins: Joi.object({
          authorizedOrigins: origins,
          requireStrictSsl: Joi.boolean(),
          requireStrictSslToAuthenticate: Joi.boolean(),
        }).default({ authorizedOrigins: [] }),
        nexus: defaultService,
        npm: defaultService,
        sonar: defaultService,
        teamcity: defaultService,
        trace: Joi.boolean().required(),
      }).required(),
      cacheHeaders: {
        defaultCacheLengthSeconds: Joi.number().integer().required(),
      },
      rateLimit: Joi.boolean().required(),
      handleInternalErrors: Joi.boolean().required(),
      fetchLimit: Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i),
      documentRoot: Joi.string().default(
        path.resolve(__dirname, '..', '..', 'public')
      ),
      requireCloudflare: Joi.boolean().required(),
    }).required()
    
    const privateConfigSchema = Joi.object({
      azure_devops_token: Joi.string(),
      bintray_user: Joi.string(),
      bintray_apikey: Joi.string(),
      discord_bot_token: Joi.string(),
      drone_token: Joi.string(),
      gh_client_id: Joi.string(),
      gh_client_secret: Joi.string(),
      gh_token: Joi.string(),
      jenkins_user: Joi.string(),
      jenkins_pass: Joi.string(),
      jira_user: Joi.string(),
      jira_pass: Joi.string(),
      bitbucket_server_username: Joi.string(),
      bitbucket_server_password: Joi.string(),
      nexus_user: Joi.string(),
      nexus_pass: Joi.string(),
      npm_token: Joi.string(),
      redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
      sentry_dsn: Joi.string(),
      shields_secret: Joi.string(),
      sl_insight_userUuid: Joi.string(),
      sl_insight_apiToken: Joi.string(),
      sonarqube_token: Joi.string(),
      teamcity_user: Joi.string(),
      teamcity_pass: Joi.string(),
      twitch_client_id: Joi.string(),
      twitch_client_secret: Joi.string(),
      wheelmap_token: Joi.string(),
      influx_username: Joi.string(),
      influx_password: Joi.string(),
      youtube_api_key: Joi.string(),
    }).required()
    const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
      influx_username: Joi.string().required(),
      influx_password: Joi.string().required(),
    })
    
    function addHandlerAtIndex(camp, index, handlerFn) {
      camp.stack.splice(index, 0, handlerFn)
    }
    
    /**
     * The Server is based on the web framework Scoutcamp. It creates
     * an http server, sets up helpers for token persistence and monitoring.
     * Then it loads all the services, injecting dependencies as it
     * asks each one to register its route with Scoutcamp.
     */
    class Server {
      /**
       * Badge Server Constructor
       *
       * @param {object} config Configuration object read from config yaml files
       * by https://www.npmjs.com/package/config and validated against
       * publicConfigSchema and privateConfigSchema
       * @see https://github.com/badges/shields/blob/master/doc/production-hosting.md#configuration
       * @see https://github.com/badges/shields/blob/master/doc/server-secrets.md
       */
      constructor(config) {
        const publicConfig = Joi.attempt(config.public, publicConfigSchema)
        const privateConfig = this.validatePrivateConfig(
          config.private,
          privateConfigSchema
        )
        // We want to require an username and a password for the influx metrics
        // only if the influx metrics are enabled. The private config schema
        // and the public config schema are two separate schemas so we have to run
        // validation manually.
        if (publicConfig.metrics.influx &amp;&amp; publicConfig.metrics.influx.enabled) {
          this.validatePrivateConfig(
            config.private,
            privateMetricsInfluxConfigSchema
          )
        }
        this.config = {
          public: publicConfig,
          private: privateConfig,
        }
    
        this.githubConstellation = new GithubConstellation({
          service: publicConfig.services.github,
          private: privateConfig,
        })
    
        if (publicConfig.metrics.prometheus.enabled) {
          this.metricInstance = new PrometheusMetrics()
          if (publicConfig.metrics.influx.enabled) {
            this.influxMetrics = new InfluxMetrics(
              this.metricInstance,
              Object.assign({}, publicConfig.metrics.influx, {
                username: privateConfig.influx_username,
                password: privateConfig.influx_password,
              })
            )
          }
        }
      }
    
      validatePrivateConfig(privateConfig, privateConfigSchema) {
        try {
          return Joi.attempt(privateConfig, privateConfigSchema)
        } catch (e) {
          const badPaths = e.details.map(({ path }) => path)
          throw Error(
            `Private configuration is invalid. Check these paths: ${badPaths.join(
              ','
            )}`
          )
        }
      }
    
      get port() {
        const {
          port,
          ssl: { isSecure },
        } = this.config.public
        return port || (isSecure ? 443 : 80)
      }
    
      get baseUrl() {
        const {
          bind: { address, port },
          ssl: { isSecure },
        } = this.config.public
    
        return url.format({
          protocol: isSecure ? 'https' : 'http',
          hostname: address,
          port,
          pathname: '/',
        })
      }
    
      // See https://www.viget.com/articles/heroku-cloudflare-the-right-way/
      requireCloudflare() {
        // Set `req.ip`, which is expected by `cloudflareMiddleware()`. This is set
        // by Express but not Scoutcamp.
        addHandlerAtIndex(this.camp, 0, function (req, res, next) {
          // On Heroku, `req.socket.remoteAddress` is the Heroku router. However,
          // the router ensures that the last item in the `X-Forwarded-For` header
          // is the real origin.
          // https://stackoverflow.com/a/18517550/893113
          req.ip = process.env.DYNO
            ? req.headers['x-forwarded-for'].split(', ').pop()
            : req.socket.remoteAddress
          next()
        })
        addHandlerAtIndex(this.camp, 1, cloudflareMiddleware())
      }
    
      /**
       * Set up Scoutcamp routes for 404/not found responses
       */
      registerErrorHandlers() {
        const { camp, config } = this
        const {
          public: { rasterUrl },
        } = config
    
        camp.route(/\.(gif|jpg)$/, (query, match, end, request) => {
          const [, format] = match
          makeSend(
            'svg',
            request.res,
            end
          )(
            makeBadge({
              label: '410',
              message: `${format} no longer available`,
              color: 'lightgray',
              format: 'svg',
            })
          )
        })
    
        if (!rasterUrl) {
          camp.route(/\.png$/, (query, match, end, request) => {
            makeSend(
              'svg',
              request.res,
              end
            )(
              makeBadge({
                label: '404',
                message: 'raster badges not available',
                color: 'lightgray',
                format: 'svg',
              })
            )
          })
        }
    
        camp.notfound(/(\.svg|\.json|)$/, (query, match, end, request) => {
          const [, extension] = match
          const format = (extension || '.svg').replace(/^\./, '')
    
          makeSend(
            format,
            request.res,
            end
          )(
            makeBadge({
              label: '404',
              message: 'badge not found',
              color: 'red',
              format,
            })
          )
        })
      }
    
      /**
       * Set up a couple of redirects:
       * One for the raster badges.
       * Another to redirect the base URL /
       * (we use this to redirect {@link https://img.shields.io/}
       * to {@link https://shields.io/} )
       */
      registerRedirects() {
        const { config, camp } = this
        const {
          public: { rasterUrl, redirectUrl },
        } = config
    
        if (rasterUrl) {
          // Redirect to the raster server for raster versions of modern badges.
          camp.route(/\.png$/, (queryParams, match, end, ask) => {
            ask.res.statusCode = 301
            ask.res.setHeader(
              'Location',
              rasterRedirectUrl({ rasterUrl }, ask.req.url)
            )
    
            const cacheDuration = (30 * 24 * 3600) | 0 // 30 days.
            ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
    
            ask.res.end()
          })
        }
    
        if (redirectUrl) {
          camp.route(/^\/$/, (data, match, end, ask) => {
            ask.res.statusCode = 302
            ask.res.setHeader('Location', redirectUrl)
            ask.res.end()
          })
        }
      }
    
      /**
       * Iterate all the service classes defined in /services,
       * load each service and register a Scoutcamp route for each service.
       */
      registerServices() {
        const { config, camp, metricInstance } = this
        const { apiProvider: githubApiProvider } = this.githubConstellation
    
        loadServiceClasses().forEach(serviceClass =>
          serviceClass.register(
            { camp, handleRequest, githubApiProvider, metricInstance },
            {
              handleInternalErrors: config.public.handleInternalErrors,
              cacheHeaders: config.public.cacheHeaders,
              fetchLimitBytes: bytes(config.public.fetchLimit),
              rasterUrl: config.public.rasterUrl,
              private: config.private,
              public: config.public,
            }
          )
        )
      }
    
      /**
       * Start the HTTP server:
       * Bootstrap Scoutcamp,
       * Register handlers,
       * Start listening for requests on this.baseUrl()
       */
      async start() {
        const {
          bind: { port, address: hostname },
          ssl: { isSecure: secure, cert, key },
          cors: { allowedOrigin },
          rateLimit,
          requireCloudflare,
        } = this.config.public
    
        log(`Server is starting up: ${this.baseUrl}`)
    
        const camp = (this.camp = Camp.create({
          documentRoot: this.config.public.documentRoot,
          port,
          hostname,
          secure,
          staticMaxAge: 300,
          cert,
          key,
        }))
    
        if (requireCloudflare) {
          this.requireCloudflare()
        }
    
        const { metricInstance } = this
        this.cleanupMonitor = sysMonitor.setRoutes(
          { rateLimit },
          { server: camp, metricInstance }
        )
    
        const { githubConstellation } = this
        await githubConstellation.initialize(camp)
        if (metricInstance) {
          if (this.config.public.metrics.prometheus.endpointEnabled) {
            metricInstance.registerMetricsEndpoint(camp)
          }
          if (this.influxMetrics) {
            this.influxMetrics.startPushingMetrics()
          }
        }
    
        const { apiProvider: githubApiProvider } = this.githubConstellation
        suggest.setRoutes(allowedOrigin, githubApiProvider, camp)
    
        this.registerErrorHandlers()
        this.registerRedirects()
        this.registerServices()
    
        camp.listenAsConfigured()
    
        await new Promise(resolve => camp.on('listening', () => resolve()))
      }
    
      static resetGlobalState() {
        // This state should be migrated to instance state. When possible, do not add new
        // global state.
        clearRegularUpdateCache()
      }
    
      reset() {
        this.constructor.resetGlobalState()
      }
    
      /**
       * Stop the HTTP server and clean up helpers
       */
      async stop() {
        if (this.camp) {
          await new Promise(resolve => this.camp.close(resolve))
          this.camp = undefined
        }
    
        if (this.cleanupMonitor) {
          this.cleanupMonitor()
          this.cleanupMonitor = undefined
        }
    
        if (this.githubConstellation) {
          await this.githubConstellation.stop()
          this.githubConstellation = undefined
        }
    
        if (this.metricInstance) {
          if (this.influxMetrics) {
            this.influxMetrics.stopPushingMetrics()
          }
          this.metricInstance.stop()
        }
      }
    }
    
    module.exports = Server
    </code></pre>
            </article>
        </section>
    
    
    
    
    </div>
    
    <nav>
        <h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-badge-maker.html">badge-maker</a></li><li><a href="module-core_base-service_base.html">core/base-service/base</a></li><li><a href="module-core_base-service_base-graphql.html">core/base-service/base-graphql</a></li><li><a href="module-core_base-service_base-json.html">core/base-service/base-json</a></li><li><a href="module-core_base-service_base-svg-scraping.html">core/base-service/base-svg-scraping</a></li><li><a href="module-core_base-service_base-xml.html">core/base-service/base-xml</a></li><li><a href="module-core_base-service_base-yaml.html">core/base-service/base-yaml</a></li><li><a href="module-core_base-service_errors.html">core/base-service/errors</a></li><li><a href="module-core_base-service_graphql.html">core/base-service/graphql</a></li><li><a href="module-core_server_server.html">core/server/server</a></li><li><a href="module-core_service-test-runner_create-service-tester.html">core/service-test-runner/create-service-tester</a></li><li><a href="module-core_service-test-runner_icedfrisby-shields.html">core/service-test-runner/icedfrisby-shields</a></li><li><a href="module-core_service-test-runner_infer-pull-request.html">core/service-test-runner/infer-pull-request</a></li><li><a href="module-core_service-test-runner_runner.html">core/service-test-runner/runner</a></li><li><a href="module-core_service-test-runner_service-tester.html">core/service-test-runner/service-tester</a></li><li><a href="module-core_service-test-runner_services-for-title.html">core/service-test-runner/services-for-title</a></li><li><a href="module-core_token-pooling_token-pool.html">core/token-pooling/token-pool</a></li><li><a href="module-services_dynamic_json-path.html">services/dynamic/json-path</a></li><li><a href="module-services_steam_steam-base.html">services/steam/steam-base</a></li></ul><h3>Classes</h3><ul><li><a href="module.exports.html">exports</a></li><li><a href="module-core_base-service_base-graphql-BaseGraphqlService.html">BaseGraphqlService</a></li><li><a href="module-core_base-service_base-json-BaseJsonService.html">BaseJsonService</a></li><li><a href="module-core_base-service_base-svg-scraping-BaseSvgScrapingService.html">BaseSvgScrapingService</a></li><li><a href="module-core_base-service_base-xml-BaseXmlService.html">BaseXmlService</a></li><li><a href="module-core_base-service_base-yaml-BaseYamlService.html">BaseYamlService</a></li><li><a href="module-core_base-service_base-BaseService.html">BaseService</a></li><li><a href="module-core_base-service_errors-Deprecated.html">Deprecated</a></li><li><a href="module-core_base-service_errors-ImproperlyConfigured.html">ImproperlyConfigured</a></li><li><a href="module-core_base-service_errors-Inaccessible.html">Inaccessible</a></li><li><a href="module-core_base-service_errors-InvalidParameter.html">InvalidParameter</a></li><li><a href="module-core_base-service_errors-InvalidResponse.html">InvalidResponse</a></li><li><a href="module-core_base-service_errors-NotFound.html">NotFound</a></li><li><a href="module-core_base-service_errors-ShieldsRuntimeError.html">ShieldsRuntimeError</a></li><li><a href="module-core_server_server-Server.html">Server</a></li><li><a href="module-core_service-test-runner_runner-Runner.html">Runner</a></li><li><a href="module-core_service-test-runner_service-tester-ServiceTester.html">ServiceTester</a></li><li><a href="module-core_token-pooling_token-pool-Token.html">Token</a></li><li><a href="module-core_token-pooling_token-pool-TokenPool.html">TokenPool</a></li><li><a href="module-services_steam_steam-base-BaseSteamAPI.html">BaseSteamAPI</a></li></ul><h3>Tutorials</h3><ul><li><a href="tutorial-TUTORIAL.html">TUTORIAL</a></li><li><a href="tutorial-badge-urls.html">badge-urls</a></li><li><a href="tutorial-code-walkthrough.html">code-walkthrough</a></li><li><a href="tutorial-deprecating-badges.html">deprecating-badges</a></li><li><a href="tutorial-input-validation.html">input-validation</a></li><li><a href="tutorial-json-format.html">json-format</a></li><li><a href="tutorial-logos.html">logos</a></li><li><a href="tutorial-performance-testing.html">performance-testing</a></li><li><a href="tutorial-production-hosting.html">production-hosting</a></li><li><a href="tutorial-rewriting-services.html">rewriting-services</a></li><li><a href="tutorial-self-hosting.html">self-hosting</a></li><li><a href="tutorial-server-secrets.html">server-secrets</a></li><li><a href="tutorial-service-tests.html">service-tests</a></li><li><a href="tutorial-users.html">users</a></li></ul><h3>Global</h3><ul><li><a href="global.html#validateAffiliations">validateAffiliations</a></li></ul>
    </nav>
    
    <br class="clear">
    
    <footer>
        Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.6</a> on Mon Mar 01 2021 19:22:42 GMT+0000 (Coordinated Universal Time)
    </footer>
    
    <script> prettyPrint(); </script>
    <script src="scripts/linenumber.js"> </script>
    </body>
    </html>