| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 15:05:45 +00:00
										 |  |  | var PromiseA = require('bluebird'); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  | module.exports.create = function (letsencrypt, defaults, options) { | 
					
						
							| 
									
										
										
										
											2015-12-12 15:05:45 +00:00
										 |  |  |   letsencrypt = PromiseA.promisifyAll(letsencrypt); | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |   var tls = require('tls'); | 
					
						
							|  |  |  |   var fs = PromiseA.promisifyAll(require('fs')); | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |   var utils = require('./utils'); | 
					
						
							|  |  |  |   var registerAsync = PromiseA.promisify(function (args) { | 
					
						
							|  |  |  |     return letsencrypt.registerAsync('certonly', args); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   var fetchAsync = PromiseA.promisify(function (args) { | 
					
						
							|  |  |  |     var hostname = args.domains[0]; | 
					
						
							|  |  |  |     var crtpath = defaults.configDir + defaults.fullchainTpl.replace(/:hostname/, hostname); | 
					
						
							|  |  |  |     var privpath = defaults.configDir + defaults.privkeyTpl.replace(/:hostname/, hostname); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return PromiseA.all([ | 
					
						
							|  |  |  |       fs.readFileAsync(privpath, 'ascii') | 
					
						
							|  |  |  |     , fs.readFileAsync(crtpath, 'ascii') | 
					
						
							|  |  |  |       // stat the file, not the link
 | 
					
						
							|  |  |  |     , fs.statAsync(crtpath, 'ascii') | 
					
						
							|  |  |  |     ]); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   //var attempts = {};  // should exist in master process only
 | 
					
						
							|  |  |  |   var ipc = {};       // in-process cache
 | 
					
						
							|  |  |  |   var count = 0; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |   var now; | 
					
						
							|  |  |  |   var le; | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 15:05:45 +00:00
										 |  |  |   // TODO check certs on initial load
 | 
					
						
							|  |  |  |   // TODO expect that certs expire every 90 days
 | 
					
						
							|  |  |  |   // TODO check certs with setInterval?
 | 
					
						
							|  |  |  |   //options.cacheContextsFor = options.cacheContextsFor || (1 * 60 * 60 * 1000);
 | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   defaults.webroot = true; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |   function merge(args) { | 
					
						
							|  |  |  |     var copy = {}; | 
					
						
							| 
									
										
										
										
											2015-12-12 15:05:45 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |     Object.keys(defaults).forEach(function (key) { | 
					
						
							|  |  |  |       copy[key] = defaults[key]; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     Object.keys(args).forEach(function (key) { | 
					
						
							|  |  |  |       copy[key] = args[key]; | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2015-12-12 15:05:45 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     return copy; | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 15:50:00 +00:00
										 |  |  |   function isCurrent(cache) { | 
					
						
							|  |  |  |     return cache; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |   function sniCallback(hostname, cb) { | 
					
						
							|  |  |  |     var args = merge({}); | 
					
						
							|  |  |  |     args.domains = [hostname]; | 
					
						
							|  |  |  |     le.fetch(args, function (err, cache) { | 
					
						
							|  |  |  |       if (err) { | 
					
						
							|  |  |  |         cb(err); | 
					
						
							|  |  |  |         return; | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 15:50:00 +00:00
										 |  |  |       function respond(c2) { | 
					
						
							|  |  |  |         cache = c2 || cache; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!cache.context) { | 
					
						
							|  |  |  |           cache.context = tls.createSecureContext({ | 
					
						
							|  |  |  |             key: cache.key    // privkey.pem
 | 
					
						
							|  |  |  |           , cert: cache.cert  // fullchain.pem
 | 
					
						
							|  |  |  |           //, ciphers         // node's defaults are great
 | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         cb(null, cache.context); | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-12 15:50:00 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |       if (isCurrent(cache)) { | 
					
						
							|  |  |  |         respond(); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       defaults.needsRegistration(hostname, respond); | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   le = { | 
					
						
							|  |  |  |     validate: function () { | 
					
						
							| 
									
										
										
										
											2015-12-12 15:05:45 +00:00
										 |  |  |       // TODO check dns, etc
 | 
					
						
							|  |  |  |       return PromiseA.resolve(); | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |     } | 
					
						
							|  |  |  |   , middleware: function () { | 
					
						
							| 
									
										
										
										
											2015-12-12 15:19:11 +00:00
										 |  |  |       //console.log('[DEBUG] webrootPath', defaults.webrootPath);
 | 
					
						
							|  |  |  |       var serveStatic = require('serve-static')(defaults.webrootPath, { dotfiles: 'allow' }); | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |       var prefix = '/.well-known/acme-challenge/'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return function (req, res, next) { | 
					
						
							| 
									
										
										
										
											2015-12-12 15:05:45 +00:00
										 |  |  |         if (0 !== req.url.indexOf(prefix)) { | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |           next(); | 
					
						
							|  |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 15:19:11 +00:00
										 |  |  |         serveStatic(req, res, next); | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , SNICallback: sniCallback | 
					
						
							|  |  |  |   , sniCallback: sniCallback | 
					
						
							|  |  |  |   , cacheCerts: function (args, certs) { | 
					
						
							|  |  |  |       var hostname = args.domains[0]; | 
					
						
							|  |  |  |       // assume 90 day renewals based on stat time, for now
 | 
					
						
							|  |  |  |       ipc[hostname] = { | 
					
						
							|  |  |  |         context: tls.createSecureContext({ | 
					
						
							|  |  |  |           key: certs[0]  // privkey.pem
 | 
					
						
							|  |  |  |         , cert: certs[1] // fullchain.pem
 | 
					
						
							|  |  |  |         //, ciphers // node's defaults are great
 | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |       , updated: Date.now() | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return ipc[hostname]; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , readAndCacheCerts: function (args) { | 
					
						
							|  |  |  |       return fetchAsync(args).then(function (certs) { | 
					
						
							|  |  |  |         return le.cacheCerts(args, certs); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , register: function (args) { | 
					
						
							|  |  |  |       // TODO validate domains and such
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       var copy = merge(args); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (!utils.isValidDomain(args.domains[0])) { | 
					
						
							|  |  |  |         return PromiseA.reject({ | 
					
						
							|  |  |  |           message: "invalid domain" | 
					
						
							|  |  |  |         , code: "INVALID_DOMAIN" | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |       return le.validate(args.domains).then(function () { | 
					
						
							|  |  |  |         return registerAsync(copy).then(function () { | 
					
						
							|  |  |  |           return fetchAsync(args); | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |         }); | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , fetch: function (args, cb) { | 
					
						
							|  |  |  |       var hostname = args.domains[0]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       count += 1; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (count >= 1000) { | 
					
						
							|  |  |  |         now = Date.now(); | 
					
						
							|  |  |  |         count = 0; | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |       var cached = ipc[hostname]; | 
					
						
							|  |  |  |       // TODO handle www and no-www together
 | 
					
						
							|  |  |  |       if (cached && ((now - cached.updated) < options.cacheContextsFor)) { | 
					
						
							|  |  |  |         cb(null, cached.context); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |       return fetchAsync(args).then(function (cached) { | 
					
						
							|  |  |  |         cb(null, cached.context); | 
					
						
							|  |  |  |       }, cb); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , fetchOrRegister: function (args, cb) { | 
					
						
							|  |  |  |       le.fetch(args, function (err, hit) { | 
					
						
							|  |  |  |         var hostname = args.domains[0]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (err) { | 
					
						
							|  |  |  |           cb(err); | 
					
						
							|  |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         else if (hit) { | 
					
						
							|  |  |  |           cb(null, hit); | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |         // TODO validate domains empirically before trying le
 | 
					
						
							|  |  |  |         return registerAsync(args/*, opts*/).then(function () { | 
					
						
							|  |  |  |           // wait at least n minutes
 | 
					
						
							|  |  |  |           return fetchAsync(args).then(function (cached) { | 
					
						
							|  |  |  |             // success
 | 
					
						
							|  |  |  |             cb(null, cached.context); | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |           }, function (err) { | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |             // still couldn't read the certs after success... that's weird
 | 
					
						
							|  |  |  |             cb(err); | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |           }); | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |         }, function (err) { | 
					
						
							|  |  |  |           console.error("[Error] Let's Encrypt failed:"); | 
					
						
							|  |  |  |           console.error(err.stack || new Error(err.message || err.toString())); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // wasn't successful with lets encrypt, don't try again for n minutes
 | 
					
						
							|  |  |  |           ipc[hostname] = { | 
					
						
							|  |  |  |             context: null | 
					
						
							|  |  |  |           , updated: Date.now() | 
					
						
							|  |  |  |           }; | 
					
						
							|  |  |  |           cb(null, ipc[hostname]); | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |         }); | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |   return le; | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | }; |