frontend.js

'use strict';

const { HSTS_POLICY_HEADER } = require('./server');
const path = require('path');
const express = require('express');
const serveStatic = require('serve-static');
const pkg = require('../package');
const merge = require('merge');
const tld = require('tldjs');
const http = require('http');
const https = require('https');
const { EventEmitter } = require('events');

/**
 * Frontend application that provides an interface to route incoming
 * connections
 */
class Frontend extends EventEmitter {

  static get DEFAULTS() {
    return {
      tlsCredentials: { key: null, cert: null }
    };
  }

  /**
   * @constructor
   */
  constructor(options){
    super();
    this._opts = this._checkOptions(merge(Frontend.DEFAULTS, options));
    this._credentials = this._opts.tlsCredentials;
    this._started = Date.now();
    this._app = express();

    this._bootstrap();
  }

  /**
   * @private
   */
  _bootstrap() {
    this._app.set('view engine', 'pug');
    this._app.set('views', path.join(__dirname, '../web/views'));
    this._app.use(serveStatic(path.join(__dirname, '../web/static')));
    this._app.use(this.handleServerRequest.bind(this));
    this._app.get('/', this._serveRoot.bind(this));
    this._app.get('/:id', this._serveTunnelInfo.bind(this));
    this._app.use((req, res) => this._serveMissing(req, res));
    this._app.use((err, req, res, next) =>
      this._serveError(err, req, res, next));
  }

  /**
   * @private
   */
  _serveRoot(request, response) {
    const locals = {
      version: pkg.version,
      started: this._started
    };

    response.format({
      html: () => response.render('landing', locals),
      json: () => response.json(locals)
    });
  }

  /**
   * @private
   */
  _serveTunnelInfo(request, response, next) {
    const proxy = request.params.id;

    this.emit('PROXY_QUERY', {
      proxy,
      queryHandler(info) {
        if (!info) {
          const err = new Error(
            `Our server moles could not find info on the tunnel "${proxy}".`
          );
          err.code = 404;
          return next(err);
        }

        response.format({
          html: () => response.render('info', info),
          json: () => response.json(info)
        });
      }
    });
  }

  /**
   * @private
   */
  _serveMissing(request, response) {
    const error = {
      code: 404,
      message: 'Our server moles could not find that resource.'
    };
    this._serveMissingOrError(error, request, response);
  }

  /**
   * @private
   */
  _serveError(error, request, response, next) {
    if (!error) {
      return next();
    }
    error.code = 500;
    this._serveMissingOrError(error, request, response);
  }

  /**
   * @private
   */
  _serveMissingOrError(error, request, response) {
    const code = error.code || 500;

    response.append('Strict-Transport-Security', HSTS_POLICY_HEADER);
    response.status(code);
    response.format({
      html: () => response.render('error', { code, message: error.message }),
      json: () => response.json({ code, message: error.message })
    });
    response.connection.destroy();
  }

  /**
   * @private
   */
  _checkOptions(o) {
    return o;
  }

  static getProxyIdFromSubdomain(request, hostname) {
    let subdomain = tld.getSubdomain(request.headers.host);
    let parts = subdomain ? subdomain.split('.') : [];

    if (request.headers.host === hostname) {
      return '';
    } else if (parts.length > 1) {
      return parts[0];
    } else {
      return subdomain;
    }
  }

  handleServerRequest(request, response, next) {
    let proxyId = Frontend.getProxyIdFromSubdomain(request,
      this._opts.hostname);

    if (!proxyId) {
      return next();
    }

    this.emit('INCOMING_HTTPS', { proxy: proxyId, request, response, next });
  }

  handleServerUpgrade(request, socket) {
    let proxyId = Frontend.getProxyIdFromSubdomain(request,
      this._opts.hostname);

    if (!proxyId) {
      return socket.destroy();
    }

    this.emit('INCOMING_WSS', { proxy: proxyId, request, socket });
  }

  listen() {
    this.proxy = https.createServer(this._credentials, this._app);

    this.proxy.on('upgrade', (req, sock) => {
      this.handleServerUpgrade(req, sock);
    });

    this.proxy.listen(...arguments);
  }

  redirect() {
    this.redirect = http.createServer(function(req, res) {
      res.writeHead(301, {
        Location: `https://${req.headers.host}${req.url}`
      });
      res.end();
    }).listen(...arguments);
  }

}

module.exports = Frontend;