353 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			353 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| var PromiseA = require('bluebird');
 | |
| var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
 | |
| var path = require('path');
 | |
| var fs = PromiseA.promisifyAll(require('fs'));
 | |
| var sfs = require('safe-replace');
 | |
| 
 | |
| var LE = require('../');
 | |
| var LeCore = PromiseA.promisifyAll(require('letiny-core'));
 | |
| var leCrypto = PromiseA.promisifyAll(LeCore.leCrypto);
 | |
| 
 | |
| var fetchFromConfigLiveDir = require('./common').fetchFromDisk;
 | |
| 
 | |
| var ipc = {}; // in-process cache
 | |
| 
 | |
| 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 createAccount(args, handlers) {
 | |
|   var os = require("os");
 | |
|   var localname = os.hostname();
 | |
| 
 | |
|   // TODO support ECDSA
 | |
|   // arg.rsaBitLength args.rsaExponent
 | |
|   return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) {
 | |
|     /* pems = { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 } */
 | |
| 
 | |
|     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);
 | |
|       }
 | |
|     , accountPrivateKeyPem: pems.privateKeyPem
 | |
| 
 | |
|     , debug: args.debug || handlers.debug
 | |
|     }).then(function (body) {
 | |
|       var accountDir = path.join(args.accountsDir, pems.publicKeyMd5);
 | |
| 
 | |
|       return mkdirpAsync(accountDir).then(function () {
 | |
| 
 | |
|         var isoDate = new Date().toISOString();
 | |
|         var accountMeta = {
 | |
|           creation_host: localname
 | |
|         , creation_dt: isoDate
 | |
|         };
 | |
| 
 | |
|         return PromiseA.all([
 | |
|           // meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"}
 | |
|           fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify(accountMeta), 'utf8')
 | |
|           // private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" }
 | |
|         , fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(pems.privateKeyJwk), 'utf8')
 | |
|           // regr.json:
 | |
|           /*
 | |
|           { 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' }
 | |
|            */
 | |
|         , fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify({ body: body }), 'utf8')
 | |
|         ]).then(function () {
 | |
|           return pems;
 | |
|         });
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| function getAccount(accountId, args, handlers) {
 | |
|   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] = { error: err };
 | |
|     });
 | |
|   })).then(function () {
 | |
| 
 | |
|     if (!Object.keys(files).every(function (key) {
 | |
|       return !files[key].error;
 | |
|     })) {
 | |
|       // TODO log renewal.conf
 | |
|       console.warn("Account '" + accountId + "' was currupt. No big deal (I think?). Creating a new one...");
 | |
|       return createAccount(args, handlers);
 | |
|     }
 | |
| 
 | |
|     return leCrypto.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) {
 | |
|       files.accountId = accountId;                  // md5sum(publicKeyPem)
 | |
|       files.publicKeyMd5 = 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);
 | |
| }
 | |
| 
 | |
| function getCertificateAsync(account, args, defaults, handlers) {
 | |
|   var pyconf = PromiseA.promisifyAll(require('pyconf'));
 | |
| 
 | |
|   return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (domain) {
 | |
|     return LeCore.getCertificateAsync({
 | |
|       newAuthzUrl: args._acmeUrls.newAuthz
 | |
|     , newCertUrl: args._acmeUrls.newCert
 | |
| 
 | |
|     , accountPrivateKeyPem: account.privateKeyPem
 | |
|     , domainPrivateKeyPem: domain.privateKeyPem
 | |
|     , domains: args.domains
 | |
| 
 | |
|     , setChallenge: function (domain, key, value, done) {
 | |
|         args.domains = [domain];
 | |
|         args.webrootPath = args.webrootPath || defaults.webrootPath;
 | |
|         handlers.setChallenge(args, key, value, done);
 | |
|       }
 | |
|     , removeChallenge: function (domain, key, done) {
 | |
|         args.domains = [domain];
 | |
|         args.webrootPath = args.webrootPath || defaults.webrootPath;
 | |
|         handlers.removeChallenge(args, key, done);
 | |
|       }
 | |
|     }).then(function (result) {
 | |
|       // TODO write pems={ca,cert,key} to disk
 | |
|       var liveDir = path.join(args.configDir, 'live', args.domains[0]);
 | |
|       var certPath = path.join(liveDir, 'cert.pem');
 | |
|       var fullchainPath = path.join(liveDir, 'fullchain.pem');
 | |
|       var chainPath = path.join(liveDir, 'chain.pem');
 | |
|       var privkeyPath = path.join(liveDir, 'privkey.pem');
 | |
| 
 | |
|       result.fullchain = result.cert + '\n' + result.ca;
 | |
| 
 | |
|       // TODO write to archive first, then write to live
 | |
|       return mkdirpAsync(liveDir).then(function () {
 | |
|         return PromiseA.all([
 | |
|           sfs.writeFileAsync(certPath, result.cert, 'ascii')
 | |
|         , sfs.writeFileAsync(chainPath, result.chain, 'ascii')
 | |
|         , sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii')
 | |
|         , sfs.writeFileAsync(privkeyPath, result.key, 'ascii')
 | |
|         ]).then(function () {
 | |
|         // TODO format result licesy
 | |
|           //console.log(liveDir);
 | |
|           //console.log(result);
 | |
|           return {
 | |
|             certPath: certPath
 | |
|           , chainPath: chainPath
 | |
|           , fullchainPath: fullchainPath
 | |
|           , privkeyPath: privkeyPath
 | |
|           };
 | |
|         });
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| function registerWithAcme(args, defaults, handlers) {
 | |
|   var pyconf = PromiseA.promisifyAll(require('pyconf'));
 | |
|   var server = args.server || defaults.server || LeCore.stagingServerUrl; // https://acme-v01.api.letsencrypt.org/directory
 | |
|   var acmeHostname = require('url').parse(server).hostname;
 | |
|   var configDir = args.configDir || defaults.configDir || LE.configDir;
 | |
| 
 | |
|   args.server = server;
 | |
|   args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', args.domains[0] + '.conf');
 | |
|   args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', acmeHostname, 'directory');
 | |
| 
 | |
|   return pyconf.readFileAsync(args.renewalDir).then(function (renewal) {
 | |
|     var accountId = renewal.account;
 | |
|     renewal = renewal.account;
 | |
| 
 | |
|     return accountId;
 | |
|   }, function (err) {
 | |
|     if ("ENOENT" === err.code) {
 | |
|       return getAccountByEmail(args, handlers);
 | |
|     }
 | |
| 
 | |
|     return PromiseA.reject(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(accountId, args, handlers);
 | |
|       } else {
 | |
|         return createAccount(args, handlers);
 | |
|       }
 | |
|     });
 | |
|   }).then(function (account) {
 | |
|     /*
 | |
|     if (renewal.account !== account) {
 | |
|       // the account has become corrupt, re-register
 | |
|       return;
 | |
|     }
 | |
|     */
 | |
| 
 | |
|     //console.log(account);
 | |
|     return fetchFromConfigLiveDir(args, defaults).then(function (certs) {
 | |
|       // if nothing, register and save
 | |
|       // if something, check date (don't register unless 30+ days)
 | |
|       // if good, don't bother registering
 | |
|       // (but if we get to the point that we're actually calling
 | |
|       // this function, that shouldn't be the case, right?)
 | |
|       //console.log(certs);
 | |
|       if (!certs) {
 | |
|         // no certs, seems like a good time to get some
 | |
|         return getCertificateAsync(account, args, defaults, handlers);
 | |
|       }
 | |
|       else if (certs.issuedAt > (27 * 24 * 60 * 60 * 1000)) {
 | |
|         // cert is at least 27 days old we can renew that
 | |
|         return getCertificateAsync(account, args, defaults, handlers);
 | |
|       }
 | |
|       else if (args.duplicate) {
 | |
|         // YOLO! I be gettin' fresh certs 'erday! Yo!
 | |
|         return getCertificateAsync(account, args, defaults, handlers);
 | |
|       }
 | |
|       else {
 | |
|         console.warn('[WARN] Ignoring renewal attempt for certificate less than 27 days old. Use args.duplicate to force.');
 | |
|         // We're happy with what we have
 | |
|         return certs;
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     /*
 | |
|     cert = /home/aj/node-letsencrypt/tests/letsencrypt.config/live/lds.io/cert.pem
 | |
|     privkey = /home/aj/node-letsencrypt/tests/letsencrypt.config/live/lds.io/privkey.pem
 | |
|     chain = /home/aj/node-letsencrypt/tests/letsencrypt.config/live/lds.io/chain.pem
 | |
|     fullchain = /home/aj/node-letsencrypt/tests/letsencrypt.config/live/lds.io/fullchain.pem
 | |
| 
 | |
|     # Options and defaults used in the renewal process
 | |
|     [renewalparams]
 | |
|     apache_enmod = a2enmod
 | |
|     no_verify_ssl = False
 | |
|     ifaces = None
 | |
|     apache_dismod = a2dismod
 | |
|     register_unsafely_without_email = False
 | |
|     uir = None
 | |
|     installer = none
 | |
|     config_dir = /home/aj/node-letsencrypt/tests/letsencrypt.config
 | |
|     text_mode = True
 | |
|     func = <function obtain_cert at 0x7f46af0f02a8>
 | |
|     prepare = False
 | |
|     work_dir = /home/aj/node-letsencrypt/tests/letsencrypt.work
 | |
|     tos = True
 | |
|     init = False
 | |
|     http01_port = 80
 | |
|     duplicate = False
 | |
|     key_path = None
 | |
|     nginx = False
 | |
|     fullchain_path = /home/aj/node-letsencrypt/chain.pem
 | |
|     email = coolaj86@gmail.com
 | |
|     csr = None
 | |
|     agree_dev_preview = None
 | |
|     redirect = None
 | |
|     verbose_count = -3
 | |
|     config_file = None
 | |
|     renew_by_default = True
 | |
|     hsts = False
 | |
|     authenticator = webroot
 | |
|     domains = lds.io,
 | |
|     rsa_key_size = 2048
 | |
|     checkpoints = 1
 | |
|     manual_test_mode = False
 | |
|     apache = False
 | |
|     cert_path = /home/aj/node-letsencrypt/cert.pem
 | |
|     webroot_path = /home/aj/node-letsencrypt/examples/../tests/acme-challenge,
 | |
|     strict_permissions = False
 | |
|     apache_server_root = /etc/apache2
 | |
|     account = 1c41c64dfaf10d511db8aef0cc33b27f
 | |
|     manual_public_ip_logging_ok = False
 | |
|     chain_path = /home/aj/node-letsencrypt/chain.pem
 | |
|     standalone = False
 | |
|     manual = False
 | |
|     server = https://acme-staging.api.letsencrypt.org/directory
 | |
|     standalone_supported_challenges = "http-01,tls-sni-01"
 | |
|     webroot = True
 | |
|     apache_init_script = None
 | |
|     user_agent = None
 | |
|     apache_ctl = apache2ctl
 | |
|     apache_le_vhost_ext = -le-ssl.conf
 | |
|     debug = False
 | |
|     tls_sni_01_port = 443
 | |
|     logs_dir = /home/aj/node-letsencrypt/tests/letsencrypt.logs
 | |
|     configurator = None
 | |
|     [[webroot_map]]
 | |
|     lds.io = /home/aj/node-letsencrypt/examples/../tests/acme-challenge
 | |
|     */
 | |
|   });
 | |
| /*
 | |
|   return fs.readdirAsync(accountsDir, function (nodes) {
 | |
|     return PromiseA.all(nodes.map(function (node) {
 | |
|       var reMd5 = /[a-f0-9]{32}/i;
 | |
|       if (reMd5.test(node)) {
 | |
|       }
 | |
|     }));
 | |
|   });
 | |
| */
 | |
| }
 | |
| 
 | |
| module.exports.create = function (defaults, handlers) {
 | |
|   defaults.server = defaults.server || LE.liveServer;
 | |
| 
 | |
|   var wrapped = {
 | |
|     registerAsync: function (args) {
 | |
|       //require('./common').registerWithAcme(args, defaults, handlers);
 | |
|       return registerWithAcme(args, defaults, handlers);
 | |
|     }
 | |
|   , fetchAsync: function (args) {
 | |
|       return fetchFromConfigLiveDir(args, defaults);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return wrapped;
 | |
| };
 |