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