v2.6.4: simplify existing defaults, add default servername support
This commit is contained in:
		
							parent
							
								
									b17805d1fb
								
							
						
					
					
						commit
						779ab234ac
					
				
							
								
								
									
										67
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										67
									
								
								README.md
									
									
									
									
									
								
							| @ -114,41 +114,37 @@ All you have to do is start the webserver and then visit it at its domain name. | |||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| require('greenlock-express').create({ | 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 |   // Using your express app: | ||||||
|   version: 'draft-11' |   // simply export it as-is, then include it here | ||||||
|  | , app: require('./app.js') | ||||||
| 
 | 
 | ||||||
|   // Note: If at first you don't succeed, switch to staging to debug | //, debug: true | ||||||
|   // https://acme-staging-v02.api.letsencrypt.org/directory | }).listen(80, 443); | ||||||
| , server: 'https://acme-v02.api.letsencrypt.org/directory' | ``` | ||||||
| 
 | 
 | ||||||
|   // Where the certs will be saved, MUST have write access | `app.js`: | ||||||
| , configDir: '~/.config/acme/' | ```js | ||||||
|  | 'use strict'; | ||||||
| 
 | 
 | ||||||
|   // You MUST change this to a valid email address | var express = require('express'); | ||||||
| , email: 'john.doe@example.com' | var app = express(); | ||||||
| 
 | 
 | ||||||
|   // You MUST change these to valid domains | app.use('/', function (req, res) { | ||||||
|   // 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.setHeader('Content-Type', 'text/html; charset=utf-8') | ||||||
|   res.end('Hello, World!\n\n💚 🔒.js'); |   res.end('Hello, World!\n\n💚 🔒.js'); | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
|   // Join the community to get notified of important updates | // Don't do this: | ||||||
| , communityMember: true | // app.listen(3000) | ||||||
| 
 | 
 | ||||||
|   // Contribute telemetry data to the project | // Do this instead: | ||||||
| , telemetry: true | module.exports = app; | ||||||
| 
 |  | ||||||
| //, debug: true |  | ||||||
| 
 |  | ||||||
| }).listen(80, 443); |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### `communityMember` | ### `communityMember` | ||||||
| @ -181,7 +177,6 @@ Double check the following: | |||||||
|   * You MUST set `email` to a **valid address** |   * You MUST set `email` to a **valid address** | ||||||
|   * MX records must validate (`dig MX example.com` for `'john@example.com'`) |   * MX records must validate (`dig MX example.com` for `'john@example.com'`) | ||||||
| * **valid DNS records** | * **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' ]`) |   * 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** | * **write access** | ||||||
|   * You MUST set `configDir` to a writeable location (test with `touch ~/acme/etc/tmp.tmp`) |   * 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 |   // Contribute telemetry data to the project | ||||||
| , telemetry: true | , 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 | , approveDomains: approveDomains | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| @ -345,6 +344,10 @@ var http01 = require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challen | |||||||
| function approveDomains(opts, certs, cb) { | function approveDomains(opts, certs, cb) { | ||||||
|   // This is where you check your database and associated |   // This is where you check your database and associated | ||||||
|   // email addresses with domains and agreements and such |   // 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 |   // Opt-in to submit stats and get important updates | ||||||
|   opts.communityMember = true; |   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 |   // If you wish to replace the default challenge plugin, you may do so here | ||||||
|   opts.challenges = { 'http-01': http01 }; |   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.email = 'john.doe@example.com'; | ||||||
|   opts.agreeTos = true; |   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)` | Greenlock will do a self-check on all domain registrations | ||||||
| an attacker will spoof SNI packets with bad hostnames and that will | to prevent you from hitting rate limits. | ||||||
| cause you to be rate-limited and or blocked from the ACME server. |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # API | # API | ||||||
|  | |||||||
							
								
								
									
										130
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								index.js
									
									
									
									
									
								
							| @ -30,7 +30,7 @@ module.exports.create = function (opts) { | |||||||
|     console.error(e.code + ": '" + e.address + ":" + e.port + "'"); |     console.error(e.code + ": '" + e.address + ":" + e.port + "'"); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function _listen(plainPort, plain) { |   function _createPlain(plainPort) { | ||||||
|     if (!plainPort) { plainPort = 80; } |     if (!plainPort) { plainPort = 80; } | ||||||
| 
 | 
 | ||||||
|     var parts = String(plainPort).split(':'); |     var parts = String(plainPort).split(':'); | ||||||
| @ -41,14 +41,80 @@ module.exports.create = function (opts) { | |||||||
|     var server; |     var server; | ||||||
|     var validHttpPort = (parseInt(p, 10) >= 0); |     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( |     server = require('http').createServer( | ||||||
|       greenlock.middleware.sanitizeHost(greenlock.middleware(require('redirect-https')())) |       greenlock.middleware.sanitizeHost(greenlock.middleware(require('redirect-https')())) | ||||||
|     ); |     ); | ||||||
|     httpType = 'http'; |     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; |     var https; | ||||||
|     try { |     try { | ||||||
|       https = require('spdy'); |       https = require('spdy'); | ||||||
| @ -58,6 +124,31 @@ module.exports.create = function (opts) { | |||||||
|       https = require('https'); |       https = require('https'); | ||||||
|       httpType = '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( |     server = https.createServer( | ||||||
|       greenlock.tlsOptions |       greenlock.tlsOptions | ||||||
|     , greenlock.middleware.sanitizeHost(function (req, res) { |     , greenlock.middleware.sanitizeHost(function (req, res) { | ||||||
| @ -77,17 +168,10 @@ module.exports.create = function (opts) { | |||||||
|       }) |       }) | ||||||
|     ); |     ); | ||||||
|     server.type = httpType; |     server.type = httpType; | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     if (addr) { args[1] = addr; } |     return { server: server, listen: function () { return new PromiseA(function (resolve) { | ||||||
|     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) { |  | ||||||
|       args[0] = p; |       args[0] = p; | ||||||
|       args.push(function () { resolve(server); }); |       args.push(function () { resolve(/*server*/); }); | ||||||
|       server.listen.apply(server, args).on('error', function (e) { |       server.listen.apply(server, args).on('error', function (e) { | ||||||
|         if (server.listenerCount('error') < 2) { |         if (server.listenerCount('error') < 2) { | ||||||
|           console.warn("Did not successfully create http server and bind to port '" + p + "':"); |           console.warn("Did not successfully create http server and bind to port '" + p + "':"); | ||||||
| @ -95,10 +179,7 @@ module.exports.create = function (opts) { | |||||||
|           process.exit(41); |           process.exit(41); | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|     }); |     }); } }; | ||||||
| 
 |  | ||||||
|     promise.server = server; |  | ||||||
|     return promise; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // NOTE: 'greenlock' is just 'opts' renamed
 |   // NOTE: 'greenlock' is just 'opts' renamed
 | ||||||
| @ -111,7 +192,6 @@ module.exports.create = function (opts) { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   opts.listen = function (plainPort, port, fnPlain, fn) { |   opts.listen = function (plainPort, port, fnPlain, fn) { | ||||||
|     var promises = []; |  | ||||||
|     var server; |     var server; | ||||||
|     var plainServer; |     var plainServer; | ||||||
| 
 | 
 | ||||||
| @ -122,13 +202,18 @@ module.exports.create = function (opts) { | |||||||
|       fnPlain = null; |       fnPlain = null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     promises.push(_listen(plainPort, true)); |     var obj1 = _createPlain(plainPort, true); | ||||||
|     promises.push(_listen(port, false)); |     var obj2 = _create(port, false); | ||||||
| 
 | 
 | ||||||
|     server = promises[1].server; |     plainServer = obj1.server; | ||||||
|     plainServer = promises[0].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
 |         // Report plain http status
 | ||||||
|         if ('function' === typeof fnPlain) { |         if ('function' === typeof fnPlain) { | ||||||
|           fnPlain.apply(plainServer); |           fnPlain.apply(plainServer); | ||||||
| @ -144,6 +229,7 @@ module.exports.create = function (opts) { | |||||||
|           console.info('[:' + (server.address().port || server.address()) + "] Serving " + server.type); |           console.info('[:' + (server.address().port || server.address()) + "] Serving " + server.type); | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|  |     }).then; | ||||||
| 
 | 
 | ||||||
|     server.unencrypted = plainServer; |     server.unencrypted = plainServer; | ||||||
|     return server; |     return server; | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "greenlock-express", |   "name": "greenlock-express", | ||||||
|   "version": "2.5.0", |   "version": "2.6.4", | ||||||
|   "description": "Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.", |   "description": "Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.", | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
|   "homepage": "https://git.coolaj86.com/coolaj86/greenlock-express.js", |   "homepage": "https://git.coolaj86.com/coolaj86/greenlock-express.js", | ||||||
| @ -8,7 +8,7 @@ | |||||||
|     "example": "examples" |     "example": "examples" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "greenlock": "^2.5.0", |     "greenlock": "^2.6.7", | ||||||
|     "le-challenge-fs": "^2.0.8", |     "le-challenge-fs": "^2.0.8", | ||||||
|     "le-sni-auto": "^2.1.4", |     "le-sni-auto": "^2.1.4", | ||||||
|     "le-store-certbot": "^2.1.0", |     "le-store-certbot": "^2.1.0", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user