1 измењених фајлова са 494 додато и 0 уклоњено
@ -0,0 +1,494 @@ |
|||
(function () { |
|||
'use strict'; |
|||
|
|||
/*global URLSearchParams,Headers*/ |
|||
var VERSION = '2'; |
|||
// ACME recommends ECDSA P-256, but RSA 2048 is still required by some old servers (like what replicated.io uses )
|
|||
// ECDSA P-384, P-521, and RSA 3072, 4096 are NOT recommend standards (and not properly supported)
|
|||
var BROWSER_SUPPORTS_RSA; |
|||
var ECDSA_OPTS = { kty: 'EC', namedCurve: 'P-256' }; |
|||
var RSA_OPTS = { kty: 'RSA', modulusLength: 2048 }; |
|||
var Promise = window.Promise; |
|||
var Keypairs = window.Keypairs; |
|||
var ACME = window.ACME; |
|||
var CSR = window.CSR; |
|||
var $qs = function (s) { return window.document.querySelector(s); }; |
|||
var $qsa = function (s) { return window.document.querySelectorAll(s); }; |
|||
var acme; |
|||
var accountStuff; |
|||
var info = {}; |
|||
var steps = {}; |
|||
var i = 1; |
|||
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; |
|||
|
|||
function updateApiType() { |
|||
console.log("type updated"); |
|||
/*jshint validthis: true */ |
|||
var input = this || Array.prototype.filter.call( |
|||
$qsa('.js-acme-api-type'), function ($el) { return $el.checked; } |
|||
)[0]; |
|||
console.log('ACME api type radio:', input.value); |
|||
$qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value); |
|||
} |
|||
|
|||
function hideForms() { |
|||
$qsa('.js-acme-form').forEach(function (el) { |
|||
el.hidden = true; |
|||
}); |
|||
} |
|||
|
|||
function updateProgress(currentStep) { |
|||
var progressSteps = $qs("#js-progress-bar").children; |
|||
var j; |
|||
for (j = 0; j < progressSteps.length; j += 1) { |
|||
if (j < currentStep) { |
|||
progressSteps[j].classList.add("js-progress-step-complete"); |
|||
progressSteps[j].classList.remove("js-progress-step-started"); |
|||
} else if (j === currentStep) { |
|||
progressSteps[j].classList.remove("js-progress-step-complete"); |
|||
progressSteps[j].classList.add("js-progress-step-started"); |
|||
} else { |
|||
progressSteps[j].classList.remove("js-progress-step-complete"); |
|||
progressSteps[j].classList.remove("js-progress-step-started"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function submitForm(ev) { |
|||
var j = i; |
|||
i += 1; |
|||
|
|||
return PromiseA.resolve(steps[j].submit(ev)).catch(function (err) { |
|||
console.error(err); |
|||
window.alert("Something went wrong. It's our fault not yours. Please email aj@rootprojects.org and let him know that 'step " + j + "' failed."); |
|||
}); |
|||
} |
|||
|
|||
function testEcdsaSupport() { |
|||
/* |
|||
var opts = { |
|||
kty: $('input[name="kty"]:checked').value |
|||
, namedCurve: $('input[name="ec-crv"]:checked').value |
|||
, modulusLength: $('input[name="rsa-len"]:checked').value |
|||
}; |
|||
*/ |
|||
} |
|||
function testRsaSupport() { |
|||
return Keypairs.generate(RSA_OPTS); |
|||
} |
|||
function testKeypairSupport() { |
|||
// fix previous browsers
|
|||
var isCurrent = (localStorage.getItem('version') === VERSION); |
|||
if (!isCurrent) { |
|||
localStorage.clear(); |
|||
localStorage.setItem('version', VERSION); |
|||
} |
|||
localStorage.setItem('version', VERSION); |
|||
|
|||
return testRsaSupport().then(function () { |
|||
console.info("[crypto] RSA is supported"); |
|||
BROWSER_SUPPORTS_RSA = true; |
|||
return BROWSER_SUPPORTS_RSA; |
|||
}).catch(function () { |
|||
console.warn("[crypto] RSA is NOT fully supported"); |
|||
BROWSER_SUPPORTS_RSA = false; |
|||
return BROWSER_SUPPORTS_RSA; |
|||
}); |
|||
} |
|||
|
|||
function getServerKeypair() { |
|||
var sortedAltnames = info.identifiers.map(function (ident) { return ident.value; }).sort().join(','); |
|||
var serverJwk = JSON.parse(localStorage.getItem('server:' + sortedAltnames) || 'null'); |
|||
if (serverJwk) { |
|||
return PromiseA.resolve(serverJwk); |
|||
} |
|||
|
|||
var keypairOpts; |
|||
// TODO allow for user preference
|
|||
if (BROWSER_SUPPORTS_RSA) { |
|||
keypairOpts = RSA_OPTS; |
|||
} else { |
|||
keypairOpts = ECDSA_OPTS; |
|||
} |
|||
|
|||
return Keypairs.generate(RSA_OPTS).catch(function (err) { |
|||
console.error("[Error] Keypairs.generate(" + JSON.stringify(RSA_OPTS) + "):"); |
|||
throw err; |
|||
}).then(function (pair) { |
|||
localStorage.setItem('server:'+sortedAltnames, JSON.stringify(pair.private)); |
|||
return pair.private; |
|||
}); |
|||
} |
|||
|
|||
function getAccountKeypair(email) { |
|||
var json = localStorage.getItem('account:'+email); |
|||
if (json) { |
|||
return Promise.resolve(JSON.parse(json)); |
|||
} |
|||
|
|||
return Keypairs.generate(ECDSA_OPTS).catch(function (err) { |
|||
console.warn("[Error] Keypairs.generate(" + JSON.stringify(ECDSA_OPTS) + "):\n", err); |
|||
return Keypairs.generate(RSA_OPTS).catch(function (err) { |
|||
console.error("[Error] Keypairs.generate(" + JSON.stringify(RSA_OPTS) + "):"); |
|||
throw err; |
|||
}); |
|||
}).then(function (pair) { |
|||
localStorage.setItem('account:'+email, JSON.stringify(pair.private)); |
|||
return pair.private; |
|||
}); |
|||
} |
|||
|
|||
function updateChallengeType() { |
|||
/*jshint validthis: true*/ |
|||
var input = this || Array.prototype.filter.call( |
|||
$qsa('.js-acme-challenge-type'), function ($el) { return $el.checked; } |
|||
)[0]; |
|||
console.log('ch type radio:', input.value); |
|||
$qs('.js-acme-verification-wildcard').hidden = true; |
|||
$qs('.js-acme-verification-http-01').hidden = true; |
|||
$qs('.js-acme-verification-dns-01').hidden = true; |
|||
if (info.challenges.wildcard) { |
|||
$qs('.js-acme-verification-wildcard').hidden = false; |
|||
} |
|||
if (info.challenges[input.value]) { |
|||
$qs('.js-acme-verification-' + input.value).hidden = false; |
|||
} |
|||
} |
|||
|
|||
function saveContact(email, domains) { |
|||
// to be used for good, not evil
|
|||
return window.fetch('https://api.rootprojects.org/api/rootprojects.org/public/community', { |
|||
method: 'POST' |
|||
, cors: true |
|||
, headers: new Headers({ 'Content-Type': 'application/json' }) |
|||
, body: JSON.stringify({ |
|||
address: email |
|||
, project: 'greenlock-domains@rootprojects.org' |
|||
, timezone: new Intl.DateTimeFormat().resolvedOptions().timeZone |
|||
, domain: domains.join(',') |
|||
}) |
|||
}).catch(function (err) { |
|||
console.error(err); |
|||
}); |
|||
} |
|||
|
|||
steps[1] = function () { |
|||
updateProgress(0); |
|||
hideForms(); |
|||
$qs('.js-acme-form-domains').hidden = false; |
|||
}; |
|||
steps[1].submit = function () { |
|||
info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) { |
|||
return { type: 'dns', value: hostname.toLowerCase().trim() }; |
|||
}).slice(0,1); //Disable multiple values for now. We'll just take the first and work with it.
|
|||
info.identifiers.sort(function (a, b) { |
|||
if (a === b) { return 0; } |
|||
if (a < b) { return 1; } |
|||
if (a > b) { return -1; } |
|||
}); |
|||
|
|||
var acmeDirectoryUrl = $qs('.js-acme-directory-url').value; |
|||
acme = ACME.create({ Keypairs: Keypairs, CSR: CSR }); |
|||
return acme.init(acmeDirectoryUrl).then(function (directory) { |
|||
$qs('.js-acme-tos-url').href = directory.meta.termsOfService; |
|||
console.log("MAGIC STEP NUMBER in 1 is:", i); |
|||
steps[i](); |
|||
}); |
|||
}; |
|||
|
|||
steps[2] = function () { |
|||
updateProgress(0); |
|||
hideForms(); |
|||
$qs('.js-acme-form-account').hidden = false; |
|||
}; |
|||
steps[2].submit = function () { |
|||
var email = $qs('.js-acme-account-email').value.toLowerCase().trim(); |
|||
|
|||
info.contact = [ 'mailto:' + email ]; |
|||
info.agree = $qs('.js-acme-account-tos').checked; |
|||
//info.greenlockAgree = $qs('.js-gl-tos').checked;
|
|||
|
|||
// TODO ping with version and account creation
|
|||
setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; })); |
|||
|
|||
function checkTos(tos) { |
|||
if (info.agree) { |
|||
return tos; |
|||
} else { |
|||
return ''; |
|||
} |
|||
} |
|||
|
|||
return getAccountKeypair(email).then(function (jwk) { |
|||
// TODO save account id rather than always retrieving it?
|
|||
return acme.accounts.create({ |
|||
email: email |
|||
, agreeToTerms: checkTos |
|||
, accountKeypair: { privateKeyJwk: jwk } |
|||
}).then(function (account) { |
|||
console.log("account created result:", account); |
|||
accountStuff.account = account; |
|||
accountStuff.privateJwk = jwk; |
|||
accountStuff.email = email; |
|||
accountStuff.acme = acme; // TODO XXX remove
|
|||
}).catch(function (err) { |
|||
console.error("A bad thing happened:"); |
|||
console.error(err); |
|||
window.alert(err.message || JSON.stringify(err, null, 2)); |
|||
return new Promise(function () { |
|||
// stop the process cold
|
|||
console.warn('TODO: resume at ask email?'); |
|||
}); |
|||
}); |
|||
}).then(function () { |
|||
var jwk = accountStuff.privateJwk; |
|||
var account = accountStuff.account; |
|||
|
|||
return acme.orders.create({ |
|||
account: account |
|||
, accountKeypair: { privateKeyJwk: jwk } |
|||
, identifiers: info.identifiers |
|||
}).then(function (order) { |
|||
return acme.orders.create({ |
|||
signedOrder: signedOrder |
|||
}).then(function (order) { |
|||
accountStuff.order = order; |
|||
var claims = order.challenges; |
|||
console.log('claims:'); |
|||
console.log(claims); |
|||
|
|||
var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] }; |
|||
info.challenges = obj; |
|||
var map = { |
|||
'http-01': '.js-acme-verification-http-01' |
|||
, 'dns-01': '.js-acme-verification-dns-01' |
|||
, 'wildcard': '.js-acme-verification-wildcard' |
|||
}; |
|||
options.challengePriority = [ 'http-01', 'dns-01' ]; |
|||
|
|||
// TODO make Promise-friendly
|
|||
return PromiseA.all(claims.map(function (claim) { |
|||
var hostname = claim.identifier.value; |
|||
return PromiseA.all(claim.challenges.map(function (c) { |
|||
var keyAuth = BACME.challenges['http-01']({ |
|||
token: c.token |
|||
, thumbprint: thumbprint |
|||
, challengeDomain: hostname |
|||
}); |
|||
return BACME.challenges['dns-01']({ |
|||
keyAuth: keyAuth.value |
|||
, challengeDomain: hostname |
|||
}).then(function (dnsAuth) { |
|||
var data = { |
|||
type: c.type |
|||
, hostname: hostname |
|||
, url: c.url |
|||
, token: c.token |
|||
, keyAuthorization: keyAuth |
|||
, httpPath: keyAuth.path |
|||
, httpAuth: keyAuth.value |
|||
, dnsType: dnsAuth.type |
|||
, dnsHost: dnsAuth.host |
|||
, dnsAnswer: dnsAuth.answer |
|||
}; |
|||
|
|||
console.log(''); |
|||
console.log('CHALLENGE'); |
|||
console.log(claim); |
|||
console.log(c); |
|||
console.log(data); |
|||
console.log(''); |
|||
|
|||
if (claim.wildcard) { |
|||
obj.wildcard.push(data); |
|||
let verification = $qs(".js-acme-verification-wildcard"); |
|||
verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname; |
|||
verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; |
|||
verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer; |
|||
|
|||
} else if(obj[data.type]) { |
|||
|
|||
obj[data.type].push(data); |
|||
|
|||
if ('dns-01' === data.type) { |
|||
let verification = $qs(".js-acme-verification-dns-01"); |
|||
verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname; |
|||
verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; |
|||
verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer; |
|||
} else if ('http-01' === data.type) { |
|||
$qs(".js-acme-ver-file-location").innerHTML = data.httpPath.split("/").slice(-1); |
|||
$qs(".js-acme-ver-content").innerHTML = data.httpAuth; |
|||
$qs(".js-acme-ver-uri").innerHTML = data.httpPath; |
|||
$qs(".js-download-verify-link").href = |
|||
"data:text/octet-stream;base64," + window.btoa(data.httpAuth); |
|||
$qs(".js-download-verify-link").download = data.httpPath.split("/").slice(-1); |
|||
} |
|||
} |
|||
|
|||
}); |
|||
|
|||
})); |
|||
})).then(function () { |
|||
|
|||
// hide wildcard if no wildcard
|
|||
// hide http-01 and dns-01 if only wildcard
|
|||
if (!obj.wildcard.length) { |
|||
$qs('.js-acme-wildcard-challenges').hidden = true; |
|||
} |
|||
if (!obj['http-01'].length) { |
|||
$qs('.js-acme-challenges').hidden = true; |
|||
} |
|||
|
|||
updateChallengeType(); |
|||
|
|||
console.log("MAGIC STEP NUMBER in 2 is:", i); |
|||
steps[i](); |
|||
}); |
|||
|
|||
}); |
|||
}); |
|||
}).catch(function (err) { |
|||
console.error('Step \'\' Error:'); |
|||
console.error(err, err.stack); |
|||
window.alert("An error happened at Step " + i + ", but it's not your fault. Email aj@rootprojects.org and let him know."); |
|||
}); |
|||
}; |
|||
|
|||
steps[3] = function () { |
|||
updateProgress(1); |
|||
hideForms(); |
|||
$qs('.js-acme-form-challenges').hidden = false; |
|||
}; |
|||
steps[3].submit = function () { |
|||
options.challengeTypes = [ 'dns-01' ]; |
|||
if ('http-01' === $qs('.js-acme-challenge-type:checked').value) { |
|||
options.challengeTypes.unshift('http-01'); |
|||
} |
|||
console.log('primary challenge type is:', options.challengeTypes[0]); |
|||
|
|||
return getAccountKeypair(email).then(function (jwk) { |
|||
// for now just show the next page immediately (its a spinner)
|
|||
// TODO put a test challenge in the list
|
|||
// TODO warn about wait-time if DNS
|
|||
steps[i](); |
|||
return getServerKeypair().then(function () { |
|||
return acme.orders.finalize({ |
|||
account: accountStuff.account |
|||
, accountKeypair: { privateKeyJwk: jwk } |
|||
, order: accountStuff.order |
|||
, domainKeypair: 'TODO' |
|||
}); |
|||
}).then(function (certs) { |
|||
console.log('WINNING!'); |
|||
console.log(certs); |
|||
$qs('#js-fullchain').innerHTML = certs; |
|||
$qs("#js-download-fullchain-link").href = |
|||
"data:text/octet-stream;base64," + window.btoa(certs); |
|||
|
|||
var wcOpts; |
|||
var pemName; |
|||
if (/^R/.test(info.serverJwk.kty)) { |
|||
pemName = 'RSA'; |
|||
wcOpts = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } }; |
|||
} else { |
|||
pemName = 'EC'; |
|||
wcOpts = { name: "ECDSA", namedCurve: "P-256" }; |
|||
} |
|||
return crypto.subtle.importKey( |
|||
"jwk" |
|||
, info.serverJwk |
|||
, wcOpts |
|||
, true |
|||
, ["sign"] |
|||
).then(function (privateKey) { |
|||
return window.crypto.subtle.exportKey("pkcs8", privateKey); |
|||
}).then (function (keydata) { |
|||
var pem = spkiToPEM(keydata, pemName); |
|||
$qs('#js-privkey').innerHTML = pem; |
|||
$qs("#js-download-privkey-link").href = |
|||
"data:text/octet-stream;base64," + window.btoa(pem); |
|||
steps[i](); |
|||
}); |
|||
}); |
|||
}).then(function () { |
|||
return submitForm(); |
|||
}); |
|||
}; |
|||
|
|||
// spinner
|
|||
steps[4] = function () { |
|||
updateProgress(1); |
|||
hideForms(); |
|||
$qs('.js-acme-form-poll').hidden = false; |
|||
}; |
|||
steps[4].submit = function () { |
|||
console.log('Congrats! Auto advancing...'); |
|||
|
|||
|
|||
}).catch(function (err) { |
|||
console.error(err.toString()); |
|||
window.alert("An error happened in the final step, but it's not your fault. Email aj@rootprojects.org and let him know."); |
|||
}); |
|||
}; |
|||
|
|||
steps[5] = function () { |
|||
updateProgress(2); |
|||
hideForms(); |
|||
$qs('.js-acme-form-download').hidden = false; |
|||
}; |
|||
steps[1](); |
|||
|
|||
var params = new URLSearchParams(window.location.search); |
|||
var apiType = params.get('acme-api-type') || "staging-v02"; |
|||
|
|||
$qsa('.js-acme-api-type').forEach(function ($el) { |
|||
$el.addEventListener('change', updateApiType); |
|||
}); |
|||
|
|||
updateApiType(); |
|||
|
|||
$qsa('.js-acme-form').forEach(function ($el) { |
|||
$el.addEventListener('submit', function (ev) { |
|||
ev.preventDefault(); |
|||
submitForm(ev); |
|||
}); |
|||
}); |
|||
|
|||
|
|||
$qsa('.js-acme-challenge-type').forEach(function ($el) { |
|||
$el.addEventListener('change', updateChallengeType); |
|||
}); |
|||
|
|||
if(params.has('acme-domains')) { |
|||
console.log("acme-domains param: ", params.get('acme-domains')); |
|||
$qs('.js-acme-domains').value = params.get('acme-domains'); |
|||
|
|||
$qsa('.js-acme-api-type').forEach(function(ele) { |
|||
if(ele.value === apiType) { |
|||
ele.checked = true; |
|||
} |
|||
}); |
|||
|
|||
updateApiType(); |
|||
steps[2](); |
|||
submitForm(); |
|||
} |
|||
|
|||
$qs('body').hidden = false; |
|||
|
|||
return testKeypairSupport().then(function (rsaSupport) { |
|||
if (rsaSupport) { |
|||
return true; |
|||
} |
|||
|
|||
return testRsaSupport().then(function () { |
|||
console.info('[crypto] RSA is supported'); |
|||
}).catch(function (err) { |
|||
console.error('[crypto] could not use either RSA nor EC.'); |
|||
console.error(err); |
|||
window.alert("Generating secure certificates requires a browser with cryptography support." |
|||
+ "Please consider a recent version of Chrome, Firefox, or Safari."); |
|||
throw err; |
|||
}); |
|||
}); |
|||
}()); |
Loading…
Reference in new issue