wip: yeah!
This commit is contained in:
parent
89dc5fe287
commit
7d8674cb7e
|
@ -1,3 +1,7 @@
|
|||
TODO.txt
|
||||
link.sh
|
||||
.env
|
||||
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
|
|
23
README.md
23
README.md
|
@ -1,3 +1,24 @@
|
|||
# root-greenlock.js
|
||||
|
||||
🔐 Free SSL, Free Wildcard SSL, and Fully Automated HTTPS for Node.js and Browsers, issued by Let's Encrypt v2 via ACME
|
||||
🔐 Free SSL, Free Wildcard SSL, and Fully Automated HTTPS for Node.js and Browsers, issued by Let's Encrypt v2 via ACME
|
||||
|
||||
Typically file propagation is faster and more reliably than DNS propagation.
|
||||
Therefore, http-01 will be preferred to dns-01 except when wildcards or **private domains** are in use.
|
||||
|
||||
http-01 will only be supplied as a defaut if no other challenge is provided.
|
||||
|
||||
```
|
||||
Greenlock.create
|
||||
Greenlock#add
|
||||
Greenlock#order... or Greenlock#issue?
|
||||
Greenlock#renew... or Greenlock#issue?
|
||||
Greenlock#remove
|
||||
Greenlock#get
|
||||
Greenlock#all
|
||||
```
|
||||
|
||||
Better scaling
|
||||
|
||||
cluster lazy-load, remote management
|
||||
|
||||
`server identifier (for sharding, for manager)`
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
'use strict';
|
||||
|
||||
var A = module.exports;
|
||||
var U = require('./utils.js');
|
||||
var E = require('./errors.js');
|
||||
|
||||
var pending = {};
|
||||
|
||||
A._getOrCreate = function(greenlock, db, acme, args) {
|
||||
console.log('[debug] A get or create', args);
|
||||
var email = args.subscriberEmail || greenlock._defaults.subscriberEmail;
|
||||
|
||||
if (!email) {
|
||||
console.log('[debug] throw no sub');
|
||||
throw E.NO_SUBSCRIBER('get account', args.subject);
|
||||
}
|
||||
|
||||
// TODO send welcome message with benefit info
|
||||
return U._validMx(email)
|
||||
.catch(function() {
|
||||
throw E.NO_SUBSCRIBER('get account', args.subcriberEmail);
|
||||
})
|
||||
.then(function() {
|
||||
console.log('[debug] valid email');
|
||||
if (pending[email]) {
|
||||
console.log('[debug] return pending');
|
||||
return pending[email];
|
||||
}
|
||||
|
||||
pending[email] = A._rawGetOrCreate(greenlock, db, acme, args, email)
|
||||
.catch(function(e) {
|
||||
delete pending[email];
|
||||
throw e;
|
||||
})
|
||||
.then(function(result) {
|
||||
delete pending[email];
|
||||
return result;
|
||||
});
|
||||
|
||||
console.log('[debug] return new');
|
||||
return pending[email];
|
||||
});
|
||||
};
|
||||
|
||||
// What we really need out of this is the private key and the ACME "key" id
|
||||
A._rawGetOrCreate = function(greenlock, db, acme, args, email) {
|
||||
var p;
|
||||
if (db.check) {
|
||||
p = A._checkStore(greenlock, db, acme, args, email);
|
||||
} else {
|
||||
p = Promise.resolve(null);
|
||||
}
|
||||
|
||||
return p.then(function(fullAccount) {
|
||||
console.log('[debug] full account', fullAccount);
|
||||
if (!fullAccount) {
|
||||
return A._newAccount(greenlock, db, acme, args, email, null);
|
||||
}
|
||||
|
||||
if (fullAccount.keypair && fullAccount.key && fullAccount.key.kid) {
|
||||
return fullAccount;
|
||||
}
|
||||
|
||||
return A._newAccount(greenlock, db, acme, args, email, fullAccount);
|
||||
});
|
||||
};
|
||||
|
||||
A._newAccount = function(greenlock, db, acme, args, email, fullAccount) {
|
||||
var keyType = args.accountKeyType || greenlock._defaults.accountKeyType;
|
||||
var query = {
|
||||
subject: args.subject,
|
||||
email: email,
|
||||
account: fullAccount || {}
|
||||
};
|
||||
|
||||
return U._getOrCreateKeypair(db, args.subject, query, keyType).then(
|
||||
function(kresult) {
|
||||
var keypair = kresult.keypair;
|
||||
var accReg = {
|
||||
subscriberEmail: email,
|
||||
agreeToTerms:
|
||||
args.agreeToTerms || greenlock._defaults.agreeToTerms,
|
||||
accountKeypair: keypair,
|
||||
debug: args.debug
|
||||
};
|
||||
console.log('[debug] create account', accReg);
|
||||
return acme.accounts.create(accReg).then(function(receipt) {
|
||||
var reg = {
|
||||
keypair: keypair,
|
||||
receipt: receipt,
|
||||
// shudder... not actually a KeyID... but so it is called anyway...
|
||||
kid:
|
||||
receipt &&
|
||||
receipt.key &&
|
||||
(receipt.key.kid || receipt.kid),
|
||||
email: args.email
|
||||
};
|
||||
|
||||
var keyP;
|
||||
if (kresult.exists) {
|
||||
keyP = Promise.resolve();
|
||||
} else {
|
||||
query.keypair = keypair;
|
||||
query.receipt = receipt;
|
||||
keyP = db.setKeypair(query, keypair);
|
||||
}
|
||||
|
||||
return keyP
|
||||
.then(function() {
|
||||
if (!db.set) {
|
||||
return Promise.resolve({
|
||||
keypair: keypair
|
||||
});
|
||||
}
|
||||
return db.set(
|
||||
{
|
||||
// id to be set by Store
|
||||
email: email,
|
||||
agreeTos: true
|
||||
},
|
||||
reg
|
||||
);
|
||||
})
|
||||
.then(function(fullAccount) {
|
||||
if (fullAccount && 'object' !== typeof fullAccount) {
|
||||
throw new Error(
|
||||
"accounts.set should either return 'null' or an object with an 'id' string"
|
||||
);
|
||||
}
|
||||
|
||||
if (!fullAccount) {
|
||||
fullAccount = {};
|
||||
}
|
||||
fullAccount.keypair = keypair;
|
||||
if (!fullAccount.key) {
|
||||
fullAccount.key = {};
|
||||
}
|
||||
fullAccount.key.kid = reg.kid;
|
||||
|
||||
return fullAccount;
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
A._checkStore = function(greenlock, db, acme, args, email) {
|
||||
if ((args.domain || args.domains) && !args.subject) {
|
||||
console.warn("use 'subject' instead of 'domain'");
|
||||
args.subject = args.domain;
|
||||
}
|
||||
|
||||
var account = args.account;
|
||||
if (!account) {
|
||||
account = {};
|
||||
}
|
||||
|
||||
if (args.accountKeypair) {
|
||||
console.warn(
|
||||
'rather than passing accountKeypair, put it directly into your account key store'
|
||||
);
|
||||
// TODO we probably don't need this
|
||||
return U._importKeypair(args.accountKeypair);
|
||||
}
|
||||
|
||||
if (!db.check) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return db
|
||||
.check({
|
||||
//keypair: undefined,
|
||||
//receipt: undefined,
|
||||
email: email,
|
||||
account: account
|
||||
})
|
||||
.then(function(fullAccount) {
|
||||
if (!fullAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fullAccount;
|
||||
});
|
||||
};
|
|
@ -33,11 +33,17 @@ cli.parse({
|
|||
' Domain names to apply. For multiple domains you can enter a comma separated list of domains as a parameter. (default: [])',
|
||||
'string'
|
||||
],
|
||||
'renew-offset': [
|
||||
false,
|
||||
' Positive (time after issue) or negative (time before expiry) offset, such as 30d or -45d',
|
||||
'string',
|
||||
'45d'
|
||||
],
|
||||
'renew-within': [
|
||||
false,
|
||||
' Renew certificates this many days before expiry',
|
||||
'int',
|
||||
7
|
||||
' (ignored) use renew-offset instead',
|
||||
'ignore',
|
||||
undefined
|
||||
],
|
||||
'cert-path': [
|
||||
false,
|
||||
|
@ -157,16 +163,18 @@ cli.parse({
|
|||
],
|
||||
'rsa-key-size': [
|
||||
false,
|
||||
' (ignored) use domain-key-type or account-key-type instead',
|
||||
' (ignored) use server-key-type or account-key-type instead',
|
||||
'ignore',
|
||||
2048
|
||||
],
|
||||
'domain-key-path': [
|
||||
'server-key-path': [
|
||||
false,
|
||||
' Path to privkey.pem to use for domain (default: generate new)',
|
||||
'string'
|
||||
' Path to privkey.pem to use for certificate (default: generate new)',
|
||||
'string',
|
||||
undefined,
|
||||
'domain-key-path'
|
||||
],
|
||||
'domain-key-type': [
|
||||
'server-key-type': [
|
||||
false,
|
||||
" One of 'RSA' (2048), 'RSA-3084', 'RSA-4096', 'ECDSA' (P-256), or 'P-384'. For best compatibility, security, and efficiency use the default (More bits != More security)",
|
||||
'string',
|
||||
|
@ -184,7 +192,7 @@ cli.parse({
|
|||
'P-256'
|
||||
],
|
||||
webroot: [false, ' (ignored) for certbot compatibility', 'ignore', false],
|
||||
//, 'standalone-supported-challenges': [ false, " Supported challenges, order preferences are randomly chosen. (default: http-01,tls-sni-01)", 'string', 'http-01,tls-sni-01']
|
||||
//, 'standalone-supported-challenges': [ false, " Supported challenges, order preferences are randomly chosen. (default: http-01,tls-alpn-01)", 'string', 'http-01']
|
||||
'work-dir': [
|
||||
false,
|
||||
' for certbot compatibility (ignored)',
|
||||
|
@ -286,12 +294,66 @@ cli.main(function(_, options) {
|
|||
}
|
||||
|
||||
function run() {
|
||||
var challenges = {};
|
||||
if (/http.?01/i.test(args.challenge)) {
|
||||
challenges['http-01'] = args.challengeOpts;
|
||||
}
|
||||
if (/dns.?01/i.test(args.challenge)) {
|
||||
challenges['dns-01'] = args.challengeOpts;
|
||||
}
|
||||
if (/alpn.?01/i.test(args.challenge)) {
|
||||
challenges['tls-alpn-01'] = args.challengeOpts;
|
||||
}
|
||||
if (!Object.keys(challenges).length) {
|
||||
throw new Error(
|
||||
"Could not determine the challenge type for '" +
|
||||
args.challengeOpts.module +
|
||||
"'. Expected a name like @you/acme-xxxx-01-foo. Please name the module with http-01, dns-01, or tls-alpn-01."
|
||||
);
|
||||
}
|
||||
args.challengeOpts.module = args.challenge;
|
||||
args.storeOpts.module = args.store;
|
||||
|
||||
console.log('\ngot to the run step');
|
||||
process.exit(1);
|
||||
require('../')
|
||||
.run(args)
|
||||
.then(function(status) {
|
||||
process.exit(status);
|
||||
require(args.challenge);
|
||||
require(args.store);
|
||||
|
||||
var greenlock = require('../').create({
|
||||
maintainerEmail: args.maintainerEmail || 'coolaj86@gmail.com',
|
||||
manager: './manager.js',
|
||||
configFile: '~/.config/greenlock/certs.json',
|
||||
challenges: challenges,
|
||||
store: args.storeOpts,
|
||||
renewOffset: args.renewOffset || '30d',
|
||||
renewStagger: '1d'
|
||||
});
|
||||
|
||||
// for long-running processes
|
||||
if (args.renewEvery) {
|
||||
setInterval(function() {
|
||||
greenlock.renew({
|
||||
period: args.renewEvery
|
||||
});
|
||||
}, args.renewEvery);
|
||||
}
|
||||
|
||||
// TODO should greenlock.add simply always include greenlock.renew?
|
||||
// the concern is conflating error events
|
||||
return greenlock
|
||||
.add({
|
||||
subject: args.subject,
|
||||
altnames: args.altnames,
|
||||
subscriberEmail: args.subscriberEmail || args.email
|
||||
})
|
||||
.then(function(changes) {
|
||||
console.info(changes);
|
||||
// renew should always
|
||||
return greenlock
|
||||
.renew({
|
||||
subject: args.subject,
|
||||
force: false
|
||||
})
|
||||
.then(function() {});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,65 @@
|
|||
'use strict';
|
||||
|
||||
var spawn = require('child_process').spawn;
|
||||
var spawnSync = require('child_process').spawnSync;
|
||||
var path = require('path');
|
||||
var PKG_DIR = path.join(__dirname, '..');
|
||||
|
||||
module.exports.installSync = function(moduleName) {
|
||||
var npm = 'npm';
|
||||
var args = ['install', '--save', moduleName];
|
||||
var out = '';
|
||||
var cmd;
|
||||
|
||||
try {
|
||||
cmd = spawnSync(npm, args, {
|
||||
cwd: PKG_DIR,
|
||||
windowsHide: true
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Failed to start: '" +
|
||||
npm +
|
||||
' ' +
|
||||
args.join(' ') +
|
||||
"' in '" +
|
||||
PKG_DIR +
|
||||
"'"
|
||||
);
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!cmd.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
out += cmd.stdout.toString('utf8');
|
||||
out += cmd.stderr.toString('utf8');
|
||||
|
||||
if (out) {
|
||||
console.error(out);
|
||||
console.error();
|
||||
console.error();
|
||||
}
|
||||
|
||||
console.error(
|
||||
"Failed to run: '" +
|
||||
npm +
|
||||
' ' +
|
||||
args.join(' ') +
|
||||
"' in '" +
|
||||
PKG_DIR +
|
||||
"'"
|
||||
);
|
||||
|
||||
console.error(
|
||||
'Try for yourself:\n\tcd ' + PKG_DIR + '\n\tnpm ' + args.join(' ')
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
module.exports.install = function(moduleName) {
|
||||
return new Promise(function(resolve) {
|
||||
if (!moduleName) {
|
||||
|
@ -71,5 +127,5 @@ module.exports.install = function(moduleName) {
|
|||
};
|
||||
|
||||
if (require.main === module) {
|
||||
module.exports.install(process.argv[2]);
|
||||
module.exports.installSync(process.argv[2]);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,279 @@
|
|||
'use strict';
|
||||
|
||||
var C = module.exports;
|
||||
var U = require('./utils.js');
|
||||
var CSR = require('@root/csr');
|
||||
var Enc = require('@root/encoding');
|
||||
|
||||
var pending = {};
|
||||
var rawPending = {};
|
||||
|
||||
// Certificates
|
||||
C._getOrOrder = function(greenlock, db, acme, challenges, account, args) {
|
||||
var email = args.subscriberEmail || greenlock._defaults.subscriberEmail;
|
||||
|
||||
var id = args.altnames.join(' ');
|
||||
if (pending[id]) {
|
||||
return pending[id];
|
||||
}
|
||||
|
||||
pending[id] = C._rawGetOrOrder(
|
||||
greenlock,
|
||||
db,
|
||||
acme,
|
||||
challenges,
|
||||
account,
|
||||
email,
|
||||
args
|
||||
)
|
||||
.then(function(pems) {
|
||||
delete pending[id];
|
||||
return pems;
|
||||
})
|
||||
.catch(function(err) {
|
||||
delete pending[id];
|
||||
throw err;
|
||||
});
|
||||
|
||||
return pending[id];
|
||||
};
|
||||
|
||||
// Certificates
|
||||
C._rawGetOrOrder = function(
|
||||
greenlock,
|
||||
db,
|
||||
acme,
|
||||
challenges,
|
||||
account,
|
||||
email,
|
||||
args
|
||||
) {
|
||||
return C._check(db, args).then(function(pems) {
|
||||
// No pems? get some!
|
||||
if (!pems) {
|
||||
return C._rawOrder(
|
||||
greenlock,
|
||||
db,
|
||||
acme,
|
||||
challenges,
|
||||
account,
|
||||
email,
|
||||
args
|
||||
).then(function(newPems) {
|
||||
// do not wait on notify
|
||||
greenlock.notify('cert_issue', {
|
||||
options: args,
|
||||
subject: args.subject,
|
||||
altnames: args.altnames,
|
||||
account: account,
|
||||
email: email,
|
||||
pems: newPems
|
||||
});
|
||||
return newPems;
|
||||
});
|
||||
}
|
||||
|
||||
// Nice and fresh? We're done!
|
||||
if (!C._isStale(greenlock, args, pems)) {
|
||||
// return existing unexpired (although potentially stale) certificates when available
|
||||
// there will be an additional .renewing property if the certs are being asynchronously renewed
|
||||
//pems._type = 'current';
|
||||
return pems;
|
||||
}
|
||||
|
||||
// Getting stale? Let's renew to freshen up!
|
||||
var p = C._rawOrder(
|
||||
greenlock,
|
||||
db,
|
||||
acme,
|
||||
challenges,
|
||||
account,
|
||||
email,
|
||||
args
|
||||
).then(function(renewedPems) {
|
||||
// do not wait on notify
|
||||
greenlock.notify('cert_renewal', {
|
||||
options: args,
|
||||
subject: args.subject,
|
||||
altnames: args.altnames,
|
||||
account: account,
|
||||
email: email,
|
||||
pems: renewedPems
|
||||
});
|
||||
return renewedPems;
|
||||
});
|
||||
|
||||
// TODO what should this be?
|
||||
if (args.waitForRenewal) {
|
||||
return p;
|
||||
}
|
||||
|
||||
return pems;
|
||||
});
|
||||
};
|
||||
|
||||
// we have another promise here because it the optional renewal
|
||||
// may resolve in a different stack than the returned pems
|
||||
C._rawOrder = function(greenlock, db, acme, challenges, account, email, args) {
|
||||
var id = args.altnames
|
||||
.slice(0)
|
||||
.sort()
|
||||
.join(' ');
|
||||
if (rawPending[id]) {
|
||||
return rawPending[id];
|
||||
}
|
||||
|
||||
var keyType = args.serverKeyType || greenlock._defaults.serverKeyType;
|
||||
var query = {
|
||||
subject: args.subject,
|
||||
certificate: args.certificate || {}
|
||||
};
|
||||
rawPending[id] = U._getOrCreateKeypair(db, args.subject, query, keyType)
|
||||
.then(function(kresult) {
|
||||
var serverKeypair = kresult.keypair;
|
||||
var domains = args.altnames.slice(0);
|
||||
|
||||
return CSR.csr({
|
||||
jwk: serverKeypair.privateKeyJwk,
|
||||
domains: domains,
|
||||
encoding: 'der'
|
||||
})
|
||||
.then(function(csrDer) {
|
||||
// TODO let CSR support 'urlBase64' ?
|
||||
return Enc.bufToUrlBase64(csrDer);
|
||||
})
|
||||
.then(function(csr) {
|
||||
function notify() {
|
||||
greenlock.notify('challenge_status', {
|
||||
options: args,
|
||||
subject: args.subject,
|
||||
altnames: args.altnames,
|
||||
account: account,
|
||||
email: email
|
||||
});
|
||||
}
|
||||
var certReq = {
|
||||
debug: args.debug || greenlock._defaults.debug,
|
||||
|
||||
challenges: challenges,
|
||||
account: account, // only used if accounts.key.kid exists
|
||||
accountKeypair: account.keypair,
|
||||
keypair: account.keypair, // TODO
|
||||
csr: csr,
|
||||
domains: domains, // because ACME.js v3 uses `domains` still, actually
|
||||
onChallengeStatus: notify,
|
||||
notify: notify // TODO
|
||||
|
||||
// TODO handle this in acme-v2
|
||||
//subject: args.subject,
|
||||
//altnames: args.altnames.slice(0),
|
||||
};
|
||||
return acme.certificates
|
||||
.create(certReq)
|
||||
.then(U._attachCertInfo);
|
||||
})
|
||||
.then(function(pems) {
|
||||
if (kresult.exists) {
|
||||
return pems;
|
||||
}
|
||||
return db.setKeypair(query, serverKeypair).then(function() {
|
||||
return pems;
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(function(pems) {
|
||||
// TODO put this in the docs
|
||||
// { cert, chain, privkey, subject, altnames, issuedAt, expiresAt }
|
||||
// Note: the query has been updated
|
||||
query.pems = pems;
|
||||
return db.set(query);
|
||||
})
|
||||
.then(function() {
|
||||
return C._check(db, args);
|
||||
})
|
||||
.then(function(bundle) {
|
||||
// TODO notify Manager
|
||||
delete rawPending[id];
|
||||
return bundle;
|
||||
})
|
||||
.catch(function(err) {
|
||||
// Todo notify manager
|
||||
delete rawPending[id];
|
||||
throw err;
|
||||
});
|
||||
|
||||
return rawPending[id];
|
||||
};
|
||||
|
||||
// returns pems, if they exist
|
||||
C._check = function(db, args) {
|
||||
var query = {
|
||||
subject: args.subject,
|
||||
// may contain certificate.id
|
||||
certificate: args.certificate
|
||||
};
|
||||
return db.check(query).then(function(pems) {
|
||||
if (!pems) {
|
||||
return null;
|
||||
}
|
||||
|
||||
pems = U._attachCertInfo(pems);
|
||||
|
||||
// For eager management
|
||||
if (args.subject && !U._certHasDomain(pems, args.subject)) {
|
||||
// TODO report error, but continue the process as with no cert
|
||||
return null;
|
||||
}
|
||||
|
||||
// For lazy SNI requests
|
||||
if (args.domain && !U._certHasDomain(pems, args.domain)) {
|
||||
// TODO report error, but continue the process as with no cert
|
||||
return null;
|
||||
}
|
||||
|
||||
return U._getKeypair(db, args.subject, query)
|
||||
.then(function(keypair) {
|
||||
pems.privkey = keypair.privateKeyPem;
|
||||
return pems;
|
||||
})
|
||||
.catch(function() {
|
||||
// TODO report error, but continue the process as with no cert
|
||||
return null;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Certificates
|
||||
C._isStale = function(greenlock, args, pems) {
|
||||
if (args.duplicate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var renewAt = C._renewableAt(greenlock, args, pems);
|
||||
|
||||
if (Date.now() >= renewAt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
C._renewableAt = function(greenlock, args, pems) {
|
||||
if (args.renewAt) {
|
||||
return args.renewAt;
|
||||
}
|
||||
|
||||
var renewOffset = args.renewOffset || greenlock._defaults.renewOffset || 0;
|
||||
var week = 1000 * 60 * 60 * 24 * 6;
|
||||
if (!args.force && Math.abs(renewOffset) < week) {
|
||||
throw new Error(
|
||||
'developer error: `renewOffset` should always be at least a week, use `force` to not safety-check renewOffset'
|
||||
);
|
||||
}
|
||||
|
||||
if (renewOffset > 0) {
|
||||
return pems.issuedAt + renewOffset;
|
||||
}
|
||||
|
||||
return pems.expiresAt + renewOffset;
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
'use strict';
|
||||
|
||||
var E = module.exports;
|
||||
|
||||
function create(code, msg) {
|
||||
E[code] = function(ctx, msg2) {
|
||||
var err = new Error(msg);
|
||||
err.code = code;
|
||||
err.context = ctx;
|
||||
if (msg2) {
|
||||
err.message += ': ' + msg2;
|
||||
}
|
||||
/*
|
||||
Object.keys(extras).forEach(function(k) {
|
||||
if ('message' === k) {
|
||||
err.message += ': ' + extras[k];
|
||||
} else {
|
||||
err[k] = extras[k];
|
||||
}
|
||||
});
|
||||
*/
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO open issues and link to them as the error url
|
||||
create(
|
||||
'NO_MAINTAINER',
|
||||
'please supply `maintainerEmail` as a contact for security and critical bug notices'
|
||||
);
|
||||
create(
|
||||
'BAD_ORDER',
|
||||
'altnames should be in deterministic order, with subject as the first altname'
|
||||
);
|
||||
create('NO_SUBJECT', 'no certificate subject given');
|
||||
create(
|
||||
'NO_SUBSCRIBER',
|
||||
'please supply `subscriberEmail` as a contact for failed renewal and certificate revocation'
|
||||
);
|
||||
create(
|
||||
'INVALID_SUBSCRIBER',
|
||||
'`subscriberEmail` is not a valid address, please check for typos'
|
||||
);
|
||||
create(
|
||||
'INVALID_HOSTNAME',
|
||||
'valid hostnames must be restricted to a-z0-9_.- and contain at least one "."'
|
||||
);
|
||||
create(
|
||||
'INVALID_DOMAIN',
|
||||
'one or more domains do not exist on public DNS SOA record'
|
||||
);
|
||||
create(
|
||||
'NOT_UNIQUE',
|
||||
'found duplicate domains, or a subdomain that overlaps a wildcard'
|
||||
);
|
||||
|
||||
// exported for testing only
|
||||
E._create = create;
|
|
@ -0,0 +1,60 @@
|
|||
'use strict';
|
||||
|
||||
// tradeoff - lazy load certs vs DOS invalid sni
|
||||
|
||||
var Manager = module.exports;
|
||||
|
||||
var Cache = {};
|
||||
|
||||
Manager.create = function(conf) {
|
||||
var domains = conf.domains;
|
||||
var manager = {};
|
||||
|
||||
// { servername, wildname }
|
||||
manager.getSubject = function(opts) {
|
||||
if (
|
||||
!opts.domains.includes(opts.domain) &&
|
||||
!opts.domains.includes(opts.wildname)
|
||||
) {
|
||||
throw new Error('not a registered domain');
|
||||
}
|
||||
return opts.domains[0];
|
||||
};
|
||||
|
||||
manager.add = function() {};
|
||||
|
||||
// { servername, wildname }
|
||||
manager.configure = function(opts) {};
|
||||
|
||||
// { servername }
|
||||
manager._contexts = {};
|
||||
};
|
||||
|
||||
var manager = Manager.create({
|
||||
domains: ['example.com', '*.example.com']
|
||||
});
|
||||
|
||||
Cache.getTlsContext = function(servername) {
|
||||
// TODO exponential fallback certificate renewal
|
||||
if (Cache._contexts[servername]) {
|
||||
// may be a context, or a promise for a context
|
||||
return Cache._contexts[servername];
|
||||
}
|
||||
|
||||
var wildname =
|
||||
'*.' +
|
||||
(servername || '')
|
||||
.split('.')
|
||||
.slice(1)
|
||||
.join('.');
|
||||
|
||||
var opts = {
|
||||
servername: servername,
|
||||
domain: servername,
|
||||
wildname: wildname
|
||||
};
|
||||
manager._contexts[servername] = manager
|
||||
.orderCertificate(opts)
|
||||
.then(function() {})
|
||||
.catch(function(e) {});
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
var http = require('http');
|
||||
var https = require('http2');
|
||||
var greenlock = require('../greenlock.js').create({
|
||||
maintainerEmail: 'jon@example.com'
|
||||
});
|
||||
|
||||
function app(req, res) {
|
||||
res.end('Hello, Encrypted World!');
|
||||
}
|
||||
|
||||
http.createServer(greenlock.plainMiddleware()).listen(8080);
|
||||
https
|
||||
.createServer(greenlock.tlsOptions, greenlock.secureMiddleware(app))
|
||||
.listen(8443);
|
|
@ -0,0 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
var Greenlock = module.exports;
|
||||
|
||||
Greenlock.server = function (opts) {
|
||||
var opts = Greenlock.create(opts);
|
||||
|
||||
opts.plainMiddleware = function(req, res) {
|
||||
return Greenlock._plainMiddleware(opts, req, res);
|
||||
};
|
||||
|
||||
opts.secureMiddleware = function(req, res) {
|
||||
return Greenlock._secureMiddleware(opts, req, res);
|
||||
};
|
||||
|
||||
opts.tlsOptions = {
|
||||
SNICallback: function(servername, cb) {
|
||||
return Greenlock._sniCallback(opts, servername)
|
||||
.then(function() {
|
||||
cb(null);
|
||||
})
|
||||
.catch(function(err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return opts;
|
||||
};
|
||||
|
||||
// must handle http-01 challenges
|
||||
Greenlock._plainMiddleware = function(opts, req, res) {};
|
||||
|
||||
// should check for domain fronting
|
||||
Greenlock._secureMiddleware = function(opts, req, res) {};
|
||||
|
||||
// should check to see if domain is allowed, and if domain should be renewed
|
||||
// manage should be able to clear the internal cache
|
||||
Greenlock._sniCallback = function(opts, servername) {};
|
||||
|
||||
Greenlock._onSniRejection(function () {
|
||||
});
|
|
@ -0,0 +1,541 @@
|
|||
'use strict';
|
||||
|
||||
var pkg = require('./package.json');
|
||||
|
||||
var ACME = require('@root/acme');
|
||||
var Greenlock = module.exports;
|
||||
|
||||
var G = Greenlock;
|
||||
var U = require('./utils.js');
|
||||
var E = require('./errors.js');
|
||||
var P = require('./plugins.js');
|
||||
var A = require('./accounts.js');
|
||||
var C = require('./certificates.js');
|
||||
var UserEvents = require('./user-events.js');
|
||||
|
||||
var promisify = require('util').promisify;
|
||||
|
||||
var caches = {};
|
||||
|
||||
// { maintainerEmail, directoryUrl, subscriberEmail, store, challenges }
|
||||
G.create = function(gconf) {
|
||||
var greenlock = {};
|
||||
if (!gconf) {
|
||||
gconf = {};
|
||||
}
|
||||
|
||||
if (!gconf.maintainerEmail) {
|
||||
throw E.NO_MAINTAINER('create');
|
||||
}
|
||||
|
||||
// TODO send welcome message with benefit info
|
||||
U._validMx(gconf.maintainerEmail).catch(function() {
|
||||
console.error(
|
||||
'invalid maintainer contact info:',
|
||||
gconf.maintainer.Email
|
||||
);
|
||||
// maybe a little harsh?
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// TODO default servername is GLE only
|
||||
|
||||
if (!gconf.manager) {
|
||||
gconf.manager = 'greenlock-manager-fs';
|
||||
}
|
||||
|
||||
var Manager;
|
||||
if ('string' === typeof gconf.manager) {
|
||||
try {
|
||||
Manager = require(gconf.manager);
|
||||
} catch (e) {
|
||||
if ('MODULE_NOT_FOUND' !== e.code) {
|
||||
throw e;
|
||||
}
|
||||
console.error(e.code);
|
||||
console.error(e.message);
|
||||
console.error(gconf.manager);
|
||||
P._installSync(gconf.manager);
|
||||
Manager = require(gconf.manager);
|
||||
}
|
||||
}
|
||||
|
||||
// minimal modification to the original object
|
||||
var defaults = G._defaults(gconf);
|
||||
|
||||
greenlock.manager = Manager.create(defaults);
|
||||
|
||||
// The goal here is to reduce boilerplate, such as error checking
|
||||
// and duration parsing, that a manager must implement
|
||||
greenlock.add = function(args) {
|
||||
return Promise.resolve().then(function() {
|
||||
// durations
|
||||
if (args.renewOffset) {
|
||||
args.renewOffset = U._parseDuration(args.renewOffset);
|
||||
}
|
||||
if (args.renewStagger) {
|
||||
args.renewStagger = U._parseDuration(args.renewStagger);
|
||||
}
|
||||
|
||||
if (!args.subject) {
|
||||
throw E.NO_SUBJECT('add');
|
||||
}
|
||||
|
||||
if (!args.altnames) {
|
||||
args.altnames = [args.subject];
|
||||
}
|
||||
if ('string' === typeof args.altnames) {
|
||||
args.altnames = args.altnames.split(/[,\s]+/);
|
||||
}
|
||||
if (args.subject !== args.altnames[0]) {
|
||||
throw E.BAD_ORDER(
|
||||
'add',
|
||||
'(' + args.subject + ") '" + args.altnames.join("' '") + "'"
|
||||
);
|
||||
}
|
||||
args.altnames = args.altnames.map(U._encodeName);
|
||||
|
||||
if (
|
||||
!args.altnames.every(function(d) {
|
||||
return U._validName(d);
|
||||
})
|
||||
) {
|
||||
throw E.INVALID_HOSTNAME(
|
||||
'add',
|
||||
"'" + args.altnames.join("' '") + "'"
|
||||
);
|
||||
}
|
||||
|
||||
// at this point we know that subject is the first of altnames
|
||||
return Promise.all(
|
||||
args.altnames.map(function(d) {
|
||||
d = d.replace('*.', '');
|
||||
return U._validDomain(d);
|
||||
})
|
||||
).then(function() {
|
||||
if (!U._uniqueNames(args.altnames)) {
|
||||
throw E.NOT_UNIQUE(
|
||||
'add',
|
||||
"'" + args.altnames.join("' '") + "'"
|
||||
);
|
||||
}
|
||||
|
||||
return greenlock.manager.add(args);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
greenlock._notify = function(ev, params) {
|
||||
var mng = greenlock.manager;
|
||||
if (mng.notify) {
|
||||
try {
|
||||
var p = mng.notify(ev, params);
|
||||
if (p && p.catch) {
|
||||
p.catch(function(e) {
|
||||
console.error("Error on event '" + ev + "':");
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error on event '" + ev + "':");
|
||||
console.error(e);
|
||||
}
|
||||
} else {
|
||||
if (/error/i.test(ev)) {
|
||||
console.error("Error event '" + ev + "':");
|
||||
console.error(params);
|
||||
}
|
||||
}
|
||||
/*
|
||||
*'cert_issue', {
|
||||
options: args,
|
||||
subject: args.subject,
|
||||
altnames: args.altnames,
|
||||
account: account,
|
||||
email: email,
|
||||
pems: newPems
|
||||
}
|
||||
*/
|
||||
|
||||
if (-1 !== ['cert_issue', 'cert_renewal'].indexOf(ev)) {
|
||||
// We will notify all greenlock users of mandatory and security updates
|
||||
// We'll keep track of versions and os so we can make sure things work well
|
||||
// { name, version, email, domains, action, communityMember, telemetry }
|
||||
// TODO look at the other one
|
||||
UserEvents.notify({
|
||||
// maintainer should be only on pre-publish, or maybe install, I think
|
||||
maintainerEmail: greenlock._defaults._maintainerEmail,
|
||||
name: greenlock._defaults._maintainerPackage,
|
||||
version: greenlock._defaults._maintainerPackageVersion,
|
||||
action: params.pems._type,
|
||||
domains: params.altnames,
|
||||
subscriberEmail: greenlock._defaults._subscriberEmail,
|
||||
// TODO enable for Greenlock Pro
|
||||
//customerEmail: args.customerEmail
|
||||
telemetry: greenlock._defaults.telemetry
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// needs to get info about the renewal, such as which store and challenge(s) to use
|
||||
greenlock.renew = function(args) {
|
||||
if (!args) {
|
||||
args = {};
|
||||
}
|
||||
|
||||
// durations
|
||||
if (args.renewOffset) {
|
||||
args.renewOffset = U._parseDuration(args.renewOffset);
|
||||
}
|
||||
if (args.renewStagger) {
|
||||
args.renewStagger = U._parseDuration(args.renewStagger);
|
||||
}
|
||||
|
||||
if (args.domain) {
|
||||
// this doesn't have to be the subject, it can be anything
|
||||
// however, not sure how useful this really is...
|
||||
args.domain = args.toLowerCase();
|
||||
}
|
||||
|
||||
args.defaults = greenlock.defaults;
|
||||
return greenlock.manager.find(args).then(function(sites) {
|
||||
// Note: the manager must guaranteed that these are mutable copies
|
||||
|
||||
console.log('[debug] found what?', sites);
|
||||
var renewedOrFailed = [];
|
||||
|
||||
function next() {
|
||||
var site = sites.shift();
|
||||
if (!site) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var order = {
|
||||
site: site
|
||||
};
|
||||
renewedOrFailed.push(order);
|
||||
// TODO merge args + result?
|
||||
return greenlock
|
||||
.order(site)
|
||||
.then(function(pems) {
|
||||
order.pems = pems;
|
||||
})
|
||||
.catch(function(err) {
|
||||
order.error = err;
|
||||
greenlock._notify('order_error', order);
|
||||
})
|
||||
.then(function() {
|
||||
return next();
|
||||
});
|
||||
}
|
||||
|
||||
return next().then(function() {
|
||||
return renewedOrFailed;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
greenlock._acme = function(args) {
|
||||
var acme = ACME.create({
|
||||
debug: args.debug
|
||||
});
|
||||
var dirUrl = args.directoryUrl || greenlock._defaults.directoryUrl;
|
||||
|
||||
var dir = caches[dirUrl];
|
||||
|
||||
// don't cache more than an hour
|
||||
if (dir && Date.now() - dir.ts < 1 * 60 * 60 * 1000) {
|
||||
return dir.promise;
|
||||
}
|
||||
|
||||
return acme
|
||||
.init(dirUrl)
|
||||
.then(function(/*meta*/) {
|
||||
caches[dirUrl] = {
|
||||
promise: Promise.resolve(acme),
|
||||
ts: Date.now()
|
||||
};
|
||||
return acme;
|
||||
})
|
||||
.catch(function(err) {
|
||||
// TODO
|
||||
// let's encrypt is possibly down for maintenaince...
|
||||
// this is a special kind of failure mode
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
greenlock.order = function(args) {
|
||||
return greenlock._acme(args).then(function(acme) {
|
||||
console.log('[debug] acme meta', acme);
|
||||
var storeConf = args.store || greenlock._defaults.store;
|
||||
return P._load(storeConf.module).then(function(plugin) {
|
||||
var store = Greenlock._normalizeStore(
|
||||
storeConf.module,
|
||||
plugin.create(storeConf)
|
||||
);
|
||||
|
||||
console.log('[debug] store', storeConf);
|
||||
return A._getOrCreate(
|
||||
greenlock,
|
||||
store.accounts,
|
||||
acme,
|
||||
args
|
||||
).then(function(account) {
|
||||
console.log('[debug] account', account);
|
||||
var challengeConfs =
|
||||
args.challenges || greenlock._defaults.challenges;
|
||||
console.log('[debug] challenge confs', challengeConfs);
|
||||
return Promise.all(
|
||||
Object.keys(challengeConfs).map(function(typ01) {
|
||||
var chConf = challengeConfs[typ01];
|
||||
return P._load(chConf.module).then(function(
|
||||
plugin
|
||||
) {
|
||||
var ch = Greenlock._normalizeChallenge(
|
||||
chConf.module,
|
||||
plugin.create(chConf)
|
||||
);
|
||||
ch._type = typ01;
|
||||
return ch;
|
||||
});
|
||||
})
|
||||
).then(function(arr) {
|
||||
var challenges = {};
|
||||
arr.forEach(function(el) {
|
||||
challenges[el._type] = el;
|
||||
});
|
||||
return C._getOrOrder(
|
||||
greenlock,
|
||||
store.certificates,
|
||||
acme,
|
||||
challenges,
|
||||
account,
|
||||
args
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
greenlock._options = gconf;
|
||||
greenlock._defaults = defaults;
|
||||
|
||||
if (!gconf.onOrderFailure) {
|
||||
gconf.onOrderFailure = function(err) {
|
||||
G._onOrderFailure(gconf, err);
|
||||
};
|
||||
}
|
||||
|
||||
return greenlock;
|
||||
};
|
||||
|
||||
G._defaults = function(opts) {
|
||||
var defaults = {};
|
||||
|
||||
// [ 'store', 'challenges' ]
|
||||
Object.keys(opts).forEach(function(k) {
|
||||
// manage is the only thing that is, potentially, not plain-old JSON
|
||||
if ('manage' === k && 'string' !== typeof opts[k]) {
|
||||
return;
|
||||
}
|
||||
defaults[k] = opts[k];
|
||||
});
|
||||
|
||||
if (!defaults._maintainerPackage) {
|
||||
defaults._maintainerPackage = pkg.name;
|
||||
defaults._maintainerPackageVersion = pkg.version;
|
||||
}
|
||||
|
||||
if (!defaults.directoryUrl) {
|
||||
if (defaults.staging) {
|
||||
defaults.directoryUrl =
|
||||
'https://acme-staging-v02.api.letsencrypt.org/directory';
|
||||
} else {
|
||||
defaults.directoryUrl =
|
||||
'https://acme-v02.api.letsencrypt.org/directory';
|
||||
}
|
||||
} else {
|
||||
if (defaults.staging) {
|
||||
throw new Error('supply `directoryUrl` or `staging`, but not both');
|
||||
}
|
||||
}
|
||||
console.info('ACME Directory URL:', defaults.directoryUrl);
|
||||
|
||||
// Load the default store module
|
||||
if (!defaults.store) {
|
||||
defaults.store = {
|
||||
module: 'greenlock-store-fs',
|
||||
basePath: '~/.config/greenlock/'
|
||||
};
|
||||
}
|
||||
P._loadSync(defaults.store.module);
|
||||
//defaults.store = store;
|
||||
|
||||
// Load the default challenge modules
|
||||
var challenges;
|
||||
if (!defaults.challenges) {
|
||||
defaults.challenges = {};
|
||||
}
|
||||
challenges = defaults.challenges;
|
||||
|
||||
// TODO provide http-01 when http-01 and/or(?) dns-01 don't exist
|
||||
if (!challenges['http-01'] && !challenges['dns-01']) {
|
||||
challenges['http-01'] = {
|
||||
module: 'acme-http-01-standalone'
|
||||
};
|
||||
}
|
||||
|
||||
if (challenges['http-01']) {
|
||||
if ('string' === typeof challenges['http-01'].module) {
|
||||
P._loadSync(challenges['http-01'].module);
|
||||
}
|
||||
}
|
||||
|
||||
if (challenges['dns-01']) {
|
||||
if ('string' === typeof challenges['dns-01'].module) {
|
||||
P._loadSync(challenges['dns-01'].module);
|
||||
}
|
||||
}
|
||||
|
||||
if (defaults.agreeToTerms === true || defaults.agreeTos === true) {
|
||||
defaults.agreeToTerms = function(tos) {
|
||||
return Promise.resolve(tos);
|
||||
};
|
||||
}
|
||||
|
||||
if (!defaults.accountKeyType) {
|
||||
defaults.accountKeyType = 'EC-P256';
|
||||
}
|
||||
if (!defaults.serverKeyType) {
|
||||
if (defaults.domainKeyType) {
|
||||
console.warn('use serverKeyType instead of domainKeyType');
|
||||
defaults.serverKeyType = defaults.domainKeyType;
|
||||
}
|
||||
defaults.serverKeyType = 'RSA-2048';
|
||||
}
|
||||
if (defaults.domainKeypair) {
|
||||
console.warn('use serverKeypair instead of domainKeypair');
|
||||
defaults.serverKeypair =
|
||||
defaults.serverKeypair || defaults.domainKeypair;
|
||||
}
|
||||
|
||||
Object.defineProperty(defaults, 'domainKeypair', {
|
||||
write: false,
|
||||
get: function() {
|
||||
console.warn('use serverKeypair instead of domainKeypair');
|
||||
return defaults.serverKeypair;
|
||||
}
|
||||
});
|
||||
|
||||
return defaults;
|
||||
};
|
||||
|
||||
Greenlock._normalizeStore = function(name, store) {
|
||||
var acc = store.accounts;
|
||||
var crt = store.certificates;
|
||||
|
||||
var warned = false;
|
||||
function warn() {
|
||||
if (warned) {
|
||||
return;
|
||||
}
|
||||
warned = true;
|
||||
console.warn(
|
||||
"'" +
|
||||
name +
|
||||
"' may have incorrect function signatures, or contains deprecated use of callbacks"
|
||||
);
|
||||
}
|
||||
|
||||
// accs
|
||||
if (acc.check && 2 === acc.check.length) {
|
||||
warn();
|
||||
acc._thunk_check = acc.check;
|
||||
acc.check = promisify(acc._thunk_check);
|
||||
}
|
||||
if (acc.set && 3 === acc.set.length) {
|
||||
warn();
|
||||
acc._thunk_set = acc.set;
|
||||
acc.set = promisify(acc._thunk_set);
|
||||
}
|
||||
if (2 === acc.checkKeypair.length) {
|
||||
warn();
|
||||
acc._thunk_checkKeypair = acc.checkKeypair;
|
||||
acc.checkKeypair = promisify(acc._thunk_checkKeypair);
|
||||
}
|
||||
if (3 === acc.setKeypair.length) {
|
||||
warn();
|
||||
acc._thunk_setKeypair = acc.setKeypair;
|
||||
acc.setKeypair = promisify(acc._thunk_setKeypair);
|
||||
}
|
||||
|
||||
// certs
|
||||
if (2 === crt.check.length) {
|
||||
warn();
|
||||
crt._thunk_check = crt.check;
|
||||
crt.check = promisify(crt._thunk_check);
|
||||
}
|
||||
if (3 === crt.set.length) {
|
||||
warn();
|
||||
crt._thunk_set = crt.set;
|
||||
crt.set = promisify(crt._thunk_set);
|
||||
}
|
||||
if (2 === crt.checkKeypair.length) {
|
||||
warn();
|
||||
crt._thunk_checkKeypair = crt.checkKeypair;
|
||||
crt.checkKeypair = promisify(crt._thunk_checkKeypair);
|
||||
}
|
||||
if (2 === crt.setKeypair.length) {
|
||||
warn();
|
||||
crt._thunk_setKeypair = crt.setKeypair;
|
||||
crt.setKeypair = promisify(crt._thunk_setKeypair);
|
||||
}
|
||||
|
||||
return store;
|
||||
};
|
||||
|
||||
Greenlock._normalizeChallenge = function(name, ch) {
|
||||
var warned = false;
|
||||
function warn() {
|
||||
if (warned) {
|
||||
return;
|
||||
}
|
||||
warned = true;
|
||||
console.warn(
|
||||
"'" +
|
||||
name +
|
||||
"' may have incorrect function signatures, or contains deprecated use of callbacks"
|
||||
);
|
||||
}
|
||||
|
||||
// init, zones, set, get, remove
|
||||
if (ch.init && 2 === ch.init.length) {
|
||||
warn();
|
||||
ch._thunk_init = ch.init;
|
||||
ch.init = promisify(ch._thunk_init);
|
||||
}
|
||||
if (ch.zones && 2 === ch.zones.length) {
|
||||
warn();
|
||||
ch._thunk_zones = ch.zones;
|
||||
ch.zones = promisify(ch._thunk_zones);
|
||||
}
|
||||
if (2 === ch.set.length) {
|
||||
warn();
|
||||
ch._thunk_set = ch.set;
|
||||
ch.set = promisify(ch._thunk_set);
|
||||
}
|
||||
if (2 === ch.remove.length) {
|
||||
warn();
|
||||
ch._thunk_remove = ch.remove;
|
||||
ch.remove = promisify(ch._thunk_remove);
|
||||
}
|
||||
if (ch.get && 2 === ch.get.length) {
|
||||
warn();
|
||||
ch._thunk_get = ch.get;
|
||||
ch.get = promisify(ch._thunk_get);
|
||||
}
|
||||
|
||||
return ch;
|
||||
};
|
|
@ -0,0 +1,97 @@
|
|||
var accountKeypair = await Keypairs.generate({ kty: accKty });
|
||||
if (config.debug) {
|
||||
console.info('Account Key Created');
|
||||
console.info(JSON.stringify(accountKeypair, null, 2));
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
|
||||
var account = await acme.accounts.create({
|
||||
agreeToTerms: agree,
|
||||
// TODO detect jwk/pem/der?
|
||||
accountKeypair: { privateKeyJwk: accountKeypair.private },
|
||||
subscriberEmail: config.email
|
||||
});
|
||||
|
||||
// TODO top-level agree
|
||||
function agree(tos) {
|
||||
if (config.debug) {
|
||||
console.info('Agreeing to Terms of Service:');
|
||||
console.info(tos);
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
agreed = true;
|
||||
return Promise.resolve(tos);
|
||||
}
|
||||
if (config.debug) {
|
||||
console.info('New Subscriber Account');
|
||||
console.info(JSON.stringify(account, null, 2));
|
||||
console.info();
|
||||
console.info();
|
||||
}
|
||||
if (!agreed) {
|
||||
throw new Error('Failed to ask the user to agree to terms');
|
||||
}
|
||||
|
||||
var certKeypair = await Keypairs.generate({ kty: srvKty });
|
||||
var pem = await Keypairs.export({
|
||||
jwk: certKeypair.private,
|
||||
encoding: 'pem'
|
||||
});
|
||||
if (config.debug) {
|
||||
console.info('Server Key Created');
|
||||
console.info('privkey.jwk.json');
|
||||
console.info(JSON.stringify(certKeypair, null, 2));
|
||||
// This should be saved as `privkey.pem`
|
||||
console.info();
|
||||
console.info('privkey.' + srvKty.toLowerCase() + '.pem:');
|
||||
console.info(pem);
|
||||
console.info();
|
||||
}
|
||||
|
||||
// 'subject' should be first in list
|
||||
var domains = randomDomains(rnd);
|
||||
if (config.debug) {
|
||||
console.info('Get certificates for random domains:');
|
||||
console.info(
|
||||
domains
|
||||
.map(function(puny) {
|
||||
var uni = punycode.toUnicode(puny);
|
||||
if (puny !== uni) {
|
||||
return puny + ' (' + uni + ')';
|
||||
}
|
||||
return puny;
|
||||
})
|
||||
.join('\n')
|
||||
);
|
||||
console.info();
|
||||
}
|
||||
|
||||
// Create CSR
|
||||
var csrDer = await CSR.csr({
|
||||
jwk: certKeypair.private,
|
||||
domains: domains,
|
||||
encoding: 'der'
|
||||
});
|
||||
var csr = Enc.bufToUrlBase64(csrDer);
|
||||
var csrPem = PEM.packBlock({
|
||||
type: 'CERTIFICATE REQUEST',
|
||||
bytes: csrDer /* { jwk: jwk, domains: opts.domains } */
|
||||
});
|
||||
if (config.debug) {
|
||||
console.info('Certificate Signing Request');
|
||||
console.info(csrPem);
|
||||
console.info();
|
||||
}
|
||||
|
||||
var results = await acme.certificates.create({
|
||||
account: account,
|
||||
accountKeypair: { privateKeyJwk: accountKeypair.private },
|
||||
csr: csr,
|
||||
domains: domains,
|
||||
challenges: challenges, // must be implemented
|
||||
customerEmail: null
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
{
|
||||
"name": "@root/greenlock",
|
||||
"version": "3.0.0-wip.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@root/acme": {
|
||||
"version": "3.0.0-wip.3",
|
||||
"resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.0.0-wip.3.tgz",
|
||||
"integrity": "sha512-7Fq9FuO0WQgKPgyYmKHst71EbIqH764A3j6vF1aKemgWXXq2Wqy8G+2SJwt3/MSXhQ7X+qLmWRLLJ7U4Zlygsg==",
|
||||
"requires": {
|
||||
"@root/csr": "^0.8.1",
|
||||
"@root/encoding": "^1.0.1",
|
||||
"@root/keypairs": "^0.9.0",
|
||||
"@root/pem": "^1.0.4",
|
||||
"@root/request": "^1.3.11",
|
||||
"@root/x509": "^0.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@root/csr": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@root/csr/-/csr-0.8.1.tgz",
|
||||
"integrity": "sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==",
|
||||
"requires": {
|
||||
"@root/asn1": "^1.0.0",
|
||||
"@root/pem": "^1.0.4",
|
||||
"@root/x509": "^0.7.2"
|
||||
}
|
||||
},
|
||||
"@root/keypairs": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.9.0.tgz",
|
||||
"integrity": "sha512-NXE2L9Gv7r3iC4kB/gTPZE1vO9Ox/p14zDzAJ5cGpTpytbWOlWF7QoHSJbtVX4H7mRG/Hp7HR3jWdWdb2xaaXg==",
|
||||
"requires": {
|
||||
"@root/encoding": "^1.0.1",
|
||||
"@root/pem": "^1.0.4",
|
||||
"@root/x509": "^0.7.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@root/asn1": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz",
|
||||
"integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==",
|
||||
"requires": {
|
||||
"@root/encoding": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@root/csr": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@root/csr/-/csr-0.8.1.tgz",
|
||||
"integrity": "sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==",
|
||||
"requires": {
|
||||
"@root/asn1": "^1.0.0",
|
||||
"@root/pem": "^1.0.4",
|
||||
"@root/x509": "^0.7.2"
|
||||
}
|
||||
},
|
||||
"@root/encoding": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz",
|
||||
"integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ=="
|
||||
},
|
||||
"@root/keypairs": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.9.0.tgz",
|
||||
"integrity": "sha512-NXE2L9Gv7r3iC4kB/gTPZE1vO9Ox/p14zDzAJ5cGpTpytbWOlWF7QoHSJbtVX4H7mRG/Hp7HR3jWdWdb2xaaXg==",
|
||||
"requires": {
|
||||
"@root/encoding": "^1.0.1",
|
||||
"@root/pem": "^1.0.4",
|
||||
"@root/x509": "^0.7.2"
|
||||
}
|
||||
},
|
||||
"@root/mkdirp": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz",
|
||||
"integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA=="
|
||||
},
|
||||
"@root/pem": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.4.tgz",
|
||||
"integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA=="
|
||||
},
|
||||
"@root/request": {
|
||||
"version": "1.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
|
||||
"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw=="
|
||||
},
|
||||
"@root/x509": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz",
|
||||
"integrity": "sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==",
|
||||
"requires": {
|
||||
"@root/asn1": "^1.0.0",
|
||||
"@root/encoding": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"acme-dns-01-digitalocean": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/acme-dns-01-digitalocean/-/acme-dns-01-digitalocean-3.0.1.tgz",
|
||||
"integrity": "sha512-LUdOGluDERQWJG4CwlC9HbzUai4mtKzCz8nzpVTirXup2WwH60iRFAcd81hRGaoWbd0Bc0m6RVjN9YFkXB84yA=="
|
||||
},
|
||||
"acme-http-01-standalone": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/acme-http-01-standalone/-/acme-http-01-standalone-3.0.0.tgz",
|
||||
"integrity": "sha512-lZqVab2UZ1Dp36HemfhGEvdYOcVNg5wyVXNjtPUqGSAOVUOKqwi3gDrTGwqz+FBrEEEEpTngDPaZn2g3hfmPLA=="
|
||||
},
|
||||
"cert-info": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/cert-info/-/cert-info-1.5.1.tgz",
|
||||
"integrity": "sha512-eoQC/yAgW3gKTKxjzyClvi+UzuY97YCjcl+lSqbsGIy7HeGaWxCPOQFivhUYm27hgsBMhsJJFya3kGvK6PMIcQ=="
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
|
||||
"dev": true
|
||||
},
|
||||
"greenlock-store-fs": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/greenlock-store-fs/-/greenlock-store-fs-3.0.2.tgz",
|
||||
"integrity": "sha512-t4So75yKs1+7TqmxD5UKdf+zOQU0/4o0lb2auf5zUcAo7fwwNLOAXyWnnZRL3WuFBUiBGh1qXWleuMua0d3LPg==",
|
||||
"requires": {
|
||||
"@root/mkdirp": "^1.0.0",
|
||||
"safe-replace": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
|
||||
"dev": true
|
||||
},
|
||||
"safe-replace": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz",
|
||||
"integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw=="
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "@root/greenlock",
|
||||
"version": "3.0.0-wip.0",
|
||||
"description": "The easiest Let's Encrypt client for Node.js and Browsers",
|
||||
"homepage": "https://rootprojects.org/greenlock/",
|
||||
"main": "greenlock.js",
|
||||
"browser": {},
|
||||
"files": [
|
||||
"*.js",
|
||||
"lib",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "nodex bin/bundle.js",
|
||||
"lint": "jshint lib bin",
|
||||
"test": "node server.js",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.coolaj86.com/coolaj86/bluecrypt-acme.js.git"
|
||||
},
|
||||
"keywords": [
|
||||
"ACME",
|
||||
"Let's Encrypt",
|
||||
"browser",
|
||||
"EC",
|
||||
"RSA",
|
||||
"CSR",
|
||||
"greenlock",
|
||||
"VanillaJS",
|
||||
"ZeroSSL"
|
||||
],
|
||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"@root/acme": "^3.0.0-wip.3",
|
||||
"@root/csr": "^0.8.1",
|
||||
"@root/keypairs": "^0.9.0",
|
||||
"@root/mkdirp": "^1.0.0",
|
||||
"@root/request": "^1.3.10",
|
||||
"acme-dns-01-digitalocean": "^3.0.1",
|
||||
"acme-http-01-standalone": "^3.0.0",
|
||||
"cert-info": "^1.5.1",
|
||||
"greenlock-store-fs": "^3.0.2",
|
||||
"safe-replace": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "^8.2.0",
|
||||
"punycode": "^1.4.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
'use strict';
|
||||
|
||||
var P = module.exports;
|
||||
|
||||
var spawn = require('child_process').spawn;
|
||||
var spawnSync = require('child_process').spawnSync;
|
||||
var PKG_DIR = __dirname;
|
||||
|
||||
P._load = function(modname) {
|
||||
try {
|
||||
return Promise.resolve(require(modname));
|
||||
} catch (e) {
|
||||
return P._install(modname).then(function() {
|
||||
return require(modname);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
P._loadSync = function(modname) {
|
||||
var mod;
|
||||
try {
|
||||
mod = require(modname);
|
||||
} catch (e) {
|
||||
P._installSync(modname);
|
||||
mod = require(modname);
|
||||
}
|
||||
return mod;
|
||||
};
|
||||
|
||||
P._installSync = function(moduleName) {
|
||||
var npm = 'npm';
|
||||
var args = ['install', '--save', moduleName];
|
||||
var out = '';
|
||||
var cmd;
|
||||
|
||||
try {
|
||||
cmd = spawnSync(npm, args, {
|
||||
cwd: PKG_DIR,
|
||||
windowsHide: true
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Failed to start: '" +
|
||||
npm +
|
||||
' ' +
|
||||
args.join(' ') +
|
||||
"' in '" +
|
||||
PKG_DIR +
|
||||
"'"
|
||||
);
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!cmd.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
out += cmd.stdout.toString('utf8');
|
||||
out += cmd.stderr.toString('utf8');
|
||||
|
||||
if (out) {
|
||||
console.error(out);
|
||||
console.error();
|
||||
console.error();
|
||||
}
|
||||
|
||||
console.error(
|
||||
"Failed to run: '" +
|
||||
npm +
|
||||
' ' +
|
||||
args.join(' ') +
|
||||
"' in '" +
|
||||
PKG_DIR +
|
||||
"'"
|
||||
);
|
||||
|
||||
console.error(
|
||||
'Try for yourself:\n\tcd ' + PKG_DIR + '\n\tnpm ' + args.join(' ')
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
P._install = function(moduleName) {
|
||||
return new Promise(function(resolve) {
|
||||
if (!moduleName) {
|
||||
throw new Error('no module name given');
|
||||
}
|
||||
|
||||
var npm = 'npm';
|
||||
var args = ['install', '--save', moduleName];
|
||||
var out = '';
|
||||
var cmd = spawn(npm, args, {
|
||||
cwd: PKG_DIR,
|
||||
windowsHide: true
|
||||
});
|
||||
|
||||
cmd.stdout.on('data', function(chunk) {
|
||||
out += chunk.toString('utf8');
|
||||
});
|
||||
cmd.stdout.on('data', function(chunk) {
|
||||
out += chunk.toString('utf8');
|
||||
});
|
||||
|
||||
cmd.on('error', function(e) {
|
||||
console.error(
|
||||
"Failed to start: '" +
|
||||
npm +
|
||||
' ' +
|
||||
args.join(' ') +
|
||||
"' in '" +
|
||||
PKG_DIR +
|
||||
"'"
|
||||
);
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
cmd.on('exit', function(code) {
|
||||
if (!code) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (out) {
|
||||
console.error(out);
|
||||
console.error();
|
||||
console.error();
|
||||
}
|
||||
console.error(
|
||||
"Failed to run: '" +
|
||||
npm +
|
||||
' ' +
|
||||
args.join(' ') +
|
||||
"' in '" +
|
||||
PKG_DIR +
|
||||
"'"
|
||||
);
|
||||
console.error(
|
||||
'Try for yourself:\n\tcd ' +
|
||||
PKG_DIR +
|
||||
'\n\tnpm ' +
|
||||
args.join(' ')
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
P._installSync(process.argv[2]);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
'use strict';
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
var path = require('path');
|
||||
var Greenlock = require('../');
|
||||
|
||||
var subject = process.env.BASE_DOMAIN;
|
||||
var altnames = [subject, '*.' + subject, 'foo.bar.' + subject];
|
||||
var email = process.env.SUBSCRIBER_EMAIL;
|
||||
var challenge = JSON.parse(process.env.CHALLENGE_OPTIONS);
|
||||
challenge.module = process.env.CHALLENGE_MODULE;
|
||||
|
||||
var greenlock = Greenlock.create({
|
||||
agreeTos: true,
|
||||
maintainerEmail: email,
|
||||
staging: true,
|
||||
manager: path.join(__dirname, 'manager.js'),
|
||||
challenges: {
|
||||
'dns-01': challenge
|
||||
}
|
||||
//configFile: '~/.config/greenlock/certs.json',
|
||||
//challenges: challenges,
|
||||
//store: args.storeOpts,
|
||||
//renewOffset: args.renewOffset || '30d',
|
||||
//renewStagger: '1d'
|
||||
});
|
||||
|
||||
greenlock
|
||||
.add({
|
||||
subject: subject,
|
||||
altnames: altnames,
|
||||
subscriberEmail: email
|
||||
})
|
||||
.then(function() {
|
||||
return greenlock.renew();
|
||||
})
|
||||
.catch(function(e) {
|
||||
console.error('yo', e.code);
|
||||
console.error(e);
|
||||
});
|
|
@ -0,0 +1,259 @@
|
|||
'use strict';
|
||||
|
||||
var Manage = module.exports;
|
||||
var sfs = require('safe-replace').create({ tmp: 'tmp', bak: 'bak' });
|
||||
var promisify = require('util').promisify;
|
||||
var fs = require('fs');
|
||||
var readFile = promisify(fs.readFile);
|
||||
var statFile = promisify(fs.stat);
|
||||
var homedir = require('os').homedir();
|
||||
var path = require('path');
|
||||
var mkdirp = promisify(require('@root/mkdirp'));
|
||||
|
||||
Manage.create = function(opts) {
|
||||
if (!opts) {
|
||||
opts = {};
|
||||
}
|
||||
if (!opts.configFile) {
|
||||
opts.configFile = '~/.config/greenlock/config.json';
|
||||
}
|
||||
opts.configFile = opts.configFile.replace('~/', homedir + '/');
|
||||
|
||||
var manage = {};
|
||||
|
||||
manage.ping = function() {
|
||||
return Manage._ping(manage, opts);
|
||||
};
|
||||
|
||||
manage._txPromise = new Promise(function(resolve) {
|
||||
resolve();
|
||||
});
|
||||
|
||||
manage._lastStat = {
|
||||
size: 0,
|
||||
mtimeMs: 0
|
||||
};
|
||||
manage._config = {};
|
||||
|
||||
manage._save = function(config) {
|
||||
return mkdirp(path.dirname(opts.configFile)).then(function() {
|
||||
return sfs
|
||||
.writeFileAsync(opts.configFile, JSON.stringify(config), 'utf8')
|
||||
.then(function() {
|
||||
return statFile(opts.configFile).then(function(stat) {
|
||||
manage._lastStat.size = stat.size;
|
||||
manage._lastStat.mtimeMs = stat.mtimeMs;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
manage.add = function(args) {
|
||||
manage._txPromise = manage._txPromise.then(function() {
|
||||
// if the fs has changed since we last wrote, get the lastest from disk
|
||||
return Manage._getLatest(manage, opts).then(function(config) {
|
||||
// TODO move to Greenlock.add
|
||||
var subject = args.subject || args.domain;
|
||||
var primary = subject;
|
||||
var altnames = args.altnames || args.domains;
|
||||
if ('string' !== typeof primary) {
|
||||
if (!Array.isArray(altnames) || !altnames.length) {
|
||||
throw new Error('there needs to be a subject');
|
||||
}
|
||||
primary = altnames.slice(0).sort()[0];
|
||||
}
|
||||
if (!Array.isArray(altnames) || !altnames.length) {
|
||||
altnames = [primary];
|
||||
}
|
||||
primary = primary.toLowerCase();
|
||||
altnames = altnames.map(function(name) {
|
||||
return name.toLowerCase();
|
||||
});
|
||||
|
||||
if (!config.sites) {
|
||||
config.sites = {};
|
||||
}
|
||||
|
||||
var site = config.sites[primary];
|
||||
if (!site) {
|
||||
site = config.sites[primary] = { altnames: [] };
|
||||
}
|
||||
|
||||
// The goal is to make this decently easy to manage by hand without mistakes
|
||||
// but also reasonably easy to error check and correct
|
||||
// and to make deterministic auto-corrections
|
||||
|
||||
// TODO added, removed, moved (duplicate), changed
|
||||
site.subscriberEmail = site.subscriberEmail;
|
||||
site.subject = subject;
|
||||
site.altnames = altnames;
|
||||
site.issuedAt = site.issuedAt || 0;
|
||||
site.expiresAt = site.expiresAt || 0;
|
||||
site.lastAttemptAt = site.lastAttemptAt || 0;
|
||||
// re-add if this was deleted
|
||||
site.deletedAt = 0;
|
||||
if (
|
||||
site.altnames
|
||||
.slice(0)
|
||||
.sort()
|
||||
.join() !==
|
||||
altnames
|
||||
.slice(0)
|
||||
.sort()
|
||||
.join()
|
||||
) {
|
||||
site.expiresAt = 0;
|
||||
site.issuedAt = 0;
|
||||
}
|
||||
|
||||
// These should usually be empty, for most situations
|
||||
site.subscriberEmail = args.subscriberEmail;
|
||||
site.customerEmail = args.customerEmail;
|
||||
site.challenges = args.challenges;
|
||||
site.store = args.store;
|
||||
console.log('[debug] save site', site);
|
||||
|
||||
return manage._save(config).then(function() {
|
||||
return JSON.parse(JSON.stringify(site));
|
||||
});
|
||||
});
|
||||
});
|
||||
return manage._txPromise;
|
||||
};
|
||||
|
||||
manage.find = function(args) {
|
||||
return Manage._getLatest(manage, opts).then(function(config) {
|
||||
// i.e. find certs more than 30 days old
|
||||
//args.issuedBefore = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
||||
// i.e. find certs more that will expire in less than 45 days
|
||||
//args.expiresBefore = Date.now() + 45 * 24 * 60 * 60 * 1000;
|
||||
var issuedBefore = args.issuedBefore || 0;
|
||||
var expiresBefore =
|
||||
args.expiresBefore || Date.now() + 21 * 24 * 60 * 60 * 1000;
|
||||
|
||||
// TODO match ANY domain on any cert
|
||||
var sites = Object.keys(config.sites)
|
||||
.filter(function(sub) {
|
||||
var site = config.sites[sub];
|
||||
if (
|
||||
!site.deletedAt ||
|
||||
site.expiresAt < expiresBefore ||
|
||||
site.issuedAt < issuedBefore
|
||||
) {
|
||||
if (!args.subject || sub === args.subject) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
})
|
||||
.map(function(name) {
|
||||
var site = config.sites[name];
|
||||
console.debug('debug', site);
|
||||
return {
|
||||
subject: site.subject,
|
||||
altnames: site.altnames,
|
||||
issuedAt: site.issuedAt,
|
||||
expiresAt: site.expiresAt,
|
||||
renewOffset: site.renewOffset,
|
||||
renewStagger: site.renewStagger,
|
||||
renewAt: site.renewAt,
|
||||
subscriberEmail: site.subscriberEmail,
|
||||
customerEmail: site.customerEmail,
|
||||
challenges: site.challenges,
|
||||
store: site.store
|
||||
};
|
||||
});
|
||||
|
||||
return sites;
|
||||
});
|
||||
};
|
||||
|
||||
manage.remove = function(args) {
|
||||
if (!args.subject) {
|
||||
throw new Error('should have a subject for sites to remove');
|
||||
}
|
||||
manage._txPromise = manage.txPromise.then(function() {
|
||||
return Manage._getLatest(manage, opts).then(function(config) {
|
||||
var site = config.sites[args.subject];
|
||||
if (!site) {
|
||||
return {};
|
||||
}
|
||||
site.deletedAt = Date.now();
|
||||
|
||||
return JSON.parse(JSON.stringify(site));
|
||||
});
|
||||
});
|
||||
return manage._txPromise;
|
||||
};
|
||||
|
||||
manage.notifications = function(args) {
|
||||
// TODO define message types
|
||||
console.info(args.event, args.message);
|
||||
};
|
||||
|
||||
manage.errors = function(err) {
|
||||
// err.subject
|
||||
// err.altnames
|
||||
// err.challenge
|
||||
// err.challengeOptions
|
||||
// err.store
|
||||
// err.storeOptions
|
||||
console.error('Failure with ', err.subject);
|
||||
};
|
||||
|
||||
manage.update = function(args) {
|
||||
manage._txPromise = manage.txPromise.then(function() {
|
||||
return Manage._getLatest(manage, opts).then(function(config) {
|
||||
var site = config.sites[args.subject];
|
||||
site.issuedAt = args.issuedAt;
|
||||
site.expiresAt = args.expiresAt;
|
||||
site.renewAt = args.renewAt;
|
||||
// foo
|
||||
});
|
||||
});
|
||||
return manage._txPromise;
|
||||
};
|
||||
|
||||
return manage;
|
||||
};
|
||||
|
||||
Manage._getLatest = function(mng, opts) {
|
||||
return statFile(opts.configFile)
|
||||
.catch(function(err) {
|
||||
if ('ENOENT' === err.code) {
|
||||
return {
|
||||
size: 0,
|
||||
mtimeMs: 0
|
||||
};
|
||||
}
|
||||
err.context = 'manager_read';
|
||||
throw err;
|
||||
})
|
||||
.then(function(stat) {
|
||||
if (
|
||||
stat.size === mng._lastStat.size &&
|
||||
stat.mtimeMs === mng._lastStat.mtimeMs
|
||||
) {
|
||||
return mng._config;
|
||||
}
|
||||
return readFile(opts.configFile, 'utf8').then(function(data) {
|
||||
mng._lastStat = stat;
|
||||
mng._config = JSON.parse(data);
|
||||
return mng._config;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Manage._ping = function(mng, opts) {
|
||||
if (mng._pingPromise) {
|
||||
return mng._pingPromise;
|
||||
}
|
||||
|
||||
mng._pringPromise = Promise.resolve().then(function() {
|
||||
// TODO file permissions
|
||||
if (!opts.configFile) {
|
||||
throw new Error('no config file location provided');
|
||||
}
|
||||
JSON.parse(fs.readFileSync(opts.configFile, 'utf8'));
|
||||
});
|
||||
return mng._pingPromise;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var UserEvents = module.exports;
|
||||
|
||||
UserEvents.notify = function() {
|
||||
// TODO not implemented yet
|
||||
};
|
|
@ -0,0 +1,267 @@
|
|||
'use strict';
|
||||
|
||||
var U = module.exports;
|
||||
|
||||
var promisify = require('util').promisify;
|
||||
var resolveSoa = promisify(require('dns').resolveSoa);
|
||||
var resolveMx = promisify(require('dns').resolveMx);
|
||||
var punycode = require('punycode');
|
||||
var Keypairs = require('@root/keypairs');
|
||||
// TODO move to @root
|
||||
var certParser = require('cert-info');
|
||||
|
||||
U._parseDuration = function(str) {
|
||||
if ('number' === typeof str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
var pattern = /^(\-?\d+(\.\d+)?)([wdhms]|ms)$/;
|
||||
var matches = str.match(pattern);
|
||||
if (!matches || !matches[0]) {
|
||||
throw new Error('invalid duration string: ' + str);
|
||||
}
|
||||
|
||||
var n = parseInt(matches[1], 10);
|
||||
var unit = matches[3];
|
||||
|
||||
switch (unit) {
|
||||
case 'w':
|
||||
n *= 7;
|
||||
/*falls through*/
|
||||
case 'd':
|
||||
n *= 24;
|
||||
/*falls through*/
|
||||
case 'h':
|
||||
n *= 60;
|
||||
/*falls through*/
|
||||
case 'm':
|
||||
n *= 60;
|
||||
/*falls through*/
|
||||
case 's':
|
||||
n *= 1000;
|
||||
/*falls through*/
|
||||
case 'ms':
|
||||
n *= 1; // for completeness
|
||||
}
|
||||
|
||||
return n;
|
||||
};
|
||||
|
||||
U._encodeName = function(str) {
|
||||
return punycode.toASCII(str.toLowerCase(str));
|
||||
};
|
||||
|
||||
U._validName = function(str) {
|
||||
// A quick check of the 38 and two ½ valid characters
|
||||
// 253 char max full domain, including dots
|
||||
// 63 char max each label segment
|
||||
// Note: * is not allowed, but it's allowable here
|
||||
// Note: _ (underscore) is only allowed for "domain names", not "hostnames"
|
||||
// Note: - (hyphen) is not allowed as a first character (but a number is)
|
||||
return (
|
||||
/^(\*\.)?[a-z0-9_\.\-]+$/.test(str) &&
|
||||
str.length < 254 &&
|
||||
str.split('.').every(function(label) {
|
||||
return label.length > 0 && label.length < 64;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
U._validMx = function(email) {
|
||||
var host = email.split('@').slice(1)[0];
|
||||
// try twice, just because DNS hiccups sometimes
|
||||
// Note: we don't care if the domain exists, just that it *can* exist
|
||||
return resolveMx(host).catch(function() {
|
||||
return U._timeout(1000).then(function() {
|
||||
return resolveMx(host);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// should be called after _validName
|
||||
U._validDomain = function(str) {
|
||||
// TODO use @root/dns (currently dns-suite)
|
||||
// because node's dns can't read Authority records
|
||||
return Promise.resolve(str);
|
||||
/*
|
||||
// try twice, just because DNS hiccups sometimes
|
||||
// Note: we don't care if the domain exists, just that it *can* exist
|
||||
return resolveSoa(str).catch(function() {
|
||||
return U._timeout(1000).then(function() {
|
||||
return resolveSoa(str);
|
||||
});
|
||||
});
|
||||
*/
|
||||
};
|
||||
|
||||
// foo.example.com and *.example.com overlap
|
||||
// should be called after _validName
|
||||
// (which enforces *. or no *)
|
||||
U._uniqueNames = function(altnames) {
|
||||
var dups = {};
|
||||
var wilds = {};
|
||||
if (
|
||||
altnames.some(function(w) {
|
||||
if ('*.' !== w.slice(0, 2)) {
|
||||
return;
|
||||
}
|
||||
if (wilds[w]) {
|
||||
return true;
|
||||
}
|
||||
wilds[w] = true;
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return altnames.every(function(name) {
|
||||
var w;
|
||||
if ('*.' !== name.slice(0, 2)) {
|
||||
w =
|
||||
'*.' +
|
||||
name
|
||||
.split('.')
|
||||
.slice(1)
|
||||
.join('.');
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!dups[name] && !dups[w]) {
|
||||
dups[name] = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
U._timeout = function(d) {
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, d);
|
||||
});
|
||||
};
|
||||
|
||||
U._genKeypair = function(keyType) {
|
||||
var keyopts;
|
||||
var len = parseInt(keyType.replace(/.*?(\d)/, '$1') || 0, 10);
|
||||
if (/RSA/.test(keyType)) {
|
||||
keyopts = {
|
||||
kty: 'RSA',
|
||||
modulusLength: len || 2048
|
||||
};
|
||||
} else if (/^(EC|P\-?\d)/i.test(keyType)) {
|
||||
keyopts = {
|
||||
kty: 'EC',
|
||||
namedCurve: 'P-' + (len || 256)
|
||||
};
|
||||
} else {
|
||||
// TODO put in ./errors.js
|
||||
throw new Error('invalid key type: ' + keyType);
|
||||
}
|
||||
|
||||
return Keypairs.generate(keyopts).then(function(pair) {
|
||||
return U._jwkToSet(pair.private);
|
||||
});
|
||||
};
|
||||
|
||||
// TODO use ACME._importKeypair ??
|
||||
U._importKeypair = function(keypair) {
|
||||
if (keypair.privateKeyJwk) {
|
||||
return U._jwkToSet(keypair.privateKeyJwk);
|
||||
}
|
||||
|
||||
if (!keypair.privateKeyPem) {
|
||||
// TODO put in errors
|
||||
throw new Error('missing private key');
|
||||
}
|
||||
|
||||
return Keypairs.import({ pem: keypair.privateKeyPem }).then(function(pair) {
|
||||
return U._jwkToSet(pair.private);
|
||||
});
|
||||
};
|
||||
|
||||
U._jwkToSet = function(jwk) {
|
||||
var keypair = {
|
||||
privateKeyJwk: jwk
|
||||
};
|
||||
return Promise.all([
|
||||
Keypairs.export({
|
||||
jwk: jwk,
|
||||
encoding: 'pem'
|
||||
}).then(function(pem) {
|
||||
keypair.privateKeyPem = pem;
|
||||
}),
|
||||
Keypairs.export({
|
||||
jwk: jwk,
|
||||
encoding: 'pem',
|
||||
public: true
|
||||
}).then(function(pem) {
|
||||
keypair.publicKeyPem = pem;
|
||||
}),
|
||||
Keypairs.publish({
|
||||
jwk: jwk
|
||||
}).then(function(pub) {
|
||||
keypair.publicKeyJwk = pub;
|
||||
})
|
||||
]).then(function() {
|
||||
return keypair;
|
||||
});
|
||||
};
|
||||
|
||||
U._attachCertInfo = function(results) {
|
||||
var certInfo = certParser.info(results.cert);
|
||||
|
||||
// subject, altnames, issuedAt, expiresAt
|
||||
Object.keys(certInfo).forEach(function(key) {
|
||||
results[key] = certInfo[key];
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
U._certHasDomain = function(certInfo, _domain) {
|
||||
var names = (certInfo.altnames || []).slice(0);
|
||||
return names.some(function(name) {
|
||||
var domain = _domain.toLowerCase();
|
||||
name = name.toLowerCase();
|
||||
if ('*.' === name.substr(0, 2)) {
|
||||
name = name.substr(2);
|
||||
domain = domain
|
||||
.split('.')
|
||||
.slice(1)
|
||||
.join('.');
|
||||
}
|
||||
return name === domain;
|
||||
});
|
||||
};
|
||||
|
||||
// a bit heavy to be labeled 'utils'... perhaps 'common' would be better?
|
||||
U._getOrCreateKeypair = function(db, subject, query, keyType, mustExist) {
|
||||
var exists = false;
|
||||
return db
|
||||
.checkKeypair(query)
|
||||
.then(function(kp) {
|
||||
if (kp) {
|
||||
exists = true;
|
||||
return U._importKeypair(kp);
|
||||
}
|
||||
|
||||
if (mustExist) {
|
||||
// TODO put in errors
|
||||
throw new Error(
|
||||
'required keypair not found: ' +
|
||||
(subject || '') +
|
||||
' ' +
|
||||
JSON.stringify(query)
|
||||
);
|
||||
}
|
||||
|
||||
return U._genKeypair(keyType);
|
||||
})
|
||||
.then(function(keypair) {
|
||||
return { exists: exists, keypair: keypair };
|
||||
});
|
||||
};
|
||||
|
||||
U._getKeypair = function(db, subject, query) {
|
||||
return U._getOrCreateKeypair(db, subject, query, '', true);
|
||||
};
|
Loading…
Reference in New Issue