mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2024-11-22 02:17:26 +00:00
don't try to get certs for IDNA domains and report all reasons for not fetching a certificate
fixes #646
This commit is contained in:
parent
b6933a73fa
commit
b1b57f9bfd
@ -5,6 +5,8 @@ import os, os.path, re, shutil
|
|||||||
|
|
||||||
from utils import shell, safe_domain_name, sort_domains
|
from utils import shell, safe_domain_name, sort_domains
|
||||||
|
|
||||||
|
import idna
|
||||||
|
|
||||||
# SELECTING SSL CERTIFICATES FOR USE IN WEB
|
# SELECTING SSL CERTIFICATES FOR USE IN WEB
|
||||||
|
|
||||||
def get_ssl_certificates(env):
|
def get_ssl_certificates(env):
|
||||||
@ -155,7 +157,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):
|
def get_certificates_to_provision(env):
|
||||||
# Get a list 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
|
||||||
# provision certificates expiring within 30 days. The period between
|
# provision certificates expiring within 30 days. The period between
|
||||||
@ -171,11 +173,13 @@ def get_certificates_to_provision(env):
|
|||||||
certs = get_ssl_certificates(env)
|
certs = get_ssl_certificates(env)
|
||||||
domains = set()
|
domains = set()
|
||||||
domains_if_any = set()
|
domains_if_any = set()
|
||||||
|
problems = { }
|
||||||
for domain in get_web_domains(env):
|
for domain in get_web_domains(env):
|
||||||
try:
|
try:
|
||||||
cert = get_domain_ssl_files(domain, certs, env, allow_missing_cert=True)
|
cert = get_domain_ssl_files(domain, certs, env, allow_missing_cert=True)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError as e:
|
||||||
# system certificate is not present
|
# system certificate is not present
|
||||||
|
problems[domain] = "Error: " + str(e)
|
||||||
continue
|
continue
|
||||||
if cert is None:
|
if cert is None:
|
||||||
# No valid certificate available.
|
# No valid certificate available.
|
||||||
@ -192,37 +196,49 @@ def get_certificates_to_provision(env):
|
|||||||
elif cert.not_valid_after-now < datetime.timedelta(days=30):
|
elif cert.not_valid_after-now < datetime.timedelta(days=30):
|
||||||
domains_if_any.add(domain)
|
domains_if_any.add(domain)
|
||||||
|
|
||||||
# Filter out domains that don't have correct DNS, because then the CA
|
# It's valid.
|
||||||
# won't be able to do DNS validation.
|
problems[domain] = "The certificate is valid for at least another 30 days --- no need to replace."
|
||||||
def is_domain_dns_correct(domain):
|
|
||||||
# Must make qname absolute to prevent a fall-back lookup with a
|
# Filter out domains that we can't provision a certificate for.
|
||||||
# search domain appended.
|
def can_provision_for_domain(domain):
|
||||||
import dns.resolver
|
# Let's Encrypt doesn't yet support IDNA domains.
|
||||||
try:
|
# We store domains in IDNA (ASCII). To see if this domain is IDNA,
|
||||||
response = dns.resolver.query(domain + ".", "A")
|
# we'll see if its IDNA-decoded form is different.
|
||||||
except:
|
if idna.decode(domain.encode("ascii")) != domain:
|
||||||
|
problems[domain] = "Let's Encrypt does not yet support provisioning certificates for internationalized domains."
|
||||||
return False
|
return False
|
||||||
return len(response) == 1 and str(response[0]) == env["PUBLIC_IP"]
|
|
||||||
domains = set(d for d in domains if is_domain_dns_correct(d))
|
# Does the domain resolve to this machine in public DNS? If not,
|
||||||
domains_if_any = set(d for d in domains_if_any if is_domain_dns_correct(d))
|
# we can't do domain control validation. For IPv6 is configured,
|
||||||
|
# make sure both IPv4 and IPv6 are correct because we don't know
|
||||||
|
# how Let's Encrypt will connect.
|
||||||
|
import dns.resolver
|
||||||
|
for rtype, value in [("A", env["PUBLIC_IP"]), ("AAAA", env.get("PUBLIC_IPV6"))]:
|
||||||
|
if not value: continue # IPv6 is not configured
|
||||||
|
try:
|
||||||
|
# Must make the qname absolute to prevent a fall-back lookup with a
|
||||||
|
# search domain appended, by adding a period to the end.
|
||||||
|
response = dns.resolver.query(domain + ".", rtype)
|
||||||
|
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
|
||||||
|
problems[domain] = "DNS isn't configured properly for this domain: DNS resolution failed (%s: %s)." % (rtype, str(e) or repr(e)) # NoAnswer's str is empty
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
problems[domain] = "DNS isn't configured properly for this domain: DNS lookup had an error: %s." % str(e)
|
||||||
|
return False
|
||||||
|
if len(response) != 1 or str(response[0]) != value:
|
||||||
|
problems[domain] = "Domain control validation cannot be performed for this domain because DNS points the domain to another machine (%s %s)." % (rtype, ", ".join(str(r) for r in response))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
domains = set(filter(can_provision_for_domain, domains))
|
||||||
|
|
||||||
# If there are any domains we definitely will provision for, add in
|
# If there are any domains we definitely will provision for, add in
|
||||||
# additional domains to do at this time.
|
# additional domains to do at this time.
|
||||||
if len(domains) > 0:
|
if len(domains) > 0:
|
||||||
domains |= domains_if_any
|
domains |= set(filter(can_provision_for_domain, domains_if_any))
|
||||||
|
|
||||||
# Sort, just to keep related domain names together in the next step.
|
return (domains, problems)
|
||||||
domains = sort_domains(domains, env)
|
|
||||||
|
|
||||||
# Break into groups of up to 100 certificates at a time, which is Let's Encrypt's
|
|
||||||
# limit for a single certificate.
|
|
||||||
cert_domains = []
|
|
||||||
while len(domains) > 0:
|
|
||||||
cert_domains.append( domains[0:100] )
|
|
||||||
domains = domains[100:]
|
|
||||||
|
|
||||||
# Return a list of lists of domain names.
|
|
||||||
return cert_domains
|
|
||||||
|
|
||||||
def provision_certificates(env, agree_to_tos_url=None, logger=None):
|
def provision_certificates(env, agree_to_tos_url=None, logger=None):
|
||||||
import requests.exceptions
|
import requests.exceptions
|
||||||
@ -230,14 +246,25 @@ def provision_certificates(env, agree_to_tos_url=None, logger=None):
|
|||||||
|
|
||||||
from free_tls_certificates import client
|
from free_tls_certificates import client
|
||||||
|
|
||||||
# What domains to provision certificates for? This is a list of
|
# What domains should we provision certificates for? And what
|
||||||
# lists of domains.
|
# errors prevent provisioning for other domains.
|
||||||
certs = get_certificates_to_provision(env)
|
domains, problems = get_certificates_to_provision(env)
|
||||||
if len(certs) == 0:
|
|
||||||
|
# Exit fast if there is nothing to do.
|
||||||
|
if len(domains) == 0:
|
||||||
return {
|
return {
|
||||||
"requests": [],
|
"requests": [],
|
||||||
|
"problems": problems,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Break into groups of up to 100 certificates at a time, which is Let's Encrypt's
|
||||||
|
# limit for a single certificate. We'll sort to put related domains together.
|
||||||
|
domains = sort_domains(domains, env)
|
||||||
|
certs = []
|
||||||
|
while len(domains) > 0:
|
||||||
|
certs.append( domains[0:100] )
|
||||||
|
domains = domains[100:]
|
||||||
|
|
||||||
# Prepare to provision.
|
# Prepare to provision.
|
||||||
|
|
||||||
# Where should we put our Let's Encrypt account info and state cache.
|
# Where should we put our Let's Encrypt account info and state cache.
|
||||||
@ -352,7 +379,8 @@ def provision_certificates(env, agree_to_tos_url=None, logger=None):
|
|||||||
|
|
||||||
# Return what happened with each certificate request.
|
# Return what happened with each certificate request.
|
||||||
return {
|
return {
|
||||||
"requests": ret
|
"requests": ret,
|
||||||
|
"problems": problems,
|
||||||
}
|
}
|
||||||
|
|
||||||
def provision_certificates_cmdline():
|
def provision_certificates_cmdline():
|
||||||
@ -377,7 +405,14 @@ def provision_certificates_cmdline():
|
|||||||
if not status["requests"]:
|
if not status["requests"]:
|
||||||
# No domains need certificates.
|
# No domains need certificates.
|
||||||
if "--headless" not in sys.argv or "-v" in sys.argv:
|
if "--headless" not in sys.argv or "-v" in sys.argv:
|
||||||
print("No domains hosted on this box need a certificate at this time.")
|
if len(status["problems"]) == 0:
|
||||||
|
print("No domains hosted on this box need a new TLS certificate at this time.")
|
||||||
|
elif len(status["problems"]) > 0:
|
||||||
|
print("No TLS certificates could be provisoned at this time:")
|
||||||
|
print()
|
||||||
|
for domain in sort_domains(status["problems"], env):
|
||||||
|
print("%s: %s" % (domain, status["problems"][domain]))
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# What happened?
|
# What happened?
|
||||||
@ -459,6 +494,12 @@ Do you agree to the agreement? Type Y or N and press <ENTER>: """
|
|||||||
# we're done for now.
|
# we're done for now.
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# And finally show the domains with problems.
|
||||||
|
if len(status["problems"]) > 0:
|
||||||
|
print("TLS certificates could not be provisoned for:")
|
||||||
|
for domain in sort_domains(status["problems"], env):
|
||||||
|
print("%s: %s" % (domain, status["problems"][domain]))
|
||||||
|
|
||||||
# INSTALLING A NEW CERTIFICATE FROM THE CONTROL PANEL
|
# INSTALLING A NEW CERTIFICATE FROM THE CONTROL PANEL
|
||||||
|
|
||||||
def create_csr(domain, ssl_key, country_code, env):
|
def create_csr(domain, ssl_key, country_code, env):
|
||||||
|
Loading…
Reference in New Issue
Block a user