HTTPS Server with multiple Domains on same Port & Instance

In this lesson we will see an example how to manage a HTTPS Server that will serve specific SSL Certificates for different domain names.

My sample represents one file, I will explain it’s parts step-by-step

Libraries Used


var express = require("express");

var app = express();

var _classes = {
    fs : require("fs"),
    tls : require("tls"),
    https : require("https")
};

Certificate ca-bundle.crt usually contains multiple certificates.. we need a function for splitting them.

Sample of content structure of ca-bundle.crt


/**************************
-----BEGIN CERTIFICATE-----
MSDIE..
... base64 encoded Content
..FSDS
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
ASDDIE..
... base64 encoded Content
..FSDS
-----END CERTIFICATE-----
..... othermultiple certificates....
*/

We will need a function for splitting ca-bundle.crt file into an array.


var sslCAdecode = function (source) {
    var ca    = [];
    if (!source || typeof(source) !== "string")
        return ca;
        ca    = source.split(/-----END CERTIFICATE-----[\s\n]+-----BEGIN CERTIFICATE-----/).map(function (v, k, arr) {
            if (k) {
                v = '-----BEGIN CERTIFICATE-----' + v;
            }
            if (k !== arr.length - 1) {
                v = v + '-----END CERTIFICATE-----';
            }
            v = v.replace(/^\n+/,'').replace(/\n+$/,'');
            return v;
        });
    return ca;
};

Preparing configuration

We need to prepare secure context for all domains/servernames.

SSL Secure Context will be prepared using native module “tls".

The tls.createSecureContext() method creates a credentials object.

A key is required for ciphers that make use of certificates. Either key or pfx can be used to provide it.

If the ‘ca’ option is not given, then Node.js will use the default
publicly trusted list of CAs as given in
http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt.


var sslConfig    = {
    privateKey    : "/home/nodejs-md/ssl/nodejs-ssl.key",
    certificate   : "/home/nodejs-md/ssl/nodejs-ssl.crt",
    bundleChain   : "/home/nodejs-md/ssl/nodejs-ssl-ca-bundle.crt"
};
sslConfig.sslCertsContext = {
    "sgapps.io" : _classes.tls.createSecureContext({
            key   : _classes.fs.readFileSync(
                "/home/sgapps-io/ssl/sgapps-ssl.key"
            ).toString(),
            cert  : _classes.fs.readFileSync(
                "/home/sgapps-io/ssl/sgapps-ssl.crt"
            ).toString(),
            ca    : sslCAdecode(
                _classes.fs.readFileSync(
                    "/home/sgapps-io/ssl/sgapps-ssl-ca-bundle.crt",
                    "utf-8"
                )
            )
    }),
    "nodejs.md" : _classes.tls.createSecureContext({
            key   : _classes.fs.readFileSync(
                "/home/nodejs-md/ssl/nodejs-ssl.key"
            ).toString(),
            cert  : _classes.fs.readFileSync(
                "/home/nodejs-md/ssl/nodejs-ssl.crt"
            ).toString(),
            ca    : sslCAdecode(
                _classes.fs.readFileSync(
                    "/home/nodejs-md/ssl/nodejs-ssl-ca-bundle.crt",
                    "utf-8"
                )
            )
    }),
    "js.md" : _classes.tls.createSecureContext({
            key   : _classes.fs.readFileSync(
                "/home/js-md/ssl/js-ssl.key"
            ).toString(),
            cert  : _classes.fs.readFileSync(
                "/home/js-md/ssl/js-ssl.crt"
            ).toString(),
            ca    : sslCAdecode(
                _classes.fs.readFileSync(
                    "/home/js-md/ssl/js-ssl-ca-bundle.crt",
                    "utf-8"
                )
            )
    })
};

Initializing HTTPS Server

we will send ExpressJS instance into HTTPS Server Instance as Main Request handler.

  • key <string> | <string[]> | <Buffer> | <Buffer[]> | <Object[]> Optional private keys in PEM format. PEM allows the option of private keys being encrypted. Encrypted keys will be decrypted with options.passphrase. Multiple keys using different algorithms can be provided either as an array of unencrypted key strings or buffers, or an array of objects in the form {pem: <string|buffer>[, passphrase: <string>]}. The object form can only occur in an array. object.passphrase is optional. Encrypted keys will be decrypted with object.passphrase if provided, or options.passphrase if it is not.
  • cert <string> | <string[]> | <Buffer> | <Buffer[]> Optional cert chains in PEM format. One cert chain should be provided per private key. Each cert chain should consist of the PEM formatted certificate for a provided private key, followed by the PEM formatted intermediate certificates (if any), in order, and not including the root CA (the root CA must be pre-known to the peer, see ca). When providing multiple cert chains, they do not have to be in the same order as their private keys in key. If the intermediate certificates are not provided, the peer will not be able to validate the certificate, and the handshake will fail.
  • SNICallback(servername, cb) A function that will be called if the client supports SNI TLS extension. Two arguments will be passed when called: servername and cb. SNICallback should invoke cb(null, ctx), where ctx is a SecureContext instance. (tls.createSecureContext(...) can be used to get a proper SecureContext.) If SNICallback wasn't provided the default callback with high-level API will be used (see below).

_classes.https.createServer({
        key    : _classes.fs.readFileSync(sslConfig.privateKey),
        cert    : _classes.fs.readFileSync(sslConfig.certificate),
        ca    : sslCAdecode(
            _classes.fs.readFileSync(sslConfig.bundleChain, "utf-8")
        ),
        SNICallback : function (servername, cb) {
            var ctx = sslConfig.sslCertsContext[servername] || sslConfig.sslCertsContext["nodejs.md"];
            if (cb) {
                cb(null, ctx);
            } else {
                return ctx;
            }
        }
    }, app).listen(
        443,       // default HTTPS port
        "0.0.0.0", // listen all IPs
        function (err) {
            if (err)
                return console.error(err);

            console.log("Listen Server on https://0.0.0.0:443");
        }
    );

Creating a HTTP Server instance that will redirect traffic to HTTPS

Usually Users are accessing sites over HTTP Protocol by entering http://yoursize.com instead of https://yoursize.com.

So to redirect automatically all users/ all traffic to your HTTPS Server you will need a HTTP Server with a very simple functionality, just one redirect…


require("http").createServer(function (request, response) {
    response.statusCode = 302;
    response.setHeader("Location", (request.headers || {}).origin || "https://nodejs.md");
    response.end();
}).listen(
    80,        // default HTTP posrt
    "0.0.0.0", // listen all IPs
    function (err) {
        if (err)
        return console.error(err);

        console.log("HTTP Redirect Server on http://0.0.0.0:80");
    }
);

Conclusion

So we obtained a HTTPS Server that is able to offer different certificates depending of server-name.

In this example we have 3 TLS Security Contexts for following sites: sgapps.io, nodejs.md, js.md

Entire File ( all code in one file )


var express = require("express");

var app = express();

var _classes = {
    fs : require("fs"),
    tls : require("tls"),
    https : require("https")
};


/**************************
-----BEGIN CERTIFICATE-----
MSDIE..
... base64 encoded Content
..FSDS
-----END CERTIFICATE-----
   
-----BEGIN CERTIFICATE-----
ASDDIE..
... base64 encoded Content
..FSDS
-----END CERTIFICATE-----
   
   
..... othermultiple certificates....
*/
var sslCAdecode = function (source) {
    var ca    = [];
    if (!source || typeof(source) !== "string")
        return ca;
        ca    = source.split(/-----END CERTIFICATE-----[\s\n]+-----BEGIN CERTIFICATE-----/).map(function (v, k, arr) {
            if (k) {
                v = '-----BEGIN CERTIFICATE-----' + v;
            }
            if (k !== arr.length - 1) {
                v = v + '-----END CERTIFICATE-----';
            }
            v = v.replace(/^\n+/,'').replace(/\n+$/,'');
            return v;
        });
    return ca;
};



_classes.https.createServer({
        key    : _classes.fs.readFileSync(sslConfig.privateKey),
        cert    : _classes.fs.readFileSync(sslConfig.certificate),
        ca    : sslCAdecode(
            _classes.fs.readFileSync(sslConfig.bundleChain, "utf-8")
        ),
        SNICallback : function (servername, cb) {
            var ctx = sslConfig.sslCertsContext[servername] || sslConfig.sslCertsContext["nodejs.md"];
            if (cb) {
                cb(null, ctx);
            } else {
                return ctx;
            }
        }
    }, app).listen(
        443,       // default HTTPS port
        "0.0.0.0", // listen all IPs
        function (err) {
            if (err)
                return console.error(err);

            console.log("Listen Server on https://0.0.0.0:443");
        }
    );

require("http").createServer(function (request, response) {
    response.statusCode = 302;
    response.setHeader("Location", (request.headers || {}).origin || "https://nodejs.md");
    response.end();
}).listen(
    80,        // default HTTP posrt
    "0.0.0.0", // listen all IPs
    function (err) {
        if (err)
        return console.error(err);

        console.log("HTTP Redirect Server on http://0.0.0.0:80");
    }
);


0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *