303 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			303 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| var path = require('path');
 | |
| var challengeStore = require('./challenge-handlers');
 | |
| var createSniCallback = require('./sni-callback').create;
 | |
| var LE = require('letsencrypt');
 | |
| 
 | |
| function LEX(obj, app) {
 | |
|   var https = require('https');
 | |
|   var http = require('http');
 | |
|   var defaultPems = require('localhost.daplie.com-certificates');
 | |
| 
 | |
|   if (!obj) {
 | |
|     obj = {};
 | |
|   }
 | |
| 
 | |
|   if ('string' === typeof obj) {
 | |
|     obj = {
 | |
|       configDir: obj
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   if ('function' === typeof obj) {
 | |
|     obj = {
 | |
|       onRequest: obj
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   obj.debug = LEX.debug;
 | |
|   
 | |
|   if ('function' === typeof app) {
 | |
|     obj.onRequest = obj.onRequest || app;
 | |
|   }
 | |
| 
 | |
|   if (!obj.getChallenge) {
 | |
|     if (false !== obj.getChallenge) {
 | |
|       obj.getChallenge = challengeStore.get;
 | |
|     }
 | |
|     if (!obj.webrootPath) {
 | |
|       obj.webrootPath = path.join(require('os').tmpdir(), 'acme-challenge');
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (!obj.onRequest && false !== obj.onRequest) {
 | |
|     console.warn("You should either do args.onRequest = app or server.on('request', app),"
 | |
|       + " otherwise only acme-challenge requests will be handled (and the rest will hang)");
 | |
|     console.warn("You can silence this warning by setting args.onRequest = false");
 | |
|   }
 | |
| 
 | |
|   if (!obj.configDir) {
 | |
|     obj.configDir = path.join(require('homedir')(), '/letsencrypt/etc');
 | |
|   }
 | |
|   if (!obj.privkeyPath) {
 | |
|     obj.privkeyPath = ':config/live/:hostname/privkey.pem';
 | |
|   }
 | |
|   if (!obj.fullchainPath) {
 | |
|     obj.fullchainPath = ':config/live/:hostname/fullchain.pem';
 | |
|   }
 | |
|   if (!obj.certPath) {
 | |
|     obj.certPath = ':config/live/:hostname/cert.pem';
 | |
|   }
 | |
|   if (!obj.chainPath) {
 | |
|     obj.chainPath = ':config/live/:hostname/chain.pem';
 | |
|   }
 | |
| 
 | |
|   if (!obj.server) {
 | |
|     obj.server = LEX.defaultServerUrl;
 | |
|   }
 | |
| 
 | |
|   if (!obj.letsencrypt) {
 | |
|     //LE.merge(obj, );
 | |
|     // { configDir, webrootPath, server }
 | |
|     obj.letsencrypt = LE.create(obj, {
 | |
|       setChallenge: challengeStore.set
 | |
|     , removeChallenge: challengeStore.remove
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   function acmeResponder(req, res) {
 | |
|     if (LEX.debug) {
 | |
|       console.debug('[LEX] ', req.method, req.headers.host, req.url);
 | |
|     }
 | |
|     var acmeChallengePrefix = '/.well-known/acme-challenge/';
 | |
| 
 | |
|     if (0 !== req.url.indexOf(acmeChallengePrefix)) {
 | |
|       obj.onRequest(req, res);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var key = req.url.slice(acmeChallengePrefix.length);
 | |
| 
 | |
|     obj.getChallenge(obj, req.headers.host, key, function (err, val) {
 | |
|       if (LEX.debug) {
 | |
|         console.debug('[LEX] GET challenge, response:');
 | |
|         console.debug('challenge:', key);
 | |
|         console.debug('response:', val);
 | |
|         if (err) {
 | |
|           console.debug(err.stack);
 | |
|         }
 | |
|       }
 | |
|       res.end(val || '_');
 | |
|     });
 | |
|   }
 | |
| 
 | |
| 
 | |
|   var httpsOptions = obj.httpsOptions || {};
 | |
|   var sniCallback = httpsOptions.SNICallback;
 | |
| 
 | |
|   // https://nodejs.org/api/https.html
 | |
|   // pfx, key, cert, passphrase, ca, ciphers, rejectUnauthorized, secureProtocol
 | |
|   if (!httpsOptions.pfx) {
 | |
|     if (!(httpsOptions.cert || httpsOptions.key)) {
 | |
|       httpsOptions.key = defaultPems.key;
 | |
|       httpsOptions.cert = defaultPems.cert;
 | |
|     }
 | |
|     else if (!(httpsOptions.cert && httpsOptions.key)) {
 | |
|       if (!httpsOptions.cert) {
 | |
|         console.warn("You specified httpsOptions.cert, but not httpsOptions.key");
 | |
|       }
 | |
|       if (!httpsOptions.key) {
 | |
|         console.warn("You specified httpsOptions.key, but not httpsOptions.cert");
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (!obj.approveRegistration && LEX.defaultApproveRegistration) {
 | |
|     obj.approveRegistration = function (domain, cb) {
 | |
|       if (LEX.debug) {
 | |
|         console.debug('[LEX] auto register against staging server');
 | |
|       }
 | |
|       cb(null, {
 | |
|         email: 'example@gmail.com'
 | |
|       , domains: [domain]
 | |
|       , agreeTos: true
 | |
|       , server: LEX.stagingServerUrl
 | |
|       });
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   if (obj.sniCallback) {
 | |
|     if (sniCallback) {
 | |
|       console.warn("You specified both args.sniCallback and args.httpsOptions.SNICallback,"
 | |
|       + " but only args.sniCallback will be used.");
 | |
|     }
 | |
|     httpsOptions.SNICallback = obj.sniCallback;
 | |
|   }
 | |
|   else if (sniCallback) {
 | |
|     obj._sniCallback = createSniCallback(obj);
 | |
|     httpsOptions.SNICallback = function (domain, cb) {
 | |
|       sniCallback(domain, function (err, context) {
 | |
|         if (context) {
 | |
|           cb(err, context);
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         obj._sniCallback(domain, cb);
 | |
|       });
 | |
|     };
 | |
|   }
 | |
|   else {
 | |
|     httpsOptions.SNICallback = createSniCallback(obj);
 | |
|   }
 | |
| 
 | |
|   function listen(plainPorts, tlsPorts, onListening) {
 | |
|     var results = {
 | |
|       plainServers: []
 | |
|     , tlsServers: []
 | |
|     };
 | |
| 
 | |
|     plainPorts = plainPorts || [80];
 | |
|     tlsPorts = tlsPorts || [443, 5001];
 | |
| 
 | |
|     function defaultOnListening() {
 | |
|       /*jshint validthis: true*/
 | |
|       var server = this;
 | |
|       var protocol = ('honorCipherOrder' in server || 'rejectUnauthorized' in server) ? 'https': 'http';
 | |
|       var addr = server.address();
 | |
|       var port;
 | |
| 
 | |
|       if (80 === addr.port || 443 === addr.port) {
 | |
|         port = '';
 | |
|       } else {
 | |
|         port = ':' + addr.port;
 | |
|       }
 | |
|       console.info('Listening ' + protocol + '://' + addr.address + port + '/');
 | |
|     }
 | |
| 
 | |
|     plainPorts.forEach(function (addr) {
 | |
|       var port = addr.port || addr;
 | |
|       var address = addr.address || '';
 | |
|       var server = http.createServer(acmeResponder);
 | |
| 
 | |
|       server.__le_onListening = addr.onListen;
 | |
|       server.__le_port = port;
 | |
|       server.__le_address = address;
 | |
| 
 | |
|       results.plainServers.push(server);
 | |
|     });
 | |
| 
 | |
|     tlsPorts.forEach(function (addr) {
 | |
|       var port = addr.port || addr;
 | |
|       var address = addr.address || '';
 | |
|       var options = addr.httpsOptions || httpsOptions;
 | |
|       var server = https.createServer(options, acmeResponder);
 | |
| 
 | |
|       server.__le_onListen = addr.onListen;
 | |
|       server.__le_port = port;
 | |
|       server.__le_address = address;
 | |
| 
 | |
|       results.tlsServers.push(server);
 | |
|     });
 | |
| 
 | |
|     results.plainServers.forEach(function (server) {
 | |
|       var listen = server.__le_onListening || onListening || defaultOnListening;
 | |
|       server.listen(server.__le_port, server.__le_address, listen);
 | |
|     });
 | |
| 
 | |
|     results.tlsServers.forEach(function (server) {
 | |
|       var listen = server.__le_onListening || onListening || defaultOnListening;
 | |
|       server.listen(server.__le_port, server.__le_address, listen);
 | |
|     });
 | |
| 
 | |
|     // deleting creates a "slow object", but that's okay (we only use it once)
 | |
|     return results;
 | |
|   }
 | |
| 
 | |
|   return {
 | |
|     listen: listen
 | |
|   };
 | |
| }
 | |
| 
 | |
| module.exports = LEX;
 | |
| 
 | |
| LEX.create = LEX;
 | |
| LEX.setChallenge = challengeStore.set;
 | |
| LEX.getChallenge = challengeStore.get;
 | |
| LEX.removeChallenge = challengeStore.remove;
 | |
| LEX.createSniCallback = createSniCallback;
 | |
| // TODO not sure how well this works
 | |
| LEX.middleware = function (defaults) {
 | |
|   var leCore = require('letiny-core');
 | |
|   var merge = require('letsencrypt/common').merge;
 | |
|   var tplConfigDir = require('letsencrypt/common').tplConfigDir;
 | |
|   var tplHostname = require('letsencrypt/common').tplHostname;
 | |
|   var prefix = leCore.acmeChallengePrefix;
 | |
| 
 | |
|   tplConfigDir(defaults.configDir || '', defaults);
 | |
| 
 | |
|   return function (req, res, next) {
 | |
|     if (LEX.debug) {
 | |
|       console.debug('[LEX middleware]:', req.hostname, req.url, req.url.slice(prefix.length));
 | |
|     }
 | |
| 
 | |
|     if (0 !== req.url.indexOf(prefix)) {
 | |
|       next();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     function done(err, token) {
 | |
|       if (err) {
 | |
|         res.send("Error: These aren't the tokens you're looking for. Move along.");
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       res.send(token);
 | |
|     }
 | |
| 
 | |
|     var copy = merge(defaults, { domains: [req.hostname] });
 | |
|     tplHostname(req.hostname, copy);
 | |
| 
 | |
|     LEX.getChallenge(copy, req.hostname, req.url.slice(prefix.length), done);
 | |
|   };
 | |
| };
 | |
| 
 | |
| LEX.stagingServerUrl = LE.stagingServerUrl;
 | |
| LEX.productionServerUrl = LE.productionServerUrl || LE.liveServerUrl;
 | |
| LEX.defaultServerUrl = LEX.productionServerUrl;
 | |
| LEX.testing = function () {
 | |
|   LEX.debug = true;
 | |
|   LEX.defaultServerUrl = LEX.stagingServerUrl;
 | |
|   LEX.defaultApproveRegistration = true;
 | |
|   console.debug = console.log;
 | |
|   console.debug('[LEX] testing mode turned on');
 | |
|   console.debug('[LEX] default server: ' + LEX.defaultServerUrl);
 | |
|   console.debug('\n');
 | |
|   console.debug('###################################################');
 | |
|   console.debug('#                                                 #');
 | |
|   console.debug('#     Open up a browser and visit this server     #');
 | |
|   console.debug('#     at its domain name.                         #');
 | |
|   console.debug('#                                                 #');
 | |
|   console.debug('#                                 ENJOY!          #');
 | |
|   console.debug('#                                                 #');
 | |
|   console.debug('###################################################');
 | |
|   console.debug('\n');
 | |
|   console.debug('Note: testing certs will be installed because .testing() was called.');
 | |
|   console.debug('      remove .testing() to get live certs.');
 | |
|   console.debug('\n');
 | |
|   console.debug('[LEX] automatic registration handling turned on for testing.');
 | |
|   console.debug('\n');
 | |
| 
 | |
|   return module.exports;
 | |
| };
 |