256 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			256 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | 'use strict'; | ||
|  | 
 | ||
|  | var PromiseA = require('bluebird'); | ||
|  | var path = require('path'); | ||
|  | var fs = PromiseA.promisifyAll(require('fs')); | ||
|  | var cutils = PromiseA.promisifyAll(require('crypto-utils-ursa')); | ||
|  | //var futils = require('letsencrypt-forge/lib/crypto-utils');
 | ||
|  | var requestAsync = PromiseA.promisify(require('request')); | ||
|  | var lef = PromiseA.promisifyAll(require('letsencrypt-forge')); | ||
|  | var knownUrls = ['new-authz', 'new-cert', 'new-reg', 'revoke-cert']; | ||
|  | 
 | ||
|  | var ipc = {}; // in-process cache
 | ||
|  | 
 | ||
|  | //function noop() {}
 | ||
|  | 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 requestAsync({ | ||
|  |     url: args.server | ||
|  |   }).then(function (data) { | ||
|  |     if (4 !== Object.keys(data).length) { | ||
|  |       console.warn("This Let's Encrypt / ACME server has been updated with urls that this client doesn't understand"); | ||
|  |     } | ||
|  |     if (!knownUrls.every(function (url) { | ||
|  |       return data[url]; | ||
|  |     })) { | ||
|  |       console.warn("This Let's Encrypt / ACME server is missing urls that this client may need."); | ||
|  |     } | ||
|  | 
 | ||
|  |     ipc.acmeUrlsUpdatedAt = Date.now(); | ||
|  |     ipc.acmeUrls = { | ||
|  |       newAuthz: data['new-authz'] | ||
|  |     , newCert: data['new-cert'] | ||
|  |     , newReg: data['new-reg'] | ||
|  |     , revokeCert: data['revoke-cert'] | ||
|  |     }; | ||
|  | 
 | ||
|  |     return ipc.acmeUrls; | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | function createAccount(args, handlers) { | ||
|  |   var mkdirpAsync = PromiseA.promisify(require('mkdirp')); | ||
|  |   var os = require("os"); | ||
|  |   var localname = os.hostname(); | ||
|  | 
 | ||
|  |   // TODO support ECDSA
 | ||
|  |   // arg.rsaBitLength args.rsaExponent
 | ||
|  |   return cutils.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (obj) { | ||
|  |     /* obj = { privateKeyPem, publicKeyPem, publicKeyMd5 } */ | ||
|  | 
 | ||
|  |     var accountId = obj.publicKeyMd5; // I would have chosen sha1 or sha2... but whatever
 | ||
|  |     var accountDir = path.join(args.accountsDir, accountId); | ||
|  |     var isoDate = new Date().toISOString(); | ||
|  | 
 | ||
|  |     /* | ||
|  |       files.accountId = accountId;                  // md5sum(publicKeyPem)
 | ||
|  |       files.publicKeyPem = keypair.publicKeyPem;    // ascii PEM: ----BEGIN...
 | ||
|  |       files.privateKeyPem = keypair.privateKeyPem;  // ascii PEM: ----BEGIN...
 | ||
|  |       files.privateKeyJson = keypair.private_key;   // json { n: ..., e: ..., iq: ..., etc }
 | ||
|  |     */ | ||
|  | 
 | ||
|  |     // TODO register
 | ||
|  |     return lef.registerNewAccountAsync({ | ||
|  |       email: args.email | ||
|  |     , domains: Array.isArray(args.domains) || (args.domains||'').split(',') | ||
|  |     , newReg: args.server | ||
|  |     , debug: args.debug || handlers.debug | ||
|  |     , webroot: args.webrootPath | ||
|  |     , setChallenge: function (domain, key, value, done) { | ||
|  |         args.domains = [domain]; | ||
|  |         handlers.setChallenge(args, key, value, done); | ||
|  |       } | ||
|  |     , removeChallenge: function (domain, key, done) { | ||
|  |         args.domains = [domain]; | ||
|  |         handlers.removeChallenge(args, key, done); | ||
|  |       } | ||
|  |     , agreeToTerms: function (tosUrl, agree) { | ||
|  |         // args.email = email;
 | ||
|  |         args.tosUrl = tosUrl; | ||
|  |         handlers.agreeToTerms(args, agree); | ||
|  |       } | ||
|  |       // TODO send either privateKeyPem or privateKeyJson or privateKeyJwk (?)
 | ||
|  |     , privateKeyPem: obj.privateKeyPem | ||
|  |     }).then(function (body) { | ||
|  |       if ('string' === typeof body) { | ||
|  |         try { | ||
|  |           body = JSON.parse(body); | ||
|  |         } catch(e) { | ||
|  |           // ignore
 | ||
|  |         } | ||
|  |       } | ||
|  |       return mkdirpAsync(args.accountDir, function () { | ||
|  |         var jwk = cutils.toAcmePrivateKey(obj.privateKeyPem); | ||
|  |         // meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"}
 | ||
|  |         // private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" }
 | ||
|  |         // regr.json { "body" }
 | ||
|  |         /* | ||
|  |         { body: | ||
|  |         { contact: [ 'mailto:coolaj86@gmail.com' ], | ||
|  |          agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', | ||
|  |          key: | ||
|  |           { e: 'AQAB', | ||
|  |             kty: 'RSA', | ||
|  |             n: '...' } }, | ||
|  |           uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272', | ||
|  |           new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz', | ||
|  |           terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' } | ||
|  |          */ | ||
|  |         return PromiseA.all([ | ||
|  |           fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify({ creation_host: localname, creation_dt: isoDate }), 'utf8') | ||
|  |         , fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(jwk), 'utf8') | ||
|  |         , fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify({ body: body }), 'utf8') | ||
|  |         ]); | ||
|  |       }); | ||
|  |     }); | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | function getAccount(args, accountId) { | ||
|  |   var accountDir = path.join(args.accountsDir, accountId); | ||
|  |   var files = {}; | ||
|  |   var configs = ['meta.json', 'private_key.json', 'regr.json']; | ||
|  | 
 | ||
|  |   return PromiseA.all(configs.map(function (filename) { | ||
|  |     var keyname = filename.slice(0, -5); | ||
|  | 
 | ||
|  |     return fs.readFileAsync(path.join(accountDir, filename), 'utf8').then(function (text) { | ||
|  |       var data; | ||
|  | 
 | ||
|  |       try { | ||
|  |         data = JSON.parse(text); | ||
|  |       } catch(e) { | ||
|  |         files[keyname] = { error: e }; | ||
|  |         return; | ||
|  |       } | ||
|  | 
 | ||
|  |       files[keyname] = data; | ||
|  |     }, function (err) { | ||
|  |       files[keyname] = err; | ||
|  |     }); | ||
|  |   })).then(function () { | ||
|  | 
 | ||
|  |     if (!Object.keys(files).every(function (key) { | ||
|  |       return !files[key].error; | ||
|  |     })) { | ||
|  |       console.warn("Account '" + accountId + "' was currupt. No big deal (I think?). Creating a new one..."); | ||
|  |       return createAccount(args); | ||
|  |     } | ||
|  | 
 | ||
|  |     return cutils.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) { | ||
|  |       files.accountId = accountId;                  // md5sum(publicKeyPem)
 | ||
|  |       files.publicKeyPem = keypair.publicKeyPem;    // ascii PEM: ----BEGIN...
 | ||
|  |       files.privateKeyPem = keypair.privateKeyPem;  // ascii PEM: ----BEGIN...
 | ||
|  |       files.privateKeyJson = keypair.private_key;   // json { n: ..., e: ..., iq: ..., etc }
 | ||
|  | 
 | ||
|  |       return files; | ||
|  |     }); | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | function getAccountByEmail(args) { | ||
|  |   // If we read 10,000 account directories looking for
 | ||
|  |   // just one email address, that could get crazy.
 | ||
|  |   // We should have a folder per email and list
 | ||
|  |   // each account as a file in the folder
 | ||
|  |   // TODO
 | ||
|  |   return PromiseA.resolve(null); | ||
|  | } | ||
|  | 
 | ||
|  | module.exports.create = function (defaults, opts) { | ||
|  |   var LE = require('./'); | ||
|  |   var pyconf = PromiseA.promisifyAll(require('pyconf')); | ||
|  | 
 | ||
|  |   if (!opts) { | ||
|  |     opts = {}; | ||
|  |   } | ||
|  | 
 | ||
|  |   /* | ||
|  |   defaults.webroot = true; | ||
|  |   defaults.renewByDefault = true; | ||
|  |   defaults.text = true; | ||
|  |   */ | ||
|  |   defaults.server = defaults.server || LE.liveServer; | ||
|  | 
 | ||
|  |   var wrapped = { | ||
|  |     registerAsync: function (args) { | ||
|  |       args.server = args.server || defaults.server || LE.liveServer; // https://acme-v01.api.letsencrypt.org/directory
 | ||
|  |       var hostname = require('url').parse(args.server).hostname; | ||
|  |       var configDir = args.configDir || defaults.configDir || LE.configDir; | ||
|  |       args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', hostname + '.conf'); | ||
|  |       args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', hostname, 'directory'); | ||
|  | 
 | ||
|  |       pyconf.readFileAsync(args.renewalDir).then(function (renewal) { | ||
|  |         return renewal.account; | ||
|  |       }, function (err) { | ||
|  |         if ("EENOENT" === err.code) { | ||
|  |           return getAccountByEmail(args); | ||
|  |         } | ||
|  | 
 | ||
|  |         return err; | ||
|  |       }).then(function (accountId) { | ||
|  |         // Note: the ACME urls are always fetched fresh on purpose
 | ||
|  |         return getAcmeUrls(args).then(function (urls) { | ||
|  |           args._acmeUrls = urls; | ||
|  | 
 | ||
|  |           if (accountId) { | ||
|  |             return getAccount(args, accountId); | ||
|  |           } else { | ||
|  |             return createAccount(args); | ||
|  |           } | ||
|  |         }); | ||
|  |       }).then(function (account) { | ||
|  |         throw new Error("IMPLEMENTATION NOT COMPLETE"); | ||
|  |       }); | ||
|  | /* | ||
|  |       return fs.readdirAsync(accountsDir, function (nodes) { | ||
|  |         return PromiseA.all(nodes.map(function (node) { | ||
|  |           var reMd5 = /[a-f0-9]{32}/i; | ||
|  |           if (reMd5.test(node)) { | ||
|  |           } | ||
|  |         })); | ||
|  |       }); | ||
|  | */ | ||
|  |     } | ||
|  |   , fetchAsync: function (args) { | ||
|  |       var hostname = args.domains[0]; | ||
|  |       var crtpath = defaults.configDir + defaults.fullchainTpl.replace(/:hostname/, hostname); | ||
|  |       var privpath = defaults.configDir + defaults.privkeyTpl.replace(/:hostname/, hostname); | ||
|  | 
 | ||
|  |       return PromiseA.all([ | ||
|  |         fs.readFileAsync(privpath, 'ascii') | ||
|  |       , fs.readFileAsync(crtpath, 'ascii') | ||
|  |         // stat the file, not the link
 | ||
|  |       , fs.statAsync(crtpath) | ||
|  |       ]).then(function (arr) { | ||
|  |         return { | ||
|  |           key: arr[0]  // privkey.pem
 | ||
|  |         , cert: arr[1] // fullchain.pem
 | ||
|  |           // TODO parse centificate for lifetime / expiresAt
 | ||
|  |         , issuedAt: arr[2].mtime.valueOf() | ||
|  |         }; | ||
|  |       }, function () { | ||
|  |         return null; | ||
|  |       }); | ||
|  |     } | ||
|  |   }; | ||
|  | 
 | ||
|  |   return wrapped; | ||
|  | }; |