v2.6.0: simplify existing defaults, add default servername support
This commit is contained in:
		
							parent
							
								
									b17805d1fb
								
							
						
					
					
						commit
						9a086e62ba
					
				
							
								
								
									
										79
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										79
									
								
								README.md
									
									
									
									
									
								
							| @ -114,43 +114,39 @@ All you have to do is start the webserver and then visit it at its domain name. | ||||
| 'use strict'; | ||||
| 
 | ||||
| require('greenlock-express').create({ | ||||
|   email: 'john.doe@example.com'     // The email address of the ACME user / hosting provider | ||||
| , agreeTos: true                    // You must accept the ToS as the host which handles the certs | ||||
| , configDir: '~/.config/acme/'      // Writable directory where certs will be saved | ||||
| , communityMember: true             // Join the community to get notified of important updates | ||||
| , telemetry: true                   // Contribute telemetry data to the project | ||||
| 
 | ||||
|   // Let's Encrypt v2 is ACME draft 11 | ||||
|   version: 'draft-11' | ||||
| 
 | ||||
|   // Note: If at first you don't succeed, switch to staging to debug | ||||
|   // https://acme-staging-v02.api.letsencrypt.org/directory | ||||
| , server: 'https://acme-v02.api.letsencrypt.org/directory' | ||||
| 
 | ||||
|   // Where the certs will be saved, MUST have write access | ||||
| , configDir: '~/.config/acme/' | ||||
| 
 | ||||
|   // You MUST change this to a valid email address | ||||
| , email: 'john.doe@example.com' | ||||
| 
 | ||||
|   // You MUST change these to valid domains | ||||
|   // NOTE: all domains will validated and listed on the certificate | ||||
| , approvedDomains: [ 'example.com', 'www.example.com' ] | ||||
| 
 | ||||
|   // You MUST NOT build clients that accept the ToS without asking the user | ||||
| , agreeTos: true | ||||
| 
 | ||||
| , app: require('express')().use('/', function (req, res) { | ||||
|     res.setHeader('Content-Type', 'text/html; charset=utf-8') | ||||
|     res.end('Hello, World!\n\n💚 🔒.js'); | ||||
|   }) | ||||
| 
 | ||||
|   // Join the community to get notified of important updates | ||||
| , communityMember: true | ||||
| 
 | ||||
|   // Contribute telemetry data to the project | ||||
| , telemetry: true | ||||
|   // Using your express app: | ||||
|   // simply export it as-is, then include it here | ||||
| , app: require('./app.js') | ||||
| 
 | ||||
| //, debug: true | ||||
| 
 | ||||
| }).listen(80, 443); | ||||
| ``` | ||||
| 
 | ||||
| `app.js`: | ||||
| ```js | ||||
| 'use strict'; | ||||
| 
 | ||||
| var express = require('express'); | ||||
| var app = express(); | ||||
| 
 | ||||
| app.use('/', function (req, res) { | ||||
|   res.setHeader('Content-Type', 'text/html; charset=utf-8') | ||||
|   res.end('Hello, World!\n\n💚 🔒.js'); | ||||
| }) | ||||
| 
 | ||||
| // Don't do this: | ||||
| // app.listen(3000) | ||||
| 
 | ||||
| // Do this instead: | ||||
| module.exports = app; | ||||
| ``` | ||||
| 
 | ||||
| ### `communityMember` | ||||
| 
 | ||||
| If you're the kind of person that likes the kinds of stuff that I do, | ||||
| @ -181,7 +177,6 @@ Double check the following: | ||||
|   * You MUST set `email` to a **valid address** | ||||
|   * MX records must validate (`dig MX example.com` for `'john@example.com'`) | ||||
| * **valid DNS records** | ||||
|   * You MUST set `approveDomains` to real domains | ||||
|   * Must have public DNS records (test with `dig +trace A example.com; dig +trace www.example.com` for `[ 'example.com', 'www.example.com' ]`) | ||||
| * **write access** | ||||
|   * You MUST set `configDir` to a writeable location (test with `touch ~/acme/etc/tmp.tmp`) | ||||
| @ -320,6 +315,10 @@ var glx = require('greenlock-express').create({ | ||||
|   // Contribute telemetry data to the project | ||||
| , telemetry: true | ||||
| 
 | ||||
|   // the default servername to use when the client doesn't specify | ||||
|   // (because some IoT devices don't support servername indication) | ||||
| , servername: 'example.com' | ||||
| 
 | ||||
| , approveDomains: approveDomains | ||||
| }); | ||||
| 
 | ||||
| @ -345,6 +344,10 @@ var http01 = require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challen | ||||
| function approveDomains(opts, certs, cb) { | ||||
|   // This is where you check your database and associated | ||||
|   // email addresses with domains and agreements and such | ||||
|   // if (!isAllowed(opts.domains)) { return cb(new Error("not allowed")); } | ||||
| 
 | ||||
|   // The domains being approved for the first time are listed in opts.domains | ||||
|   // Certs being renewed are listed in certs.altnames (if that's useful) | ||||
| 
 | ||||
|   // Opt-in to submit stats and get important updates | ||||
|   opts.communityMember = true; | ||||
| @ -352,11 +355,6 @@ function approveDomains(opts, certs, cb) { | ||||
|   // If you wish to replace the default challenge plugin, you may do so here | ||||
|   opts.challenges = { 'http-01': http01 }; | ||||
| 
 | ||||
|   // The domains being approved for the first time are listed in opts.domains | ||||
|   // Certs being renewed are listed in certs.altnames | ||||
|   if (certs) { | ||||
|     opts.domains = [certs.subject].concat(certs.altnames); | ||||
|   } | ||||
|   opts.email = 'john.doe@example.com'; | ||||
|   opts.agreeTos = true; | ||||
| 
 | ||||
| @ -388,11 +386,10 @@ require('https').createServer(glx.httpsOptions, app).listen(443, function () { | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| **Security Warning**: | ||||
| **Security**: | ||||
| 
 | ||||
| If you don't do proper checks in `approveDomains(opts, certs, cb)` | ||||
| an attacker will spoof SNI packets with bad hostnames and that will | ||||
| cause you to be rate-limited and or blocked from the ACME server. | ||||
| Greenlock will do a self-check on all domain registrations | ||||
| to prevent you from hitting rate limits. | ||||
| 
 | ||||
| 
 | ||||
| # API | ||||
|  | ||||
							
								
								
									
										138
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										138
									
								
								index.js
									
									
									
									
									
								
							| @ -15,6 +15,14 @@ module.exports.create = function (opts) { | ||||
|     opts._communityPackageVersion = require('./package.json').version; | ||||
|   } | ||||
| 
 | ||||
|   if (!opts.approvedDomains && !opts.approveDomains) { | ||||
|     opts.approveDomains = function (opts, certs, cb) { | ||||
|       // acme-v2's self-test pre-checks are secure enough as of v1.5.0
 | ||||
|       // that there's minimal risk to not explicitly listing domains
 | ||||
|       cb(null, { options: opts, certs: certs }); | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   function explainError(e) { | ||||
|     console.error('Error:' + e.message); | ||||
|     if ('EACCES' === e.errno) { | ||||
| @ -30,7 +38,7 @@ module.exports.create = function (opts) { | ||||
|     console.error(e.code + ": '" + e.address + ":" + e.port + "'"); | ||||
|   } | ||||
| 
 | ||||
|   function _listen(plainPort, plain) { | ||||
|   function _createPlain(plainPort) { | ||||
|     if (!plainPort) { plainPort = 80; } | ||||
| 
 | ||||
|     var parts = String(plainPort).split(':'); | ||||
| @ -41,14 +49,80 @@ module.exports.create = function (opts) { | ||||
|     var server; | ||||
|     var validHttpPort = (parseInt(p, 10) >= 0); | ||||
| 
 | ||||
|     function tryPlain() { | ||||
|     if (addr) { args[1] = addr; } | ||||
|     if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) { | ||||
|       console.warn("'" + p + "' doesn't seem to be a valid port number, socket path, or pipe"); | ||||
|     } | ||||
| 
 | ||||
|     server = require('http').createServer( | ||||
|       greenlock.middleware.sanitizeHost(greenlock.middleware(require('redirect-https')())) | ||||
|     ); | ||||
|     httpType = 'http'; | ||||
| 
 | ||||
|     return { server: server, listen: function () { return new Promise(function (resolve, reject) { | ||||
|       args[0] = p; | ||||
|       args.push(function () { | ||||
|         if (!greenlock.servername) { | ||||
|           if (Array.isArray(greenlock.approvedDomains) && greenlock.approvedDomains.length) { | ||||
|             greenlock.servername = greenlock.approvedDomains[0]; | ||||
|           } | ||||
|           if (Array.isArray(greenlock.approveDomains) && greenlock.approvedDomains.length) { | ||||
|             greenlock.servername = greenlock.approvedDomains[0]; | ||||
|           } | ||||
|         } | ||||
|         if (!greenlock.servername) { | ||||
|           resolve(null); | ||||
|           return; | ||||
|         } | ||||
|         return greenlock.check({ domains: [ greenlock.servername ] }).then(function (certs) { | ||||
|           if (certs) { | ||||
|             console.info("Using '%s' as default certificate", greenlock.servername); | ||||
|             return { | ||||
|               key: Buffer.from(certs.privkey, 'ascii') | ||||
|             , cert: Buffer.from(certs.cert + '\r\n' + certs.chain, 'ascii') | ||||
|             }; | ||||
|           } | ||||
|           console.info("Fetching certificate for '%s' to use as default for HTTPS server...", greenlock.servername); | ||||
|           return new PromiseA(function (resolve, reject) { | ||||
|             // using SNICallback because all options will be set
 | ||||
|             greenlock.tlsOptions.SNICallback(greenlock.servername, function (err/*, secureContext*/) { | ||||
|               if (err) { reject(err); return; } | ||||
|               return greenlock.check({ domains: [ greenlock.servername ] }).then(function (certs) { | ||||
|                 resolve({ | ||||
|                   key: Buffer.from(certs.privkey, 'ascii') | ||||
|                 , cert: Buffer.from(certs.cert + '\r\n' + certs.chain, 'ascii') | ||||
|                 }); | ||||
|               }).catch(reject); | ||||
|             }); | ||||
|           }); | ||||
|         }).then(resolve).catch(reject); | ||||
|       }); | ||||
|       server.listen.apply(server, args).on('error', function (e) { | ||||
|         if (server.listenerCount('error') < 2) { | ||||
|           console.warn("Did not successfully create http server and bind to port '" + p + "':"); | ||||
|           explainError(e); | ||||
|           process.exit(41); | ||||
|         } | ||||
|       }); | ||||
|     }); } }; | ||||
|   } | ||||
| 
 | ||||
|   function _create(port) { | ||||
|     if (!port) { port = 443; } | ||||
| 
 | ||||
|     var parts = String(port).split(':'); | ||||
|     var p = parts.pop(); | ||||
|     var addr = parts.join(':').replace(/^\[/, '').replace(/\]$/, ''); | ||||
|     var args = []; | ||||
|     var httpType; | ||||
|     var server; | ||||
|     var validHttpPort = (parseInt(p, 10) >= 0); | ||||
| 
 | ||||
|     if (addr) { args[1] = addr; } | ||||
|     if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) { | ||||
|       console.warn("'" + p + "' doesn't seem to be a valid port number, socket path, or pipe"); | ||||
|     } | ||||
| 
 | ||||
|     function trySecure() { | ||||
|     var https; | ||||
|     try { | ||||
|       https = require('spdy'); | ||||
| @ -58,6 +132,31 @@ module.exports.create = function (opts) { | ||||
|       https = require('https'); | ||||
|       httpType = 'https'; | ||||
|     } | ||||
|     var sniCallback = greenlock.tlsOptions.SNICallback; | ||||
|     greenlock.tlsOptions.SNICallback = function (domain, cb) { | ||||
|       sniCallback(domain, function (err, context) { | ||||
|         cb(err, context); | ||||
|         if (!context || server._hasDefaultSecureContext) { | ||||
|           return; | ||||
|         } | ||||
|         return greenlock.check({ domains: [ domain ] }).then(function (certs) { | ||||
|           // ignore the case that check doesn't have all the right args here
 | ||||
|           // to get the same certs that it just got (eventually the right ones will come in)
 | ||||
|           if (!certs) { return; } | ||||
|           console.info("Using '%s' as default certificate", domain); | ||||
|           server.setSecureContext({ | ||||
|             key: Buffer.from(certs.privkey, 'ascii') | ||||
|           , cert: Buffer.from(certs.cert + '\r\n' + certs.chain, 'ascii') | ||||
|           }); | ||||
|           server._hasDefaultSecureContext = true; | ||||
|         }).catch(function (/*e*/) { | ||||
|           // this may be that the test.example.com was requested, but it's listed
 | ||||
|           // on the cert for demo.example.com which is in its own directory, not the other
 | ||||
|           //console.warn("Unusual error: couldn't get newly authorized certificate:");
 | ||||
|           //console.warn(e.message);
 | ||||
|         }); | ||||
|       }); | ||||
|     }; | ||||
|     server = https.createServer( | ||||
|       greenlock.tlsOptions | ||||
|     , greenlock.middleware.sanitizeHost(function (req, res) { | ||||
| @ -77,17 +176,10 @@ module.exports.create = function (opts) { | ||||
|       }) | ||||
|     ); | ||||
|     server.type = httpType; | ||||
|     } | ||||
| 
 | ||||
|     if (addr) { args[1] = addr; } | ||||
|     if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) { | ||||
|       console.warn("'" + p + "' doesn't seem to be a valid port number, socket path, or pipe"); | ||||
|     } | ||||
|     if (plain) { tryPlain(); } else { trySecure(); } | ||||
| 
 | ||||
|     var promise = new PromiseA(function (resolve) { | ||||
|     return { server: server, listen: function () { return new PromiseA(function (resolve) { | ||||
|       args[0] = p; | ||||
|       args.push(function () { resolve(server); }); | ||||
|       args.push(function () { resolve(/*server*/); }); | ||||
|       server.listen.apply(server, args).on('error', function (e) { | ||||
|         if (server.listenerCount('error') < 2) { | ||||
|           console.warn("Did not successfully create http server and bind to port '" + p + "':"); | ||||
| @ -95,10 +187,7 @@ module.exports.create = function (opts) { | ||||
|           process.exit(41); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     promise.server = server; | ||||
|     return promise; | ||||
|     }); } }; | ||||
|   } | ||||
| 
 | ||||
|   // NOTE: 'greenlock' is just 'opts' renamed
 | ||||
| @ -111,7 +200,6 @@ module.exports.create = function (opts) { | ||||
|   } | ||||
| 
 | ||||
|   opts.listen = function (plainPort, port, fnPlain, fn) { | ||||
|     var promises = []; | ||||
|     var server; | ||||
|     var plainServer; | ||||
| 
 | ||||
| @ -122,13 +210,18 @@ module.exports.create = function (opts) { | ||||
|       fnPlain = null; | ||||
|     } | ||||
| 
 | ||||
|     promises.push(_listen(plainPort, true)); | ||||
|     promises.push(_listen(port, false)); | ||||
|     var obj1 = _createPlain(plainPort, true); | ||||
|     var obj2 = _create(port, false); | ||||
| 
 | ||||
|     server = promises[1].server; | ||||
|     plainServer = promises[0].server; | ||||
|     plainServer = obj1.server; | ||||
|     server = obj2.server; | ||||
| 
 | ||||
|     PromiseA.all(promises).then(function () { | ||||
|     server.then = obj1.listen().then(function (tlsOptions) { | ||||
|       if (tlsOptions) { | ||||
|         server.setSecureContext(tlsOptions); | ||||
|         server._hasDefaultSecureContext = true; | ||||
|       } | ||||
|       return obj2.listen().then(function () { | ||||
|         // Report plain http status
 | ||||
|         if ('function' === typeof fnPlain) { | ||||
|           fnPlain.apply(plainServer); | ||||
| @ -144,6 +237,7 @@ module.exports.create = function (opts) { | ||||
|           console.info('[:' + (server.address().port || server.address()) + "] Serving " + server.type); | ||||
|         } | ||||
|       }); | ||||
|     }).then; | ||||
| 
 | ||||
|     server.unencrypted = plainServer; | ||||
|     return server; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "greenlock-express", | ||||
|   "version": "2.5.0", | ||||
|   "version": "2.6.0", | ||||
|   "description": "Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.", | ||||
|   "main": "index.js", | ||||
|   "homepage": "https://git.coolaj86.com/coolaj86/greenlock-express.js", | ||||
| @ -8,7 +8,7 @@ | ||||
|     "example": "examples" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "greenlock": "^2.5.0", | ||||
|     "greenlock": "^2.6.3", | ||||
|     "le-challenge-fs": "^2.0.8", | ||||
|     "le-sni-auto": "^2.1.4", | ||||
|     "le-store-certbot": "^2.1.0", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user