310 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			310 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| /*global Promise*/
 | |
| var PromiseA;
 | |
| var util = require('util');
 | |
| if (!util.promisify) {
 | |
|   try {
 | |
|     PromiseA = require('bluebird');
 | |
|     util.promisify = PromiseA.promisify;
 | |
|   } catch(e) {
 | |
|     console.error("Your version of node is missing Promise. Please run `npm install --save bluebird` in your project to fix");
 | |
|     process.exit(10);
 | |
|   }
 | |
| }
 | |
| if ('undefined' !== typeof Promise) { PromiseA = Promise; }
 | |
| var fs = require('fs');
 | |
| var path = require('path');
 | |
| var readFileAsync = util.promisify(fs.readFile);
 | |
| var writeFileAsync = util.promisify(fs.writeFile);
 | |
| var sfs = require('safe-replace');
 | |
| var mkdirpAsync = util.promisify(require('mkdirp'));
 | |
| var os = require("os");
 | |
| 
 | |
| // create():
 | |
| // Your storage plugin may take special options, or it may not.
 | |
| // If it does, document to your users that they must call create() with those options.
 | |
| // If you user does not call create(), greenlock will call it for you with the options it has.
 | |
| // It's kind of stupid, but it's done this way so that it can be more convenient for users to not repeat shared options
 | |
| // (such as the config directory), but sometimes configs would clash. I hate having ambiguity, so I may change this in
 | |
| // a future version, but it's very much an issue of "looks cleaner" vs "behaves cleaner".
 | |
| module.exports.create = function (config) {
 | |
| 
 | |
|   // This file has been laid out in the order that options are used and calls are made
 | |
|   // SNICallback() // le-sni-auto has a cache
 | |
|   //   greenlock.approveDomains()
 | |
|   //        // you get opts.domain passed to you from SNI
 | |
|   //        // you should set opts.subject as the cert "id" domain
 | |
|   //        // you should set opts.domains as all domains on the cert
 | |
|   //        // you should set opts.account.id, otherwise opts.email will be used
 | |
|   //     greenlock.store.certificates.checkAsync() // on success -> SNI cache, on fail -> checkAccount
 | |
|   //     greenlock.store.accounts.checkAsync()     // optional (you can always return null)
 | |
|   //     greenlock.store.accounts.checkKeypairAsync()
 | |
|   //       greenlock.core.RSA.generateKeypair()             // TODO double check name
 | |
|   //       greenlock.core.accounts.register()               // TODO double check name
 | |
|   //     greenlock.store.accounts.setKeypairAsync()         // TODO make sure this only happens on generate
 | |
|   //     greenlock.store.accounts.setAsync() // optional
 | |
|   //     greenlock.store.certificates.checkKeypairAsync()
 | |
|   //       greenlock.core.RSA.generateKeypair()             // TODO double check name
 | |
|   //       greenlock.core.certificates.register()           // TODO double check name
 | |
|   //     greenlock.store.certificates.setKeypairAsync()
 | |
|   //     greenlock.store.certificates.setAsync()
 | |
| 
 | |
|   // store
 | |
|   // Bear in mind that the only time any of this gets called is on first access after startup, new registration, and
 | |
|   // renewal - so none of this needs to be particularly fast. It may need to be memory efficient, however - if you have
 | |
|   // more than 10,000 domains, for example.
 | |
|   var store = {};
 | |
| 
 | |
|   // options:
 | |
|   //
 | |
|   // If your module requires options (i.e. file paths or database urls) you should check what you get from create()
 | |
|   // and copy over the things you'll use into this options object. You should also merge in any defaults for options
 | |
|   // that have not been set. This object should not be circular, should not be changed after it is set, and should
 | |
|   // contain every property that you can use, using falsey JSON-able values like 0, null, false, or '' for "unset"
 | |
|   // values.
 | |
|   // See the note on create() above.
 | |
|   store.options = mergeOptions(config);
 | |
| 
 | |
|   // set and check account keypairs and account data
 | |
|   store.accounts = {};
 | |
|   // set and check domain keypairs and domain certificates
 | |
|   store.certificates = {};
 | |
| 
 | |
|   // certificates.checkAsync({ subject, ... }):
 | |
|   //
 | |
|   // The first check is that a certificate looked for by its subject (primary domain name).
 | |
|   // If that lookup succeeds, then nothing else needs to happen. Otherwise accounts.checkAsync will happen next.
 | |
|   // The only input you need to be concerned with is opts.subject (which falls back to opts.domains[0] if not set).
 | |
|   // And since this is called after `approveDomains()`, any options that you set there will be available here too.
 | |
|   store.certificates.checkAsync = function (opts) {
 | |
|     // { certificate.id, subject, domains, ... }
 | |
|     var id = opts.certificate && opts.certificate.id || opts.subject;
 | |
|     //console.log('certificates.checkAsync for', opts.domain, opts.subject, opts.domains);
 | |
|     //console.log(opts);
 | |
| 
 | |
|     // Just to show that any options set in approveDomains() will be available here
 | |
|     // (the same is true for all of the hooks in this file)
 | |
|     if (opts.exampleThrowError) { return PromiseA.reject(new Error("You want an error? You got it!")); }
 | |
|     if (opts.exampleReturnNull) { return PromiseA.resolve(null); }
 | |
|     if (opts.exampleReturnCerts) { return PromiseA.resolve(opts.exampleReturnCerts); }
 | |
| 
 | |
|     var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject);
 | |
|     // TODO this shouldn't be necessary here (we should get it from checkKeypairAsync)
 | |
|     var privkeyPath = opts.privkeyPath || opts.domainKeyPath || path.join(liveDir, 'privkey.pem');
 | |
|     var certPath = opts.certPath || path.join(liveDir, 'cert.pem');
 | |
|     var chainPath = opts.chainPath || path.join(liveDir, 'chain.pem');
 | |
| 
 | |
|     return PromiseA.all([
 | |
|       // all other PEM files are arrangements of these three
 | |
|       readFileAsync(tameWild(privkeyPath, id), 'ascii')   // 0
 | |
|     , readFileAsync(tameWild(certPath, id), 'ascii')      // 1
 | |
|     , readFileAsync(tameWild(chainPath, id), 'ascii')     // 2
 | |
|     ]).then(function (all) {
 | |
|       // Success
 | |
|       return {
 | |
|         privkey: all[0]
 | |
|       , cert: all[1]
 | |
|       , chain: all[2]
 | |
|       // When using a database, these should be retrieved too
 | |
|       // (when not available they'll be generated from cert-info)
 | |
|       //, subject: certinfo.subject
 | |
|       //, altnames: certinfo.altnames
 | |
|       //, issuedAt: certinfo.issuedAt // a.k.a. NotBefore
 | |
|       //, expiresAt: certinfo.expiresAt // a.k.a. NotAfter
 | |
|       };
 | |
|     }).catch(function (err) {
 | |
|       // Non-success
 | |
|       if ('ENOENT' === err.code) { return null; }
 | |
|       // Failure
 | |
|       throw err;
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   // accounts.checkAsync({ accountId, email, [...] }): // Optional
 | |
|   //
 | |
|   // This is where you promise an account corresponding to the given the email and ID. All options set in
 | |
|   // approveDomains() are also available. You can ignore them unless your implementation is using them in some way.
 | |
|   //
 | |
|   // Since accounts are based on public key, the act of creating a new account or returning an existing account
 | |
|   // are the same in regards to the API and so we don't really need to store the account id or retrieve it.
 | |
|   // This method only needs to be implemented if you need it for your own purposes
 | |
|   //
 | |
|   // On Success: Promise.resolve({ id, keypair, ... }) - an id and, for backwards compatibility, the abstract keypair
 | |
|   // On Failure: Promise.resolve(null) - do not return undefined, do not throw, do not reject
 | |
|   // On Error: Promise.reject(new Error("something descriptive for the user"))
 | |
|   store.accounts.checkAsync = function (/*opts*/) {
 | |
|     //var id = opts.account.id || 'single-user';
 | |
|     //console.log('accounts.checkAsync for', id);
 | |
|     return PromiseA.resolve(null);
 | |
|   };
 | |
| 
 | |
|   // accounts.checkKeypairAsync({ email, ... }):
 | |
|   //
 | |
|   // Same rules as above apply, except for the private key of the account, not the account object itself.
 | |
|   //
 | |
|   // On Success: Promise.resolve({ ... }) - the abstract object representing the keypair
 | |
|   // On Failure: Promise.resolve(null) - do not return undefined, do not throw, do not reject
 | |
|   // On Error: Promise.reject(new Error("something descriptive for the user"))
 | |
|   store.accounts.checkKeypairAsync = function (opts) {
 | |
|     var id = opts.account.id || 'single-user';
 | |
|     //console.log('accounts.checkKeypairAsync for', id);
 | |
|     if (!opts.account.id) { return PromiseA.reject(new Error("'account.id' should have been set in approveDomains()")); }
 | |
| 
 | |
|     var pathname = path.join(tameWild(opts.accountsDir, opts.subject), sanitizeFilename(id) + '.json');
 | |
|     return readFileAsync(tameWild(pathname, opts.subject), 'utf8').then(function (blob) {
 | |
|       // keypair is an opaque object that should be treated as blob
 | |
|       return JSON.parse(blob);
 | |
|     }).catch(function (err) {
 | |
|       if ('ENOENT' === err.code) { return null; }
 | |
|       throw err;
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   // accounts.setKeypairAsync({ keypair, email, ... }):
 | |
|   //
 | |
|   // The keypair details (RSA, ECDSA, etc) are chosen either by the greenlock defaults, global user defaults,
 | |
|   // or whatever you set in approveDomains(). This is called *after* the account is successfully created.
 | |
|   //
 | |
|   // On Success: Promise.resolve(null) - just knowing the operation is successful will do
 | |
|   // On Error: Promise.reject(new Error("something descriptive for the user"))
 | |
|   store.accounts.setKeypairAsync = function (opts, keypair) {
 | |
|     var id = opts.account.id || 'single-user';
 | |
|     //console.log('accounts.setKeypairAsync for', id);
 | |
|     keypair = opts.keypair || keypair;
 | |
|     if (!opts.account.id) { return PromiseA.reject(new Error("'account.id' should have been set in approveDomains()")); }
 | |
|     return mkdirpAsync(tameWild(opts.accountsDir, opts.subject)).then(function () {
 | |
|       // keypair is an opaque object that should be treated as blob
 | |
|       var pathname = tameWild(path.join(opts.accountsDir, sanitizeFilename(id) + '.json'), opts.subject);
 | |
|       return writeFileAsync(tameWild(pathname, opts.subject), JSON.stringify(keypair), 'utf8');
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   // accounts.setAsync({ account, keypair, email, ... }):
 | |
|   //
 | |
|   // The account details, from ACME, if everything is successful. Unless you need to do something with those account
 | |
|   // details, this implementation can remain empty.
 | |
|   //
 | |
|   // On Success: Promise.resolve(null||{ id }) - do not return undefined, do not throw, do not reject
 | |
|   // On Error: Promise.reject(new Error("something descriptive for the user"))
 | |
|   store.accounts.setAsync = function (/*opts*/) {
 | |
|     //receipt = opts.receipt || receipt;
 | |
|     //console.log('account.setAsync:', receipt);
 | |
|     return PromiseA.resolve(null);
 | |
|   };
 | |
| 
 | |
|   // certificates.checkKeypairAsync({ subject, ... }):
 | |
|   //
 | |
|   // Same rules as certificates.checkAsync apply, except for the private key of the certificate, not the public
 | |
|   // certificate itself (similar to accounts.checkKeyPairAsync, but for certs).
 | |
|   store.certificates.checkKeypairAsync = function (opts) {
 | |
|     //console.log('certificates.checkKeypairAsync:');
 | |
|     var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject);
 | |
|     var privkeyPath = opts.privkeyPath || opts.domainKeyPath || path.join(liveDir, 'privkey.pem');
 | |
| 
 | |
|     return readFileAsync(tameWild(privkeyPath, opts.subject), 'ascii').then(function (key) {
 | |
|       // keypair is normally an opaque object, but here it's a pem for the filesystem
 | |
|       return { privateKeyPem: key };
 | |
|     }).catch(function (err) {
 | |
|       if ('ENOENT' === err.code) { return null; }
 | |
|       throw err;
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   // certificates.setKeypairAsync({ domain, keypair, ... }):
 | |
|   //
 | |
|   // Same as accounts.setKeypairAsync, but by domains rather than email / accountId
 | |
|   store.certificates.setKeypairAsync = function (opts, keypair) {
 | |
|     keypair = opts.keypair || keypair;
 | |
|     var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject);
 | |
|     var privkeyPath = opts.privkeyPath || opts.domainKeyPath || path.join(liveDir, 'privkey.pem');
 | |
| 
 | |
|     // keypair is normally an opaque object, but here it's a PEM for the FS
 | |
|     return mkdirpAsync(tameWild(path.dirname(privkeyPath), opts.subject)).then(function () {
 | |
|       return writeFileAsync(tameWild(privkeyPath, opts.subject), keypair.privateKeyPem, 'ascii').then(function () {
 | |
|         return null;
 | |
|       });
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   // certificates.setAsync({ domain, certs, ... }):
 | |
|   //
 | |
|   // This is where certificates are set, as well as certinfo
 | |
|   store.certificates.setAsync = function (opts) {
 | |
|     //console.log('certificates.setAsync:');
 | |
|     //console.log(opts.domain, '<=', opts.subject);
 | |
|     var pems = {
 | |
|       privkey: opts.pems.privkey
 | |
|     , cert: opts.pems.cert
 | |
|     , chain: opts.pems.chain
 | |
|     };
 | |
|     var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject);
 | |
|     var certPath = opts.certPath || path.join(liveDir, 'cert.pem');
 | |
|     var fullchainPath = opts.fullchainPath || path.join(liveDir, 'fullchain.pem');
 | |
|     var chainPath = opts.chainPath || path.join(liveDir, 'chain.pem');
 | |
|     //var privkeyPath = opts.privkeyPath || opts.domainKeyPath || path.join(liveDir, 'privkey.pem');
 | |
|     var bundlePath = opts.bundlePath || path.join(liveDir, 'bundle.pem');
 | |
| 
 | |
|     return mkdirpAsync(path.dirname(tameWild(certPath, opts.subject))).then(function () {
 | |
|       return mkdirpAsync(path.dirname(tameWild(chainPath, opts.subject))).then(function () {
 | |
|         return mkdirpAsync(path.dirname(tameWild(fullchainPath, opts.subject))).then(function () {
 | |
|           return mkdirpAsync(path.dirname(tameWild(bundlePath, opts.subject))).then(function () {
 | |
|             var fullchainPem = [ pems.cert, pems.chain ].join('\n'); // for Apache, Nginx, etc
 | |
|             var bundlePem = [ pems.privkey, pems.cert, pems.chain ].join('\n'); // for HAProxy
 | |
|             return PromiseA.all([
 | |
|               sfs.writeFileAsync(tameWild(certPath, opts.subject), pems.cert, 'ascii')
 | |
|             , sfs.writeFileAsync(tameWild(chainPath, opts.subject), pems.chain, 'ascii')
 | |
|               // Most web servers need these two
 | |
|             , sfs.writeFileAsync(tameWild(fullchainPath, opts.subject), fullchainPem, 'ascii')
 | |
|               // HAProxy needs "bundle.pem" aka "combined.pem"
 | |
|             , sfs.writeFileAsync(tameWild(bundlePath, opts.subject), bundlePem, 'ascii')
 | |
|             ]);
 | |
|           });
 | |
|         });
 | |
|       });
 | |
|     }).then(function () {
 | |
|       return null;
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   return store;
 | |
| };
 | |
| 
 | |
| var defaults = {
 | |
|   configDir: path.join(os.homedir(), 'acme', 'etc')
 | |
| 
 | |
| , accountsDir: path.join(':configDir', 'accounts', ':serverDir')
 | |
| , serverDirGet: function (copy) {
 | |
|     return (copy.server || '').replace('https://', '').replace(/(\/)$/, '').replace(/\//g, path.sep);
 | |
|   }
 | |
| , privkeyPath: path.join(':configDir', 'live', ':hostname', 'privkey.pem')
 | |
| , fullchainPath: path.join(':configDir', 'live', ':hostname', 'fullchain.pem')
 | |
| , certPath: path.join(':configDir', 'live', ':hostname', 'cert.pem')
 | |
| , chainPath: path.join(':configDir', 'live', ':hostname', 'chain.pem')
 | |
| , bundlePath: path.join(':configDir', 'live', ':hostname', 'bundle.pem')
 | |
| };
 | |
| 
 | |
| function mergeOptions(configs) {
 | |
|   if (!configs.domainKeyPath) {
 | |
|     configs.domainKeyPath = configs.privkeyPath || defaults.privkeyPath;
 | |
|   }
 | |
| 
 | |
|   Object.keys(defaults).forEach(function (key) {
 | |
|     if (!configs[key]) {
 | |
|       configs[key] = defaults[key];
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   return configs;
 | |
| }
 | |
| 
 | |
| function sanitizeFilename(id) {
 | |
|   return id.replace(/(\.\.)|\\|\//g, '_').replace(/[^!-~]/g, '_');
 | |
| }
 | |
| 
 | |
| // because not all file systems like '*' in a name (and they're scary)
 | |
| function tameWild(path, wild) {
 | |
|   var tame = wild.replace(/\*/g, '_');
 | |
|   return path.replace(wild, tame);
 | |
| }
 |