Compare commits
12 Commits
Author | SHA1 | Date |
---|---|---|
AJ ONeal | b8fe2e5cbe | |
AJ ONeal | 5490a194eb | |
AJ ONeal | e0a9fff07d | |
AJ ONeal | de0f4d25b4 | |
AJ ONeal | ba284d0004 | |
AJ ONeal | 6d398d36c4 | |
AJ ONeal | a4aae8647d | |
AJ ONeal | aeca3c069a | |
AJ ONeal | 602a4c012a | |
AJ ONeal | 879b278d5f | |
AJ ONeal | 317dc3853f | |
AJ ONeal | 3230c31138 |
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": false
|
||||||
|
}
|
228
README.md
228
README.md
|
@ -1,31 +1,58 @@
|
||||||
# le-store-fs
|
# [greenlock-store-fs](https://git.rootprojects.org/root/greenlock-store-fs.js) | A [Root](https://rootprojects.org) project
|
||||||
|
|
||||||
A greenlock keypair and certificate storage strategy with wildcard support (simpler successor to le-store-certbot).
|
A keypair and certificate storage strategy for Greenlock v2.7+ (and v3).
|
||||||
|
The (much simpler) successor to le-store-certbot.
|
||||||
|
|
||||||
|
Works with all ACME (Let's Encrypt) SSL certificate sytles:
|
||||||
|
|
||||||
|
- [x] single domains
|
||||||
|
- [x] multiple domains (SANs, AltNames)
|
||||||
|
- [x] wildcards
|
||||||
|
- [x] private / localhost domains
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
|
**Global** config:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var greenlock = require('greenlock');
|
greenlock.manager.defaults({
|
||||||
var gl = greenlock.create({
|
store: {
|
||||||
configDir: '~/.config/acme'
|
module: "greenlock-store-fs",
|
||||||
, store: require('le-store-fs')
|
basePath: "~/.config/greenlock"
|
||||||
, 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 le-store-certbot in order to make transitioning effortless,
|
The default file system layout mirrors that of certbot (python Let's Encrypt implementation) and
|
||||||
in most situations:
|
the prior le-store-certbot in order to make transitioning effortless.
|
||||||
|
|
||||||
```
|
The default structure looks like this:
|
||||||
acme
|
|
||||||
├── accounts
|
```txt
|
||||||
│ └── acme-staging-v02.api.letsencrypt.org
|
.config
|
||||||
│ └── directory
|
└── greenlock
|
||||||
│ └── sites@example.com.json
|
├── accounts
|
||||||
└── live
|
│ └── acme-staging-v02.api.letsencrypt.org
|
||||||
|
│ └── directory
|
||||||
|
│ └── sites@example.com.json
|
||||||
|
├── staging
|
||||||
|
│ └── (same as live)
|
||||||
|
└── live
|
||||||
├── example.com
|
├── example.com
|
||||||
│ ├── bundle.pem
|
│ ├── bundle.pem
|
||||||
│ ├── cert.pem
|
│ ├── cert.pem
|
||||||
|
@ -40,72 +67,129 @@ acme
|
||||||
└── privkey.pem
|
└── privkey.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
# Wildcards & AltNames
|
# Internal Implementation Details
|
||||||
|
|
||||||
Working with wildcards and multiple altnames requires greenlock >= v2.7.
|
You **DO NOT NEED TO KNOW** these details.
|
||||||
|
|
||||||
To do so you must set `opts.subject` and `opts.domains` within the `approvedomains()` callback.
|
They're provided for the sake of understanding what happens "under the hood"
|
||||||
|
to help you make better choices "in the seat".
|
||||||
|
|
||||||
`subject` refers to "the subject of the ssl certificate" as opposed to `domain` which indicates "the domain servername
|
**Note**: The actual code could stand to be tidied up.
|
||||||
used in the current request". For single-domain certificates they're always the same, but for multiple-domain
|
It does need to continue to support Greenlock v2 for a few more months,
|
||||||
certificates `subject` must be the name no matter what `domain` is receiving a request. `subject` is used as
|
so I didn't rip out the old v1 -> v2 -> v3 cruft yet.
|
||||||
part of the name of the file storage path where the certificate will be saved (or retrieved).
|
|
||||||
|
|
||||||
`domains` should be the list of "altnames" on the certificate, which should include the `subject`.
|
# Parameters
|
||||||
|
|
||||||
## Simple Example
|
| parameters | example | notes |
|
||||||
|
| ----------------- | -------------------------------------------------------- | ---------------- |
|
||||||
|
| `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
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function approveDomains(opts) {
|
accounts.setKeypair = async function({
|
||||||
// Allow only example.com and *.example.com (such as foo.example.com)
|
env,
|
||||||
|
basePath,
|
||||||
// foo.example.com => *.example.com
|
directoryUrl,
|
||||||
var wild = '*.' + opts.domain.split('.').slice(1).join('.');
|
email,
|
||||||
if ('example.com' !== opts.domain && '*.example.com' !== wild) {
|
account
|
||||||
cb(new Error(opts.domain + " is not allowed"));
|
}) {
|
||||||
}
|
var id = account.id || email;
|
||||||
|
var serverDir = directoryUrl.replace("https://", "");
|
||||||
opts.subject = 'example.com';
|
};
|
||||||
opts.domains = [ 'example.com', '*.example.com' ];
|
|
||||||
|
|
||||||
return Promise.resolve(opts);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
||||||
function getRelated(domain) {
|
accounts.checkKeypair = async function({
|
||||||
var related;
|
env,
|
||||||
var wild = '*.' + domain.split('.').slice(1).join('.');
|
basePath,
|
||||||
if (Object.keys(allAllowedDomains).some(function (k) {
|
directoryUrl,
|
||||||
return allAllowedDomains[k].some(function (name) {
|
email,
|
||||||
if (domain === name || wild === name) {
|
account
|
||||||
related = { subject: k, altnames: allAllowedDomains[k] };
|
}) {
|
||||||
return true;
|
var id = account.id || email;
|
||||||
}
|
var serverDir = directoryUrl.replace("https://", "");
|
||||||
});
|
|
||||||
})) {
|
return {
|
||||||
return related;
|
privateKeyPem,
|
||||||
}
|
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
|
||||||
var allAllowedDomains = {
|
certificate.checkKeypair = async function({
|
||||||
'example.com': ['example.com', '*.example.com']
|
env,
|
||||||
, 'example.net': ['example.net', '*.example.net']
|
basePath,
|
||||||
}
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
|
@ -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 || "");
|
||||||
|
}
|
|
@ -0,0 +1,265 @@
|
||||||
|
"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) {
|
||||||
|
// { directoryUrl, subject, certificate.id, ... }
|
||||||
|
var id = (opts.certificate && opts.certificate.id) || opts.subject;
|
||||||
|
//console.log('certificates.check for', 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
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 pathname =
|
||||||
|
opts.certPath ||
|
||||||
|
store.options.certPath ||
|
||||||
|
path.join(liveDir(), "cert.pem");
|
||||||
|
|
||||||
|
var dir = U._tpl(store, opts, pathname);
|
||||||
|
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 || "");
|
||||||
|
}
|
370
index.js
370
index.js
|
@ -1,295 +1,115 @@
|
||||||
'use strict';
|
"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");
|
var os = require("os");
|
||||||
|
var path = require("path");
|
||||||
|
|
||||||
// create():
|
// How Storage Works in Greenlock: High-Level Call Stack
|
||||||
// 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.
|
// nested === skipped if parent succeeds (or has cached result)
|
||||||
// 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
|
// tls.SNICallback() // TLS connection with SNI kicks of the request
|
||||||
// (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".
|
// greenlock.approveDomains(opts) // Greenlokc does some housekeeping, checks for a cert in
|
||||||
module.exports.create = function (config) {
|
// // 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: ['...'] }
|
||||||
|
// // Anything returned by approveDomains() will be received
|
||||||
|
// // by all plugins at all stages
|
||||||
|
//
|
||||||
|
// greenlock.store.certificates.check() // Certificate checking happens after approval for several
|
||||||
|
// // reasons, including preventing duplicate registrations
|
||||||
|
// // but most importantly because you can dynamically swap the
|
||||||
|
// // storage plugin right from approveDomains().
|
||||||
|
// greenlock.store.certificates.checkKeypair() // Check for a keypair associated with the domain
|
||||||
|
//
|
||||||
|
// greenlock.store.accounts.check() // Optional. If you need it, look at other Greenlock docs
|
||||||
|
//
|
||||||
|
// greenlock.store.accounts.checkKeypair() // Check storage for registered account key
|
||||||
|
// (opts.generateKeypair||RSA.generateKeypair)() // Generates a new keypair
|
||||||
|
// greenlock.core.accounts.register() // Registers the keypair as an ACME account
|
||||||
|
// greenlock.store.accounts.setKeypair() // Saves the keypair of the registered account
|
||||||
|
// greenlock.store.accounts.set() // Optional. Saves superfluous ACME account metadata
|
||||||
|
//
|
||||||
|
// greenlock.core.certificates.register() // Begin certificate registration process & housekeeping
|
||||||
|
// (opts.generateKeypair||RSA.generateKeypair)() // Generates a new certificate keypair
|
||||||
|
// greenlock.acme.certificates.register() // Performs the ACME challenge processes
|
||||||
|
// greenlock.store.certificates.setKeypair() // Saves the keypair for the valid certificate
|
||||||
|
// greenlock.store.certificates.set() // Saves the valid certificate
|
||||||
|
|
||||||
// This file has been laid out in the order that options are used and calls are made
|
////////////////////////////////////////////
|
||||||
// SNICallback() // le-sni-auto has a cache
|
// Recap of the high-level overview above //
|
||||||
// greenlock.approveDomains()
|
////////////////////////////////////////////
|
||||||
// // you get opts.domain passed to you from SNI
|
//
|
||||||
// // you should set opts.subject as the cert "id" domain
|
// None of this ever gets called except if there's not a cert already cached.
|
||||||
// // you should set opts.domains as all domains on the cert
|
// That only happens on service boot, and about every 75 days for each cert's renewal.
|
||||||
// // you should set opts.account.id, otherwise opts.email will be used
|
//
|
||||||
// greenlock.store.certificates.checkAsync() // on success -> SNI cache, on fail -> checkAccount
|
// Therefore, none of this needs to be fast, fancy, or clever
|
||||||
// greenlock.store.accounts.checkAsync() // optional (you can always return null)
|
//
|
||||||
// greenlock.store.accounts.checkKeypairAsync()
|
// For any type of customization, whatever is set in `approveDomains()` is available everywhere else.
|
||||||
// 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
|
// 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
|
// 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.
|
||||||
var store = {};
|
|
||||||
|
|
||||||
// options:
|
// basic setup
|
||||||
//
|
var store = {
|
||||||
// If your module requires options (i.e. file paths or database urls) you should check what you get from create()
|
accounts: require("./accounts.js"),
|
||||||
// and copy over the things you'll use into this options object. You should also merge in any defaults for options
|
certificates: require("./certificates.js")
|
||||||
// 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.
|
// For you store.options should probably start empty and get a minimal set of options copied from `config` above.
|
||||||
// See the note on create() 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);
|
store.options = mergeOptions(config);
|
||||||
|
store.accounts.options = store.options;
|
||||||
|
store.certificates.options = store.options;
|
||||||
|
|
||||||
// set and check account keypairs and account data
|
if (!config.basePath && !config.configDir) {
|
||||||
store.accounts = {};
|
console.info("Greenlock Store FS Path:", store.options.configDir);
|
||||||
// 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;
|
return store;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Ignore //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Everything below this line is just implementation specific
|
||||||
var defaults = {
|
var defaults = {
|
||||||
configDir: path.join(os.homedir(), 'acme', 'etc')
|
basePath: path.join(os.homedir(), ".config", "greenlock"),
|
||||||
|
|
||||||
, accountsDir: path.join(':configDir', 'accounts', ':serverDir')
|
accountsDir: path.join(":basePath", "accounts", ":directoryUrl"),
|
||||||
, serverDirGet: function (copy) {
|
serverDirGet: function(copy) {
|
||||||
return (copy.server || '').replace('https://', '').replace(/(\/)$/, '').replace(/\//g, path.sep);
|
return (copy.directoryUrl || copy.server || "")
|
||||||
}
|
.replace("https://", "")
|
||||||
, privkeyPath: path.join(':configDir', 'live', ':hostname', 'privkey.pem')
|
.replace(/(\/)$/, "")
|
||||||
, fullchainPath: path.join(':configDir', 'live', ':hostname', 'fullchain.pem')
|
.replace(/\//g, path.sep);
|
||||||
, certPath: path.join(':configDir', 'live', ':hostname', 'cert.pem')
|
},
|
||||||
, chainPath: path.join(':configDir', 'live', ':hostname', 'chain.pem')
|
privkeyPath: path.join(":basePath", ":env", ":subject", "privkey.pem"),
|
||||||
, bundlePath: path.join(':configDir', 'live', ':hostname', 'bundle.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) {
|
function mergeOptions(configs) {
|
||||||
if (!configs.domainKeyPath) {
|
if (!configs.serverKeyPath) {
|
||||||
configs.domainKeyPath = configs.privkeyPath || defaults.privkeyPath;
|
configs.serverKeyPath =
|
||||||
|
configs.domainKeyPath ||
|
||||||
|
configs.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];
|
||||||
}
|
}
|
||||||
|
@ -297,13 +117,3 @@ function mergeOptions(configs) {
|
||||||
|
|
||||||
return configs;
|
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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,21 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "le-store-json",
|
"name": "greenlock-store-fs",
|
||||||
"version": "1.0.0",
|
"version": "3.2.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": {
|
"@root/mkdirp": {
|
||||||
"version": "0.0.8",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz",
|
||||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
|
"integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA=="
|
||||||
},
|
|
||||||
"mkdirp": {
|
|
||||||
"version": "0.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
|
||||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
|
||||||
"requires": {
|
|
||||||
"minimist": "0.0.8"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"safe-replace": {
|
"safe-replace": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
|
12
package.json
12
package.json
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "le-store-fs",
|
"name": "greenlock-store-fs",
|
||||||
"version": "1.0.1",
|
"version": "3.2.2",
|
||||||
"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.coolaj86.com/coolaj86/le-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"
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.coolaj86.com/coolaj86/le-store-fs.js.git"
|
"url": "https://git.rootprojects.org/root/greenlock-store-fs.js.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"greenlock",
|
"greenlock",
|
||||||
|
@ -22,10 +22,10 @@
|
||||||
"store",
|
"store",
|
||||||
"database"
|
"database"
|
||||||
],
|
],
|
||||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
"author": "AJ ONeal <solderjs@gmail.com> (https://solderjs.com/)",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mkdirp": "^0.5.1",
|
"@root/mkdirp": "^1.0.0",
|
||||||
"safe-replace": "^1.1.0"
|
"safe-replace": "^1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
@ -0,0 +1,33 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var tester = require("greenlock-store-test");
|
||||||
|
|
||||||
|
var crypto = require("crypto");
|
||||||
|
var os = require("os");
|
||||||
|
var path = require("path");
|
||||||
|
var basedir = path.join(
|
||||||
|
os.tmpdir(),
|
||||||
|
"greenlock-store-fs-test-" + crypto.randomBytes(4).toString("hex")
|
||||||
|
);
|
||||||
|
var domain = "*.example.com";
|
||||||
|
var store = require("./").create({
|
||||||
|
configDir: basedir,
|
||||||
|
accountsDir: path.join(basedir, "accounts"),
|
||||||
|
privkeyPath: path.join(basedir, "live", domain, "privkey.pem"),
|
||||||
|
fullchainPath: path.join(basedir, "live", domain, "fullchain.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);
|
||||||
|
|
||||||
|
tester
|
||||||
|
.test(store)
|
||||||
|
.then(function() {
|
||||||
|
console.info("PASS");
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.error("FAIL");
|
||||||
|
console.error(err);
|
||||||
|
process.exit(20);
|
||||||
|
});
|
|
@ -0,0 +1,51 @@
|
||||||
|
"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 = opts[key] || store.options[key];
|
||||||
|
if ("string" !== typeof item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
Loading…
Reference in New Issue