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'; | '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 |  | ||||||
|   // 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 |  | ||||||
| 
 | 
 | ||||||
| //, debug: true | //, debug: true | ||||||
| 
 |  | ||||||
| }).listen(80, 443); | }).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` | ### `communityMember` | ||||||
| 
 | 
 | ||||||
| If you're the kind of person that likes the kinds of stuff that I do, | 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** |   * 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 | ||||||
|  | |||||||
							
								
								
									
										222
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										222
									
								
								index.js
									
									
									
									
									
								
							| @ -15,6 +15,14 @@ module.exports.create = function (opts) { | |||||||
|     opts._communityPackageVersion = require('./package.json').version; |     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) { |   function explainError(e) { | ||||||
|     console.error('Error:' + e.message); |     console.error('Error:' + e.message); | ||||||
|     if ('EACCES' === e.errno) { |     if ('EACCES' === e.errno) { | ||||||
| @ -30,7 +38,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,53 +49,54 @@ module.exports.create = function (opts) { | |||||||
|     var server; |     var server; | ||||||
|     var validHttpPort = (parseInt(p, 10) >= 0); |     var validHttpPort = (parseInt(p, 10) >= 0); | ||||||
| 
 | 
 | ||||||
|     function tryPlain() { |  | ||||||
|       server = require('http').createServer( |  | ||||||
|         greenlock.middleware.sanitizeHost(greenlock.middleware(require('redirect-https')())) |  | ||||||
|       ); |  | ||||||
|       httpType = 'http'; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function trySecure() { |  | ||||||
|       var https; |  | ||||||
|       try { |  | ||||||
|         https = require('spdy'); |  | ||||||
|         greenlock.tlsOptions.spdy = { protocols: [ 'h2', 'http/1.1' ], plain: false }; |  | ||||||
|         httpType = 'http2 (spdy/h2)'; |  | ||||||
|       } catch(e) { |  | ||||||
|         https = require('https'); |  | ||||||
|         httpType = 'https'; |  | ||||||
|       } |  | ||||||
|       server = https.createServer( |  | ||||||
|         greenlock.tlsOptions |  | ||||||
|       , greenlock.middleware.sanitizeHost(function (req, res) { |  | ||||||
|           try { |  | ||||||
|             greenlock.app(req, res); |  | ||||||
|           } catch(e) { |  | ||||||
|             console.error("[error] [greenlock.app] Your HTTP handler had an uncaught error:"); |  | ||||||
|             console.error(e); |  | ||||||
|             try { |  | ||||||
|               res.statusCode = 500; |  | ||||||
|               res.end("Internal Server Error: [Greenlock] HTTP exception logged for user-provided handler."); |  | ||||||
|             } catch(e) { |  | ||||||
|               // ignore
 |  | ||||||
|               // (headers may have already been sent, etc)
 |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|       ); |  | ||||||
|       server.type = httpType; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (addr) { args[1] = addr; } |     if (addr) { args[1] = addr; } | ||||||
|     if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) { |     if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) { | ||||||
|       console.warn("'" + p + "' doesn't seem to be a valid port number, socket path, or pipe"); |       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) { |     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[0] = p; | ||||||
|       args.push(function () { resolve(server); }); |       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) { |       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 +104,90 @@ module.exports.create = function (opts) { | |||||||
|           process.exit(41); |           process.exit(41); | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|     }); |     }); } }; | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     promise.server = server; |   function _create(port) { | ||||||
|     return promise; |     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"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var https; | ||||||
|  |     try { | ||||||
|  |       https = require('spdy'); | ||||||
|  |       greenlock.tlsOptions.spdy = { protocols: [ 'h2', 'http/1.1' ], plain: false }; | ||||||
|  |       httpType = 'http2 (spdy/h2)'; | ||||||
|  |     } catch(e) { | ||||||
|  |       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) { | ||||||
|  |         try { | ||||||
|  |           greenlock.app(req, res); | ||||||
|  |         } catch(e) { | ||||||
|  |           console.error("[error] [greenlock.app] Your HTTP handler had an uncaught error:"); | ||||||
|  |           console.error(e); | ||||||
|  |           try { | ||||||
|  |             res.statusCode = 500; | ||||||
|  |             res.end("Internal Server Error: [Greenlock] HTTP exception logged for user-provided handler."); | ||||||
|  |           } catch(e) { | ||||||
|  |             // ignore
 | ||||||
|  |             // (headers may have already been sent, etc)
 | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |     server.type = httpType; | ||||||
|  | 
 | ||||||
|  |     return { server: server, listen: function () { return new PromiseA(function (resolve) { | ||||||
|  |       args[0] = p; | ||||||
|  |       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 + "':"); | ||||||
|  |           explainError(e); | ||||||
|  |           process.exit(41); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); } }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // NOTE: 'greenlock' is just 'opts' renamed
 |   // NOTE: 'greenlock' is just 'opts' renamed
 | ||||||
| @ -111,7 +200,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,28 +210,34 @@ 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) { | ||||||
|       // Report plain http status
 |       if (tlsOptions) { | ||||||
|       if ('function' === typeof fnPlain) { |         server.setSecureContext(tlsOptions); | ||||||
|         fnPlain.apply(plainServer); |         server._hasDefaultSecureContext = true; | ||||||
|       } else if (!fn && !plainServer.listenerCount('listening') && !server.listenerCount('listening')) { |  | ||||||
|         console.info('[:' + (plainServer.address().port || plainServer.address()) |  | ||||||
|           + "] Handling ACME challenges and redirecting to " + server.type); |  | ||||||
|       } |       } | ||||||
|  |       return obj2.listen().then(function () { | ||||||
|  |         // Report plain http status
 | ||||||
|  |         if ('function' === typeof fnPlain) { | ||||||
|  |           fnPlain.apply(plainServer); | ||||||
|  |         } else if (!fn && !plainServer.listenerCount('listening') && !server.listenerCount('listening')) { | ||||||
|  |           console.info('[:' + (plainServer.address().port || plainServer.address()) | ||||||
|  |             + "] Handling ACME challenges and redirecting to " + server.type); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|       // Report h2/https status
 |         // Report h2/https status
 | ||||||
|       if ('function' === typeof fn) { |         if ('function' === typeof fn) { | ||||||
|         fn.apply(server); |           fn.apply(server); | ||||||
|       } else if (!server.listenerCount('listening')) { |         } else if (!server.listenerCount('listening')) { | ||||||
|         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.0", | ||||||
|   "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.3", | ||||||
|     "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