'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*/) { //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); }