Yrkki/cv-generator-fe

View on GitHub
server.js

Summary

Maintainability
A
0 mins
Test Coverage
#!/usr/bin/env node

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2018 Georgi Marinov
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

'use strict';

// Install new relic monitoring
require('newrelic');

// Configure port
const port = process.env.PORT || 5000;

// Install express server
const express = require('express');
const app = express();
const compression = require('compression');
const path = require('path');
const listener = require('./listener');

const mapEnv2ConfigData = {
  debug: {
    message: 'Debug mode', envVar: process.env.CV_GENERATOR_FE_DEBUG,
    configKey: 'debug', defaultValue: false
  },
  appName: {
    message: 'Application name', envVar: process.env.CV_GENERATOR_FE_APP_NAME,
    configKey: 'appName', defaultValue: 'CV Generator'
  },
  appPackageName: {
    message: 'Application package name', envVar: process.env.CV_GENERATOR_FE_APP_PACKAGE_NAME,
    configKey: 'appPackageName', defaultValue: 'cv-generator-fe'
  },
  serverEndpointUri: {
    message: 'Server endpoint', envVar: process.env.serverEndpointUri,
    configKey: 'serverEndpointUri', defaultValue: 'http://localhost:3000'
  },
  skipRedirectHttp: {
    message: 'Skip redirect to https', envVar: process.env.CV_GENERATOR_FE_SKIP_REDIRECT_TO_HTTPS,
    configKey: 'skipRedirectHttp', defaultValue: false
  },
  useSpdy: {
    message: 'Use HTTP/2', envVar: process.env.CV_GENERATOR_FE_USE_SPDY,
    configKey: 'useSpdy', defaultValue: false
  },
};

/* Map environment to configuration. */
function mapEnv2Config(data) {
  const message = data.message;
  const envVar = data.envVar;
  const configKey = data.configKey;
  const defaultValue = data.defaultValue || message;
  const key = data.key || configKey;

  const retVal = (envVar || defaultValue);
  app.set(key, retVal);
  // eslint-disable-next-line no-console
  console.info(`${message}: ${retVal}`);
  return retVal;
}

// eslint-disable-next-line no-console
console.log();
const debug = mapEnv2Config(mapEnv2ConfigData.debug);
// override console log
require('./override-console-log')(debug);
// eslint-disable-next-line no-console
console.log();

mapEnv2Config(mapEnv2ConfigData.appName);
mapEnv2Config(mapEnv2ConfigData.appPackageName);
mapEnv2Config(mapEnv2ConfigData.serverEndpointUri);
mapEnv2Config(mapEnv2ConfigData.skipRedirectHttp);
mapEnv2Config(mapEnv2ConfigData.useSpdy);
// eslint-disable-next-line no-console
console.log();

// Set up rate limiter: maximum number of requests per minute
const expressRateLimit = require('express-rate-limit');
const limiter = expressRateLimit.rateLimit({ windowMs: 1000, max: 5000 });
app.use('/*', limiter);

// Node prometheus exporter setup
const options = {
  appName: app.get('appPackageName'),
  collectDefaultMetrics: true
};
const prometheusExporter = require('@tailorbrands/node-exporter-prometheus');
const promExporter = prometheusExporter(options);
app.use(promExporter.middleware);
app.get('/metrics', promExporter.metrics);

// Compress responses
app.use(compression());


// Load geolocation tools
// ~security: codacy: Found require("child_process"): ESLint_security_detect-child-process
const { execSync } = require('child_process');

const projectServerLocations = [
  'https://cv-generator-project-server.herokuapp.com',
  'https://cv-generator-project-server-eu.herokuapp.com',
  'http://localhost:3000',
];

const ownEcosystemLocations = [
  ...projectServerLocations,

  'https://marinov.link',
  'http://marinov.tk',
  'http://marinov.ml',

  'https://cv-generator-fe.herokuapp.com',
  'https://cv-generator-fe-eu.herokuapp.com',

  'https://cv-generator-life-map.herokuapp.com',

  'https://cv-generator-life-adapter.herokuapp.com',
  'https://cv-generator-life-adapter-eu.herokuapp.com',
];

const originalImgSrc = [
  'https://stackshare.io',
  'https://www.npmjs.com',
  'https://img.shields.io',
  'https://s3.amazonaws.com',
  'https://ci.appveyor.com',
  'https://app.circleci.com',
  'https://codecov.io',
  'https://coveralls.io',
  'https://david-dm.org',
  'https://app.snyk.io',
  'https://app.codacy.com',
  'https://codeclimate.com',
  'https://sonarcloud.io',
  'https://dashboard.heroku.com',
  'https://gitlab.com',
  'https://bitbucket.org',
  'https://app.stackhawk.com',
  'https://www.codefactor.io',
  'https://app.datadoghq.eu',
];

const additionalImgSrc = [
  'https://github.com',

  'https://circleci.com',
  'https://api.travis-ci.com',

  'https://scan.coverity.com',
  'https://api.codeclimate.com',

  'https://snyk.io',

  'https://www.bridgecrew.cloud',

  'https://api.netlify.com',

  'https://github-readme-stats.vercel.app',
  'https://app.fossa.com',
  'https://contrib.rocks',

  'https://bestpractices.coreinfrastructure.org',

  'https://ipgeolocation.io',
];

const imgSrc = [
  'img-src \'self\'',
  'data:',

  ...ownEcosystemLocations,

  ...originalImgSrc,
  ...additionalImgSrc,
];

const defaultSrc = [
  'default-src \'self\'',

  ...projectServerLocations,

  'https://ka-f.fontawesome.com',
  'https://cdn.plot.ly/world_50m.json',

  'https://ci.appveyor.com/api/projects/Yrkki/cv-generator-fe/history',
  'https://api.ipgeolocation.io',
];

const scriptSrc = [
  'script-src \'self\'',
  '\'unsafe-inline\'',

  'https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js',
  'https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js',

  '\'unsafe-eval\'',
  'https://cdn.plot.ly/plotly-latest.min.js',

  'https://kit.fontawesome.com/b6f929f75b.js',
];

const mediaSrc = [
  'media-src \'self\'',
];

const styleSrc = [
  'style-src \'self\'',
  '\'unsafe-inline\'',

  'https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css',
];

const fontSrc = [
  'font-src \'self\'',

  'https://ka-f.fontawesome.com',
  'https://fonts.gstatic.com',
];

/**
 * Construct header section.
 */

function constructHeaderSection(array) {
  return array.join(' ');
}

/**
 * Construct CSP header.
 */

function constructCSPHeader() {
  return [
    constructHeaderSection(defaultSrc),
    constructHeaderSection(scriptSrc),
    constructHeaderSection(imgSrc),
    constructHeaderSection(mediaSrc),
    constructHeaderSection(styleSrc),
    constructHeaderSection(fontSrc),

    // 'base-uri \'self\'',
    'base-uri \'none\'',

    'form-action \'self\'',
    'frame-ancestors \'self\'',
    'frame-src \'self\'',
    'object-src \'none\'',
    // 'report-to default',
    // 'script-src-attr \'none\'',
    // 'upgrade-insecure-requests',

    // 'require-trusted-types-for \'script\'',
    // 'trusted-types default',
  ].join('; ');
}

/**
 * Set response headers.
 */

function setResponseHeaders(res) {
  // res.setHeader('Access-Control-Allow-Origin', '*');
  // res.setHeader('Access-Control-Allow-Headers', '*');

  res.setHeader('Content-Security-Policy', constructCSPHeader());

  // Cross-Origin-Embedder-Policy: (unsafe-none|require-corp); report-to="default"
  // // res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
  res.setHeader('Cross-Origin-Embedder-Policy', 'unsafe-none; report-to=default');

  // Cross-Origin-Embedder-Policy-Report-Only: (unsafe-none|require-corp); report-to="default"
  res.setHeader('Cross-Origin-Embedder-Policy-Report-Only', 'unsafe-none; report-to=default');

  // Cross-Origin-Opener-Policy: (same-origin|same-origin-allow-popups|unsafe-none); report-to="default"
  // // res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
  res.setHeader('Cross-Origin-Opener-Policy', 'unsafe-none; report-to=default');

  // Cross-Origin-Opener-Policy-Report-Only: (same-origin|same-origin-allow-popups|unsafe-none); report-to="default"
  res.setHeader('Cross-Origin-Opener-Policy-Report-Only', 'unsafe-none; report-to=default');

  // Cross-Origin-Resource-Policy: (same-site|same-origin|cross-origin)
  // res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
  res.setHeader('Cross-Origin-Resource-Policy', 'unsafe-none; report-to=default');

  // res.setHeader('Origin-Agent-Cluster', '?1');

  // res.setHeader('Permissions-Policy', 'fullscreen=(), geolocation=()');
  res.setHeader('Permissions-Policy', 'fullscreen=()');

  // res.setHeader('Referrer-Policy', 'same-origin, strict-origin-when-cross-origin');
  res.setHeader('Referrer-Policy', 'no-referrer, strict-origin-when-cross-origin');

  res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');

  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-DNS-Prefetch-Control', 'off');
  res.setHeader('X-Download-Options', 'noopen');
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');
  res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');

  res.setHeader('X-XSS-Protection', '1; mode=block');
  // res.setHeader('X-XSS-Protection', '0');

  res.removeHeader('X-Powered-By');
}

// Send server config to app
app.get('/config', function (req, res, next) {
  setResponseHeaders(res);

  res.send({
    debug: app.get('debug'),
    appName: app.get('appName'),
    appPackageName: app.get('appPackageName'),
    serverEndpointUri: app.get('serverEndpointUri'),
    skipRedirectHttp: app.get('skipRedirectHttp'),
    useSpdy: app.get('useSpdy')
  });
});

// Get geolocation
app.use('/geolocation', limiter);
app.get('/geolocation', function (req, res, next) {
  setResponseHeaders(res);

  // eslint-disable-next-line no-console
  console.info(`server.js: get: /geolocation: req: ${req.protocol} ${req.hostname} ${req.url}`);
  const ip = execSync('curl api.ipify.org').toString();
  res.redirect(`https://api.ipgeolocation.io/ipgeo?ip=${ip}&apiKey=d0650adcae4143cfb48580bf521ffdd0`);
});

// Redirect http to https
/*eslint complexity: ["error", 5]*/
app.get('*', function (req, res, next) {
  setResponseHeaders(res);

  // // eslint-disable-next-line no-console
  // console.debug(`server.js: get: req: ${req.protocol} ${req.hostname} ${req.url}`);
  if ((!req.secure || req.headers['x-forwarded-proto'] !== 'https') &&
    !['true', 'TRUE'].includes(app.get('skipRedirectHttp')) &&
    !['localhost', '192.168.1.2', '192.168.1.6', '192.168.99.100'].includes(req.hostname)
  ) {
    var url = 'https://';
    url += req.hostname;
    url += req.url;
    res.redirect(301, url);
  }
  else
    next(); /* Continue to other routes if we're not redirecting */
});

// Calc the root path
const root = path.join(__dirname, '/dist');

// Serve only the static files form the dist directory
app.use(express.static(root));

// Configure Express Rewrites
app.all('/*', function (req, res, next) {
  setResponseHeaders(res);

  // Just send the index.html for other files to support HTML5Mode
  res.sendFile('index.html', { root: root });
});

// Prepare listener options
const listenerOptions = {
  welcome: void 0,
  server: void 0,
  config: {
    useSpdy: app.get('useSpdy') === 'true',
    useHttp: false
  },
  certPath: void 0,
  certName: void 0,
};

// Start the app by listening on the default port provided, on all network interfaces. Options parameter optional.
listener.listen(app, port, listenerOptions);