🔐 Free SSL, Free Wildcard SSL, and Fully Automated HTTPS for node.js, issued by Let's Encrypt v2 via ACME. Issues and PRs on Github.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

517 lines
17 KiB

'use strict';
var PromiseA = require('bluebird');
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
var path = require('path');
var fs = PromiseA.promisifyAll(require('fs'));
var sfs = require('safe-replace');
var fetchFromConfigLiveDir = function (args) {
// TODO NO HARD-CODED DEFAULTS
if (!args.fullchainPath || !args.privkeyPath || !args.certPath || !args.chainPath) {
console.warn("missing one or more of args.privkeyPath, args.fullchainPath, args.certPath, args.chainPath");
console.warn("hard-coded conventional pathnames were for debugging and are not a stable part of the API");
}
//, fs.readFileAsync(fullchainPath, 'ascii')
// note: if this ^^ gets added back in, the arrays below must change
return PromiseA.all([
fs.readFileAsync(args.privkeyPath, 'ascii') // 0
, fs.readFileAsync(args.certPath, 'ascii') // 1
, fs.readFileAsync(args.chainPath, 'ascii') // 2
// stat the file, not the link
, fs.statAsync(args.certPath) // 3
]).then(function (arr) {
var cert = arr[1];
var getCertInfo = require('./cert-info').getCertInfo;
// XXX Note: Parsing the certificate info comes at a great cost (~500kb)
var certInfo = getCertInfo(cert);
return {
key: arr[0] // privkey.pem
, privkey: arr[0] // privkey.pem
, fullchain: arr[1] + '\n' + arr[2] // fullchain.pem
, cert: cert // cert.pem
, chain: arr[2] // chain.pem
, ca: arr[2] // chain.pem
, privkeyPath: args.privkeyPath
, fullchainPath: args.fullchainPath
, certPath: args.certPath
, chainPath: args.chainPath
//, issuedAt: arr[3].mtime.valueOf()
, issuedAt: Date(certInfo.notBefore.value).valueOf() // Date.now()
, expiresAt: Date(certInfo.notAfter.value).valueOf()
, lifetime: args.lifetime
};
}, function (err) {
if (args.debug) {
console.error("[letsencrypt/lib/common.js] fetchFromDisk");
console.error(err.stack);
}
return null;
});
};
function getAccount(args) {
var accountId = args.accountId;
var accountDir = path.join(args.accountsDir, accountId);
var files = {};
var configs = [ 'meta.json', 'private_key.json', 'regr.json' ];
return PromiseA.all(configs.map(function (filename) {
var keyname = filename.slice(0, -5);
return fs.readFileAsync(path.join(accountDir, filename), 'utf8').then(function (text) {
var data;
try {
data = JSON.parse(text);
} catch(e) {
files[keyname] = { error: e };
return;
}
files[keyname] = data;
}, function (err) {
files[keyname] = { error: err };
});
})).then(function () {
var err;
if (!Object.keys(files).every(function (key) {
return !files[key].error;
}) || !files.private_key || !files.private_key.n) {
err = new Error("Account '" + accountId + "' was corrupt. No big deal (I think?). Creating a new one...");
err.code = 'E_ACCOUNT_CORRUPT';
err.data = files;
return PromiseA.reject(err);
}
//files.private_key;
//files.regr;
//files.meta;
files.accountId = accountId; // preserve current account id
files.id = accountId;
files.keypair = { privateKeyJwk: files.private_key };
return files;
});
}
function getAccountIdByEmail(args) {
// If we read 10,000 account directories looking for
// just one email address, that could get crazy.
// We should have a folder per email and list
// each account as a file in the folder
// TODO
var email = args.email;
if ('string' !== typeof email) {
if (args.debug) {
console.log("[LE] No email given");
}
return PromiseA.resolve(null);
}
return fs.readdirAsync(args.accountsDir).then(function (nodes) {
if (args.debug) {
console.log("[LE] arg.accountsDir success");
}
return PromiseA.all(nodes.map(function (node) {
return fs.readFileAsync(path.join(args.accountsDir, node, 'regr.json'), 'utf8').then(function (text) {
var regr = JSON.parse(text);
regr.__accountId = node;
return regr;
});
})).then(function (regrs) {
var accountId;
/*
if (args.debug) {
console.log('read many regrs');
console.log('regrs', regrs);
}
*/
regrs.some(function (regr) {
return regr.body.contact.some(function (contact) {
var match = contact.toLowerCase() === 'mailto:' + email.toLowerCase();
if (match) {
accountId = regr.__accountId;
return true;
}
});
});
if (!accountId) {
return null;
}
return accountId;
});
}).then(function (accountId) {
return accountId;
}, function (err) {
if ('ENOENT' === err.code) {
// ignore error
return null;
}
return PromiseA.reject(err);
});
}
function readRenewalConfig(args) {
var pyconf = PromiseA.promisifyAll(require('pyconf'));
return pyconf.readFileAsync(args.renewalPath).then(function (pyobj) {
return pyobj;
}, function () {
return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (pyobj) {
return pyobj;
});
});
}
function writeRenewalConfig(args) {
function log() {
if (args.debug) {
console.log.apply(console, arguments);
}
}
var pyobj = args.pyobj;
pyobj.checkpoints = parseInt(pyobj.checkpoints, 10) || 0;
var pyconf = PromiseA.promisifyAll(require('pyconf'));
var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]);
var certPath = args.certPath || pyobj.cert || path.join(liveDir, 'cert.pem');
var fullchainPath = args.fullchainPath || pyobj.fullchain || path.join(liveDir, 'fullchain.pem');
var chainPath = args.chainPath || pyobj.chain || path.join(liveDir, 'chain.pem');
var privkeyPath = args.privkeyPath || pyobj.privkey
//|| args.domainPrivateKeyPath || args.domainKeyPath || pyobj.keyPath
|| path.join(liveDir, 'privkey.pem');
log('[le/core.js] privkeyPath', privkeyPath);
var updates = {
account: args.account.id
, configDir: args.configDir
, domains: args.domains
, email: args.email
, tos: args.agreeTos && true
// yes, it's an array. weird, right?
, webrootPath: args.webrootPath && [args.webrootPath] || []
, server: args.server || args.acmeDiscoveryUrl
, privkey: privkeyPath
, fullchain: fullchainPath
, cert: certPath
, chain: chainPath
, http01Port: args.http01Port
, keyPath: args.domainPrivateKeyPath || args.privkeyPath
, rsaKeySize: args.rsaKeySize
, checkpoints: pyobj.checkpoints
/* // TODO XXX what's the deal with these? they don't make sense
// are they just old junk? or do they have a meaning that I don't know about?
, fullchainPath: path.join(args.configDir, 'chain.pem')
, certPath: path.join(args.configDir, 'cert.pem')
, chainPath: path.join(args.configDir, 'chain.pem')
*/ // TODO XXX end
, workDir: args.workDir
, logsDir: args.logsDir
};
// final section is completely dynamic
// :hostname = :webroot_path
args.domains.forEach(function (hostname) {
updates[hostname] = args.webrootPath;
});
// must write back to the original pyobject or
// annotations will be lost
Object.keys(updates).forEach(function (key) {
pyobj[key] = updates[key];
});
return mkdirpAsync(path.dirname(args.renewalPath)).then(function () {
return pyconf.writeFileAsync(args.renewalPath, pyobj);
}).then(function () {
// NOTE
// writing twice seems to causes a bug,
// so instead we re-read the file from the disk
return pyconf.readFileAsync(args.renewalPath);
});
}
function getOrCreateRenewal(args) {
return readRenewalConfig(args).then(function (pyobj) {
var minver = pyobj.checkpoints >= 0;
args.pyobj = pyobj;
if (!minver) {
args.checkpoints = 0;
pyobj.checkpoints = 0;
return writeRenewalConfig(args);
}
// args.account.id = pyobj.account
// args.configDir = args.configDir || pyobj.configDir;
args.checkpoints = pyobj.checkpoints;
args.agreeTos = (args.agreeTos || pyobj.tos) && true;
args.email = args.email || pyobj.email;
args.domains = args.domains || pyobj.domains;
// yes, it's an array. weird, right?
args.webrootPath = args.webrootPath || pyobj.webrootPath[0];
args.server = args.server || args.acmeDiscoveryUrl || pyobj.server;
args.certPath = args.certPath || pyobj.cert;
args.privkeyPath = args.privkeyPath || pyobj.privkey;
args.chainPath = args.chainPath || pyobj.chain;
args.fullchainPath = args.fullchainPath || pyobj.fullchain;
//, workDir: args.workDir
//, logsDir: args.logsDir
args.rsaKeySize = args.rsaKeySize || pyobj.rsaKeySize;
args.http01Port = args.http01Port || pyobj.http01Port;
args.domainKeyPath = args.domainPrivateKeyPath || args.domainKeyPath || args.keyPath || pyobj.keyPath;
return writeRenewalConfig(args);
});
}
function writeCertificateAsync(args) {
function log() {
if (args.debug) {
console.log.apply(console, arguments);
}
}
log("[le/core.js] got certificate!");
var obj = args.pyobj;
var pems = args.pems;
pems.fullchain = pems.cert + '\n' + (pems.chain || pems.ca);
obj.checkpoints = parseInt(obj.checkpoints, 10) || 0;
var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]);
var certPath = args.certPath || obj.cert || path.join(liveDir, 'cert.pem');
var fullchainPath = args.fullchainPath || obj.fullchain || path.join(liveDir, 'fullchain.pem');
var chainPath = args.chainPath || obj.chain || path.join(liveDir, 'chain.pem');
var privkeyPath = args.privkeyPath || obj.privkey
//|| args.domainPrivateKeyPath || args.domainKeyPath || obj.keyPath
|| path.join(liveDir, 'privkey.pem');
log('[le/core.js] privkeyPath', privkeyPath);
var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]);
var checkpoints = obj.checkpoints.toString();
var certArchive = path.join(archiveDir, 'cert' + checkpoints + '.pem');
var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoints + '.pem');
var chainArchive = path.join(archiveDir, 'chain'+ checkpoints + '.pem');
var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoints + '.pem');
return mkdirpAsync(archiveDir).then(function () {
return PromiseA.all([
sfs.writeFileAsync(certArchive, pems.cert, 'ascii')
, sfs.writeFileAsync(chainArchive, (pems.chain || pems.ca), 'ascii')
, sfs.writeFileAsync(fullchainArchive, pems.fullchain, 'ascii')
, sfs.writeFileAsync(
privkeyArchive
// TODO nix args.key, args.domainPrivateKeyPem ??
, (pems.privkey || pems.key) // || RSA.exportPrivatePem(args.domainKeypair)
, 'ascii'
)
]);
}).then(function () {
return mkdirpAsync(liveDir);
}).then(function () {
return PromiseA.all([
sfs.writeFileAsync(certPath, pems.cert, 'ascii')
, sfs.writeFileAsync(chainPath, (pems.chain || pems.ca), 'ascii')
, sfs.writeFileAsync(fullchainPath, pems.fullchain, 'ascii')
, sfs.writeFileAsync(
privkeyPath
// TODO nix args.key, args.domainPrivateKeyPem ??
, (pems.privkey || pems.key) // || RSA.exportPrivatePem(args.domainKeypair)
, 'ascii'
)
]);
}).then(function () {
obj.checkpoints += 1;
args.checkpoints += 1;
return writeRenewalConfig(args);
}).then(function () {
var getCertInfo = require('./cert-info').getCertInfo;
// XXX Note: Parsing the certificate info comes at a great cost (~500kb)
var certInfo = getCertInfo(pems.cert);
return {
certPath: certPath
, chainPath: chainPath
, fullchainPath: fullchainPath
, privkeyPath: privkeyPath
// TODO nix keypair
, keypair: args.domainKeypair
// TODO nix args.key, args.domainPrivateKeyPem ??
// some ambiguity here...
, privkey: (pems.privkey || pems.key) //|| RSA.exportPrivatePem(args.domainKeypair)
, fullchain: pems.fullchain || (pems.cert + '\n' + pems.chain)
, chain: (pems.chain || pems.ca)
// especially this one... might be cert only, might be fullchain
, cert: pems.cert
, issuedAt: Date(certInfo.notBefore.value).valueOf() // Date.now()
, expiresAt: Date(certInfo.notAfter.value).valueOf()
};
});
}
module.exports.create = function (/*defaults*/) {
function getConfigAsync(copy) {
copy.domains = [];
return readRenewalConfig(copy).then(function (pyobj) {
var exists = pyobj.checkpoints >= 0;
if (!exists) {
return null;
}
return pyobj;
});
}
return {
getDefaults: function () {
LE.tplConfigDir = require('./lib/common').tplConfigDir;
// replaces strings of workDir, certPath, etc
// if they have :config/etc/live or :conf/etc/archive
// to instead have the path of the configDir
LE.tplConfigDir(defaults.configDir, defaults);
return {
configDir: require('homedir')() + '/letsencrypt/etc' // /etc/letsencrypt/
, logsDir: ':config/log' // /var/log/letsencrypt/
, workDir: leCore.workDir // /var/lib/letsencrypt/
, accountsDir: ':config/accounts/:server'
, renewalPath: ':config/renewal/:hostname.conf'
, renewalDir: ':config/renewal/'
, privkeyPath: ':config/live/:hostname/privkey.pem'
, fullchainPath: ':config/live/:hostname/fullchain.pem'
, certPath: ':config/live/:hostname/cert.pem'
, chainPath: ':config/live/:hostname/chain.pem'
, renewalPath: ':config/renewal/:hostname.conf'
, accountsDir: ':config/accounts/:server'
};
}
, getPrivatePemAsync: function (args) {
return fs.readFileAsync(args.domainKeyPath, 'ascii');
}
, setPrivatePemAsync: function (args, keypair) {
return mkdirpAsync(path.dirname(args.domainKeyPath)).then(function () {
return fs.writeFileAsync(args.domainKeyPath, keypair.privateKeyPem, 'ascii').then(function () {
return keypair;
});
});
}
, setRegistrationAsync: function (args) {
return writeCertificateAsync(args);
}
, getRegistrationAsync: function (args) {
return fetchFromConfigLiveDir(args);
}
, getOrCreateRenewalAsync: function (args) {
return getOrCreateRenewal(args);
}
, getConfigAsync: getConfigAsync
, getConfigsAsync: function (copy) {
copy.domains = [];
return fs.readdirAsync(copy.renewalDir).then(function (nodes) {
nodes = nodes.filter(function (node) {
return /^[a-z0-9]+.*\.conf$/.test(node);
});
return PromiseA.all(nodes.map(function (node) {
copy.domains = [node.replace(/\.conf$/, '')];
return getConfigAsync(copy);
}));
});
}
, fetchAsync: function (args) {
return fetchFromConfigLiveDir(args);
}
, getAccountIdByEmailAsync: getAccountIdByEmail
, getAccountAsync: getAccount
, setAccountAsync: function (args, account) {
var isoDate = new Date().toISOString();
var os = require("os");
var localname = os.hostname();
var accountDir = path.join(args.accountsDir, account.accountId);
account.meta = account.meta || {
creation_host: localname
, creation_dt: isoDate
};
return mkdirpAsync(accountDir).then(function () {
var RSA = require('rsa-compat').RSA;
// TODO abstract file writing
return PromiseA.all([
// meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"}
fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify(account.meta), 'utf8')
// private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" }
, fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(RSA.exportPrivateJwk(account.keypair)), 'utf8')
// regr.json:
/*
{ body: { contact: [ 'mailto:coolaj86@gmail.com' ],
agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf',
key: { e: 'AQAB', kty: 'RSA', n: '...' } },
uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272',
new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz',
terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' }
*/
, fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify(account.regr), 'utf8')
]);
});
}
, getAccountIdAsync: function (args) {
var pyconf = PromiseA.promisifyAll(require('pyconf'));
return pyconf.readFileAsync(args.renewalPath).then(function (renewal) {
var accountId = renewal.account;
renewal = renewal.account;
return accountId;
}, function (err) {
if ("ENOENT" === err.code) {
return getAccountIdByEmail(args);
}
return PromiseA.reject(err);
});
}
};
};