AJ ONeal
5年前
19個のファイルの変更、2356行の追加、16行の削除
@ -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; |
|||
}); |
|||
}; |
@ -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); |
|||
}; |
読み込み中…
新しい課題から参照