From 6d398d36c4b366f0cbe4070e4abc3f78ef828b49 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 27 Oct 2019 04:01:42 -0600 Subject: [PATCH] updates for Greenlock v3 --- accounts.js | 113 ++++++++++++++++ certificates.js | 267 +++++++++++++++++++++++++++++++++++++ index.js | 341 +++++++----------------------------------------- promise.js | 22 ++++ utils.js | 46 +++++++ 5 files changed, 498 insertions(+), 291 deletions(-) create mode 100644 accounts.js create mode 100644 certificates.js create mode 100644 promise.js create mode 100644 utils.js diff --git a/accounts.js b/accounts.js new file mode 100644 index 0000000..aee2d68 --- /dev/null +++ b/accounts.js @@ -0,0 +1,113 @@ +'use strict'; + +var accounts = module.exports; +var store = accounts; +var U = require('./utils.js'); + +var fs = require('fs'); +var path = require('path'); +var PromiseA = require('./promise.js'); +var readFileAsync = PromiseA.promisify(fs.readFile); +var writeFileAsync = PromiseA.promisify(fs.writeFile); +var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp')); + +// Implement if you need the ACME account metadata elsewhere in the chain of events +//store.accounts.check = function (opts) { +// console.log('accounts.check for', opts.account, opts.email); +// return PromiseA.resolve(null); +//}; + +// Accounts.checkKeypair +// +// Use account.id, or email, if id hasn't been set, to find an account keypair. +// Return an object with string privateKeyPem and/or object privateKeyJwk (or null, not undefined) +accounts.checkKeypair = function(opts) { + var id = + (opts.account && opts.account.id) || + (opts.subscriberEmail || opts.email) || + 'single-user'; + //console.log('accounts.checkKeypair for', id); + + var pathname = path.join( + accountsDir(store, opts), + sanitizeFilename(id) + '.json' + ); + return readFileAsync(U._tameWild(pathname, opts.subject), 'utf8') + .then(function(blob) { + // keypair can treated as an opaque object and just passed along, + // but just to show you what it is... + var keypair = JSON.parse(blob); + return keypair; + /* + { + privateKeyPem: keypair.privateKeyPem, // string PEM private key + privateKeyJwk: keypair.privateKeyJwk, // object JWK private key + private: keypair.private, + public: keypair.public + }; + */ + }) + .catch(function(err) { + if ('ENOENT' === err.code) { + return null; + } + throw err; + }); +}; + +// Accounts.setKeypair({ account, email, keypair, ... }): +// +// Use account.id (or email if no id is present) to save an account keypair +// Return null (not undefined) on success, or throw on error +accounts.setKeypair = function(opts) { + //console.log('accounts.setKeypair for', opts.account, opts.email, opts.keypair); + var id = opts.account.id || opts.email || 'single-user'; + + // you can just treat the keypair as opaque and save and retrieve it as JSON + var keyblob = JSON.stringify(opts.keypair); + /* + var keyblob = JSON.stringify({ + privateKeyPem: opts.keypair.privateKeyPem, // string PEM + privateKeyJwk: opts.keypair.privateKeyJwk, // object JWK + private: opts.keypair.private + }); + */ + + // Ignore. + // Just implementation specific details here. + return mkdirpAsync(accountsDir(store, opts)) + .then(function() { + var pathname = path.join( + accountsDir(store, opts), + sanitizeFilename(id) + '.json' + ); + return writeFileAsync( + U._tameWild(pathname, opts.subject), + keyblob, + 'utf8' + ); + }) + .then(function() { + // This is your job: return null, not undefined + return null; + }); +}; + +// Implement if you need the ACME account metadata elsewhere in the chain of events +//accounts.set = function (opts) { +// console.log('account.set:', opts.account, opts.email, opts.receipt); +// return PromiseA.resolve(null); +//}; + +function sanitizeFilename(id) { + return id.replace(/(\.\.)|\\|\//g, '_').replace(/[^!-~]/g, '_'); +} + +function accountsDir(store, opts) { + var dir = U._tpl( + store, + opts, + opts.accountsDir || store.options.accountsDir + ); + return U._tameWild(dir, opts.subject || ''); +} diff --git a/certificates.js b/certificates.js new file mode 100644 index 0000000..93afe34 --- /dev/null +++ b/certificates.js @@ -0,0 +1,267 @@ +'use strict'; + +var certificates = module.exports; +var store = certificates; +var U = require('./utils.js'); + +var fs = require('fs'); +var path = require('path'); +var PromiseA = require('./promise.js'); +var sfs = require('safe-replace'); +var readFileAsync = PromiseA.promisify(fs.readFile); +var writeFileAsync = PromiseA.promisify(fs.writeFile); +var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp')); + +// Certificates.check +// +// Use certificate.id, or subject, if id hasn't been set, to find a certificate. +// Return an object with string PEMs for cert and chain (or null, not undefined) +certificates.check = function(opts) { + // { certificate.id, subject, ... } + var id = (opts.certificate && opts.certificate.id) || opts.subject; + //console.log('certificates.check for', opts.certificate, opts.subject); + //console.log(opts); + + // For advanced use cases: + // This just goes 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 Promise.reject(new Error('You want an error? You got it!')); + } + if (opts.exampleReturnNull) { + return Promise.resolve(null); + } + if (opts.exampleReturnCerts) { + return Promise.resolve(opts.exampleReturnCerts); + } + + return Promise.all([ + readFileAsync(U._tameWild(privkeyPath(store, opts), id), 'ascii'), // 0 // all other PEM types are just + readFileAsync(U._tameWild(certPath(store, opts), id), 'ascii'), // 1 // some arrangement of these 3 + readFileAsync(U._tameWild(chainPath(store, opts), id), 'ascii') // 2 // (bundle, combined, fullchain, etc) + ]) + .then(function(all) { + //////////////////////// + // PAY ATTENTION HERE // + //////////////////////// + // This is all you have to return: cert, chain + return { + cert: all[1], // string PEM. the bare cert, half of the concatonated fullchain.pem you need + chain: all[2], // string PEM. the bare chain, the second half of the fullchain.pem + privkey: all[0] // string PEM. optional, allows checkKeypair to be skipped + + // These can be useful to store in your database, + // but otherwise they're easy to derive from the cert. + // (when not available they'll be generated from cert-info) + //, subject: certinfo.subject // string domain name + //, altnames: certinfo.altnames // array of domain name strings + //, issuedAt: certinfo.issuedAt // number in ms (a.k.a. NotBefore) + //, expiresAt: certinfo.expiresAt // number in ms (a.k.a. NotAfter) + }; + }) + .catch(function(err) { + // Treat non-exceptional failures as null returns (not undefined) + if ('ENOENT' === err.code) { + return null; + } + throw err; // True exceptions should be thrown + }); +}; + +// Certificates.checkKeypair +// +// Use certificate.kid, certificate.id, or subject to find a certificate keypair +// Return an object with string privateKeyPem and/or object privateKeyJwk (or null, not undefined) +certificates.checkKeypair = function(opts) { + //console.log('certificates.checkKeypair:', opts.certificate, opts.subject); + + return readFileAsync( + U._tameWild(privkeyPath(store, opts), opts.subject), + 'ascii' + ) + .then(function(key) { + //////////////////////// + // PAY ATTENTION HERE // + //////////////////////// + return { + privateKeyPem: key // In this case we only saved privateKeyPem, so we only return it + //privateKeyJwk: null // (but it's fine, just different encodings of the same thing) + }; + }) + .catch(function(err) { + if ('ENOENT' === err.code) { + return null; + } + throw err; + }); +}; + +// Certificates.setKeypair({ certificate, subject, keypair, ... }): +// +// Use certificate.kid (or certificate.id or subject if no kid is present) to find a certificate keypair +// Return null (not undefined) on success, or throw on error +certificates.setKeypair = function(opts) { + var keypair = opts.keypair || keypair; + + // Ignore. + // Just specific implementation details. + return mkdirpAsync( + U._tameWild(path.dirname(privkeyPath(store, opts)), opts.subject) + ).then(function() { + // keypair is normally an opaque object, but here it's a PEM for the FS (for things like Apache and Nginx) + return writeFileAsync( + U._tameWild(privkeyPath(store, opts), opts.subject), + keypair.privateKeyPem, + 'ascii' + ).then(function() { + return null; + }); + }); +}; + +// Certificates.set({ subject, pems, ... }): +// +// Use certificate.id (or subject if no ki is present) to save a certificate +// Return null (not undefined) on success, or throw on error +certificates.set = function(opts) { + //console.log('certificates.set:', opts.subject, opts.pems); + var pems = { + cert: opts.pems.cert, // string PEM the first half of the concatonated fullchain.pem cert + chain: opts.pems.chain, // string PEM the second half (yes, you need this too) + privkey: opts.pems.privkey // Ignore. string PEM, useful if you have to create bundle.pem + }; + + // Ignore + // Just implementation specific details (writing lots of combinatons of files) + return mkdirpAsync(path.dirname(certPath(store, opts))) + .then(function() { + return mkdirpAsync( + path.dirname(U._tameWild(chainPath(store, opts), opts.subject)) + ).then(function() { + return mkdirpAsync( + path.dirname( + U._tameWild(fullchainPath(store, opts), opts.subject) + ) + ).then(function() { + return mkdirpAsync( + path.dirname( + U._tameWild(bundlePath(store, opts), opts.subject) + ) + ).then(function() { + var fullchainPem = [ + pems.cert.trim() + '\n', + pems.chain.trim() + '\n' + ].join('\n'); // for Apache, Nginx, etc + var bundlePem = [ + pems.privkey, + pems.cert, + pems.chain + ].join('\n'); // for HAProxy + return PromiseA.all([ + sfs.writeFileAsync( + U._tameWild( + certPath(store, opts), + opts.subject + ), + pems.cert, + 'ascii' + ), + sfs.writeFileAsync( + U._tameWild( + chainPath(store, opts), + opts.subject + ), + pems.chain, + 'ascii' + ), + // Most web servers need these two + sfs.writeFileAsync( + U._tameWild( + fullchainPath(store, opts), + opts.subject + ), + fullchainPem, + 'ascii' + ), + // HAProxy needs "bundle.pem" aka "combined.pem" + sfs.writeFileAsync( + U._tameWild( + bundlePath(store, opts), + opts.subject + ), + bundlePem, + 'ascii' + ) + ]); + }); + }); + }); + }) + .then(function() { + // That's your job: return null + return null; + }); +}; + +function liveDir(store, opts) { + return opts.liveDir || path.join(opts.configDir, 'live', opts.subject); +} + +function privkeyPath(store, opts) { + var dir = U._tpl( + store, + opts, + opts.serverKeyPath || + opts.privkeyPath || + opts.domainKeyPath || + store.options.serverKeyPath || + store.options.privkeyPath || + store.options.domainKeyPath || + path.join(liveDir(), 'privkey.pem') + ); + return U._tameWild(dir, opts.subject || ''); +} + +function certPath(store, opts) { + var dir = U._tpl( + store, + opts, + opts.certPath || + store.options.certPath || + path.join(liveDir(), 'cert.pem') + ); + return U._tameWild(dir, opts.subject || ''); +} + +function fullchainPath(store, opts) { + var dir = U._tpl( + store, + opts, + opts.fullchainPath || + store.options.fullchainPath || + path.join(liveDir(), 'fullchain.pem') + ); + return U._tameWild(dir, opts.subject || ''); +} + +function chainPath(store, opts) { + var dir = U._tpl( + store, + opts, + opts.chainPath || + store.options.chainPath || + path.join(liveDir(), 'chain.pem') + ); + return U._tameWild(dir, opts.subject || ''); +} + +function bundlePath(store, opts) { + var dir = U._tpl( + store, + opts, + opts.bundlePath || + store.options.bundlePath || + path.join(liveDir(), 'bundle.pem') + ); + return U._tameWild(dir, opts.subject || ''); +} diff --git a/index.js b/index.js index f157157..6354396 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,7 @@ 'use strict'; -var os = require("os"); -var fs = require('fs'); +var os = require('os'); var path = require('path'); -var sfs = require('safe-replace'); -var PromiseA = getPromise(); -var readFileAsync = PromiseA.promisify(fs.readFile); -var writeFileAsync = PromiseA.promisify(fs.writeFile); -// TODO replace with zero-depenency version -var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp')); - // How Storage Works in Greenlock: High-Level Call Stack // @@ -18,7 +10,7 @@ var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp')); // tls.SNICallback() // TLS connection with SNI kicks of the request // // greenlock.approveDomains(opts) // Greenlokc does some housekeeping, checks for a cert in -// // an internal cash, and only asks you to approve new +// // an internal cache, and only asks you to approve new // // certificate // registration if it doesn't find anything. // // In `opts` you'll receive `domain` and a few other things. // // You should return { subject: '...', altnames: ['...'] } @@ -45,7 +37,6 @@ var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp')); // greenlock.store.certificates.setKeypair() // Saves the keypair for the valid certificate // greenlock.store.certificates.set() // Saves the valid certificate - //////////////////////////////////////////// // Recap of the high-level overview above // //////////////////////////////////////////// @@ -57,246 +48,34 @@ var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp')); // // For any type of customization, whatever is set in `approveDomains()` is available everywhere else. - - // Either your user calls create with specific options, or greenlock calls it for you with a big options blob -module.exports.create = function (config) { - // 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. +module.exports.create = function(config) { + // 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. - // basic setup - var store = { accounts: {}, certificates: {} }; + // basic setup + var store = { + accounts: require('./accounts.js'), + certificates: require('./certificates.js') + }; - // For you store.options should probably start empty and get a minimal set of options copied from `config` above. - // Example: - //store.options = {}; - //store.options.databaseUrl = config.databaseUrl; + // For you store.options should probably start empty and get a minimal set of options copied from `config` above. + // Example: + //store.options = {}; + //store.options.databaseUrl = config.databaseUrl; - // In the case of greenlock-store-fs there's a bunch of legacy stuff that goes on, so we just clobber it all on. - // Don't be like greenlock-store-fs (see note above). - store.options = mergeOptions(config); + // In the case of greenlock-store-fs there's a bunch of legacy stuff that goes on, so we just clobber it all on. + // Don't be like greenlock-store-fs (see note above). + store.options = mergeOptions(config); + store.accounts.options = store.options; + store.certificates.options = store.options; + if (!config.basePath && !config.configDir) { + console.info('Greenlock Store FS Path:', store.options.configDir); + } - - - - - // Certificates.check - // - // Use certificate.id, or subject, if id hasn't been set, to find a certificate. - // Return an object with string PEMs for cert and chain (or null, not undefined) - store.certificates.check = function (opts) { - // { certificate.id, subject, ... } - var id = opts.certificate && opts.certificate.id || opts.subject; - //console.log('certificates.check for', opts.certificate, opts.subject); - //console.log(opts); - - // For advanced use cases: - // This just goes 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); } - - - // Ignore this first bit, it's just file system template / compatibility stuff - var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject); - 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([ - readFileAsync(tameWild(privkeyPath, id), 'ascii') // 0 // all other PEM types are just - , readFileAsync(tameWild(certPath, id), 'ascii') // 1 // some arrangement of these 3 - , readFileAsync(tameWild(chainPath, id), 'ascii') // 2 // (bundle, combined, fullchain, etc) - ]).then(function (all) { - - //////////////////////// - // PAY ATTENTION HERE // - //////////////////////// - // This is all you have to return: cert, chain - return { - cert: all[1] // string PEM. the bare cert, half of the concatonated fullchain.pem you need - , chain: all[2] // string PEM. the bare chain, the second half of the fullchain.pem - , privkey: all[0] // string PEM. optional, allows checkKeypair to be skipped - - // These can be useful to store in your database, - // but otherwise they're easy to derive from the cert. - // (when not available they'll be generated from cert-info) - //, subject: certinfo.subject // string domain name - //, altnames: certinfo.altnames // array of domain name strings - //, issuedAt: certinfo.issuedAt // number in ms (a.k.a. NotBefore) - //, expiresAt: certinfo.expiresAt // number in ms (a.k.a. NotAfter) - }; - }).catch(function (err) { - // Treat non-exceptional failures as null returns (not undefined) - if ('ENOENT' === err.code) { return null; } - throw err; // True exceptions should be thrown - }); - }; - - - - // Implement if you need the ACME account metadata elsewhere in the chain of events - //store.accounts.check = function (opts) { - // console.log('accounts.check for', opts.account, opts.email); - // return PromiseA.resolve(null); - //}; - - - - // Accounts.checkKeypair - // - // Use account.id, or email, if id hasn't been set, to find an account keypair. - // Return an object with string privateKeyPem and/or object privateKeyJwk (or null, not undefined) - store.accounts.checkKeypair = function (opts) { - var id = opts.account.id || opts.email || 'single-user'; - //console.log('accounts.checkKeypair for', id); - - var pathname = path.join(tameWild(opts.accountsDir, opts.subject), sanitizeFilename(id) + '.json'); - return readFileAsync(tameWild(pathname, opts.subject), 'utf8').then(function (blob) { - // keypair can treated as an opaque object and just passed along, - // but just to show you what it is... - var keypair = JSON.parse(blob); - return { - privateKeyPem: keypair.privateKeyPem // string PEM private key - , privateKeyJwk: keypair.privateKeyJwk // object JWK private key - }; - }).catch(function (err) { - if ('ENOENT' === err.code) { return null; } - throw err; - }); - }; - - - - // Accounts.setKeypair({ account, email, keypair, ... }): - // - // Use account.id (or email if no id is present) to save an account keypair - // Return null (not undefined) on success, or throw on error - store.accounts.setKeypair = function (opts) { - //console.log('accounts.setKeypair for', opts.account, opts.email, opts.keypair); - var id = opts.account.id || opts.email || 'single-user'; - - // you can just treat the keypair as opaque and save and retrieve it as JSON - var keyblob = JSON.stringify({ - privateKeyPem: opts.keypair.privateKeyPem // string PEM - , privateKeyJwk: opts.keypair.privateKeyJwk // object JWK - }); - - // Ignore. - // Just implementation specific details here. - return mkdirpAsync(tameWild(opts.accountsDir, opts.subject)).then(function () { - var pathname = tameWild(path.join(opts.accountsDir, sanitizeFilename(id) + '.json'), opts.subject); - return writeFileAsync(tameWild(pathname, opts.subject), keyblob, 'utf8'); - }).then(function () { - // This is your job: return null, not undefined - return null; - }); - }; - - - - // Implement if you need the ACME account metadata elsewhere in the chain of events - //store.accounts.set = function (opts) { - // console.log('account.set:', opts.account, opts.email, opts.receipt); - // return PromiseA.resolve(null); - //}; - - - - // Certificates.checkKeypair - // - // Use certificate.kid, certificate.id, or subject to find a certificate keypair - // Return an object with string privateKeyPem and/or object privateKeyJwk (or null, not undefined) - store.certificates.checkKeypair = function (opts) { - //console.log('certificates.checkKeypair:', opts.certificate, opts.subject); - - // Ignore this. It's just special stuff for file system compat with the old le-store-certbot - 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) { - //////////////////////// - // PAY ATTENTION HERE // - //////////////////////// - return { - privateKeyPem: key // In this case we only saved privateKeyPem, so we only return it - //privateKeyJwk: null // (but it's fine, just different encodings of the same thing) - }; - }).catch(function (err) { - if ('ENOENT' === err.code) { return null; } - throw err; - }); - }; - - - - // Certificates.setKeypair({ certificate, subject, keypair, ... }): - // - // Use certificate.kid (or certificate.id or subject if no kid is present) to find a certificate keypair - // Return null (not undefined) on success, or throw on error - store.certificates.setKeypair = function (opts) { - var keypair = opts.keypair || keypair; - - // Ignore. - // Just specific implementation details. - var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject); - var privkeyPath = opts.privkeyPath || opts.domainKeyPath || path.join(liveDir, 'privkey.pem'); - return mkdirpAsync(tameWild(path.dirname(privkeyPath), opts.subject)).then(function () { - // keypair is normally an opaque object, but here it's a PEM for the FS (for things like Apache and Nginx) - return writeFileAsync(tameWild(privkeyPath, opts.subject), keypair.privateKeyPem, 'ascii').then(function () { - return null; - }); - }); - }; - - - - // Certificates.set({ subject, pems, ... }): - // - // Use certificate.id (or subject if no ki is present) to save a certificate - // Return null (not undefined) on success, or throw on error - store.certificates.set = function (opts) { - //console.log('certificates.set:', opts.subject, opts.pems); - var pems = { - cert: opts.pems.cert // string PEM the first half of the concatonated fullchain.pem cert - , chain: opts.pems.chain // string PEM the second half (yes, you need this too) - , privkey: opts.pems.privkey // Ignore. string PEM, useful if you have to create bundle.pem - }; - - // Ignore - // Just implementation specific details (writing lots of combinatons of files) - 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 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 () { - // That's your job: return null - return null; - }); - }; - - - - return store; + return store; }; /////////////////////////////////////////////////////////////////////////////// @@ -305,56 +84,36 @@ module.exports.create = function (config) { // // Everything below this line is just implementation specific var defaults = { - configDir: path.join(os.homedir(), 'acme', 'etc') + basePath: path.join(os.homedir(), '.config', 'greenlock'), -, 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') + accountsDir: path.join(':basePath', 'accounts', ':directoryUrl'), + serverDirGet: function(copy) { + return (copy.directoryUrl || copy.server || '') + .replace('https://', '') + .replace(/(\/)$/, '') + .replace(/\//g, path.sep); + }, + privkeyPath: path.join(':basePath', ':env', ':subject', 'privkey.pem'), + fullchainPath: path.join(':basePath', ':env', ':subject', 'fullchain.pem'), + certPath: path.join(':basePath', ':env', ':subject', 'cert.pem'), + chainPath: path.join(':basePath', ':env', ':subject', 'chain.pem'), + bundlePath: path.join(':basePath', ':env', ':subject', 'bundle.pem') }; +defaults.configDir = defaults.basePath; function mergeOptions(configs) { - if (!configs.domainKeyPath) { - configs.domainKeyPath = configs.privkeyPath || defaults.privkeyPath; - } + if (!configs.serverKeyPath) { + configs.serverKeyPath = + configs.domainKeyPath || + configs.privkeyPath || + defaults.privkeyPath; + } - Object.keys(defaults).forEach(function (key) { - if (!configs[key]) { - configs[key] = defaults[key]; - } - }); + 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); -} - -function getPromise() { - var util = require('util'); - var PromiseA; - if (util.promisify && global.Promise) { - PromiseA = global.Promise; - PromiseA.promisify = util.promisify; - } else { - try { - PromiseA = require('bluebird'); - } 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); - } - } - return PromiseA; + return configs; } diff --git a/promise.js b/promise.js new file mode 100644 index 0000000..b351781 --- /dev/null +++ b/promise.js @@ -0,0 +1,22 @@ +'use strict'; + +function getPromise() { + var util = require('util'); + var PromiseA; + if (util.promisify && global.Promise) { + PromiseA = global.Promise; + PromiseA.promisify = util.promisify; + } else { + try { + PromiseA = require('bluebird'); + } 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); + } + } + return PromiseA; +} + +module.exports = getPromise(); diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..f334c45 --- /dev/null +++ b/utils.js @@ -0,0 +1,46 @@ +'use strict'; + +var U = module.exports; + +// because not all file systems like '*' in a name (and they're scary) +U._tameWild = function tameWild(pathname, wild) { + if (!wild) { + return pathname; + } + var tame = wild.replace(/\*/g, '_'); + return pathname.replace(wild, tame); +}; + +U._tpl = function tpl(store, opts, str) { + var server = ['directoryUrl', 'serverDir', 'server']; + var env = ['env', 'directoryUrl']; + [ + ['basePath', 'configDir'], + server, + ['subject', 'hostname', 'domain'], + env + ].forEach(function(group) { + group.forEach(function(tmpl) { + group.forEach(function(key) { + var item = store.options[key] || opts[key] || ''; + if ('directoryUrl' === key) { + item = item.replace(/^https?:\/\//i, ''); + } + if ('env' === tmpl) { + if (/staging/.test(item)) { + item = 'staging'; + } else if (/acme-v02/.test(item)) { + item = 'live'; + } else { + // item = item; + } + } + if (-1 === str.indexOf(':' + tmpl)) { + return; + } + str = str.replace(':' + tmpl, item); + }); + }); + }); + return str; +};