diff --git a/README.md b/README.md index baa2083..c83d2d3 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,19 @@ part of the name of the file storage path where the certificate will be saved (o ## Simple Example ```js -function approveDomains(opts, certs, cb) { +function approveDomains(opts) { + // Allow only example.com and *.example.com (such as foo.example.com) + // foo.example.com => *.example.com var wild = '*.' + opts.domain.split('.').slice(1).join('.'); - if ('*.example.com' !== wild) { cb(new Error(opts.domain + " is not allowed")); } + if ('example.com' !== opts.domain && '*.example.com' !== wild) { + cb(new Error(opts.domain + " is not allowed")); + } - opts.subject = '*.example.com'; - opts.domains = ['*.example.com']; + opts.subject = 'example.com'; + opts.domains = [ 'example.com', '*.example.com' ]; - cb({ options: opts, certs: certs }); + return Promise.resolve(opts); } ``` diff --git a/index.js b/index.js index ac14445..b64208f 100644 --- a/index.js +++ b/index.js @@ -37,23 +37,23 @@ module.exports.create = function (config) { // // 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 \/ + // 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.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.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). + // 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: @@ -66,10 +66,6 @@ module.exports.create = function (config) { // See the note on create() above. store.options = mergeOptions(config); - // getOptions(): - // This must be implemented for backwards compatibility. That is all. - store.getOptions = function () { return store.options; }; - // set and check account keypairs and account data store.accounts = {}; // set and check domain keypairs and domain certificates @@ -77,27 +73,17 @@ module.exports.create = function (config) { // certificates.checkAsync({ subject, ... }): // - // The first check is that a certificate looked for by domain name. + // 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. - // What should happen here is a lookup in a database (or filesystem). Generally the pattern will be to see if the - // domain is an exact match for a single-subject (single domain) or multi-subject (many domains via SANS/AltName) - // and then stripping the first part of the domain to see if there's a wildcard match. If you're clever you could - // also do these checks in parallel, but this only happens at startup and before renewal, so you don't have to get - // unless you want to for fun. // The only input you need to be concerned with is opts.subject (which falls back to opts.domains[0] if not set). - // However, this is called after `approveDomains)`, so any options that you set there will be available here too, - // as well as any other config you might need to access from other modules, if you're doing something special. - // - // On Success: Promise.resolve({ ... }) - the pem or jwk for the certificate - // 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")) + // And since this is called after `approveDomains()`, any options that you set there will be available here too. store.certificates.checkAsync = function (opts) { - // { domain, ... } - console.log('certificates.checkAsync for', opts.domain, opts.subject, opts.domains); - console.log(opts); - console.log(new Error("just for the stack trace:").stack); + // { 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 + // 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 Promise.reject(new Error("You want an error? You got it!")); } if (opts.exampleReturnNull) { return Promise.resolve(null); } @@ -110,45 +96,46 @@ module.exports.create = function (config) { var chainPath = opts.chainPath || path.join(liveDir, 'chain.pem'); return PromiseA.all([ - readFileAsync(tameWild(privkeyPath, opts.subject), 'ascii') // 0 - , readFileAsync(tameWild(certPath, opts.subject), 'ascii') // 1 - , readFileAsync(tameWild(chainPath, opts.subject), 'ascii') // 2 + // 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 - // (as is they'll be read via cert-info) + // 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 instance options - // (i.e. 'options' above, merged with other "override" or per-use options, such as from 'approveDomains)') - // are also available. You can ignore them unless your implementation is using them in some way. - // You should error if the account cannot be found (otherwise an unexpected error will be thrown) - // Although you can supply a 'check' thunk (node-style callback) here, it's going to be converted to a proper - // promise, so just go ahead and use that from the get-go. + // 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); - // 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 + store.accounts.checkAsync = function (/*opts*/) { + //var id = opts.account.id || 'single-user'; + //console.log('accounts.checkAsync for', id); return Promise.resolve(null); }; @@ -177,7 +164,7 @@ module.exports.create = function (config) { // 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) + // 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")) @@ -195,23 +182,26 @@ module.exports.create = function (config) { // accounts.setAsync({ account, keypair, email, ... }): // - // The account details, from ACME, if everything is successful. + // 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); + store.accounts.setAsync = function (/*opts, receipt*/) { + //receipt = opts.receipt || receipt; + //console.log('account.setAsync:', receipt); return Promise.resolve(null); }; // certificates.checkKeypairAsync({ subject, ... }): // - // Same rules as above apply, except for the private key of the certificate, not the public certificate itself. + // 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:'); + //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 }; @@ -228,6 +218,7 @@ module.exports.create = function (config) { 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 () { @@ -240,8 +231,8 @@ module.exports.create = function (config) { // // 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); + //console.log('certificates.setAsync:'); + //console.log(opts.domain, '<=', opts.subject); var pems = { privkey: opts.pems.privkey , cert: opts.pems.cert diff --git a/package.json b/package.json index 48d7155..00b9009 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "le-store-fs", - "version": "0.9.2", + "version": "1.0.0", "description": "A file-based certificate store for greenlock that supports wildcards.", "homepage": "https://git.coolaj86.com/coolaj86/le-store-fs.js", "main": "index.js",