Writing your Own Production Grade HTTP(S) Base Server Module

Back To Blog

In this article, we will go throught building your own HTTP base server package, production ready. It's quite useful to reuse if you want to host multiple websites. We will first make our own server base which we will reuse to make a website server, and we will then look at setuping NGINX to route the traffic properly.

serverBase.js: The Base Server Module

Let's first look at a first version of a base server module, which is similar to what we use at opeNode.


const http = require('http');
const https = require('https');
const fs = require("fs");
const read = fs.readFileSync;
const express = require("express");
const log = require("./lib/log"): // your logger package. For example winston: https://www.npmjs.com/package/winston

// When it's in test/dev mode, we do not want https, easier to debug
const test_mode = process.env.TEST_MODE === "true";

if (test_mode) {
  log.debug("IN TEST MODE"):
}

// this will catch any uncaught exception and exit the process
process.on('uncaughtException', function (err) {
  log.error(" ----- SERVER BASE UNCAUGHT EXCEPTION");
  log.error(err.stack);
  process.exit(1);
});


function onError(error) {

  log.error("----- SERVER ONERROR");
  log.error(error);

  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      log.error("requires elevated privileges");
      process.exit(1);
      break;
    case 'EADDRINUSE':
      log.error(`port ${bind} is already in use`);
      process.exit(1);
      break;
    default:
      throw error;
  }
}

function start(app, httpPort, httpsPort) {
  log.info("will server on http " + httpPort + " https " + httpsPort);

  /**
   * Create HTTP server.
   */
  let server = null;
  let secServer = null;
  let io = null;

  if ( ! test_mode) {
    const httpApp = express();
    const httpRouter = express.Router();
    httpApp.use('*', httpRouter);
    httpRouter.get('*', function(req, res){
        let host = req.get('host');

        if ( ! host)
          host = "yourhost.com";

        // replace the port in the host
        //host = host.replace(/:\d+$/, ":"+app.get('port'));
        // determine the redirect destination
        const destination = ['https://', host, req.url].join('');
        return res.redirect(destination);
    });

    server = http.createServer(httpApp).listen(httpPort);
    server.on('error', onError);


    /**
     * Listen on provided port, on all network interfaces.
     */

    let certificate = read("./certs/your.crt", 'utf8');
    let chainLines = read("./certs/intermediate_domain_ca.crt", 'utf8').split("\n");
    let cert = [];
    let ca = [];
    chainLines.forEach(function(line) {
      cert.push(line);
      if (line.match(/-END CERTIFICATE-/)) {
        ca.push(cert.join("\n"));
        cert = [];
      }
    });

    let httpOptions = {
      key: read('./certs/privatekey.key'),
      cert: certificate,
      ca: ca
    };

    secServer = https.createServer(httpOptions, app);

    // Create the socket.io server
    io = require('socket.io')(secServer);

    secServer.listen(httpsPort);
    secServer.on('error', onError);

    server = secServer;

  } else {
    server = http.createServer(app).listen(httpPort);
    io = require('socket.io')(server);
  }

  return [
    server,
    secServer,
    io
  ];
}

module.exports = {
  start,
  test_mode
}

This module contains a main function, start having the following signature:

function start(app, httpPort, httpsPort)

Given your app (i.e., express), an http port, https port, it setups both servers and provides a socket.io server properly, ready to use.

It's always useful to have a global exception catch, in order to log it:

process.on('uncaughtException', function (err) {
  log.error(" ----- SERVER BASE UNCAUGHT EXCEPTION");
  log.error(err.stack);
  process.exit(1);
});

It is worth noting that once we have logged the error, we immediately exit. As the error was uncaught, if we let it run, it could let the process in an unknown state, which we want to avoid.

When we create both HTTP and HTTPs servers in this example, the HTTP server simply redirects to HTTPS:

const httpApp = express();
const httpRouter = express.Router();
httpApp.use('*', httpRouter);
httpRouter.get('*', function(req, res){
    let host = req.get('host');

    if ( ! host)
      host = "yourhost.com";

    // replace the port in the host
    //host = host.replace(/:\d+$/, ":"+app.get('port'));
    // determine the redirect destination
    const destination = ['https://', host, req.url].join('');
    return res.redirect(destination);
});

server = http.createServer(httpApp).listen(httpPort);

Note that this can be avoided by simply redirecting the traffic via NGINX (for example).

On opeNode, you can just listen to port HTTPS and set a configuration via:

openode set-config REDIR_HTTP_TO_HTTPS true

Next, the HTTPS server is setup with valid certificate files, which are assumed to be in ./certs with conventional names.

server.js: An example website using serverBase.js

So far we just defined a base server module allowing to setup a fully functional HTTP(S) server with socket.io support.

The following server uses it to start an express-based website:

const app = require('./app'); // the express application
const serverBase = require("./serverBase"); // our custom module

serverBase.start(app, 3001, 3243);

That's it, in 3 lines we have a complete HTTP-HTTPS servers. The implementation details of the express application are in ./app and runs using our serverBase.js module. HTTP listens in this example on port 3001 and HTTPS on port 3243.

But, wtf are 3001 and 3243 ports ? We want 80 and 443 right ? We just need NGINX to forward the traffic from 80 to 3001 and from 443 to 3243.

NGINX

A typical /etc/nginx/nginx.conf file looks like this:

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    client_max_body_size 1000M;
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  300;

    #gzip  on;

    #include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/www;
}

include /etc/nginx/sites-enabled/https;

The interesting parts are in sites-enabled, which are related to our applications.

/etc/nginx/sites-enabled/www

For HTTP, we just redirect all traffic to HTTPS, because we want to be secure right?

server {
    listen       80;

    server_name _;
    return 301 https://$host$request_uri;
}

/etc/nginx/sites-enabled/https

For HTTPS, we want to just stream the traffic directly to our Node.js application, without playing with anything related to the HTTP headers. To accomplish this, you need a recent NGINX version supporting SSL passthrough - see http://nginx.org/en/docs/stream/ngx_stream_ssl_module.html

stream {
  map $ssl_preread_server_name $name {
    myhost.com backendhttps;
  }

  upstream backendhttps {
    server 127.0.0.1:3243;
  }

  server {
    listen 443;
    proxy_pass $name;
    ssl_preread on;
  }
}

And voilà, we have a clean production ready website up and running!


Added on Sun May 13 2018 15:15:47 GMT+0000 (Coordinated Universal Time)