| // 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 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 sanitize from 'sanitize-filename'; |
| 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 {ElementType} from './lib/common-utils'; |
| 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 {startWineInit} from './lib/exec'; |
| 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 {loadSiteTemplates} from './lib/handlers/site-templates'; |
| import {SourceHandler} from './lib/handlers/source'; |
| import {languages as allLanguages} from './lib/languages'; |
| import {logger, logToLoki, logToPapertrail, makeLogStream, suppressConsoleLog} from './lib/logger'; |
| import {setupMetricsServer} from './lib/metrics-server'; |
| import {ClientOptionsHandler} from './lib/options-handler'; |
| import * as props from './lib/properties'; |
| import {ShortLinkResolver} from './lib/shortener/google'; |
| import {sources} from './lib/sources'; |
| import {loadSponsorsFromString} from './lib/sponsors'; |
| import {getStorageTypeByKey} from './lib/storage'; |
| import * as utils from './lib/utils'; |
| import {Language, LanguageKey} from './types/languages.interfaces'; |
| |
| // Used by assert.ts |
| global.ce_base_directory = __dirname; // eslint-disable-line unicorn/prefer-module |
| |
| // 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 languages, 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], |
| discoveryonly: [String], |
| prediscovered: [String], |
| version: [Boolean], |
| webpackContent: [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')) { |
| // Node wants process.env is essentially a Record<key, string | undefined>. Any non-empty string should be fine. |
| 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('.'); |
| |
| 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, |
| wantedLanguages: 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'; |
| |
| function getFaviconFilename() { |
| if (isDevMode()) { |
| return 'favicon-dev.ico'; |
| } else if (opts.env && opts.env.includes('beta')) { |
| return 'favicon-beta.ico'; |
| } else if (opts.env && opts.env.includes('staging')) { |
| return 'favicon-staging.ico'; |
| } else { |
| return 'favicon.ico'; |
| } |
| } |
| |
| const propHierarchy = [ |
| 'defaults', |
| defArgs.env, |
| _.map(defArgs.env, e => `${e}.${process.platform}`), |
| process.platform, |
| os.hostname(), |
| 'local', |
| ].flat(); |
| 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'); |
| defArgs.wantedLanguages = ceProps('restrictToLanguages', defArgs.wantedLanguages); |
| |
| const languages = (() => { |
| if (defArgs.wantedLanguages) { |
| const filteredLangs: Partial<Record<LanguageKey, Language>> = {}; |
| const passedLangs = defArgs.wantedLanguages.split(','); |
| for (const wantedLang of passedLangs) { |
| for (const lang of Object.values(allLanguages)) { |
| if (lang.id === wantedLang || lang.name === wantedLang || lang.alias === wantedLang) { |
| filteredLangs[lang.id] = lang; |
| } |
| } |
| } |
| // Always keep cmake for IDE mode, just in case |
| filteredLangs[allLanguages.cmake.id] = allLanguages.cmake; |
| return filteredLangs; |
| } else { |
| return allLanguages; |
| } |
| })(); |
| |
| if (Object.keys(languages).length === 0) { |
| logger.error('Trying to start Compiler Explorer without a language'); |
| } |
| |
| const compilerProps = new props.CompilerProps(languages, ceProps); |
| |
| const staticPath = opts.webpackContent || path.join(distPath, 'static'); |
| 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: express.Response) { |
| // TODO: re-enable CSP |
| // if (csp) { |
| // res.setHeader('Content-Security-Policy', csp); |
| // } |
| } |
| |
| function measureEventLoopLag(delayMs: number) { |
| return new Promise<number>(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: (path: string) => any = () => { |
| logger.error('pug require handler not configured'); |
| }; |
| |
| async function setupWebPackDevMiddleware(router: express.Router) { |
| logger.info(' using webpack dev middleware'); |
| |
| /* eslint-disable node/no-unpublished-import,import/extensions, */ |
| const {default: webpackDevMiddleware} = await import('webpack-dev-middleware'); |
| const {default: webpackConfig} = await import('./webpack.config.esm.js'); |
| const {default: webpack} = await import('webpack'); |
| /* eslint-enable */ |
| type WebpackConfiguration = ElementType<Parameters<typeof webpack>[0]>; |
| |
| const webpackCompiler = webpack([webpackConfig as WebpackConfiguration]); |
| router.use( |
| webpackDevMiddleware(webpackCompiler, { |
| publicPath: '/static', |
| stats: { |
| preset: 'errors-only', |
| timings: true, |
| }, |
| }), |
| ); |
| |
| pugRequireHandler = path => urljoin(httpRoot, 'static', path); |
| } |
| |
| async function setupStaticMiddleware(router: express.Router) { |
| const staticManifest = await fs.readJson(path.join(distPath, 'manifest.json')); |
| |
| if (staticUrl) { |
| logger.info(` using static files from '${staticUrl}'`); |
| } else { |
| 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: string) { |
| try { |
| const parsed = JSON.parse(data); |
| return !parsed['allowStoreCodeDebug']; |
| } catch (e) { |
| return true; |
| } |
| } |
| |
| const googleShortUrlResolver = new ShortLinkResolver(); |
| |
| function oldGoogleUrlHandler(req: express.Request, res: express.Response, next: express.NextFunction) { |
| 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<string>('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: express.Express) { |
| const ss = systemdSocket(); |
| let _port; |
| if (ss) { |
| // ms (5 min default) |
| const idleTimeout = process.env.IDLE_TIMEOUT; |
| const timeout = (idleTimeout === undefined ? 300 : parseInt(idleTimeout)) * 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 startupGauge = new PromClient.Gauge({ |
| name: 'ce_startup_seconds', |
| help: 'Time taken from process start to serving requests', |
| }); |
| startupGauge.set(process.uptime()); |
| 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: string) { |
| 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; |
| }, |
| }); |
| 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); |
| // Initialise express and then sentry. Sentry as early as possible to catch errors during startup. |
| const webServer = express(), |
| router = express.Router(); |
| setupSentry(aws.getConfig('sentryDsn')); |
| |
| startWineInit(); |
| |
| 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}`); |
| |
| let initialCompilers; |
| let prevCompilers; |
| |
| if (opts.prediscovered) { |
| const prediscoveredCompilersJson = await fs.readFile(opts.prediscovered, 'utf8'); |
| initialCompilers = JSON.parse(prediscoveredCompilersJson); |
| await compilerFinder.loadPrediscovered(initialCompilers); |
| } else { |
| const initialFindResults = await compilerFinder.find(); |
| initialCompilers = initialFindResults.compilers; |
| 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...'); |
| } |
| } |
| } |
| |
| if (opts.discoveryonly) { |
| for (const compiler of initialCompilers) { |
| if (compiler.buildenvsetup && compiler.buildenvsetup.id === '') delete compiler.buildenvsetup; |
| |
| if (compiler.externalparser && compiler.externalparser.id === '') delete compiler.externalparser; |
| |
| const compilerInstance = compilerFinder.compileHandler.findCompiler(compiler.lang, compiler.id); |
| if (compilerInstance) { |
| compiler.cachedPossibleArguments = compilerInstance.possibleArguments.possibleArguments; |
| } |
| } |
| await fs.writeFile(opts.discoveryonly, JSON.stringify(initialCompilers)); |
| logger.info(`Discovered compilers saved to ${opts.discoveryonly}`); |
| process.exit(0); |
| } |
| |
| const healthCheckFilePath = ceProps('healthCheckFilePath', false); |
| |
| const handlerConfig = { |
| compileHandler, |
| clientOptionsHandler, |
| storageHandler, |
| ceProps, |
| opts, |
| defArgs, |
| renderConfig, |
| renderGoldenLayout, |
| staticHeaders, |
| contentPolicyHeader, |
| }; |
| |
| const noscriptHandler = new NoScriptHandler(router, handlerConfig); |
| const routeApi = new RouteAPI(router, handlerConfig); |
| |
| async function onCompilerChange(compilers) { |
| if (JSON.stringify(prevCompilers) === JSON.stringify(compilers)) { |
| return; |
| } |
| logger.info(`Compiler scan count: ${_.size(compilers)}`); |
| logger.debug('Compilers:', compilers); |
| prevCompilers = compilers; |
| await clientOptionsHandler.setCompilers(compilers); |
| routeApi.apiHandler.setCompilers(compilers); |
| routeApi.apiHandler.setLanguages(languages); |
| routeApi.apiHandler.setOptions(clientOptionsHandler); |
| } |
| |
| await onCompilerChange(initialCompilers); |
| |
| const rescanCompilerSecs = ceProps('rescanCompilerSecs', 0); |
| if (rescanCompilerSecs && !opts.prediscovered) { |
| 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}`); |
| setupMetricsServer(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, |
| }), |
| ) |
| // 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', 'utf8')); |
| |
| loadSiteTemplates(configDir); |
| |
| 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); |
| |
| const embedded = req.query.embedded === 'true' ? true : false; |
| |
| res.render( |
| embedded ? 'embed' : 'index', |
| renderConfig( |
| { |
| embedded: embedded, |
| mobileViewer: isMobileViewer(req), |
| config: config, |
| metadata: metadata, |
| storedStateId: req.params.id || false, |
| }, |
| req.query, |
| ), |
| ); |
| } |
| |
| const embeddedHandler = function (req: express.Request, res: express.Response) { |
| staticHeaders(res); |
| contentPolicyHeader(res); |
| res.render( |
| 'embed', |
| renderConfig( |
| { |
| embedded: true, |
| mobileViewer: isMobileViewer(req), |
| }, |
| req.query, |
| ), |
| ); |
| }; |
| |
| await (isDevMode() ? setupWebPackDevMiddleware(router) : setupStaticMiddleware(router)); |
| |
| morgan.token('gdpr_ip', (req: any) => (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: makeLogStream('info'), |
| // Skip for non errors (2xx, 3xx) |
| skip: (req, res) => res.statusCode >= 400, |
| }), |
| ) |
| .use( |
| morgan(morganFormat, { |
| stream: makeLogStream('warn'), |
| // Skip for non user errors (4xx) |
| skip: (req, res) => res.statusCode < 400 || res.statusCode >= 500, |
| }), |
| ) |
| .use( |
| morgan(morganFormat, { |
| stream: makeLogStream('error'), |
| // 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/favicons', getFaviconFilename()))) |
| .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/${sanitize(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); |
| } |
| |
| if (opts.version) { |
| logger.info('Compiler Explorer version info:'); |
| logger.info(` git release ${gitReleaseName}`); |
| logger.info(` release build ${releaseBuildNumber}`); |
| logger.info('Exiting'); |
| process.exit(0); |
| } |
| |
| process.on('uncaughtException', uncaughtHandler); |
| process.on('SIGINT', signalHandler('SIGINT')); |
| process.on('SIGTERM', signalHandler('SIGTERM')); |
| process.on('SIGQUIT', signalHandler('SIGQUIT')); |
| |
| function signalHandler(name: string) { |
| return () => { |
| logger.info(`stopping process: ${name}`); |
| process.exit(0); |
| }; |
| } |
| |
| function uncaughtHandler(err: Error, origin: NodeJS.UncaughtExceptionOrigin) { |
| logger.info(`stopping process: Uncaught exception: ${err}\nException origin: ${origin}`); |
| // The app will exit naturally from here, but if we call `process.exit()` we may lose log lines. |
| // see https://github.com/winstonjs/winston/issues/1504#issuecomment-1033087411 |
| process.exitCode = 1; |
| } |
| |
| // Once we move to modules, we can remove this and use a top level await. |
| // eslint-disable-next-line unicorn/prefer-top-level-await |
| main().catch(err => { |
| logger.error('Top-level error (shutting down):', err); |
| // Shut down after a second to hopefully let logs flush. |
| setTimeout(() => process.exit(1), 1000); |
| }); |