Compare commits

..

No commits in common. "5490a194eb947b09269cc005a7a79a8c924e6527" and "de0f4d25b431a1793d52fccb7e90b5c0f982f84a" have entirely different histories.

10 changed files with 524 additions and 610 deletions

View File

@ -1,7 +0,0 @@
{
"bracketSpacing": true,
"printWidth": 80,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": false
}

245
README.md
View File

@ -4,188 +4,115 @@ A keypair and certificate storage strategy for Greenlock v2.7+ (and v3).
The (much simpler) successor to le-store-certbot. The (much simpler) successor to le-store-certbot.
Works with all ACME (Let's Encrypt) SSL certificate sytles: Works with all ACME (Let's Encrypt) SSL certificate sytles:
* [x] single domains
- [x] single domains * [x] multiple domains (SANs, AltNames)
- [x] multiple domains (SANs, AltNames) * [x] wildcards
- [x] wildcards * [x] private / localhost domains
- [x] private / localhost domains
# Usage # Usage
**Global** config:
```js ```js
greenlock.manager.defaults({ var greenlock = require('greenlock');
store: { var gl = greenlock.create({
module: "greenlock-store-fs", configDir: '~/.config/acme'
basePath: "~/.config/greenlock" , store: require('greenlock-store-fs')
} , approveDomains: approveDomains
}); , ...
```
**Per-site** config:
```js
greenlock.add({
subject: "example.com",
altnames: ["example.com", "www.example.com"],
store: {
module: "greenlock-store-fs",
basePath: "~/.config/greenlock"
}
}); });
``` ```
# File System # File System
The default file system layout mirrors that of certbot (python Let's Encrypt implementation) and The default file system layout mirrors that of le-store-certbot in order to make transitioning effortless,
the prior le-store-certbot in order to make transitioning effortless. in most situations:
The default structure looks like this: ```
acme
```txt ├── accounts
.config │   └── acme-staging-v02.api.letsencrypt.org
└── greenlock │   └── directory
├── accounts │   └── sites@example.com.json
│   └── acme-staging-v02.api.letsencrypt.org └── live
│   └── directory ├── example.com
│   └── sites@example.com.json │   ├── bundle.pem
├── staging │   ├── cert.pem
│ └── (same as live) │   ├── chain.pem
└── live │   ├── fullchain.pem
├── example.com │   └── privkey.pem
│   ├── bundle.pem └── www.example.com
│   ├── cert.pem ├── bundle.pem
│   ├── chain.pem ├── cert.pem
│   ├── fullchain.pem ├── chain.pem
│   └── privkey.pem ├── fullchain.pem
└── www.example.com └── privkey.pem
├── bundle.pem
├── cert.pem
├── chain.pem
├── fullchain.pem
└── privkey.pem
``` ```
# Internal Implementation Details # Wildcards & AltNames
You **DO NOT NEED TO KNOW** these details. Working with wildcards and multiple altnames requires greenlock >= v2.7 (or v3).
They're provided for the sake of understanding what happens "under the hood" To do so you must return `{ subject: '...', altnames: ['...', ...] }` within the `approveDomains()` callback.
to help you make better choices "in the seat".
# Parameters `subject` refers to "the subject of the ssl certificate" as opposed to `domain` which indicates "the domain servername
used in the current request". For single-domain certificates they're always the same, but for multiple-domain
certificates `subject` must be the name no matter what `domain` is receiving a request. `subject` is used as
part of the name of the file storage path where the certificate will be saved (or retrieved).
| parameters | example | notes | `altnames` should be the list of SubjectAlternativeNames (SANs) on the certificate.
| ----------------- | -------------------------------------------------------- | ---------------- | The subject and the first altname must be an exact match: `subject === altnames[0]`.
| `env` | `staging` or `live` | - |
| `directoryUrl` | `https://acme-staging-v02.api.letsencrypt.org/directory` | - |
| `keypair` | `{ privateKeyPem, privateKeyJwk }` | |
| `account` | `{ id: "an-arbitrary-id" }` | account only |
| `subscriberEmail` | `webhost@example.com` | account only |
| `certificate` | `{ id: "an-arbitrary-id" }` | certificate only |
| `subject` | `example.com` | certificate only |
| `pems` | `{ privkey, cert, chain, issuedAt, expiresAt }` | certificate only |
### Account Keypair ## Simple Example
```js ```js
accounts.setKeypair = async function({ function approveDomains(opts) {
env, // Allow only example.com and *.example.com (such as foo.example.com)
basePath,
directoryUrl, // foo.example.com => *.example.com
email, var wild = '*.' + opts.domain.split('.').slice(1).join('.');
account
}) { if ('example.com' !== opts.domain && '*.example.com' !== wild) {
var id = account.id || email; cb(new Error(opts.domain + " is not allowed"));
var serverDir = directoryUrl.replace("https://", ""); }
};
var result = { subject: 'example.com', altnames: [ 'example.com', '*.example.com' ] };
return Promise.resolve(result);
}
```
## Realistic Example
```js
function approveDomains(opts, certs, cb) {
var related = getRelated(opts.domain);
if (!related) { cb(new Error(opts.domain + " is not allowed")); };
opts.subject = related.subject;
opts.domains = related.domains;
cb({ options: opts, certs: certs });
}
``` ```
```js ```js
accounts.checkKeypair = async function({ function getRelated(domain) {
env, var related;
basePath, var wild = '*.' + domain.split('.').slice(1).join('.');
directoryUrl, if (Object.keys(allAllowedDomains).some(function (k) {
email, return allAllowedDomains[k].some(function (name) {
account if (domain === name || wild === name) {
}) { related = { subject: k, altnames: allAllowedDomains[k] };
var id = account.id || email; return true;
var serverDir = directoryUrl.replace("https://", ""); }
});
return { })) {
privateKeyPem, return related;
privateKeyJwk }
}; }
};
```
### Certificate Keypair
```js
certificate.setKeypair = async function({
env,
basePath,
directoryUrl,
subject,
certificate
}) {
var id = account.id || email;
env = env || directoryUrl.replace("https://", "");
};
``` ```
```js ```js
certificate.checkKeypair = async function({ var allAllowedDomains = {
env, 'example.com': ['example.com', '*.example.com']
basePath, , 'example.net': ['example.net', '*.example.net']
directoryUrl, }
subject,
certificate
}) {
var id = account.id || email;
env = env || directoryUrl.replace("https://", "");
return {
privateKeyPem,
privateKeyJwk
};
};
```
### Certificate PEMs
```js
certificate.set = async function({
env,
basePath,
directoryUrl,
subject,
certificate,
pems
}) {
var id = account.id || email;
env = env || directoryUrl.replace("https://", "");
};
```
```js
certificate.check = async function({
env,
basePath,
directoryUrl,
subject,
certificate
}) {
var id = account.id || email;
env = env || directoryUrl.replace("https://", "");
return {
privkey,
cert,
chain,
issuedAt,
expiresAt
};
};
``` ```

View File

@ -1,15 +1,15 @@
"use strict"; 'use strict';
var accounts = module.exports; var accounts = module.exports;
var store = accounts; var store = accounts;
var U = require("./utils.js"); var U = require('./utils.js');
var fs = require("fs"); var fs = require('fs');
var path = require("path"); var path = require('path');
var PromiseA = require("./promise.js"); var PromiseA = require('./promise.js');
var readFileAsync = PromiseA.promisify(fs.readFile); var readFileAsync = PromiseA.promisify(fs.readFile);
var writeFileAsync = PromiseA.promisify(fs.writeFile); var writeFileAsync = PromiseA.promisify(fs.writeFile);
var mkdirpAsync = PromiseA.promisify(require("@root/mkdirp")); var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp'));
// Implement if you need the ACME account metadata elsewhere in the chain of events // Implement if you need the ACME account metadata elsewhere in the chain of events
//store.accounts.check = function (opts) { //store.accounts.check = function (opts) {
@ -22,23 +22,23 @@ var mkdirpAsync = PromiseA.promisify(require("@root/mkdirp"));
// Use account.id, or email, if id hasn't been set, to find an account keypair. // 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) // Return an object with string privateKeyPem and/or object privateKeyJwk (or null, not undefined)
accounts.checkKeypair = function(opts) { accounts.checkKeypair = function(opts) {
var id = var id =
(opts.account && opts.account.id) || (opts.account && opts.account.id) ||
(opts.subscriberEmail || opts.email) || (opts.subscriberEmail || opts.email) ||
"single-user"; 'single-user';
//console.log('accounts.checkKeypair for', id); //console.log('accounts.checkKeypair for', id);
var pathname = path.join( var pathname = path.join(
accountsDir(store, opts), accountsDir(store, opts),
sanitizeFilename(id) + ".json" sanitizeFilename(id) + '.json'
); );
return readFileAsync(U._tameWild(pathname, opts.subject), "utf8") return readFileAsync(U._tameWild(pathname, opts.subject), 'utf8')
.then(function(blob) { .then(function(blob) {
// keypair can treated as an opaque object and just passed along, // keypair can treated as an opaque object and just passed along,
// but just to show you what it is... // but just to show you what it is...
var keypair = JSON.parse(blob); var keypair = JSON.parse(blob);
return keypair; return keypair;
/* /*
{ {
privateKeyPem: keypair.privateKeyPem, // string PEM private key privateKeyPem: keypair.privateKeyPem, // string PEM private key
privateKeyJwk: keypair.privateKeyJwk, // object JWK private key privateKeyJwk: keypair.privateKeyJwk, // object JWK private key
@ -46,13 +46,13 @@ accounts.checkKeypair = function(opts) {
public: keypair.public public: keypair.public
}; };
*/ */
}) })
.catch(function(err) { .catch(function(err) {
if ("ENOENT" === err.code) { if ('ENOENT' === err.code) {
return null; return null;
} }
throw err; throw err;
}); });
}; };
// Accounts.setKeypair({ account, email, keypair, ... }): // Accounts.setKeypair({ account, email, keypair, ... }):
@ -60,12 +60,12 @@ accounts.checkKeypair = function(opts) {
// Use account.id (or email if no id is present) to save an account 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 // Return null (not undefined) on success, or throw on error
accounts.setKeypair = function(opts) { accounts.setKeypair = function(opts) {
//console.log('accounts.setKeypair for', opts.account, opts.email, opts.keypair); //console.log('accounts.setKeypair for', opts.account, opts.email, opts.keypair);
var id = opts.account.id || opts.email || "single-user"; var id = opts.account.id || opts.email || 'single-user';
// you can just treat the keypair as opaque and save and retrieve it as JSON // 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(opts.keypair);
/* /*
var keyblob = JSON.stringify({ var keyblob = JSON.stringify({
privateKeyPem: opts.keypair.privateKeyPem, // string PEM privateKeyPem: opts.keypair.privateKeyPem, // string PEM
privateKeyJwk: opts.keypair.privateKeyJwk, // object JWK privateKeyJwk: opts.keypair.privateKeyJwk, // object JWK
@ -73,24 +73,24 @@ accounts.setKeypair = function(opts) {
}); });
*/ */
// Ignore. // Ignore.
// Just implementation specific details here. // Just implementation specific details here.
return mkdirpAsync(accountsDir(store, opts)) return mkdirpAsync(accountsDir(store, opts))
.then(function() { .then(function() {
var pathname = path.join( var pathname = path.join(
accountsDir(store, opts), accountsDir(store, opts),
sanitizeFilename(id) + ".json" sanitizeFilename(id) + '.json'
); );
return writeFileAsync( return writeFileAsync(
U._tameWild(pathname, opts.subject), U._tameWild(pathname, opts.subject),
keyblob, keyblob,
"utf8" 'utf8'
); );
}) })
.then(function() { .then(function() {
// This is your job: return null, not undefined // This is your job: return null, not undefined
return null; return null;
}); });
}; };
// Implement if you need the ACME account metadata elsewhere in the chain of events // Implement if you need the ACME account metadata elsewhere in the chain of events
@ -100,14 +100,14 @@ accounts.setKeypair = function(opts) {
//}; //};
function sanitizeFilename(id) { function sanitizeFilename(id) {
return id.replace(/(\.\.)|\\|\//g, "_").replace(/[^!-~]/g, "_"); return id.replace(/(\.\.)|\\|\//g, '_').replace(/[^!-~]/g, '_');
} }
function accountsDir(store, opts) { function accountsDir(store, opts) {
var dir = U._tpl( var dir = U._tpl(
store, store,
opts, opts,
opts.accountsDir || store.options.accountsDir opts.accountsDir || store.options.accountsDir
); );
return U._tameWild(dir, opts.subject || ""); return U._tameWild(dir, opts.subject || '');
} }

View File

@ -1,70 +1,70 @@
"use strict"; 'use strict';
var certificates = module.exports; var certificates = module.exports;
var store = certificates; var store = certificates;
var U = require("./utils.js"); var U = require('./utils.js');
var fs = require("fs"); var fs = require('fs');
var path = require("path"); var path = require('path');
var PromiseA = require("./promise.js"); var PromiseA = require('./promise.js');
var sfs = require("safe-replace"); var sfs = require('safe-replace');
var readFileAsync = PromiseA.promisify(fs.readFile); var readFileAsync = PromiseA.promisify(fs.readFile);
var writeFileAsync = PromiseA.promisify(fs.writeFile); var writeFileAsync = PromiseA.promisify(fs.writeFile);
var mkdirpAsync = PromiseA.promisify(require("@root/mkdirp")); var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp'));
// Certificates.check // Certificates.check
// //
// Use certificate.id, or subject, if id hasn't been set, to find a certificate. // 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) // Return an object with string PEMs for cert and chain (or null, not undefined)
certificates.check = function(opts) { certificates.check = function(opts) {
// { directoryUrl, subject, certificate.id, ... } // { certificate.id, subject, ... }
var id = (opts.certificate && opts.certificate.id) || opts.subject; var id = (opts.certificate && opts.certificate.id) || opts.subject;
//console.log('certificates.check for', opts); //console.log('certificates.check for', opts);
// For advanced use cases: // For advanced use cases:
// This just goes to show that any options set in approveDomains() will be available here // 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) // (the same is true for all of the hooks in this file)
if (opts.exampleThrowError) { if (opts.exampleThrowError) {
return Promise.reject(new Error("You want an error? You got it!")); return Promise.reject(new Error('You want an error? You got it!'));
} }
if (opts.exampleReturnNull) { if (opts.exampleReturnNull) {
return Promise.resolve(null); return Promise.resolve(null);
} }
if (opts.exampleReturnCerts) { if (opts.exampleReturnCerts) {
return Promise.resolve(opts.exampleReturnCerts); return Promise.resolve(opts.exampleReturnCerts);
} }
return Promise.all([ return Promise.all([
readFileAsync(U._tameWild(privkeyPath(store, opts), id), "ascii"), // 0 // all other PEM types are just 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(certPath(store, opts), id), 'ascii'), // 1 // some arrangement of these 3
readFileAsync(U._tameWild(chainPath(store, opts), id), "ascii") // 2 // (bundle, combined, fullchain, etc) readFileAsync(U._tameWild(chainPath(store, opts), id), 'ascii') // 2 // (bundle, combined, fullchain, etc)
]) ])
.then(function(all) { .then(function(all) {
//////////////////////// ////////////////////////
// PAY ATTENTION HERE // // PAY ATTENTION HERE //
//////////////////////// ////////////////////////
// This is all you have to return: cert, chain // This is all you have to return: cert, chain
return { return {
cert: all[1], // string PEM. the bare cert, half of the concatonated fullchain.pem you need 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 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 privkey: all[0] // string PEM. optional, allows checkKeypair to be skipped
// These can be useful to store in your database, // These can be useful to store in your database,
// but otherwise they're easy to derive from the cert. // but otherwise they're easy to derive from the cert.
// (when not available they'll be generated from cert-info) // (when not available they'll be generated from cert-info)
//, subject: certinfo.subject // string domain name //, subject: certinfo.subject // string domain name
//, altnames: certinfo.altnames // array of domain name strings //, altnames: certinfo.altnames // array of domain name strings
//, issuedAt: certinfo.issuedAt // number in ms (a.k.a. NotBefore) //, issuedAt: certinfo.issuedAt // number in ms (a.k.a. NotBefore)
//, expiresAt: certinfo.expiresAt // number in ms (a.k.a. NotAfter) //, expiresAt: certinfo.expiresAt // number in ms (a.k.a. NotAfter)
}; };
}) })
.catch(function(err) { .catch(function(err) {
// Treat non-exceptional failures as null returns (not undefined) // Treat non-exceptional failures as null returns (not undefined)
if ("ENOENT" === err.code) { if ('ENOENT' === err.code) {
return null; return null;
} }
throw err; // True exceptions should be thrown throw err; // True exceptions should be thrown
}); });
}; };
// Certificates.checkKeypair // Certificates.checkKeypair
@ -72,27 +72,27 @@ certificates.check = function(opts) {
// Use certificate.kid, certificate.id, or subject to find a certificate keypair // 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) // Return an object with string privateKeyPem and/or object privateKeyJwk (or null, not undefined)
certificates.checkKeypair = function(opts) { certificates.checkKeypair = function(opts) {
//console.log('certificates.checkKeypair:', opts); //console.log('certificates.checkKeypair:', opts);
return readFileAsync( return readFileAsync(
U._tameWild(privkeyPath(store, opts), opts.subject), U._tameWild(privkeyPath(store, opts), opts.subject),
"ascii" 'ascii'
) )
.then(function(key) { .then(function(key) {
//////////////////////// ////////////////////////
// PAY ATTENTION HERE // // PAY ATTENTION HERE //
//////////////////////// ////////////////////////
return { return {
privateKeyPem: key // In this case we only saved privateKeyPem, so we only return it 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) //privateKeyJwk: null // (but it's fine, just different encodings of the same thing)
}; };
}) })
.catch(function(err) { .catch(function(err) {
if ("ENOENT" === err.code) { if ('ENOENT' === err.code) {
return null; return null;
} }
throw err; throw err;
}); });
}; };
// Certificates.setKeypair({ certificate, subject, keypair, ... }): // Certificates.setKeypair({ certificate, subject, keypair, ... }):
@ -100,22 +100,22 @@ certificates.checkKeypair = function(opts) {
// Use certificate.kid (or certificate.id or subject if no kid is present) to find a certificate 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 // Return null (not undefined) on success, or throw on error
certificates.setKeypair = function(opts) { certificates.setKeypair = function(opts) {
var keypair = opts.keypair || keypair; var keypair = opts.keypair || keypair;
// Ignore. // Ignore.
// Just specific implementation details. // Just specific implementation details.
return mkdirpAsync( return mkdirpAsync(
U._tameWild(path.dirname(privkeyPath(store, opts)), opts.subject) U._tameWild(path.dirname(privkeyPath(store, opts)), opts.subject)
).then(function() { ).then(function() {
// keypair is normally an opaque object, but here it's a PEM for the FS (for things like Apache and Nginx) // keypair is normally an opaque object, but here it's a PEM for the FS (for things like Apache and Nginx)
return writeFileAsync( return writeFileAsync(
U._tameWild(privkeyPath(store, opts), opts.subject), U._tameWild(privkeyPath(store, opts), opts.subject),
keypair.privateKeyPem, keypair.privateKeyPem,
"ascii" 'ascii'
).then(function() { ).then(function() {
return null; return null;
}); });
}); });
}; };
// Certificates.set({ subject, pems, ... }): // Certificates.set({ subject, pems, ... }):
@ -123,143 +123,143 @@ certificates.setKeypair = function(opts) {
// Use certificate.id (or subject if no ki is present) to save a certificate // Use certificate.id (or subject if no ki is present) to save a certificate
// Return null (not undefined) on success, or throw on error // Return null (not undefined) on success, or throw on error
certificates.set = function(opts) { certificates.set = function(opts) {
//console.log('certificates.set:', opts); //console.log('certificates.set:', opts);
var pems = { var pems = {
cert: opts.pems.cert, // string PEM the first half of the concatonated fullchain.pem cert 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) 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 privkey: opts.pems.privkey // Ignore. string PEM, useful if you have to create bundle.pem
}; };
// Ignore // Ignore
// Just implementation specific details (writing lots of combinatons of files) // Just implementation specific details (writing lots of combinatons of files)
return mkdirpAsync(path.dirname(certPath(store, opts))) return mkdirpAsync(path.dirname(certPath(store, opts)))
.then(function() { .then(function() {
return mkdirpAsync( return mkdirpAsync(
path.dirname(U._tameWild(chainPath(store, opts), opts.subject)) path.dirname(U._tameWild(chainPath(store, opts), opts.subject))
).then(function() { ).then(function() {
return mkdirpAsync( return mkdirpAsync(
path.dirname( path.dirname(
U._tameWild(fullchainPath(store, opts), opts.subject) U._tameWild(fullchainPath(store, opts), opts.subject)
) )
).then(function() { ).then(function() {
return mkdirpAsync( return mkdirpAsync(
path.dirname( path.dirname(
U._tameWild(bundlePath(store, opts), opts.subject) U._tameWild(bundlePath(store, opts), opts.subject)
) )
).then(function() { ).then(function() {
var fullchainPem = [ var fullchainPem = [
pems.cert.trim() + "\n", pems.cert.trim() + '\n',
pems.chain.trim() + "\n" pems.chain.trim() + '\n'
].join("\n"); // for Apache, Nginx, etc ].join('\n'); // for Apache, Nginx, etc
var bundlePem = [ var bundlePem = [
pems.privkey, pems.privkey,
pems.cert, pems.cert,
pems.chain pems.chain
].join("\n"); // for HAProxy ].join('\n'); // for HAProxy
return PromiseA.all([ return PromiseA.all([
sfs.writeFileAsync( sfs.writeFileAsync(
U._tameWild( U._tameWild(
certPath(store, opts), certPath(store, opts),
opts.subject opts.subject
), ),
pems.cert, pems.cert,
"ascii" 'ascii'
), ),
sfs.writeFileAsync( sfs.writeFileAsync(
U._tameWild( U._tameWild(
chainPath(store, opts), chainPath(store, opts),
opts.subject opts.subject
), ),
pems.chain, pems.chain,
"ascii" 'ascii'
), ),
// Most web servers need these two // Most web servers need these two
sfs.writeFileAsync( sfs.writeFileAsync(
U._tameWild( U._tameWild(
fullchainPath(store, opts), fullchainPath(store, opts),
opts.subject opts.subject
), ),
fullchainPem, fullchainPem,
"ascii" 'ascii'
), ),
// HAProxy needs "bundle.pem" aka "combined.pem" // HAProxy needs "bundle.pem" aka "combined.pem"
sfs.writeFileAsync( sfs.writeFileAsync(
U._tameWild( U._tameWild(
bundlePath(store, opts), bundlePath(store, opts),
opts.subject opts.subject
), ),
bundlePem, bundlePem,
"ascii" 'ascii'
) )
]); ]);
}); });
}); });
}); });
}) })
.then(function() { .then(function() {
// That's your job: return null // That's your job: return null
return null; return null;
}); });
}; };
function liveDir(store, opts) { function liveDir(store, opts) {
return opts.liveDir || path.join(opts.configDir, "live", opts.subject); return opts.liveDir || path.join(opts.configDir, 'live', opts.subject);
} }
function privkeyPath(store, opts) { function privkeyPath(store, opts) {
var dir = U._tpl( var dir = U._tpl(
store, store,
opts, opts,
opts.serverKeyPath || opts.serverKeyPath ||
opts.privkeyPath || opts.privkeyPath ||
opts.domainKeyPath || opts.domainKeyPath ||
store.options.serverKeyPath || store.options.serverKeyPath ||
store.options.privkeyPath || store.options.privkeyPath ||
store.options.domainKeyPath || store.options.domainKeyPath ||
path.join(liveDir(), "privkey.pem") path.join(liveDir(), 'privkey.pem')
); );
return U._tameWild(dir, opts.subject || ""); return U._tameWild(dir, opts.subject || '');
} }
function certPath(store, opts) { function certPath(store, opts) {
var pathname = var pathname =
opts.certPath || opts.certPath ||
store.options.certPath || store.options.certPath ||
path.join(liveDir(), "cert.pem"); path.join(liveDir(), 'cert.pem');
var dir = U._tpl(store, opts, pathname); var dir = U._tpl(store, opts, pathname);
return U._tameWild(dir, opts.subject || ""); return U._tameWild(dir, opts.subject || '');
} }
function fullchainPath(store, opts) { function fullchainPath(store, opts) {
var dir = U._tpl( var dir = U._tpl(
store, store,
opts, opts,
opts.fullchainPath || opts.fullchainPath ||
store.options.fullchainPath || store.options.fullchainPath ||
path.join(liveDir(), "fullchain.pem") path.join(liveDir(), 'fullchain.pem')
); );
return U._tameWild(dir, opts.subject || ""); return U._tameWild(dir, opts.subject || '');
} }
function chainPath(store, opts) { function chainPath(store, opts) {
var dir = U._tpl( var dir = U._tpl(
store, store,
opts, opts,
opts.chainPath || opts.chainPath ||
store.options.chainPath || store.options.chainPath ||
path.join(liveDir(), "chain.pem") path.join(liveDir(), 'chain.pem')
); );
return U._tameWild(dir, opts.subject || ""); return U._tameWild(dir, opts.subject || '');
} }
function bundlePath(store, opts) { function bundlePath(store, opts) {
var dir = U._tpl( var dir = U._tpl(
store, store,
opts, opts,
opts.bundlePath || opts.bundlePath ||
store.options.bundlePath || store.options.bundlePath ||
path.join(liveDir(), "bundle.pem") path.join(liveDir(), 'bundle.pem')
); );
return U._tameWild(dir, opts.subject || ""); return U._tameWild(dir, opts.subject || '');
} }

View File

@ -1,7 +1,7 @@
"use strict"; 'use strict';
var os = require("os"); var os = require('os');
var path = require("path"); var path = require('path');
// How Storage Works in Greenlock: High-Level Call Stack // How Storage Works in Greenlock: High-Level Call Stack
// //
@ -50,32 +50,32 @@ var path = require("path");
// Either your user calls create with specific options, or greenlock calls it for you with a big options blob // Either your user calls create with specific options, or greenlock calls it for you with a big options blob
module.exports.create = function(config) { 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 // 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 // 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. // more than 10,000 domains, for example.
// basic setup // basic setup
var store = { var store = {
accounts: require("./accounts.js"), accounts: require('./accounts.js'),
certificates: require("./certificates.js") certificates: require('./certificates.js')
}; };
// For you store.options should probably start empty and get a minimal set of options copied from `config` above. // For you store.options should probably start empty and get a minimal set of options copied from `config` above.
// Example: // Example:
//store.options = {}; //store.options = {};
//store.options.databaseUrl = config.databaseUrl; //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. // 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). // Don't be like greenlock-store-fs (see note above).
store.options = mergeOptions(config); store.options = mergeOptions(config);
store.accounts.options = store.options; store.accounts.options = store.options;
store.certificates.options = store.options; store.certificates.options = store.options;
if (!config.basePath && !config.configDir) { if (!config.basePath && !config.configDir) {
console.info("Greenlock Store FS Path:", store.options.configDir); console.info('Greenlock Store FS Path:', store.options.configDir);
} }
return store; return store;
}; };
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -84,36 +84,36 @@ module.exports.create = function(config) {
// //
// Everything below this line is just implementation specific // Everything below this line is just implementation specific
var defaults = { var defaults = {
basePath: path.join(os.homedir(), ".config", "greenlock"), basePath: path.join(os.homedir(), '.config', 'greenlock'),
accountsDir: path.join(":basePath", "accounts", ":directoryUrl"), accountsDir: path.join(':basePath', 'accounts', ':directoryUrl'),
serverDirGet: function(copy) { serverDirGet: function(copy) {
return (copy.directoryUrl || copy.server || "") return (copy.directoryUrl || copy.server || '')
.replace("https://", "") .replace('https://', '')
.replace(/(\/)$/, "") .replace(/(\/)$/, '')
.replace(/\//g, path.sep); .replace(/\//g, path.sep);
}, },
privkeyPath: path.join(":basePath", ":env", ":subject", "privkey.pem"), privkeyPath: path.join(':basePath', ':env', ':subject', 'privkey.pem'),
fullchainPath: path.join(":basePath", ":env", ":subject", "fullchain.pem"), fullchainPath: path.join(':basePath', ':env', ':subject', 'fullchain.pem'),
certPath: path.join(":basePath", ":env", ":subject", "cert.pem"), certPath: path.join(':basePath', ':env', ':subject', 'cert.pem'),
chainPath: path.join(":basePath", ":env", ":subject", "chain.pem"), chainPath: path.join(':basePath', ':env', ':subject', 'chain.pem'),
bundlePath: path.join(":basePath", ":env", ":subject", "bundle.pem") bundlePath: path.join(':basePath', ':env', ':subject', 'bundle.pem')
}; };
defaults.configDir = defaults.basePath; defaults.configDir = defaults.basePath;
function mergeOptions(configs) { function mergeOptions(configs) {
if (!configs.serverKeyPath) { if (!configs.serverKeyPath) {
configs.serverKeyPath = configs.serverKeyPath =
configs.domainKeyPath || configs.domainKeyPath ||
configs.privkeyPath || configs.privkeyPath ||
defaults.privkeyPath; defaults.privkeyPath;
} }
Object.keys(defaults).forEach(function(key) { Object.keys(defaults).forEach(function(key) {
if (!configs[key]) { if (!configs[key]) {
configs[key] = defaults[key]; configs[key] = defaults[key];
} }
}); });
return configs; return configs;
} }

30
package-lock.json generated
View File

@ -1,18 +1,18 @@
{ {
"name": "greenlock-store-fs", "name": "greenlock-store-fs",
"version": "3.2.0", "version": "3.0.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@root/mkdirp": { "@root/mkdirp": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz",
"integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA==" "integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA=="
}, },
"safe-replace": { "safe-replace": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz", "resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz",
"integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw==" "integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw=="
}
} }
}
} }

View File

@ -1,31 +1,31 @@
{ {
"name": "greenlock-store-fs", "name": "greenlock-store-fs",
"version": "3.2.2", "version": "3.2.0",
"description": "A file-based certificate store for greenlock that supports wildcards.", "description": "A file-based certificate store for greenlock that supports wildcards.",
"homepage": "https://git.rootprojects.org/root/greenlock-store-fs.js", "homepage": "https://git.rootprojects.org/root/greenlock-store-fs.js",
"main": "index.js", "main": "index.js",
"directories": { "directories": {
"test": "tests" "test": "tests"
}, },
"scripts": { "scripts": {
"test": "node tests" "test": "node tests"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.rootprojects.org/root/greenlock-store-fs.js.git" "url": "https://git.rootprojects.org/root/greenlock-store-fs.js.git"
}, },
"keywords": [ "keywords": [
"greenlock", "greenlock",
"json", "json",
"keypairs", "keypairs",
"certificates", "certificates",
"store", "store",
"database" "database"
], ],
"author": "AJ ONeal <solderjs@gmail.com> (https://solderjs.com/)", "author": "AJ ONeal <solderjs@gmail.com> (https://solderjs.com/)",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"@root/mkdirp": "^1.0.0", "@root/mkdirp": "^1.0.0",
"safe-replace": "^1.1.0" "safe-replace": "^1.1.0"
} }
} }

View File

@ -1,22 +1,22 @@
"use strict"; 'use strict';
function getPromise() { function getPromise() {
var util = require("util"); var util = require('util');
var PromiseA; var PromiseA;
if (util.promisify && global.Promise) { if (util.promisify && global.Promise) {
PromiseA = global.Promise; PromiseA = global.Promise;
PromiseA.promisify = util.promisify; PromiseA.promisify = util.promisify;
} else { } else {
try { try {
PromiseA = require("bluebird"); PromiseA = require('bluebird');
} catch (e) { } catch (e) {
console.error( console.error(
"Your version of node is missing Promise. Please run `npm install --save bluebird` in your project to fix" 'Your version of node is missing Promise. Please run `npm install --save bluebird` in your project to fix'
); );
process.exit(10); process.exit(10);
} }
} }
return PromiseA; return PromiseA;
} }
module.exports = getPromise(); module.exports = getPromise();

52
test.js
View File

@ -1,33 +1,27 @@
"use strict"; 'use strict';
var tester = require("greenlock-store-test"); var tester = require('greenlock-store-test');
var crypto = require("crypto"); var crypto = require('crypto');
var os = require("os"); var os = require('os');
var path = require("path"); var path = require('path');
var basedir = path.join( var basedir = path.join(os.tmpdir(), 'greenlock-store-fs-test-' + crypto.randomBytes(4).toString('hex'));
os.tmpdir(), var domain = '*.example.com';
"greenlock-store-fs-test-" + crypto.randomBytes(4).toString("hex") var store = require('./').create({
); configDir: basedir
var domain = "*.example.com"; , accountsDir: path.join(basedir, 'accounts')
var store = require("./").create({ , privkeyPath: path.join(basedir, 'live', domain, 'privkey.pem')
configDir: basedir, , fullchainPath: path.join(basedir, 'live', domain, 'fullchain.pem')
accountsDir: path.join(basedir, "accounts"), , certPath: path.join(basedir, 'live', domain, 'cert.pem')
privkeyPath: path.join(basedir, "live", domain, "privkey.pem"), , chainPath: path.join(basedir, 'live', domain, 'chain.pem')
fullchainPath: path.join(basedir, "live", domain, "fullchain.pem"), , bundlePath: path.join(basedir, 'live', domain, 'bundle.pem')
certPath: path.join(basedir, "live", domain, "cert.pem"),
chainPath: path.join(basedir, "live", domain, "chain.pem"),
bundlePath: path.join(basedir, "live", domain, "bundle.pem")
}); });
console.info("Test Dir:", basedir); console.info('Test Dir:', basedir);
tester tester.test(store).then(function () {
.test(store) console.info("PASS");
.then(function() { }).catch(function (err) {
console.info("PASS"); console.error("FAIL");
}) console.error(err);
.catch(function(err) { process.exit(20);
console.error("FAIL"); });
console.error(err);
process.exit(20);
});

View File

@ -1,51 +1,51 @@
"use strict"; 'use strict';
var U = module.exports; var U = module.exports;
// because not all file systems like '*' in a name (and they're scary) // because not all file systems like '*' in a name (and they're scary)
U._tameWild = function tameWild(pathname, wild) { U._tameWild = function tameWild(pathname, wild) {
if (!wild) { if (!wild) {
return pathname; return pathname;
} }
var tame = wild.replace(/\*/g, "_"); var tame = wild.replace(/\*/g, '_');
return pathname.replace(wild, tame); return pathname.replace(wild, tame);
}; };
U._tpl = function tpl(store, opts, str) { U._tpl = function tpl(store, opts, str) {
var server = ["directoryUrl", "serverDir", "server"]; var server = ['directoryUrl', 'serverDir', 'server'];
var env = ["env", "directoryUrl"]; var env = ['env', 'directoryUrl'];
[ [
["basePath", "configDir"], ['basePath', 'configDir'],
server, server,
["subject", "hostname", "domain"], ['subject', 'hostname', 'domain'],
env env
].forEach(function(group) { ].forEach(function(group) {
group.forEach(function(tmpl) { group.forEach(function(tmpl) {
group.forEach(function(key) { group.forEach(function(key) {
var item = opts[key] || store.options[key]; var item = opts[key] || store.options[key];
if ("string" !== typeof item) { if ('string' !== typeof item) {
return; return;
} }
if ("directoryUrl" === key) { if ('directoryUrl' === key) {
item = item.replace(/^https?:\/\//i, ""); item = item.replace(/^https?:\/\//i, '');
} }
if ("env" === tmpl) { if ('env' === tmpl) {
if (/staging/.test(item)) { if (/staging/.test(item)) {
item = "staging"; item = 'staging';
} else if (/acme-v02/.test(item)) { } else if (/acme-v02/.test(item)) {
item = "live"; item = 'live';
} else { } else {
// item = item; // item = item;
} }
} }
if (-1 === str.indexOf(":" + tmpl)) { if (-1 === str.indexOf(':' + tmpl)) {
return; return;
} }
str = str.replace(":" + tmpl, item); str = str.replace(':' + tmpl, item);
}); });
}); });
}); });
return str; return str;
}; };