Free SSL and Automatic HTTPS (ACME / Let's Encrypt v2 client) for node.js with Express, Connect, and other middleware systems
Go to file
AJ ONeal 0071264d39 time to go nani nani... tired 2016-08-12 04:19:22 -04:00
examples update README.md 2016-08-12 03:13:33 -04:00
.gitignore Initial commit 2016-08-10 01:42:42 -06:00
LICENSE Initial commit 2016-08-10 01:42:42 -06:00
README.md time to go nani nani... tired 2016-08-12 04:19:22 -04:00
index.js v2.0.0 2016-08-12 03:02:33 -04:00
master.js time to go nani nani... tired 2016-08-12 04:19:22 -04:00
package.json time to go nani nani... tired 2016-08-12 04:19:22 -04:00

README.md

letsencrypt-express

Use automatic letsencrypt with express and other node http frameworks.

Install

npm install --save letsencrypt-express@2.x

Usage

In a cluster environment you have some main file that boots your app and then conditionally loads certain code based on whether that fork is the master or just a worker.

In such a file you might want to define some of the options that need to be shared between both the master and the worker, like this:

boot.js:

'use strict';

var cluster = require('cluster');
var path = require('path');
var os = require('os');

var main;
var sharedOptions = {
  webrootPath: path.join(os.tmpdir(), 'acme-challenge')			// /tmp/acme-challenge
                                                            // used by le-challenge-fs, the default plugin

, renewWithin: 10 * 24 * 60 * 60 * 1000 										// 10 days before expiration

, debug: true
};

if (cluster.isMaster) {
  main = require('./master');
}
else {
  main = require('./worker');
}

main.init(sharedOptions);

Master

We think it makes the most sense to load letsencrypt in master. This can prevent race conditions (see node-letsencrypt#45) as only one process is writing the to file system or database at a time.

The main implementation detail here is approveDomains(options, certs, cb) for new domain certificates and potentially agreeToTerms(opts, cb) for new accounts.

The master takes the same arguments as node-letsencrypt (challenge, store, etc), plus a few extra (approveDomains... okay, just one extra):

master.js:

'use strict';

var cluster = require('cluster');

module.exports.init = function (sharedOpts) {
  var cores = require('os').cpus();
  var leMaster = require('letsencrypt-cluster/master').create({
    debug: sharedOpts.debug

  , server: 'staging'                                                       // CHANGE TO PRODUCTION

  , renewWithin: sharedOpts.renewWithin

  , webrootPath: sharedOpts.webrootPath

  , approveDomains: function (masterOptions, certs, cb) {
      // Do any work that must be done by master to approve this domain
      // (in this example, it's assumed to be done by the worker)

      var results = { domain: masterOptions.domain                          // required
                    , options: masterOptions                                // domains, email, agreeTos
                    , certs: certs };                                       // altnames, privkey, cert
      cb(null, results);
    }
  });

  cores.forEach(function () {
    var worker = cluster.fork();
    leMaster.addWorker(worker);
  });
};

API

All options are passed directly to node-letsencrypt (in other works, leMaster is a letsencrypt instance), but a few are only actually used by letsencrypt-cluster.

  • leOptions.approveDomains(options, certs, cb) is special for letsencrypt-cluster, but will probably be included in node-letsencrypt in the future (no API change).

  • leMaster.addWorker(worker) is added by letsencrypt-cluster and must be called for each new worker.

Worker

The worker takes similar arguments to node-letsencrypt, but only ones that are useful for determining certificate renewal and for le.challenge.get.

If you want to a non-default le.challenge

worker.js:

'use strict';

module.exports.init = function (sharedOpts) {
  var leWorker = require('letsencrypt-cluster/worker').create({
    debug: sharedOpts.debug

  , renewWithin: sharedOpts.renewWithin

  , webrootPath: sharedOpts.webrootPath

  // , challenge: require('le-challenge-fs').create({ webrootPath: '...', ... })

  , approveDomains: function (workerOptions, certs, cb) {
      // opts = { domains, email, agreeTos, tosUrl }
      // certs = { subject, altnames, expiresAt, issuedAt }

      var results = {
        domain: workerOptions.domains[0]
      , options: {
          domains: workerOptions.domains
        }
      , certs: certs
      };

      if (certs) {
        // modify opts.domains to match the original request
        // email is not necessary, because the account already exists
        // this will only fail if the account has become corrupt
        results.options.domains = certs.altnames;
        cb(null, results);
        return;
      }

      // This is where one would check one's application-specific database:
      //   1. Lookup the domain to see which email it belongs to
      //   2. Assign a default email if it isn't in the system
      //   3. If the email has no le account, `agreeToTerms` will fire unless `agreeTos` is preset

      results.options.email = 'john.doe@example.com'
      results.options.agreeTos = true                                 // causes agreeToTerms to be skipped
      cb(null, results);
    }
  });

  function app(req, res) {
    res.end("Hello, World!");
  }

  var redirectHttps = require('redirect-https')();
  var plainServer = require('http').createServer(leWorker.middleware(redirectHttps));
  plainServer.listen(80);

  var server = require('https').createServer(leWorker.httpsOptions, leWorker.middleware(app));
  server.listen(443);
};

API

node-letsencrypt is not used directly by the worker, but certain options are shared because certain logic is duplicated.

  • leOptions.renewWithin is shared so that the worker knows how earlier to request a new cert
  • leOptions.renewBy is passed to le-sni-auto so that it staggers renewals between renewWithin (latest) and renewBy (earlier)
  • leWorker.middleware(nextApp) uses letsencrypt/middleware for GET-ing http-01, hence sharedOptions.webrootPath
  • leWorker.httpsOptions has a default localhost certificate and the SNICallback.

There are a few options that aren't shown in these examples, so if you need to change something that isn't shown here, look at the code (it's not that much) or open an issue.

Message Passing

The master and workers will communicate through process.on('message', fn), process.send({}), worker.on('message', fn)and worker.send({}).

All messages have a type property which is a string and begins with LE_. All other messages are ignored.