mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2024-11-26 02:57:04 +00:00
second part of provisioning tls certificates from the control panel
This commit is contained in:
parent
812ef024ef
commit
2882e63dd8
@ -331,13 +331,27 @@ def dns_get_dump():
|
|||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def ssl_get_status():
|
def ssl_get_status():
|
||||||
from ssl_certificates import get_certificates_to_provision
|
from ssl_certificates import get_certificates_to_provision
|
||||||
from web_update import get_web_domains_info
|
from web_update import get_web_domains_info, get_web_domains
|
||||||
provision, cant_provision = get_certificates_to_provision(env, ok_as_problem=False)
|
|
||||||
|
# What domains can we provision certificates for? What unexpected problems do we have?
|
||||||
|
provision, cant_provision = get_certificates_to_provision(env, show_extended_problems=False)
|
||||||
|
|
||||||
|
# What's the current status of TLS certificates on all of the domain?
|
||||||
domains_status = get_web_domains_info(env)
|
domains_status = get_web_domains_info(env)
|
||||||
|
domains_status = [{ "domain": d["domain"], "status": d["ssl_certificate"][0], "text": d["ssl_certificate"][1] } for d in domains_status ]
|
||||||
|
|
||||||
|
# Warn the user about domain names not hosted here because of other settings.
|
||||||
|
for domain in set(get_web_domains(env, exclude_dns_elsewhere=False)) - set(get_web_domains(env)):
|
||||||
|
domains_status.append({
|
||||||
|
"domain": domain,
|
||||||
|
"status": "not-applicable",
|
||||||
|
"text": "The domain's website is hosted elsewhere.",
|
||||||
|
})
|
||||||
|
|
||||||
return json_response({
|
return json_response({
|
||||||
"can_provision": utils.sort_domains(provision, env),
|
"can_provision": utils.sort_domains(provision, env),
|
||||||
"cant_provision": [{ "domain": domain, "problem": cant_provision[domain] } for domain in utils.sort_domains(cant_provision, env) ],
|
"cant_provision": [{ "domain": domain, "problem": cant_provision[domain] } for domain in utils.sort_domains(cant_provision, env) ],
|
||||||
"status": [{ "domain": d["domain"], "status": d["ssl_certificate"][0], "text": d["ssl_certificate"][1] } for d in domains_status ],
|
"status": domains_status,
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/ssl/csr/<domain>', methods=['POST'])
|
@app.route('/ssl/csr/<domain>', methods=['POST'])
|
||||||
@ -359,6 +373,17 @@ def ssl_install_cert():
|
|||||||
return "Invalid domain name."
|
return "Invalid domain name."
|
||||||
return install_cert(domain, ssl_cert, ssl_chain, env)
|
return install_cert(domain, ssl_cert, ssl_chain, env)
|
||||||
|
|
||||||
|
@app.route('/ssl/provision', methods=['POST'])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def ssl_provision_certs():
|
||||||
|
from ssl_certificates import provision_certificates
|
||||||
|
agree_to_tos_url = request.form.get('agree_to_tos_url')
|
||||||
|
status = provision_certificates(env,
|
||||||
|
agree_to_tos_url=agree_to_tos_url,
|
||||||
|
jsonable=True)
|
||||||
|
return json_response(status)
|
||||||
|
|
||||||
|
|
||||||
# WEB
|
# WEB
|
||||||
|
|
||||||
@app.route('/web/domains')
|
@app.route('/web/domains')
|
||||||
|
@ -156,7 +156,7 @@ def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False
|
|||||||
|
|
||||||
# PROVISIONING CERTIFICATES FROM LETSENCRYPT
|
# PROVISIONING CERTIFICATES FROM LETSENCRYPT
|
||||||
|
|
||||||
def get_certificates_to_provision(env, ok_as_problem=True, force_domains=None):
|
def get_certificates_to_provision(env, show_extended_problems=True, force_domains=None):
|
||||||
# Get a set of domain names that we should now provision certificates
|
# Get a set of domain names that we should now provision certificates
|
||||||
# for. Provision if a domain name has no valid certificate or if any
|
# for. Provision if a domain name has no valid certificate or if any
|
||||||
# certificate is expiring in 14 days. If provisioning anything, also
|
# certificate is expiring in 14 days. If provisioning anything, also
|
||||||
@ -204,13 +204,13 @@ def get_certificates_to_provision(env, ok_as_problem=True, force_domains=None):
|
|||||||
domains_if_any.add(domain)
|
domains_if_any.add(domain)
|
||||||
|
|
||||||
# It's valid. Should we report its validness?
|
# It's valid. Should we report its validness?
|
||||||
if ok_as_problem:
|
if show_extended_problems:
|
||||||
problems[domain] = "The certificate is valid for at least another 30 days --- no need to replace."
|
problems[domain] = "The certificate is valid for at least another 30 days --- no need to replace."
|
||||||
|
|
||||||
# Warn the user about domains hosted elsewhere.
|
# Warn the user about domains hosted elsewhere.
|
||||||
if force_domains is None:
|
if not force_domains and show_extended_problems:
|
||||||
for domain in set(get_web_domains(env, exclude_dns_elsewhere=False)) - set(get_web_domains(env)):
|
for domain in set(get_web_domains(env, exclude_dns_elsewhere=False)) - set(get_web_domains(env)):
|
||||||
problems[domain] = "The domain's DNS is pointed elsewhere, so a TLS certificate is not necessary here and cannot be provisioned automatically anyway."
|
problems[domain] = "The domain's DNS is pointed elsewhere, so there is no point to installing a TLS certificate here and we could not automatically provision one anyway because provisioning requires access to the website (which isn't here)."
|
||||||
|
|
||||||
# Filter out domains that we can't provision a certificate for.
|
# Filter out domains that we can't provision a certificate for.
|
||||||
def can_provision_for_domain(domain):
|
def can_provision_for_domain(domain):
|
||||||
@ -253,7 +253,7 @@ def get_certificates_to_provision(env, ok_as_problem=True, force_domains=None):
|
|||||||
|
|
||||||
return (domains, problems)
|
return (domains, problems)
|
||||||
|
|
||||||
def provision_certificates(env, agree_to_tos_url=None, logger=None, force_domains=None):
|
def provision_certificates(env, agree_to_tos_url=None, logger=None, force_domains=None, jsonable=False):
|
||||||
import requests.exceptions
|
import requests.exceptions
|
||||||
import acme.messages
|
import acme.messages
|
||||||
|
|
||||||
@ -324,7 +324,6 @@ def provision_certificates(env, agree_to_tos_url=None, logger=None, force_domain
|
|||||||
|
|
||||||
except client.NeedToTakeAction as e:
|
except client.NeedToTakeAction as e:
|
||||||
# Write out the ACME challenge files.
|
# Write out the ACME challenge files.
|
||||||
|
|
||||||
for action in e.actions:
|
for action in e.actions:
|
||||||
if isinstance(action, client.NeedToInstallFile):
|
if isinstance(action, client.NeedToInstallFile):
|
||||||
fn = os.path.join(challenges_path, action.file_name)
|
fn = os.path.join(challenges_path, action.file_name)
|
||||||
@ -355,7 +354,7 @@ def provision_certificates(env, agree_to_tos_url=None, logger=None, force_domain
|
|||||||
import time, datetime
|
import time, datetime
|
||||||
ret_item.update({
|
ret_item.update({
|
||||||
"result": "wait",
|
"result": "wait",
|
||||||
"until": e.until_when, #.isoformat(),
|
"until": e.until_when if not jsonable else e.until_when.isoformat(),
|
||||||
"seconds": (e.until_when - datetime.datetime.now()).total_seconds()
|
"seconds": (e.until_when - datetime.datetime.now()).total_seconds()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -7,13 +7,22 @@
|
|||||||
|
|
||||||
<p>You need a TLS certificate for this box’s hostname ({{hostname}}) and every other domain name and subdomain that this box is hosting a website for (see the list below).</p>
|
<p>You need a TLS certificate for this box’s hostname ({{hostname}}) and every other domain name and subdomain that this box is hosting a website for (see the list below).</p>
|
||||||
|
|
||||||
|
<div id="ssl_provision">
|
||||||
<h3>Provision a Certificate</h3>
|
<h3>Provision a Certificate</h3>
|
||||||
|
|
||||||
<p>We can provision an SSL certificate for you from <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>, a free SSL certificate provider.</p>
|
<div id="ssl_provision_p" style="display: none; margin-top: 1.5em">
|
||||||
|
<button onclick='return provision_tls_cert();' class='btn btn-primary' style="float: left; margin: 0 1.5em 1em 0;">Provision</button>
|
||||||
|
<p>A TLS certificate can be automatically provisioned from <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>, a free TLS certificate provider, for:<br>
|
||||||
|
<span class="text-primary"></span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p id="ssl_provision_status"></p>
|
<div class="clearfix"> </div>
|
||||||
|
|
||||||
<table id="ssl_provision_problems" style="display: none" class="table">
|
<div id="ssl_provision_result"></div>
|
||||||
|
|
||||||
|
<div id="ssl_provision_problems_div" style="display: none;">
|
||||||
|
<p style="margin-bottom: .5em;">Certificates cannot be automatically provisioned for:</p>
|
||||||
|
<table id="ssl_provision_problems" style="margin-top: 0;" class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Domain</th>
|
<th>Domain</th>
|
||||||
@ -23,9 +32,14 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<p>Use the <em>Install Certificate</em> button below for these domains.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Certificate Status</h3>
|
<h3>Certificate Status</h3>
|
||||||
|
|
||||||
|
<p style="margin-top: 1.5em">Certificates expire after a period of time. All certificates will be automatically renewed through <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a> 14 days prior to expiration.</p>
|
||||||
|
|
||||||
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto; display: none">
|
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto; display: none">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -38,7 +52,6 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p>A multi-domain or wildcard certificate will be automatically applied to any domains it is valid for.</p>
|
|
||||||
|
|
||||||
<h3 id="ssl_install_header">Install Certificate</h3>
|
<h3 id="ssl_install_header">Install Certificate</h3>
|
||||||
|
|
||||||
@ -48,6 +61,8 @@
|
|||||||
|
|
||||||
<p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p>
|
<p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p>
|
||||||
|
|
||||||
|
<p>(A multi-domain or wildcard certificate will be automatically applied to any domains it is valid for besides the one you choose above.)</p>
|
||||||
|
|
||||||
<p>What country are you in? This is required by some TLS certificate providers. You may leave this blank if you know your TLS certificate provider doesn't require it.</p>
|
<p>What country are you in? This is required by some TLS certificate providers. You may leave this blank if you know your TLS certificate provider doesn't require it.</p>
|
||||||
|
|
||||||
<p><select id="sslcc" onchange="show_csr()" class="form-control" style="width: auto">
|
<p><select id="sslcc" onchange="show_csr()" class="form-control" style="width: auto">
|
||||||
@ -78,7 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function show_tls() {
|
function show_tls(keep_provisioning_shown) {
|
||||||
api(
|
api(
|
||||||
"/ssl/status",
|
"/ssl/status",
|
||||||
"GET",
|
"GET",
|
||||||
@ -86,20 +101,15 @@ function show_tls() {
|
|||||||
},
|
},
|
||||||
function(res) {
|
function(res) {
|
||||||
// provisioning status
|
// provisioning status
|
||||||
if (res.can_provision.length > 0) {
|
|
||||||
$('#ssl_provision_status')
|
if (!keep_provisioning_shown)
|
||||||
.removeClass("text-warning").removeClass("text-success").addClass("text-danger")
|
$('#ssl_provision').toggle(res.can_provision.length + res.cant_provision.length > 0)
|
||||||
.text("Domains: " + res.can_provision.join(", "));
|
|
||||||
} else if (res.cant_provision.length == 0) {
|
$('#ssl_provision_p').toggle(res.can_provision.length > 0);
|
||||||
$('#ssl_provision_status')
|
if (res.can_provision.length > 0)
|
||||||
.addClass("text-success").removeClass("text-warning").removeClass("text-danger")
|
$('#ssl_provision_p span').text(res.can_provision.join(", "));
|
||||||
.text("No domains hosted on this box need a new TLS certificate at this time.");
|
|
||||||
} else {
|
$('#ssl_provision_problems_div').toggle(res.cant_provision.length > 0);
|
||||||
$('#ssl_provision_status')
|
|
||||||
.removeClass("text-success").addClass("text-warning").removeClass("text-danger")
|
|
||||||
.text("No TLS certificates can be provisoned at this time:");
|
|
||||||
}
|
|
||||||
$('#ssl_provision_problems').toggle(res.cant_provision.length > 0);
|
|
||||||
$('#ssl_provision_problems tbody').text("");
|
$('#ssl_provision_problems tbody').text("");
|
||||||
for (var i = 0; i < res.cant_provision.length; i++) {
|
for (var i = 0; i < res.cant_provision.length; i++) {
|
||||||
var domain = res.cant_provision[i];
|
var domain = res.cant_provision[i];
|
||||||
@ -123,6 +133,10 @@ function show_tls() {
|
|||||||
row.attr('data-domain', domains[i].domain);
|
row.attr('data-domain', domains[i].domain);
|
||||||
row.find('.domain a').text(domains[i].domain);
|
row.find('.domain a').text(domains[i].domain);
|
||||||
row.find('.domain a').attr('href', 'https://' + domains[i].domain);
|
row.find('.domain a').attr('href', 'https://' + domains[i].domain);
|
||||||
|
if (domains[i].status == "not-applicable") {
|
||||||
|
domains[i].status = "muted"; // text-muted css class
|
||||||
|
row.find('.actions a').remove(); // no actions applicable
|
||||||
|
}
|
||||||
row.addClass("text-" + domains[i].status);
|
row.addClass("text-" + domains[i].status);
|
||||||
row.find('.status').text(domains[i].text);
|
row.find('.status').text(domains[i].text);
|
||||||
if (domains[i].status == "success") {
|
if (domains[i].status == "success") {
|
||||||
@ -139,14 +153,15 @@ function show_tls() {
|
|||||||
function ssl_install(elem) {
|
function ssl_install(elem) {
|
||||||
var domain = $(elem).parents('tr').attr('data-domain');
|
var domain = $(elem).parents('tr').attr('data-domain');
|
||||||
$('#ssldomain').val(domain);
|
$('#ssldomain').val(domain);
|
||||||
$('#csr_info').slideDown();
|
|
||||||
$('#ssl_csr').text('Loading...');
|
|
||||||
show_csr();
|
show_csr();
|
||||||
$('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top - $('.navbar-fixed-top').height() - 20 })
|
$('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top - $('.navbar-fixed-top').height() - 20 })
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function show_csr() {
|
function show_csr() {
|
||||||
|
if ($('#ssldomain').val() == "") return;
|
||||||
|
$('#csr_info').slideDown();
|
||||||
|
$('#ssl_csr').text('Loading...');
|
||||||
api(
|
api(
|
||||||
"/ssl/csr/" + $('#ssldomain').val(),
|
"/ssl/csr/" + $('#ssldomain').val(),
|
||||||
"POST",
|
"POST",
|
||||||
@ -176,4 +191,94 @@ function install_cert() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var agree_to_tos_url_prompt = null;
|
||||||
|
var agree_to_tos_url = null;
|
||||||
|
function provision_tls_cert() {
|
||||||
|
// Automatically provision any certs.
|
||||||
|
$('#ssl_provision_p .btn').attr('disabled', '1'); // prevent double-clicks
|
||||||
|
api(
|
||||||
|
"/ssl/provision",
|
||||||
|
"POST",
|
||||||
|
{
|
||||||
|
agree_to_tos_url: agree_to_tos_url
|
||||||
|
},
|
||||||
|
function(status) {
|
||||||
|
// Clear last attempt.
|
||||||
|
agree_to_tos_url = null;
|
||||||
|
$('#ssl_provision_result').text("");
|
||||||
|
may_reenable_provision_button = true;
|
||||||
|
|
||||||
|
// Nothing was done. There might also be problem domains, but we've already displayed those.
|
||||||
|
if (status.requests.length == 0) {
|
||||||
|
show_modal_error("TLS Certificate Provisioning", "There were no domain names to provision certificates for.");
|
||||||
|
// don't return - haven't re-enabled the provision button
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each provisioning API call returns zero or more "requests" which represent
|
||||||
|
// a request to Let's Encrypt for a single certificate. Normally there is just
|
||||||
|
// one request (for a single multi-domain certificate).
|
||||||
|
for (var i = 0; i < status.requests.length; i++) {
|
||||||
|
var r = status.requests[i];
|
||||||
|
|
||||||
|
// create an HTML block to display the results of this request
|
||||||
|
var n = $("<div><h4/><p/></div>");
|
||||||
|
$('#ssl_provision_result').append(n);
|
||||||
|
|
||||||
|
// show a header only to disambiguate request blocks
|
||||||
|
if (status.requests.length > 0)
|
||||||
|
n.find("h4").text(r.domains.join(", "));
|
||||||
|
|
||||||
|
if (r.result == "agree-to-tos") {
|
||||||
|
// user needs to agree to Let's Encrypt's TOS
|
||||||
|
agree_to_tos_url_prompt = r.url;
|
||||||
|
$('#ssl_provision_p .btn').attr('disabled', '1');
|
||||||
|
n.find("p").html("Please open and review <a href='" + r.url + "' target='_blank'>Let's Encrypt's terms of service agreement</a>. You must agree to their terms for a certificate to be automatically provisioned from them.");
|
||||||
|
n.append($('<button onclick="agree_to_tos_url = agree_to_tos_url_prompt; return provision_tls_cert();" class="btn btn-success" style="margin-left: 2em">Agree & Try Again</button>'));
|
||||||
|
|
||||||
|
// don't re-enable the Provision button -- user must use the Agree button
|
||||||
|
may_reenable_provision_button = false;
|
||||||
|
|
||||||
|
} else if (r.result == "error") {
|
||||||
|
n.find("p").addClass("text-danger").text(r.message);
|
||||||
|
|
||||||
|
} else if (r.result == "wait") {
|
||||||
|
// Show a button that counts down to zero, at which point it becomes enabled.
|
||||||
|
n.find("p").text("A certificate is now in the process of being provisioned, but it takes some time. Please wait until the Finish button is enabled, and then click it to acquire the certificate.");
|
||||||
|
var b = $('<button onclick="return provision_tls_cert();" class="btn btn-success" style="margin-left: 2em">Finish</button>');
|
||||||
|
b.attr("disabled", "1");
|
||||||
|
var now = new Date();
|
||||||
|
n.append(b);
|
||||||
|
function ready_to_finish() {
|
||||||
|
var remaining = r.seconds - Math.round((new Date() - now)/1000);
|
||||||
|
if (remaining > 0) {
|
||||||
|
setTimeout(ready_to_finish, 1000);
|
||||||
|
b.text("Finish (" + remaining + "...)")
|
||||||
|
} else {
|
||||||
|
b.text("Finish (ready)")
|
||||||
|
b.removeAttr("disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ready_to_finish();
|
||||||
|
|
||||||
|
// don't re-enable the Provision button -- user must use the Retry button when it becomes enabled
|
||||||
|
may_reenable_provision_button = false;
|
||||||
|
|
||||||
|
} else if (r.result == "installed") {
|
||||||
|
n.find("p").addClass("text-success").text("The TLS certificate was provisioned and installed.");
|
||||||
|
setTimeout("show_tls(true)", 1); // update main table of certificate statuses, call with arg keep_provisioning_shown true so that we don't clear what we just outputted
|
||||||
|
}
|
||||||
|
|
||||||
|
// display the detailed log info in case of problems
|
||||||
|
var trace = $("<div class='small text-muted' style='margin-top: 1.5em'>Log:</div>");
|
||||||
|
n.append(trace);
|
||||||
|
for (var j = 0; j < r.log.length; j++)
|
||||||
|
trace.append($("<div/>").text(r.log[j]));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (may_reenable_provision_button)
|
||||||
|
$('#ssl_provision_p .btn').removeAttr("disabled");
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user