|  | #!/usr/bin/env node | 
|  | // shebang interferes with license header plugin | 
|  | /* eslint-disable header/header */ | 
|  |  | 
|  | // Copyright (c) 2012, Compiler Explorer Authors | 
|  | // All rights reserved. | 
|  | // | 
|  | // Redistribution and use in source and binary forms, with or without | 
|  | // modification, are permitted provided that the following conditions are met: | 
|  | // | 
|  | //     * Redistributions of source code must retain the above copyright notice, | 
|  | //       this list of conditions and the following disclaimer. | 
|  | //     * Redistributions in binary form must reproduce the above copyright | 
|  | //       notice, this list of conditions and the following disclaimer in the | 
|  | //       documentation and/or other materials provided with the distribution. | 
|  | // | 
|  | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | 
|  | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | 
|  | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | 
|  | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | 
|  | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | 
|  | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | 
|  | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | 
|  | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | 
|  | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | 
|  | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | 
|  | // POSSIBILITY OF SUCH DAMAGE. | 
|  |  | 
|  | import child_process from 'child_process'; | 
|  | import os from 'os'; | 
|  | import path from 'path'; | 
|  | import process from 'process'; | 
|  | import url from 'url'; | 
|  |  | 
|  | import * as Sentry from '@sentry/node'; | 
|  | import * as Tracing from '@sentry/tracing'; | 
|  | import bodyParser from 'body-parser'; | 
|  | import compression from 'compression'; | 
|  | import express from 'express'; | 
|  | import fs from 'fs-extra'; | 
|  | import morgan from 'morgan'; | 
|  | import nopt from 'nopt'; | 
|  | import PromClient from 'prom-client'; | 
|  | import responseTime from 'response-time'; | 
|  | import sFavicon from 'serve-favicon'; | 
|  | import systemdSocket from 'systemd-socket'; | 
|  | import _ from 'underscore'; | 
|  | import urljoin from 'url-join'; | 
|  |  | 
|  | import * as aws from './lib/aws'; | 
|  | import * as normalizer from './lib/clientstate-normalizer'; | 
|  | import { CompilationEnvironment } from './lib/compilation-env'; | 
|  | import { CompilationQueue } from './lib/compilation-queue'; | 
|  | import { CompilerFinder } from './lib/compiler-finder'; | 
|  | import { policy as csp } from './lib/csp'; | 
|  | import { initialiseWine } from './lib/exec'; | 
|  | import { ShortLinkResolver } from './lib/google'; | 
|  | import { CompileHandler } from './lib/handlers/compile'; | 
|  | import * as healthCheck from './lib/handlers/health-check'; | 
|  | import { NoScriptHandler } from './lib/handlers/noscript'; | 
|  | import { RouteAPI } from './lib/handlers/route-api'; | 
|  | import { SourceHandler } from './lib/handlers/source'; | 
|  | import { languages as allLanguages } from './lib/languages'; | 
|  | import { logger, logToLoki, logToPapertrail, suppressConsoleLog } from './lib/logger'; | 
|  | import { ClientOptionsHandler } from './lib/options-handler'; | 
|  | import * as props from './lib/properties'; | 
|  | import { sources } from './lib/sources'; | 
|  | import { loadSponsorsFromString } from './lib/sponsors'; | 
|  | import { getStorageTypeByKey } from './lib/storage'; | 
|  | import * as utils from './lib/utils'; | 
|  |  | 
|  | // Parse arguments from command line 'node ./app.js args...' | 
|  | const opts = nopt({ | 
|  | env: [String, Array], | 
|  | rootDir: [String], | 
|  | host: [String], | 
|  | port: [String, Number], | 
|  | propDebug: [Boolean], | 
|  | debug: [Boolean], | 
|  | dist: [Boolean], | 
|  | archivedVersions: [String], | 
|  | // Ignore fetch marks and assume every compiler is found locally | 
|  | noRemoteFetch: [Boolean], | 
|  | tmpDir: [String], | 
|  | wsl: [Boolean], | 
|  | // If specified, only loads the specified language, resulting in faster loadup/iteration times | 
|  | language: [String], | 
|  | // Do not use caching for compilation results (Requests might still be cached by the client's browser) | 
|  | noCache: [Boolean], | 
|  | // Don't cleanly run if two or more compilers have clashing ids | 
|  | ensureNoIdClash: [Boolean], | 
|  | logHost: [String], | 
|  | logPort: [Number], | 
|  | suppressConsoleLog: [Boolean], | 
|  | metricsPort: [Number], | 
|  | loki: [String], | 
|  | }); | 
|  |  | 
|  | if (opts.debug) logger.level = 'debug'; | 
|  |  | 
|  | // AP: Detect if we're running under Windows Subsystem for Linux. Temporary modification | 
|  | // of process.env is allowed: https://nodejs.org/api/process.html#process_process_env | 
|  | if (process.platform === 'linux' && child_process.execSync('uname -a').toString().includes('Microsoft')) { | 
|  | process.env.wsl = true; | 
|  | } | 
|  |  | 
|  | // AP: Allow setting of tmpDir (used in lib/base-compiler.js & lib/exec.js) through opts. | 
|  | // WSL requires a directory on a Windows volume. Set that to Windows %TEMP% if no tmpDir supplied. | 
|  | // If a tempDir is supplied then assume that it will work for WSL processes as well. | 
|  | if (opts.tmpDir) { | 
|  | process.env.tmpDir = opts.tmpDir; | 
|  | process.env.winTmp = opts.tmpDir; | 
|  | } else if (process.env.wsl) { | 
|  | // Dec 2017 preview builds of WSL include /bin/wslpath; do the parsing work for now. | 
|  | // Parsing example %TEMP% is C:\Users\apardoe\AppData\Local\Temp | 
|  | const windowsTemp = child_process.execSync('cmd.exe /c echo %TEMP%').toString().replace(/\\/g, '/'); | 
|  | const driveLetter = windowsTemp.substring(0, 1).toLowerCase(); | 
|  | const directoryPath = windowsTemp.substring(2).trim(); | 
|  | process.env.winTmp = path.join('/mnt', driveLetter, directoryPath); | 
|  | } | 
|  |  | 
|  | const distPath = utils.resolvePathFromAppRoot('out', 'dist'); | 
|  |  | 
|  | const gitReleaseName = (() => { | 
|  | // Use the canned git_hash if provided | 
|  | const gitHashFilePath = path.join(distPath, 'git_hash'); | 
|  | if (opts.dist && fs.existsSync(gitHashFilePath)) { | 
|  | return fs.readFileSync(gitHashFilePath).toString().trim(); | 
|  | } | 
|  |  | 
|  | // Just if we have been cloned and not downloaded (Thanks David!) | 
|  | if (fs.existsSync('.git/')) { | 
|  | return child_process.execSync('git rev-parse HEAD').toString().trim(); | 
|  | } | 
|  |  | 
|  | // unknown case | 
|  | return ''; | 
|  | })(); | 
|  |  | 
|  | const releaseBuildNumber = (() => { | 
|  | // Use the canned build only if provided | 
|  | const releaseBuildPath = path.join(distPath, 'release_build'); | 
|  | if (opts.dist && fs.existsSync(releaseBuildPath)) { | 
|  | return fs.readFileSync(releaseBuildPath).toString().trim(); | 
|  | } | 
|  | return ''; | 
|  | })(); | 
|  |  | 
|  | // Set default values for omitted arguments | 
|  | const defArgs = { | 
|  | rootDir: opts.rootDir || './etc', | 
|  | env: opts.env || ['dev'], | 
|  | hostname: opts.host, | 
|  | port: opts.port || 10240, | 
|  | gitReleaseName: gitReleaseName, | 
|  | releaseBuildNumber: releaseBuildNumber, | 
|  | wantedLanguage: opts.language || null, | 
|  | doCache: !opts.noCache, | 
|  | fetchCompilersFromRemote: !opts.noRemoteFetch, | 
|  | ensureNoCompilerClash: opts.ensureNoIdClash, | 
|  | suppressConsoleLog: opts.suppressConsoleLog || false, | 
|  | }; | 
|  |  | 
|  | if (opts.logHost && opts.logPort) { | 
|  | logToPapertrail(opts.logHost, opts.logPort, defArgs.env.join('.')); | 
|  | } | 
|  |  | 
|  | if (opts.loki) { | 
|  | logToLoki(opts.loki); | 
|  | } | 
|  |  | 
|  | if (defArgs.suppressConsoleLog) { | 
|  | logger.info('Disabling further console logging'); | 
|  | suppressConsoleLog(); | 
|  | } | 
|  |  | 
|  | const isDevMode = () => process.env.NODE_ENV !== 'production'; | 
|  |  | 
|  | const propHierarchy = _.flatten([ | 
|  | 'defaults', | 
|  | defArgs.env, | 
|  | _.map(defArgs.env, e => `${e}.${process.platform}`), | 
|  | process.platform, | 
|  | os.hostname(), | 
|  | 'local']); | 
|  | logger.info(`properties hierarchy: ${propHierarchy.join(', ')}`); | 
|  |  | 
|  | // Propagate debug mode if need be | 
|  | if (opts.propDebug) props.setDebug(true); | 
|  |  | 
|  | // *All* files in config dir are parsed | 
|  | const configDir = path.join(defArgs.rootDir, 'config'); | 
|  | props.initialize(configDir, propHierarchy); | 
|  | // Instantiate a function to access records concerning "compiler-explorer" | 
|  | // in hidden object props.properties | 
|  | const ceProps = props.propsFor('compiler-explorer'); | 
|  |  | 
|  | let languages = allLanguages; | 
|  | if (defArgs.wantedLanguage) { | 
|  | const filteredLangs = {}; | 
|  | _.each(languages, lang => { | 
|  | if (lang.id === defArgs.wantedLanguage || | 
|  | lang.name === defArgs.wantedLanguage || | 
|  | (lang.alias && lang.alias.includes(defArgs.wantedLanguage))) { | 
|  | filteredLangs[lang.id] = lang; | 
|  | } | 
|  | }); | 
|  | languages = filteredLangs; | 
|  | } | 
|  |  | 
|  | if (languages.length === 0) { | 
|  | logger.error('Trying to start Compiler Explorer without a language'); | 
|  | } | 
|  |  | 
|  | const compilerProps = new props.CompilerProps(languages, ceProps); | 
|  |  | 
|  | const staticMaxAgeSecs = ceProps('staticMaxAgeSecs', 0); | 
|  | const maxUploadSize = ceProps('maxUploadSize', '1mb'); | 
|  | const extraBodyClass = ceProps('extraBodyClass', isDevMode() ? 'dev' : ''); | 
|  | const storageSolution = compilerProps.ceProps('storageSolution', 'local'); | 
|  | const httpRoot = urljoin(ceProps('httpRoot', '/'), '/'); | 
|  |  | 
|  | const staticUrl = ceProps('staticUrl'); | 
|  | const staticRoot = urljoin(staticUrl || urljoin(httpRoot, 'static'), '/'); | 
|  |  | 
|  | function staticHeaders(res) { | 
|  | if (staticMaxAgeSecs) { | 
|  | res.setHeader('Cache-Control', 'public, max-age=' + staticMaxAgeSecs + ', must-revalidate'); | 
|  | } | 
|  | } | 
|  |  | 
|  | function contentPolicyHeader(res) { | 
|  | // TODO: re-enable CSP | 
|  | if (csp && 0) { | 
|  | res.setHeader('Content-Security-Policy', csp); | 
|  | } | 
|  | } | 
|  |  | 
|  | function measureEventLoopLag(delayMs) { | 
|  | return new Promise((resolve) => { | 
|  | const start = process.hrtime.bigint(); | 
|  | setTimeout(() => { | 
|  | const elapsed = process.hrtime.bigint() - start; | 
|  | const delta = elapsed - BigInt(delayMs * 1000000); | 
|  | return resolve(Number(delta) / 1000000); | 
|  | }, delayMs); | 
|  | }); | 
|  | } | 
|  |  | 
|  | function setupEventLoopLagLogging() { | 
|  | const lagIntervalMs = ceProps('eventLoopMeasureIntervalMs', 0); | 
|  | const thresWarn = ceProps('eventLoopLagThresholdWarn', 0); | 
|  | const thresErr = ceProps('eventLoopLagThresholdErr', 0); | 
|  |  | 
|  | let totalLag = 0; | 
|  | const ceLagSecondsTotalGauge = new PromClient.Gauge({ | 
|  | name: 'ce_lag_seconds_total', | 
|  | help: 'Total event loop lag since application startup', | 
|  | }); | 
|  |  | 
|  | async function eventLoopLagHandler() { | 
|  | const lagMs = await measureEventLoopLag(lagIntervalMs); | 
|  | totalLag += Math.max(lagMs / 1000, 0); | 
|  | ceLagSecondsTotalGauge.set(totalLag); | 
|  |  | 
|  | if (thresErr && lagMs >= thresErr) { | 
|  | logger.error(`Event Loop Lag: ${lagMs} ms`); | 
|  | } else if (thresWarn && lagMs >= thresWarn) { | 
|  | logger.warn(`Event Loop Lag: ${lagMs} ms`); | 
|  | } | 
|  |  | 
|  | setImmediate(eventLoopLagHandler); | 
|  | } | 
|  |  | 
|  | if (lagIntervalMs > 0) { | 
|  | setImmediate(eventLoopLagHandler); | 
|  | } | 
|  | } | 
|  |  | 
|  | let pugRequireHandler = () => { | 
|  | logger.error('pug require handler not configured'); | 
|  | }; | 
|  |  | 
|  | async function setupWebPackDevMiddleware(router) { | 
|  | logger.info('  using webpack dev middleware'); | 
|  |  | 
|  | /* eslint-disable node/no-unpublished-import */ | 
|  | const webpackDevMiddleware = (await import('webpack-dev-middleware')).default; | 
|  | const webpackConfig = (await import('./webpack.config.esm.js')).default; | 
|  | const webpack = (await import('webpack')).default; | 
|  | /* eslint-enable */ | 
|  |  | 
|  | const webpackCompiler = webpack(webpackConfig); | 
|  | router.use(webpackDevMiddleware(webpackCompiler, { | 
|  | publicPath: '/static', | 
|  | logger: logger, | 
|  | stats: 'errors-only', | 
|  | })); | 
|  |  | 
|  | pugRequireHandler = (path) => urljoin(httpRoot, 'static', path); | 
|  | } | 
|  |  | 
|  | async function setupStaticMiddleware(router) { | 
|  | const staticManifest = await fs.readJson(path.join(distPath, 'manifest.json')); | 
|  |  | 
|  | if (staticUrl) { | 
|  | logger.info(`  using static files from '${staticUrl}'`); | 
|  | } else { | 
|  | const staticPath = path.join(distPath, 'static'); | 
|  | logger.info(`  serving static files from '${staticPath}'`); | 
|  | router.use('/static', express.static(staticPath, { | 
|  | maxAge: staticMaxAgeSecs * 1000, | 
|  | })); | 
|  | } | 
|  |  | 
|  | pugRequireHandler = (path) => { | 
|  | if (Object.prototype.hasOwnProperty.call(staticManifest, path)) { | 
|  | return urljoin(staticRoot, staticManifest[path]); | 
|  | } else { | 
|  | logger.error(`failed to locate static asset '${path}' in manifest`); | 
|  | return ''; | 
|  | } | 
|  | }; | 
|  | } | 
|  |  | 
|  | function shouldRedactRequestData(data) { | 
|  | try { | 
|  | const parsed = JSON.parse(data); | 
|  | return !parsed['allowStoreCodeDebug']; | 
|  | } catch (e) { | 
|  | return true; | 
|  | } | 
|  | } | 
|  |  | 
|  | const googleShortUrlResolver = new ShortLinkResolver(); | 
|  |  | 
|  | function oldGoogleUrlHandler(req, res, next) { | 
|  | const id = req.params.id; | 
|  | const googleUrl = `https://goo.gl/${encodeURIComponent(id)}`; | 
|  | googleShortUrlResolver.resolve(googleUrl) | 
|  | .then(resultObj => { | 
|  | const parsed = new url.URL(resultObj.longUrl); | 
|  | const allowedRe = new RegExp(ceProps('allowedShortUrlHostRe')); | 
|  | if (parsed.host.match(allowedRe) === null) { | 
|  | logger.warn(`Denied access to short URL ${id} - linked to ${resultObj.longUrl}`); | 
|  | return next({ | 
|  | statusCode: 404, | 
|  | message: `ID "${id}" could not be found`, | 
|  | }); | 
|  | } | 
|  | res.writeHead(301, { | 
|  | Location: resultObj.longUrl, | 
|  | 'Cache-Control': 'public', | 
|  | }); | 
|  | res.end(); | 
|  | }) | 
|  | .catch(e => { | 
|  | logger.error(`Failed to expand ${googleUrl} - ${e}`); | 
|  | next({ | 
|  | statusCode: 404, | 
|  | message: `ID "${id}" could not be found`, | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | function startListening(server) { | 
|  | const ss = systemdSocket(); | 
|  | let _port; | 
|  | if (ss) { | 
|  | // ms (5 min default) | 
|  | const idleTimeout = process.env.IDLE_TIMEOUT; | 
|  | const timeout = (typeof idleTimeout !== 'undefined' ? idleTimeout : 300) * 1000; | 
|  | if (idleTimeout) { | 
|  | const exit = () => { | 
|  | logger.info('Inactivity timeout reached, exiting.'); | 
|  | process.exit(0); | 
|  | }; | 
|  | let idleTimer = setTimeout(exit, timeout); | 
|  | const reset = () => { | 
|  | clearTimeout(idleTimer); | 
|  | idleTimer = setTimeout(exit, timeout); | 
|  | }; | 
|  | server.all('*', reset); | 
|  | logger.info(`  IDLE_TIMEOUT: ${idleTimeout}`); | 
|  | } | 
|  | _port = ss; | 
|  | } else { | 
|  | _port = defArgs.port; | 
|  | } | 
|  |  | 
|  | const startupDurationMs = Math.floor(process.uptime() * 1000); | 
|  | if (isNaN(parseInt(_port))) { | 
|  | // unix socket, not a port number... | 
|  | logger.info(`  Listening on socket: //${_port}/`); | 
|  | logger.info(`  Startup duration: ${startupDurationMs}ms`); | 
|  | logger.info('======================================='); | 
|  | server.listen(_port); | 
|  | } | 
|  | else { | 
|  | // normal port number | 
|  | logger.info(`  Listening on http://${defArgs.hostname || 'localhost'}:${_port}/`); | 
|  | logger.info(`  Startup duration: ${startupDurationMs}ms`); | 
|  | logger.info('======================================='); | 
|  | server.listen(_port, defArgs.hostname); | 
|  | } | 
|  | } | 
|  |  | 
|  | function setupSentry(sentryDsn, expressApp) { | 
|  | if (!sentryDsn) { | 
|  | logger.info('Not configuring sentry'); | 
|  | return; | 
|  | } | 
|  | const sentryEnv = ceProps('sentryEnvironment'); | 
|  | Sentry.init({ | 
|  | dsn: sentryDsn, | 
|  | release: releaseBuildNumber || gitReleaseName, | 
|  | environment: sentryEnv || defArgs.env[0], | 
|  | beforeSend(event) { | 
|  | if (event.request | 
|  | && event.request.data | 
|  | && shouldRedactRequestData(event.request.data)) { | 
|  | event.request.data = JSON.stringify({redacted: true}); | 
|  | } | 
|  | return event; | 
|  | }, | 
|  | integrations: [ | 
|  | // enable HTTP calls tracing | 
|  | new Sentry.Integrations.Http({tracing: true}), | 
|  | // enable Express.js middleware tracing | 
|  | new Tracing.Integrations.Express({expressApp}), | 
|  | ], | 
|  | tracesSampler: samplingContext => { | 
|  | // always inherit | 
|  | if (samplingContext.parentSampled !== undefined) | 
|  | return samplingContext.parentSampled; | 
|  |  | 
|  | // never sample healthcheck | 
|  | if (samplingContext.transactionContext.name === 'GET /healthcheck') | 
|  | return 0; | 
|  |  | 
|  | // default sample rate of 10% | 
|  | return 0.1; | 
|  | }, | 
|  | }); | 
|  | logger.info(`Configured with Sentry endpoint ${sentryDsn}`); | 
|  | } | 
|  |  | 
|  | const awsProps = props.propsFor('aws'); | 
|  |  | 
|  | // eslint-disable-next-line max-statements | 
|  | async function main() { | 
|  | await aws.initConfig(awsProps); | 
|  | await initialiseWine(); | 
|  |  | 
|  | const clientOptionsHandler = new ClientOptionsHandler(sources, compilerProps, defArgs); | 
|  | const compilationQueue = CompilationQueue.fromProps(compilerProps.ceProps); | 
|  | const compilationEnvironment = new CompilationEnvironment(compilerProps, compilationQueue, defArgs.doCache); | 
|  | const compileHandler = new CompileHandler(compilationEnvironment, awsProps); | 
|  | const storageType = getStorageTypeByKey(storageSolution); | 
|  | const storageHandler = new storageType(httpRoot, compilerProps, awsProps); | 
|  | const sourceHandler = new SourceHandler(sources, staticHeaders); | 
|  | const compilerFinder = new CompilerFinder(compileHandler, compilerProps, awsProps, defArgs, clientOptionsHandler); | 
|  |  | 
|  | logger.info('======================================='); | 
|  | if (gitReleaseName) logger.info(`  git release ${gitReleaseName}`); | 
|  | if (releaseBuildNumber) logger.info(`  release build ${releaseBuildNumber}`); | 
|  |  | 
|  | const initialFindResults = await compilerFinder.find(); | 
|  | const initialCompilers = initialFindResults.compilers; | 
|  | let prevCompilers; | 
|  | if (defArgs.ensureNoCompilerClash) { | 
|  | logger.warn('Ensuring no compiler ids clash'); | 
|  | if (initialFindResults.foundClash) { | 
|  | // If we are forced to have no clashes, throw an error with some explanation | 
|  | throw new Error('Clashing compilers in the current environment found!'); | 
|  | } else { | 
|  | logger.info('No clashing ids found, continuing normally...'); | 
|  | } | 
|  | } | 
|  |  | 
|  | const webServer = express(), router = express.Router(); | 
|  | setupSentry(aws.getConfig('sentryDsn'), webServer); | 
|  | const healthCheckFilePath = ceProps('healthCheckFilePath', false); | 
|  |  | 
|  | const handlerConfig = { | 
|  | compileHandler, | 
|  | clientOptionsHandler, | 
|  | storageHandler, | 
|  | ceProps, | 
|  | opts, | 
|  | renderConfig, | 
|  | renderGoldenLayout, | 
|  | staticHeaders, | 
|  | contentPolicyHeader, | 
|  | }; | 
|  |  | 
|  | const noscriptHandler = new NoScriptHandler(router, handlerConfig); | 
|  | const routeApi = new RouteAPI(router, handlerConfig, noscriptHandler.renderNoScriptLayout); | 
|  |  | 
|  | function onCompilerChange(compilers) { | 
|  | if (JSON.stringify(prevCompilers) === JSON.stringify(compilers)) { | 
|  | return; | 
|  | } | 
|  | logger.info(`Compiler scan count: ${_.size(compilers)}`); | 
|  | logger.debug('Compilers:', compilers); | 
|  | if (compilers.length === 0) { | 
|  | logger.error('#### No compilers found: no compilation will be done!'); | 
|  | } | 
|  | prevCompilers = compilers; | 
|  | clientOptionsHandler.setCompilers(compilers); | 
|  | routeApi.apiHandler.setCompilers(compilers); | 
|  | routeApi.apiHandler.setLanguages(languages); | 
|  | routeApi.apiHandler.setOptions(clientOptionsHandler); | 
|  | } | 
|  |  | 
|  | onCompilerChange(initialCompilers); | 
|  |  | 
|  | const rescanCompilerSecs = ceProps('rescanCompilerSecs', 0); | 
|  | if (rescanCompilerSecs) { | 
|  | logger.info(`Rescanning compilers every ${rescanCompilerSecs} secs`); | 
|  | setInterval(() => compilerFinder.find().then(result => onCompilerChange(result.compilers)), | 
|  | rescanCompilerSecs * 1000); | 
|  | } | 
|  |  | 
|  | const sentrySlowRequestMs = ceProps('sentrySlowRequestMs', 0); | 
|  |  | 
|  | if (opts.metricsPort) { | 
|  | logger.info(`Running metrics server on port ${opts.metricsPort}`); | 
|  | PromClient.collectDefaultMetrics(); | 
|  | const metricsServer = express(); | 
|  |  | 
|  | metricsServer.get('/metrics', async (req, res) => { | 
|  | try { | 
|  | res.set('Content-Type', PromClient.register.contentType); | 
|  | res.end(await PromClient.register.metrics()); | 
|  | } catch (ex) { | 
|  | res.status(500).end(ex); | 
|  | } | 
|  | }); | 
|  |  | 
|  | metricsServer.listen(opts.metricsPort, defArgs.hostname); | 
|  | } | 
|  |  | 
|  | webServer | 
|  | .set('trust proxy', true) | 
|  | .set('view engine', 'pug') | 
|  | .on('error', err => logger.error('Caught error in web handler; continuing:', err)) | 
|  | // sentry request handler must be the first middleware on the app | 
|  | .use(Sentry.Handlers.requestHandler({ | 
|  | ip: true, | 
|  | })) | 
|  | .use(Sentry.Handlers.tracingHandler()) | 
|  | // eslint-disable-next-line no-unused-vars | 
|  | .use(responseTime((req, res, time) => { | 
|  | if (sentrySlowRequestMs > 0 && time >= sentrySlowRequestMs) { | 
|  | Sentry.withScope(scope => { | 
|  | scope.setExtra('duration_ms', time); | 
|  | Sentry.captureMessage('SlowRequest', 'warning'); | 
|  | }); | 
|  | } | 
|  | })) | 
|  | // Handle healthchecks at the root, as they're not expected from the outside world | 
|  | .use('/healthcheck', new healthCheck.HealthCheckHandler(compilationQueue, healthCheckFilePath).handle) | 
|  | .use(httpRoot, router) | 
|  | .use((req, res, next) => { | 
|  | next({status: 404, message: `page "${req.path}" could not be found`}); | 
|  | }) | 
|  | // sentry error handler must be the first error handling middleware | 
|  | .use(Sentry.Handlers.errorHandler) | 
|  | // eslint-disable-next-line no-unused-vars | 
|  | .use((err, req, res, next) => { | 
|  | const status = | 
|  | err.status || | 
|  | err.statusCode || | 
|  | err.status_code || | 
|  | (err.output && err.output.statusCode) || | 
|  | 500; | 
|  | const message = err.message || 'Internal Server Error'; | 
|  | res.status(status); | 
|  | res.render('error', renderConfig({error: {code: status, message: message}})); | 
|  | if (status >= 500) { | 
|  | logger.error('Internal server error:', err); | 
|  | } | 
|  | }); | 
|  |  | 
|  | const sponsorConfig = loadSponsorsFromString(fs.readFileSync(configDir + '/sponsors.yaml', 'utf-8')); | 
|  |  | 
|  | function renderConfig(extra, urlOptions) { | 
|  | const urlOptionsAllowed = [ | 
|  | 'readOnly', 'hideEditorToolbars', 'language', | 
|  | ]; | 
|  | const filteredUrlOptions = _.mapObject( | 
|  | _.pick(urlOptions, urlOptionsAllowed), | 
|  | val => utils.toProperty(val)); | 
|  | const allExtraOptions = _.extend({}, filteredUrlOptions, extra); | 
|  |  | 
|  | if (allExtraOptions.mobileViewer && allExtraOptions.config) { | 
|  | const clnormalizer = new normalizer.ClientStateNormalizer(); | 
|  | clnormalizer.fromGoldenLayout(allExtraOptions.config); | 
|  | const clientstate = clnormalizer.normalized; | 
|  |  | 
|  | const glnormalizer = new normalizer.ClientStateGoldenifier(); | 
|  | allExtraOptions.slides = glnormalizer.generatePresentationModeMobileViewerSlides(clientstate); | 
|  | } | 
|  |  | 
|  | const options = _.extend({}, allExtraOptions, clientOptionsHandler.get()); | 
|  | options.optionsHash = clientOptionsHandler.getHash(); | 
|  | options.compilerExplorerOptions = JSON.stringify(allExtraOptions); | 
|  | options.extraBodyClass = options.embedded ? 'embedded' : extraBodyClass; | 
|  | options.httpRoot = httpRoot; | 
|  | options.staticRoot = staticRoot; | 
|  | options.storageSolution = storageSolution; | 
|  | options.require = pugRequireHandler; | 
|  | options.sponsors = sponsorConfig; | 
|  | return options; | 
|  | } | 
|  |  | 
|  | function isMobileViewer(req) { | 
|  | return req.header('CloudFront-Is-Mobile-Viewer') === 'true'; | 
|  | } | 
|  |  | 
|  | function renderGoldenLayout(config, metadata, req, res) { | 
|  | staticHeaders(res); | 
|  | contentPolicyHeader(res); | 
|  |  | 
|  | res.render('index', renderConfig({ | 
|  | embedded: false, | 
|  | mobileViewer: isMobileViewer(req), | 
|  | config: config, | 
|  | metadata: metadata, | 
|  | storedStateId: req.params.id ? req.params.id : false, | 
|  | }, req.query)); | 
|  | } | 
|  |  | 
|  | const embeddedHandler = function (req, res) { | 
|  | staticHeaders(res); | 
|  | contentPolicyHeader(res); | 
|  | res.render('embed', renderConfig({ | 
|  | embedded: true, | 
|  | mobileViewer: isMobileViewer(req), | 
|  | }, req.query)); | 
|  | }; | 
|  | if (isDevMode()) { | 
|  | await setupWebPackDevMiddleware(router); | 
|  | } else { | 
|  | await setupStaticMiddleware(router); | 
|  | } | 
|  |  | 
|  | morgan.token('gdpr_ip', req => req.ip ? utils.anonymizeIp(req.ip) : ''); | 
|  |  | 
|  | // Based on combined format, but: GDPR compliant IP, no timestamp & no unused fields for our usecase | 
|  | const morganFormat = isDevMode() ? 'dev' : ':gdpr_ip ":method :url" :status'; | 
|  |  | 
|  | /* | 
|  | * This is a workaround to make cross origin monaco web workers function | 
|  | * in spite of the monaco webpack plugin hijacking the MonacoEnvironment global. | 
|  | * | 
|  | * see https://github.com/microsoft/monaco-editor-webpack-plugin/issues/42 | 
|  | * | 
|  | * This workaround wouldn't be so bad, if it didn't _also_ rely on *another* bug to | 
|  | * actually work. | 
|  | * | 
|  | * The webpack plugin incorrectly uses | 
|  | *     window.__webpack_public_path__ | 
|  | * when it should use | 
|  | *     __webpack_public_path__ | 
|  | * | 
|  | * see https://github.com/microsoft/monaco-editor-webpack-plugin/pull/63 | 
|  | * | 
|  | * We can leave __webpack_public_path__ with the correct value, which lets runtime chunk | 
|  | * loading continue to function correctly. | 
|  | * | 
|  | * We can then set window.__webpack_public_path__ to the below handler, which lets us | 
|  | * fabricate a worker on the fly. | 
|  | * | 
|  | * This is bad and I feel bad. | 
|  | * | 
|  | * This should no longer be needed, but is left here for safety because people with | 
|  | * workers already installed from this url may still try to hit this page for some time | 
|  | * | 
|  | * TODO: remove this route in the future now that it is not needed | 
|  | */ | 
|  | router.get('/workers/:worker', (req, res) => { | 
|  | staticHeaders(res); | 
|  | res.set('Content-Type', 'application/javascript'); | 
|  | res.end(`importScripts('${urljoin(staticRoot, req.params.worker)}');`); | 
|  | }); | 
|  |  | 
|  | router | 
|  | .use(morgan(morganFormat, { | 
|  | stream: logger.stream, | 
|  | // Skip for non errors (2xx, 3xx) | 
|  | skip: (req, res) => res.statusCode >= 400, | 
|  | })) | 
|  | .use(morgan(morganFormat, { | 
|  | stream: logger.warnStream, | 
|  | // Skip for non user errors (4xx) | 
|  | skip: (req, res) => res.statusCode < 400 || res.statusCode >= 500, | 
|  | })) | 
|  | .use(morgan(morganFormat, { | 
|  | stream: logger.errStream, | 
|  | // Skip for non server errors (5xx) | 
|  | skip: (req, res) => res.statusCode < 500, | 
|  | })) | 
|  | .use(compression()) | 
|  | .get('/', (req, res) => { | 
|  | staticHeaders(res); | 
|  | contentPolicyHeader(res); | 
|  | res.render('index', renderConfig({ | 
|  | embedded: false, | 
|  | mobileViewer: isMobileViewer(req), | 
|  | }, req.query)); | 
|  | }) | 
|  | .get('/e', embeddedHandler) | 
|  | // legacy. not a 301 to prevent any redirect loops between old e links and embed.html | 
|  | .get('/embed.html', embeddedHandler) | 
|  | .get('/embed-ro', (req, res) => { | 
|  | staticHeaders(res); | 
|  | contentPolicyHeader(res); | 
|  | res.render('embed', renderConfig({ | 
|  | embedded: true, | 
|  | readOnly: true, | 
|  | mobileViewer: isMobileViewer(req), | 
|  | }, req.query)); | 
|  | }) | 
|  | .get('/robots.txt', (req, res) => { | 
|  | staticHeaders(res); | 
|  | res.end('User-agent: *\nSitemap: https://godbolt.org/sitemap.xml\nDisallow:'); | 
|  | }) | 
|  | .get('/sitemap.xml', (req, res) => { | 
|  | staticHeaders(res); | 
|  | res.set('Content-Type', 'application/xml'); | 
|  | res.render('sitemap'); | 
|  | }) | 
|  | .use(sFavicon(utils.resolvePathFromAppRoot('static', 'favicon.ico'))) | 
|  | .get('/client-options.js', (req, res) => { | 
|  | staticHeaders(res); | 
|  | res.set('Content-Type', 'application/javascript'); | 
|  | res.end(`window.compilerExplorerOptions = ${clientOptionsHandler.getJSON()};`); | 
|  | }) | 
|  | .use('/bits/:bits(\\w+).html', (req, res) => { | 
|  | staticHeaders(res); | 
|  | contentPolicyHeader(res); | 
|  | res.render('bits/' + req.params.bits, renderConfig({ | 
|  | embedded: false, | 
|  | mobileViewer: isMobileViewer(req), | 
|  | }, req.query)); | 
|  | }) | 
|  | .use(bodyParser.json({limit: ceProps('bodyParserLimit', maxUploadSize)})) | 
|  | .use('/source', sourceHandler.handle.bind(sourceHandler)) | 
|  | .get('/g/:id', oldGoogleUrlHandler) | 
|  | // Deprecated old route for this -- TODO remove in late 2021 | 
|  | .post('/shortener', routeApi.apiHandler.shortener.handle.bind(routeApi.apiHandler.shortener)); | 
|  |  | 
|  | noscriptHandler.InitializeRoutes({limit: ceProps('bodyParserLimit', maxUploadSize)}); | 
|  | routeApi.InitializeRoutes(); | 
|  |  | 
|  | if (!defArgs.doCache) { | 
|  | logger.info('  with disabled caching'); | 
|  | } | 
|  | setupEventLoopLagLogging(); | 
|  | startListening(webServer); | 
|  | } | 
|  |  | 
|  | main() | 
|  | .then(() => { | 
|  | }) | 
|  | .catch(err => { | 
|  | logger.error('Top-level error (shutting down):', err); | 
|  | process.exit(1); | 
|  | }); |