mirror of
				https://github.com/therootcompany/greenlock.js.git
				synced 2025-10-31 03:52:46 +00:00 
			
		
		
		
	Merge branch 'v2.x' of github.com:Daplie/node-letsencrypt into v2.x
This commit is contained in:
		
						commit
						c246c196b9
					
				
							
								
								
									
										72
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								README.md
									
									
									
									
									
								
							| @ -7,23 +7,12 @@ | ||||
| | [letsencrypt-hapi](https://github.com/Daplie/letsencrypt-hapi) | ||||
| | | ||||
| 
 | ||||
| letsencrypt (v2) | ||||
| letsencrypt | ||||
| =========== | ||||
| 
 | ||||
| Automatic [Let's Encrypt](https://letsencrypt.org) HTTPS / TLS / SSL Certificates for node.js | ||||
| 
 | ||||
|   * [Automatic HTTPS with ExpressJS](https://github.com/Daplie/letsencrypt-express) | ||||
|   * [Automatic live renewal](https://github.com/Daplie/letsencrypt-express#how-automatic) | ||||
|   * On-the-fly HTTPS certificates for Dynamic DNS (in-process, no server restart) | ||||
|   * Works with node cluster out of the box | ||||
|   * usable [via commandline](https://github.com/Daplie/letsencrypt-cli) as well | ||||
|   * Free SSL (HTTPS Certificates for TLS) | ||||
|   * [90-day certificates](https://letsencrypt.org/2015/11/09/why-90-days.html) | ||||
| 
 | ||||
| **See Also** | ||||
| 
 | ||||
| * [Let's Encrypt in (exactly) 90 seconds with Caddy](https://daplie.com/articles/lets-encrypt-in-literally-90-seconds/) | ||||
| * [lego](https://github.com/xenolf/lego): Let's Encrypt for golang | ||||
| Free SLL with [90-day](https://letsencrypt.org/2015/11/09/why-90-days.html) HTTPS / TLS Certificates | ||||
| 
 | ||||
| STOP | ||||
| ==== | ||||
| @ -71,7 +60,7 @@ It's very simple and easy to use, but also very complete and easy to extend and | ||||
| 
 | ||||
| ### Overly Simplified Example | ||||
| 
 | ||||
| Against my better judgement I'm providing a terribly oversimplified exmaple | ||||
| Against my better judgement I'm providing a terribly oversimplified example | ||||
| of how to use this library: | ||||
| 
 | ||||
| ```javascript | ||||
| @ -148,37 +137,36 @@ le = LE.create({ | ||||
| 
 | ||||
| 
 | ||||
| // Check in-memory cache of certificates for the named domain | ||||
| le.exists({ domain: 'example.com' }).then(function (results) { | ||||
| le.check({ domain: 'example.com' }).then(function (results) { | ||||
|   if (results) { | ||||
|     // we already have certificates | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   // Register Certificate manually | ||||
|   le.register( | ||||
|   le.get({ | ||||
| 
 | ||||
|     { domains: ['example.com']                                // CHANGE TO YOUR DOMAIN (list for SANS) | ||||
|     , email: 'user@email.com'                                 // CHANGE TO YOUR EMAIL | ||||
|     , agreeTos: ''                                            // set to tosUrl string to pre-approve (and skip agreeToTerms) | ||||
|     , rsaKeySize: 2048                                        // 1024 or 2048 | ||||
|     , challengeType: 'http-01'                                // http-01, tls-sni-01, or dns-01 | ||||
|     } | ||||
|     domains: ['example.com']                                // CHANGE TO YOUR DOMAIN (list for SANS) | ||||
|   , email: 'user@email.com'                                 // CHANGE TO YOUR EMAIL | ||||
|   , agreeTos: ''                                            // set to tosUrl string (or true) to pre-approve (and skip agreeToTerms) | ||||
|   , rsaKeySize: 2048                                        // 2048 or higher | ||||
|   , challengeType: 'http-01'                                // http-01, tls-sni-01, or dns-01 | ||||
| 
 | ||||
|   , function (err, results) { | ||||
|       if (err) { | ||||
|         // Note: you must either use le.middleware() with express, | ||||
|         // manually use le.getChallenge(domain, key, val, done) | ||||
|         // or have a webserver running and responding | ||||
|         // to /.well-known/acme-challenge at `webrootPath` | ||||
|         console.error('[Error]: node-letsencrypt/examples/standalone'); | ||||
|         console.error(err.stack); | ||||
|         return; | ||||
|       } | ||||
|   }).then(function (results) { | ||||
| 
 | ||||
|       console.log('success'); | ||||
|     } | ||||
|     console.log('success'); | ||||
| 
 | ||||
|   ); | ||||
|   }, function (err) { | ||||
| 
 | ||||
|     // Note: you must either use le.middleware() with express, | ||||
|     // manually use le.getChallenge(domain, key, val, done) | ||||
|     // or have a webserver running and responding | ||||
|     // to /.well-known/acme-challenge at `webrootPath` | ||||
|     console.error('[Error]: node-letsencrypt/examples/standalone'); | ||||
|     console.error(err.stack); | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| }); | ||||
| ``` | ||||
| @ -200,6 +188,12 @@ API | ||||
| 
 | ||||
| The full end-user API is exposed in the example above and includes all relevant options. | ||||
| 
 | ||||
| ``` | ||||
| le.register | ||||
| le.get          // checkAndRegister | ||||
| le.check | ||||
| ``` | ||||
| 
 | ||||
| ### Helper Functions | ||||
| 
 | ||||
| We do expose a few helper functions: | ||||
| @ -241,7 +235,7 @@ TODO double check and finish | ||||
|   * accounts.get | ||||
|   * accounts.exists | ||||
| * certs | ||||
|   * certs.byDomain | ||||
|   * certs.byAccount | ||||
|   * certs.all | ||||
|   * certs.get | ||||
|   * certs.exists | ||||
| @ -250,9 +244,9 @@ TODO double check and finish | ||||
| 
 | ||||
| TODO finish | ||||
| 
 | ||||
| * setChallenge(opts, domain, key, value, done);   // opts will be saved with domain/key | ||||
| * getChallenge(domain, key, done);                // opts will be retrieved by domain/key | ||||
| * removeChallenge(domain, key, done);             // opts will be retrieved by domain/key | ||||
| * `.set(opts, domain, key, value, done);`         // opts will be saved with domain/key | ||||
| * `.get(opts, domain, key, done);`                // opts will be retrieved by domain/key | ||||
| * `.remove(opts, domain, key, done);`             // opts will be retrieved by domain/key | ||||
| 
 | ||||
| Change History | ||||
| ============== | ||||
|  | ||||
							
								
								
									
										171
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										171
									
								
								index.js
									
									
									
									
									
								
							| @ -1,21 +1,20 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| // TODO handle www and no-www together somehow?
 | ||||
| 
 | ||||
| var PromiseA = require('bluebird'); | ||||
| var leCore = require('letiny-core'); | ||||
| var ACME = require('le-acme-core').ACME; | ||||
| 
 | ||||
| var LE = module.exports; | ||||
| LE.LE = LE; | ||||
| // in-process cache, shared between all instances
 | ||||
| var ipc = {}; | ||||
| 
 | ||||
| LE.defaults = { | ||||
|   server: leCore.productionServerUrl | ||||
| , stagingServer: leCore.stagingServerUrl | ||||
| , liveServer: leCore.productionServerUrl | ||||
|   productionServerUrl: ACME.productionServerUrl | ||||
| , stagingServerUrl: ACME.stagingServerUrl | ||||
| 
 | ||||
| , productionServerUrl: leCore.productionServerUrl | ||||
| , stagingServerUrl: leCore.stagingServerUrl | ||||
| , rsaKeySize: ACME.rsaKeySize || 2048 | ||||
| , challengeType: ACME.challengeType || 'http-01' | ||||
| 
 | ||||
| , acmeChallengePrefix: leCore.acmeChallengePrefix | ||||
| , acmeChallengePrefix: ACME.acmeChallengePrefix | ||||
| }; | ||||
| 
 | ||||
| // backwards compat
 | ||||
| @ -23,58 +22,108 @@ Object.keys(LE.defaults).forEach(function (key) { | ||||
|   LE[key] = LE.defaults[key]; | ||||
| }); | ||||
| 
 | ||||
| LE.create = function (defaults, handlers, backend) { | ||||
|   var Core = require('./lib/core'); | ||||
|   var core; | ||||
|   if (!backend) { backend = require('./lib/pycompat'); } | ||||
|   if (!handlers) { handlers = {}; } | ||||
|   if (!handlers.renewWithin) { handlers.renewWithin = 3 * 24 * 60 * 60 * 1000; } | ||||
|   if (!handlers.memorizeFor) { handlers.memorizeFor = 1 * 24 * 60 * 60 * 1000; } | ||||
|   if (!handlers.sniRegisterCallback) { | ||||
|     handlers.sniRegisterCallback = function (args, cache, cb) { | ||||
|       // TODO when we have ECDSA, just do this automatically
 | ||||
|       cb(null, null); | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   if (backend.create) { | ||||
|     backend = backend.create(defaults); | ||||
|   } | ||||
|   backend = PromiseA.promisifyAll(backend); | ||||
|   core = Core.create(defaults, handlers, backend); | ||||
| 
 | ||||
|   var le = { | ||||
|     backend: backend | ||||
|   , core: core | ||||
|     // register
 | ||||
|   , create: function (args, cb) { | ||||
|       return core.registerAsync(args).then(function (pems) { | ||||
|         cb(null, pems); | ||||
|       }, cb); | ||||
| // show all possible options
 | ||||
| var u; // undefined
 | ||||
| LE._undefined = { | ||||
|   acme: u | ||||
| , store: u | ||||
| , challenger: u | ||||
| , register: u | ||||
| , check: u | ||||
| , renewWithin: u | ||||
| , memorizeFor: u | ||||
| , acmeChallengePrefix: u | ||||
| , rsaKeySize: u | ||||
| , challengeType: u | ||||
| , server: u | ||||
| , agreeToTerms: u | ||||
| , _ipc: u | ||||
| }; | ||||
| LE._undefine = function (le) { | ||||
|   Object.keys(LE._undefined).forEach(function (key) { | ||||
|     if (!(key in le)) { | ||||
|       le[key] = u; | ||||
|     } | ||||
|     // fetch
 | ||||
|   , domain: function (args, cb) { | ||||
|       // TODO must return email, domains, tos, pems
 | ||||
|       return core.fetchAsync(args).then(function (certInfo) { | ||||
|         cb(null, certInfo); | ||||
|       }, cb); | ||||
|     } | ||||
|   , domains: function (args, cb) { | ||||
|       // TODO show all domains or limit by account
 | ||||
|       throw new Error('not implemented'); | ||||
|     } | ||||
|   , accounts: function (args, cb) { | ||||
|       // TODO show all accounts or limit by domain
 | ||||
|       throw new Error('not implemented'); | ||||
|     } | ||||
|   , account: function (args, cb) { | ||||
|       // TODO return one account
 | ||||
|       throw new Error('not implemented'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   // exists
 | ||||
|   // get
 | ||||
|   }); | ||||
| 
 | ||||
|   return le; | ||||
| }; | ||||
| LE.create = function (le) { | ||||
|   var PromiseA = require('bluebird'); | ||||
| 
 | ||||
|   le.acme = le.acme || ACME.create({ debug: le.debug }); | ||||
|   le.store = le.store || require('le-store-certbot').create({ debug: le.debug }); | ||||
|   le.challenger = le.challenger || require('le-store-certbot').create({ debug: le.debug }); | ||||
|   le.core = require('./lib/core'); | ||||
| 
 | ||||
|   le = LE._undefine(le); | ||||
|   le.acmeChallengePrefix = LE.acmeChallengePrefix; | ||||
|   le.rsaKeySize = le.rsaKeySize || LE.rsaKeySize; | ||||
|   le.challengeType = le.challengeType || LE.challengeType; | ||||
|   le._ipc = ipc; | ||||
| 
 | ||||
|   if (!le.renewWithin) { le.renewWithin = 3 * 24 * 60 * 60 * 1000; } | ||||
|   if (!le.memorizeFor) { le.memorizeFor = 1 * 24 * 60 * 60 * 1000; } | ||||
| 
 | ||||
|   if (!le.server) { | ||||
|     throw new Error("opts.server must be set to 'staging' or a production url, such as LE.productionServerUrl'"); | ||||
|   } | ||||
|   if ('staging' === le.server) { | ||||
|     le.server = LE.stagingServerUrl; | ||||
|   } | ||||
|   else if ('production' === le.server) { | ||||
|     le.server = LE.productionServerUrl; | ||||
|   } | ||||
| 
 | ||||
|   if (le.acme.create) { | ||||
|     le.acme = le.acme.create(le); | ||||
|   } | ||||
|   le.acme = PromiseA.promisifyAll(le.acme); | ||||
|   le._acmeOpts = le.acme.getOptions(); | ||||
|   Object.keys(le._acmeOpts).forEach(function (key) { | ||||
|     if (!(key in le)) { | ||||
|       le[key] = le._acmeOpts[key]; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   if (le.store.create) { | ||||
|     le.store = le.store.create(le); | ||||
|   } | ||||
|   le.store = PromiseA.promisifyAll(le.store); | ||||
|   le._storeOpts = le.store.getOptions(); | ||||
|   Object.keys(le._storeOpts).forEach(function (key) { | ||||
|     if (!(key in le)) { | ||||
|       le[key] = le._storeOpts[key]; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   if (le.challenger.create) { | ||||
|     le.challenger = le.challenger.create(le); | ||||
|   } | ||||
|   le.challenger = PromiseA.promisifyAll(le.challenger); | ||||
|   le._challengerOpts = le.challenger.getOptions(); | ||||
|   Object.keys(le._challengerOpts).forEach(function (key) { | ||||
|     if (!(key in le)) { | ||||
|       le[key] = le._challengerOpts[key]; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   if (le.core.create) { | ||||
|     le.core = le.core.create(le); | ||||
|   } | ||||
| 
 | ||||
|   le.register = function (args) { | ||||
|     return le.core.certificates.getAsync(args); | ||||
|   }; | ||||
| 
 | ||||
|   le.check = function (args) { | ||||
|     // TODO must return email, domains, tos, pems
 | ||||
|     return le.core.certificates.checkAsync(args); | ||||
|   }; | ||||
| 
 | ||||
|   le.middleware = function () { | ||||
|     return require('./lib/middleware')(le); | ||||
|   }; | ||||
| 
 | ||||
|   return le; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										469
									
								
								lib/core.js
									
									
									
									
									
								
							
							
						
						
									
										469
									
								
								lib/core.js
									
									
									
									
									
								
							| @ -1,280 +1,287 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var LE = require('../'); | ||||
| var ipc = {}; // in-process cache
 | ||||
| 
 | ||||
| module.exports.create = function (defaults, handlers, backend) { | ||||
|   var backendDefaults = backend.getDefaults && backend.getDefaults || backend.defaults || {}; | ||||
| 
 | ||||
|   defaults.server = defaults.server || LE.liveServer; | ||||
|   handlers.merge = require('./common').merge; | ||||
|   handlers.tplCopy = require('./common').tplCopy; | ||||
| 
 | ||||
| module.exports.create = function (le) { | ||||
|   var PromiseA = require('bluebird'); | ||||
|   var utils = require('./utils'); // merge, tplCopy;
 | ||||
|   var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA); | ||||
|   var LeCore = PromiseA.promisifyAll(require('letiny-core')); | ||||
|   var crypto = require('crypto'); | ||||
| 
 | ||||
|   function attachCertInfo(results) { | ||||
|     var getCertInfo = require('./cert-info').getCertInfo; | ||||
|     // XXX Note: Parsing the certificate info comes at a great cost (~500kb)
 | ||||
|     var certInfo = getCertInfo(results.cert); | ||||
|   var core = { | ||||
|     //
 | ||||
|     // Helpers
 | ||||
|     //
 | ||||
|     getAcmeUrlsAsync: function (args) { | ||||
|       var now = Date.now(); | ||||
| 
 | ||||
|     //results.issuedAt = arr[3].mtime.valueOf()
 | ||||
|     results.issuedAt = Date(certInfo.notBefore.value).valueOf(); // Date.now()
 | ||||
|     results.expiresAt = Date(certInfo.notAfter.value).valueOf(); | ||||
|       // TODO check response header on request for cache time
 | ||||
|       if ((now - le._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { | ||||
|         return PromiseA.resolve(le._ipc.acmeUrls); | ||||
|       } | ||||
| 
 | ||||
|     return results; | ||||
|   } | ||||
|       return le.acme.getAcmeUrlsAsync(args.server).then(function (data) { | ||||
|         le._ipc.acmeUrlsUpdatedAt = Date.now(); | ||||
|         le._ipc.acmeUrls = data; | ||||
| 
 | ||||
|   function createAccount(args, handlers) { | ||||
|     args.rsaKeySize = args.rsaKeySize || 2048; | ||||
|         return le._ipc.acmeUrls; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { | ||||
| 
 | ||||
|       return LeCore.registerNewAccountAsync({ | ||||
|         email: args.email | ||||
|       , newRegUrl: args._acmeUrls.newReg | ||||
|       , agreeToTerms: function (tosUrl, agree) { | ||||
|           // args.email = email; // already there
 | ||||
|           args.tosUrl = tosUrl; | ||||
|           handlers.agreeToTerms(args, agree); | ||||
|     //
 | ||||
|     // The Main Enchilada
 | ||||
|     //
 | ||||
| 
 | ||||
|     //
 | ||||
|     // Accounts
 | ||||
|     //
 | ||||
|   , accounts: { | ||||
|       registerAsync: function (args) { | ||||
|         var err; | ||||
| 
 | ||||
|         if (!args.email || !args.agreeTos || (parseInt(args.rsaKeySize, 10) < 2048)) { | ||||
|           err = new Error( | ||||
|             "In order to register an account both 'email' and 'agreeTos' must be present" | ||||
|               + " and 'rsaKeySize' must be 2048 or greater." | ||||
|           ); | ||||
|           err.code = 'E_ARGS'; | ||||
|           return PromiseA.reject(err); | ||||
|         } | ||||
|       , accountKeypair: keypair | ||||
| 
 | ||||
|       , debug: defaults.debug || args.debug || handlers.debug | ||||
|       }).then(function (body) { | ||||
|         // TODO XXX use sha256 (the python client uses md5)
 | ||||
|         // TODO ssh fingerprint (noted on rsa-compat issues page, I believe)
 | ||||
|         keypair.publicKeyMd5 = crypto.createHash('md5').update(RSA.exportPublicPem(keypair)).digest('hex'); | ||||
|         keypair.publicKeySha256 = crypto.createHash('sha256').update(RSA.exportPublicPem(keypair)).digest('hex'); | ||||
|         return utils.testEmail(args.email).then(function () { | ||||
| 
 | ||||
|         var accountId = keypair.publicKeyMd5; | ||||
|         var regr = { body: body }; | ||||
|         var account = {}; | ||||
|           return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { | ||||
|             // Note: the ACME urls are always fetched fresh on purpose
 | ||||
|             // TODO is this the right place for this?
 | ||||
|             return core.getAcmeUrlsAsync(args).then(function (urls) { | ||||
|               args._acmeUrls = urls; | ||||
| 
 | ||||
|         args.accountId = accountId; | ||||
|               return le.acme.registerNewAccountAsync({ | ||||
|                 email: args.email | ||||
|               , newRegUrl: args._acmeUrls.newReg | ||||
|               , agreeToTerms: function (tosUrl, agreeCb) { | ||||
|                   if (true === args.agreeTos || tosUrl === args.agreeTos || tosUrl === le.agreeToTerms) { | ||||
|                     agreeCb(null, tosUrl); | ||||
|                     return; | ||||
|                   } | ||||
| 
 | ||||
|         account.keypair = keypair; | ||||
|         account.regr = regr; | ||||
|         account.accountId = accountId; | ||||
|         account.id = accountId; | ||||
|                   // args.email = email;      // already there
 | ||||
|                   // args.domains = domains   // already there
 | ||||
|                   args.tosUrl = tosUrl; | ||||
|                   le.agreeToTerms(args, agreeCb); | ||||
|                 } | ||||
|               , accountKeypair: keypair | ||||
| 
 | ||||
|         args.account = account; | ||||
|               , debug: le.debug || args.debug | ||||
|               }).then(function (body) { | ||||
|                 // TODO XXX use sha256 (the python client uses md5)
 | ||||
|                 // TODO ssh fingerprint (noted on rsa-compat issues page, I believe)
 | ||||
|                 keypair.publicKeyMd5 = crypto.createHash('md5').update(RSA.exportPublicPem(keypair)).digest('hex'); | ||||
|                 keypair.publicKeySha256 = crypto.createHash('sha256').update(RSA.exportPublicPem(keypair)).digest('hex'); | ||||
| 
 | ||||
|                 var accountId = keypair.publicKeyMd5; | ||||
|                 var regr = { body: body }; | ||||
|                 var account = {}; | ||||
| 
 | ||||
|                 args.accountId = accountId; | ||||
| 
 | ||||
|                 account.keypair = keypair; | ||||
|                 account.regr = regr; | ||||
|                 account.accountId = accountId; | ||||
|                 account.id = accountId; | ||||
| 
 | ||||
|                 args.account = account; | ||||
| 
 | ||||
|                 return le.store.accounts.setAsync(args, account).then(function () { | ||||
|                   return account; | ||||
|                 }); | ||||
|               }); | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|     , getAsync: function (args) { | ||||
|         return core.accounts.checkAsync(args).then(function (account) { | ||||
|           if (account) { | ||||
|             return account; | ||||
|           } else { | ||||
|             return core.accounts.registerAsync(args); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|     , checkAsync: function (args) { | ||||
|         var requiredArgs = ['accountId', 'email', 'domains', 'domain']; | ||||
|         if (!requiredArgs.some(function (key) { return -1 !== Object.keys(args).indexOf(key) })) { | ||||
|           return PromiseA.reject(new Error( | ||||
|             "In order to register or retrieve an account one of '" + requiredArgs.join("', '") + "' must be present" | ||||
|           )); | ||||
|         } | ||||
| 
 | ||||
|         var copy = utils.merge(args, le); | ||||
|         args = utils.tplCopy(copy); | ||||
| 
 | ||||
|         return le.store.accounts.checkAsync(args).then(function (account) { | ||||
| 
 | ||||
|           if (!account) { | ||||
|             return null; | ||||
|           } | ||||
| 
 | ||||
|           args.account = account; | ||||
|           args.accountId = account.id; | ||||
| 
 | ||||
|         return backend.setAccountAsync(args, account).then(function () { | ||||
|           return account; | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function getAcmeUrls(args) { | ||||
|     var now = Date.now(); | ||||
| 
 | ||||
|     // TODO check response header on request for cache time
 | ||||
|     if ((now - ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { | ||||
|       return PromiseA.resolve(ipc.acmeUrls); | ||||
|     } | ||||
| 
 | ||||
|     return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { | ||||
|       ipc.acmeUrlsUpdatedAt = Date.now(); | ||||
|       ipc.acmeUrls = data; | ||||
| 
 | ||||
|       return ipc.acmeUrls; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function getCertificateAsync(args, defaults, handlers) { | ||||
|     args.rsaKeySize = args.rsaKeySize || 2048; | ||||
|     args.challengeType = args.challengeType || 'http-01'; | ||||
| 
 | ||||
|     function log() { | ||||
|       if (args.debug || defaults.debug) { | ||||
|         console.log.apply(console, arguments); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     var account = args.account; | ||||
|     var promise; | ||||
|     var keypairOpts = { public: true, pem: true }; | ||||
|   , certificates: { | ||||
|       registerAsync: function (args) { | ||||
|         var err; | ||||
|         var copy = utils.merge(args, le); | ||||
|         args = utils.tplCopy(copy); | ||||
| 
 | ||||
|     promise = backend.getPrivatePem(args).then(function (pem) { | ||||
|       return RSA.import({ privateKeyPem: pem }); | ||||
|     }, function (/*err*/) { | ||||
|       return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) { | ||||
|         keypair.privateKeyPem = RSA.exportPrivatePem(keypair); | ||||
|         keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); | ||||
|         return backend.setPrivatePem(args, keypair); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     return promise.then(function (domainKeypair) { | ||||
|       log("[le/core.js] get certificate"); | ||||
| 
 | ||||
|       args.domainKeypair = domainKeypair; | ||||
|       //args.registration = domainKey;
 | ||||
| 
 | ||||
|       return LeCore.getCertificateAsync({ | ||||
|         debug: args.debug | ||||
| 
 | ||||
|       , newAuthzUrl: args._acmeUrls.newAuthz | ||||
|       , newCertUrl: args._acmeUrls.newCert | ||||
| 
 | ||||
|       , accountKeypair: RSA.import(account.keypair) | ||||
|       , domainKeypair: domainKeypair | ||||
|       , domains: args.domains | ||||
|       , challengeType: args.challengeType | ||||
| 
 | ||||
|         //
 | ||||
|         // IMPORTANT
 | ||||
|         //
 | ||||
|         // setChallenge and removeChallenge are handed defaults
 | ||||
|         // instead of args because getChallenge does not have
 | ||||
|         // access to args
 | ||||
|         // (args is per-request, defaults is per instance)
 | ||||
|         //
 | ||||
|       , setChallenge: function (domain, key, value, done) { | ||||
|           var copy = handlers.merge({ domains: [domain] }, defaults, backendDefaults); | ||||
|           handlers.tplCopy(copy); | ||||
| 
 | ||||
|           //args.domains = [domain];
 | ||||
|           args.domains = args.domains || [domain]; | ||||
| 
 | ||||
|           if (5 !== handlers.setChallenge.length) { | ||||
|             done(new Error("handlers.setChallenge receives the wrong number of arguments." | ||||
|               + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           handlers.setChallenge(copy, domain, key, value, done); | ||||
|         if (!Array.isArray(args.domains)) { | ||||
|           return PromiseA.reject(new Error('args.domains should be an array of domains')); | ||||
|         } | ||||
|       , removeChallenge: function (domain, key, done) { | ||||
|           var copy = handlers.merge({ domains: [domain] }, defaults, backendDefaults); | ||||
|           handlers.tplCopy(copy); | ||||
| 
 | ||||
|           if (4 !== handlers.removeChallenge.length) { | ||||
|             done(new Error("handlers.removeChallenge receives the wrong number of arguments." | ||||
|               + " You must define removeChallenge as function (opts, domain, key, cb) { }")); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           handlers.removeChallenge(copy, domain, key, done); | ||||
|         if (!(args.domains.length && args.domains.every(utils.isValidDomain))) { | ||||
|           // NOTE: this library can't assume to handle the http loopback
 | ||||
|           // (or dns-01 validation may be used)
 | ||||
|           // so we do not check dns records or attempt a loopback here
 | ||||
|           err = new Error("invalid domain name(s): '" + args.domains + "'"); | ||||
|           err.code = "INVALID_DOMAIN"; | ||||
|           return PromiseA.reject(err); | ||||
|         } | ||||
|       }).then(attachCertInfo); | ||||
|     }).then(function (results) { | ||||
|       // { cert, chain, fullchain, privkey }
 | ||||
| 
 | ||||
|       args.pems = results; | ||||
|       return backend.setRegistration(args, defaults, handlers); | ||||
|     }); | ||||
|   } | ||||
|         return core.accounts.getAsync(copy).then(function (account) { | ||||
|           copy.account = account; | ||||
| 
 | ||||
|   function getOrCreateDomainCertificate(args, defaults, handlers) { | ||||
|     if (args.duplicate) { | ||||
|       // we're forcing a refresh via 'dupliate: true'
 | ||||
|       return getCertificateAsync(args, defaults, handlers); | ||||
|     } | ||||
|           //var account = args.account;
 | ||||
|           var keypairOpts = { public: true, pem: true }; | ||||
| 
 | ||||
|     return wrapped.fetchAsync(args).then(function (certs) { | ||||
|       var halfLife = (certs.expiresAt - certs.issuedAt) / 2; | ||||
|           var promise = le.store.certificates.checkKeypairAsync(args).then(function (keypair) { | ||||
|             return RSA.import(keypair); | ||||
|           }, function (/*err*/) { | ||||
|             return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) { | ||||
|               keypair.privateKeyPem = RSA.exportPrivatePem(keypair); | ||||
|               keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); | ||||
|               return le.store.certificates.setKeypairAsync(args, keypair); | ||||
|             }); | ||||
|           }); | ||||
| 
 | ||||
|       if (!certs || (Date.now() - certs.issuedAt) > halfLife) { | ||||
|         // There is no cert available
 | ||||
|         // Or the cert is more than half-expired
 | ||||
|         return getCertificateAsync(args, defaults, handlers); | ||||
|       } | ||||
|           return promise.then(function (domainKeypair) { | ||||
|             args.domainKeypair = domainKeypair; | ||||
|             //args.registration = domainKey;
 | ||||
| 
 | ||||
|       return PromiseA.reject(new Error( | ||||
|           "[ERROR] Certificate issued at '" | ||||
|         + new Date(certs.issuedAt).toISOString() + "' and expires at '" | ||||
|         + new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until half-life at '" | ||||
|         + new Date(certs.issuedA + halfLife).toISOString() + "'. Set { duplicate: true } to force." | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|             // Note: the ACME urls are always fetched fresh on purpose
 | ||||
|             // TODO is this the right place for this?
 | ||||
|             return core.getAcmeUrlsAsync(args).then(function (urls) { | ||||
|               args._acmeUrls = urls; | ||||
| 
 | ||||
|   // returns 'account' from lib/accounts { meta, regr, keypair, accountId (id) }
 | ||||
|   function getOrCreateAcmeAccount(args, defaults, handlers) { | ||||
|     function log() { | ||||
|       if (args.debug) { | ||||
|         console.log.apply(console, arguments); | ||||
|       } | ||||
|     } | ||||
|               return le.acme.getCertificateAsync({ | ||||
|                 debug: args.debug || le.debug | ||||
| 
 | ||||
|     return backend.getAccountId(args).then(function (accountId) { | ||||
|               , newAuthzUrl: args._acmeUrls.newAuthz | ||||
|               , newCertUrl: args._acmeUrls.newCert | ||||
| 
 | ||||
|       // Note: the ACME urls are always fetched fresh on purpose
 | ||||
|       return getAcmeUrls(args).then(function (urls) { | ||||
|         args._acmeUrls = urls; | ||||
|               , accountKeypair: RSA.import(account.keypair) | ||||
|               , domainKeypair: domainKeypair | ||||
|               , domains: args.domains | ||||
|               , challengeType: args.challengeType | ||||
| 
 | ||||
|         if (accountId) { | ||||
|           log('[le/core.js] use account'); | ||||
|                 //
 | ||||
|                 // IMPORTANT
 | ||||
|                 //
 | ||||
|                 // setChallenge and removeChallenge are handed defaults
 | ||||
|                 // instead of args because getChallenge does not have
 | ||||
|                 // access to args
 | ||||
|                 // (args is per-request, defaults is per instance)
 | ||||
|                 //
 | ||||
|               , setChallenge: function (domain, key, value, done) { | ||||
|                   var copy = utils.merge({ domains: [domain] }, le); | ||||
|                   utils.tplCopy(copy); | ||||
| 
 | ||||
|           args.accountId = accountId; | ||||
|           return backend.getAccount(args, handlers); | ||||
|         } else { | ||||
|           log('[le/core.js] create account'); | ||||
|           return createAccount(args, handlers); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|                   //args.domains = [domain];
 | ||||
|                   args.domains = args.domains || [domain]; | ||||
| 
 | ||||
|   var wrapped = { | ||||
|     registerAsync: function (args) { | ||||
|       var utils = require('./lib/common'); | ||||
|       var err; | ||||
|                   if (5 !== le.challenger.set.length) { | ||||
|                     done(new Error("le.challenger.set receives the wrong number of arguments." | ||||
|                       + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); | ||||
|                     return; | ||||
|                   } | ||||
| 
 | ||||
|       if (!Array.isArray(args.domains)) { | ||||
|         return PromiseA.reject(new Error('args.domains should be an array of domains')); | ||||
|       } | ||||
|                   le.challenger.set(copy, domain, key, value, done); | ||||
|                 } | ||||
|               , removeChallenge: function (domain, key, done) { | ||||
|                   var copy = utils.merge({ domains: [domain] }, le); | ||||
|                   utils.tplCopy(copy); | ||||
| 
 | ||||
|       if (!(args.domains.length && args.domains.every(utils.isValidDomain))) { | ||||
|         // NOTE: this library can't assume to handle the http loopback
 | ||||
|         // (or dns-01 validation may be used)
 | ||||
|         // so we do not check dns records or attempt a loopback here
 | ||||
|         err = new Error("invalid domain name(s): '" + args.domains + "'"); | ||||
|         err.code = "INVALID_DOMAIN"; | ||||
|         return PromiseA.reject(err); | ||||
|       } | ||||
|                   if (4 !== le.challenger.remove.length) { | ||||
|                     done(new Error("le.challenger.remove receives the wrong number of arguments." | ||||
|                       + " You must define removeChallenge as function (opts, domain, key, cb) { }")); | ||||
|                     return; | ||||
|                   } | ||||
| 
 | ||||
|       var copy = handlers.merge(args, defaults, backendDefaults); | ||||
|       handlers.tplCopy(copy); | ||||
|                   le.challenger.remove(copy, domain, key, done); | ||||
|                 } | ||||
|               }).then(utils.attachCertInfo); | ||||
|             }); | ||||
|           }).then(function (results) { | ||||
|             // { cert, chain, privkey }
 | ||||
| 
 | ||||
|       return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { | ||||
|         copy.account = account; | ||||
| 
 | ||||
|         return backend.getOrCreateRenewal(copy).then(function (pyobj) { | ||||
| 
 | ||||
|           copy.pyobj = pyobj; | ||||
|           return getOrCreateDomainCertificate(copy, defaults, handlers); | ||||
|             args.pems = results; | ||||
|             return le.store.certificates.setAsync(args).then(function () { | ||||
|               return results; | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
|       }).then(function (result) { | ||||
|         return result; | ||||
|       }, function (err) { | ||||
|         return PromiseA.reject(err); | ||||
|       }); | ||||
|     } | ||||
|   , getOrCreateAccount: function (args) { | ||||
|       return createAccount(args, handlers); | ||||
|     } | ||||
|   , configureAsync: function (hargs) { | ||||
|       var copy = handlers.merge(hargs, defaults, backendDefaults); | ||||
|       handlers.tplCopy(copy); | ||||
|       } | ||||
|     , renewAsync: function (args) { | ||||
|         // TODO fetch email address if not present
 | ||||
|         return core.certificates.registerAsync(args); | ||||
|       } | ||||
|     , checkAsync: function (args) { | ||||
|         var copy = utils.merge(args, le); | ||||
|         utils.tplCopy(copy); | ||||
| 
 | ||||
|       return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { | ||||
|         copy.account = account; | ||||
|         return backend.getOrCreateRenewal(copy); | ||||
|       }); | ||||
|     } | ||||
|   , fetchAsync: function (args) { | ||||
|       var copy = handlers.merge(args, defaults); | ||||
|       handlers.tplCopy(copy); | ||||
|         // returns pems
 | ||||
|         return le.store.certificates.checkAsync(copy).then(utils.attachCertInfo); | ||||
|       } | ||||
|     , getAsync: function (args) { | ||||
|         var copy = utils.merge(args, le); | ||||
|         args = utils.tplCopy(copy); | ||||
| 
 | ||||
|       return backend.fetchAsync(copy).then(attachCertInfo); | ||||
|         return core.certificates.checkAsync(args).then(function (certs) { | ||||
|           if (!certs) { | ||||
|             // There is no cert available
 | ||||
|             return core.certificates.registerAsync(args); | ||||
|           } | ||||
| 
 | ||||
|           var renewableAt = certs.expiresAt - le.renewWithin; | ||||
|           //var halfLife = (certs.expiresAt - certs.issuedAt) / 2;
 | ||||
|           //var renewable = (Date.now() - certs.issuedAt) > halfLife;
 | ||||
| 
 | ||||
|           if (args.duplicate || Date.now() >= renewableAt) { | ||||
|             // The cert is more than half-expired
 | ||||
|             // We're forcing a refresh via 'dupliate: true'
 | ||||
|             return core.certificates.renewAsync(args); | ||||
|           } | ||||
| 
 | ||||
|           return PromiseA.reject(new Error( | ||||
|               "[ERROR] Certificate issued at '" | ||||
|             + new Date(certs.issuedAt).toISOString() + "' and expires at '" | ||||
|             + new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until half-life at '" | ||||
|             + new Date(renewableAt).toISOString() + "'. Set { duplicate: true } to force." | ||||
|           )); | ||||
|         }).then(function (results) { | ||||
|           // returns pems
 | ||||
|           return results; | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|   }; | ||||
| 
 | ||||
|   return wrapped; | ||||
|   return core; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										31
									
								
								lib/middleware.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								lib/middleware.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| module.exports = function (le) { | ||||
|   return function () { | ||||
|     var prefix = le.acmeChallengePrefix; // /.well-known/acme-challenge/:token
 | ||||
| 
 | ||||
|     return function (req, res, next) { | ||||
|       if (0 !== req.url.indexOf(prefix)) { | ||||
|         next(); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       var key = req.url.slice(prefix.length); | ||||
|       var hostname = req.hostname || (req.headers.host || '').toLowerCase().replace(/:*/, ''); | ||||
| 
 | ||||
|       // TODO tpl copy?
 | ||||
|       le.challenger.getAsync(le, hostname, key).then(function (token) { | ||||
|         if (!token) { | ||||
|           res.status = 404; | ||||
|           res.send("Error: These aren't the tokens you're looking for. Move along."); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         res.send(token); | ||||
|       }, function (/*err*/) { | ||||
|         res.status = 404; | ||||
|         res.send("Error: These aren't the tokens you're looking for. Move along."); | ||||
|       }); | ||||
|     }; | ||||
|   }; | ||||
| }; | ||||
| @ -4,6 +4,20 @@ var path = require('path'); | ||||
| var homeRe = new RegExp("^~(\\/|\\\|\\" + path.sep + ")"); | ||||
| var re = /^[a-zA-Z0-9\.\-]+$/; | ||||
| var punycode = require('punycode'); | ||||
| var PromiseA = require('bluebird'); | ||||
| var dns = PromiseA.promisifyAll(require('dns')); | ||||
| 
 | ||||
| module.exports.attachCertInfo = function (results) { | ||||
|   var getCertInfo = require('./cert-info').getCertInfo; | ||||
|   // XXX Note: Parsing the certificate info comes at a great cost (~500kb)
 | ||||
|   var certInfo = getCertInfo(results.cert); | ||||
| 
 | ||||
|   //results.issuedAt = arr[3].mtime.valueOf()
 | ||||
|   results.issuedAt = Date(certInfo.notBefore.value).valueOf(); // Date.now()
 | ||||
|   results.expiresAt = Date(certInfo.notAfter.value).valueOf(); | ||||
| 
 | ||||
|   return results; | ||||
| }; | ||||
| 
 | ||||
| module.exports.isValidDomain = function (domain) { | ||||
|   if (re.test(domain)) { | ||||
| @ -21,7 +35,7 @@ module.exports.isValidDomain = function (domain) { | ||||
| 
 | ||||
| module.exports.merge = function (/*defaults, args*/) { | ||||
|   var allDefaults = Array.prototype.slice.apply(arguments); | ||||
|   var args = args.shift(); | ||||
|   var args = allDefaults.shift(); | ||||
|   var copy = {}; | ||||
| 
 | ||||
|   allDefaults.forEach(function (defaults) { | ||||
| @ -63,4 +77,31 @@ module.exports.tplCopy = function (copy) { | ||||
|       copy[key] = copy[key].replace(':' + tplname, tpls[tplname]); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   return copy; | ||||
| }; | ||||
| 
 | ||||
| module.exports.testEmail = function (email) { | ||||
|   var parts = (email||'').split('@'); | ||||
|   var err; | ||||
| 
 | ||||
|   if (2 !== parts.length || !parts[0] || !parts[1]) { | ||||
|     err = new Error("malformed email address '" + email + "'"); | ||||
|     err.code = 'E_EMAIL'; | ||||
|     return PromiseA.reject(err); | ||||
|   } | ||||
| 
 | ||||
|   return dns.resolveMxAsync(parts[1]).then(function (records) { | ||||
|     // records only returns when there is data
 | ||||
|     if (!records.length) { | ||||
|       throw new Error("sanity check fail: success, but no MX records returned"); | ||||
|     } | ||||
|     return email; | ||||
|   }, function (err) { | ||||
|     if ('ENODATA' === err.code) { | ||||
|       err = new Error("no MX records found for '" + parts[1] + "'"); | ||||
|       err.code = 'E_EMAIL'; | ||||
|       return PromiseA.reject(err); | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
							
								
								
									
										117
									
								
								tests/create-account.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								tests/create-account.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,117 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var LE = require('../').LE; | ||||
| var le = LE.create({ | ||||
|   server: 'staging' | ||||
| , acme: require('le-acme-core').ACME.create() | ||||
| , store: require('le-store-certbot').create({ | ||||
|     configDir: '~/letsencrypt.test/etc/' | ||||
|   }) | ||||
| }); | ||||
| 
 | ||||
| var testId = Math.round(Date.now() / 1000).toString(); | ||||
| var fakeEmail = 'coolaj86+le.' + testId + '@example.com'; | ||||
| var testEmail = 'coolaj86+le.' + testId + '@example.com'; | ||||
| var testAccount; | ||||
| 
 | ||||
| var tests = [ | ||||
|   function () { | ||||
|     return le.core.accounts.checkAsync({ | ||||
|       email: testEmail | ||||
|     }).then(function (account) { | ||||
|       if (account) { | ||||
|         console.error(account); | ||||
|         throw new Error("Test account should not exist."); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| , function () { | ||||
|     return le.core.accounts.registerAsync({ | ||||
|       email: testEmail | ||||
|     , agreeTos: false | ||||
|     , rsaKeySize: 2048 | ||||
|     }).then(function (/*account*/) { | ||||
|       throw new Error("Should not register if 'agreeTos' is not truthy."); | ||||
|     }, function (err) { | ||||
|       if (err.code !== 'E_ARGS') { | ||||
|         throw err; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| , function () { | ||||
|     return le.core.accounts.registerAsync({ | ||||
|       email: testEmail | ||||
|     , agreeTos: true | ||||
|     , rsaKeySize: 1024 | ||||
|     }).then(function (/*account*/) { | ||||
|       throw new Error("Should not register if 'rsaKeySize' is less than 2048."); | ||||
|     }, function (err) { | ||||
|       if (err.code !== 'E_ARGS') { | ||||
|         throw err; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| , function () { | ||||
|     return le.core.accounts.registerAsync({ | ||||
|       email: fakeEmail | ||||
|     , agreeTos: true | ||||
|     , rsaKeySize: 2048 | ||||
|     }).then(function (/*account*/) { | ||||
|       // TODO test mx record
 | ||||
|       throw new Error("Registration should NOT succeed with a bad email address."); | ||||
|     }, function (err) { | ||||
|       if (err.code !== 'E_EMAIL') { | ||||
|         throw err; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| , function () { | ||||
|     throw new Error('NOT IMPLEMENTED'); | ||||
|     return le.core.accounts.registerAsync({ | ||||
|       email: 'coolaj86+le.' + testId + '@example.com' | ||||
|     , agreeTos: true | ||||
|     , rsaKeySize: 2048 | ||||
|     }).then(function (account) { | ||||
|       testAccount = account; | ||||
|       if (!account) { | ||||
|         throw new Error("Registration should always return a new account."); | ||||
|       } | ||||
|       if (!account.email) { | ||||
|         throw new Error("Registration should return the email."); | ||||
|       } | ||||
|       if (!account.id) { | ||||
|         throw new Error("Registration should return the account id."); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| , function () { | ||||
|     return le.core.accounts.checkAsync({ | ||||
|       email: testAccount.email | ||||
|     }).then(function (account) { | ||||
|       if (!account) { | ||||
|         throw new Error("Test account should exist when searched by email."); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| , function () { | ||||
|     return le.core.accounts.checkAsync({ | ||||
|       accountId: testAccount.id | ||||
|     }).then(function (account) { | ||||
|       if (!account) { | ||||
|         throw new Error("Test account should exist when searched by account id."); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| ]; | ||||
| 
 | ||||
| function run() { | ||||
|   var test = tests.shift(); | ||||
|   if (!test) { | ||||
|     console.info('All tests passed'); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   test().then(run); | ||||
| } | ||||
| 
 | ||||
| run(); | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user