def _check_domain_is_ready_for_ACME(domain): dnsrecords = Diagnoser.get_cached_report("dnsrecords", item={ "domain": domain, "category": "basic" }, warn_if_no_cache=False) or {} httpreachable = Diagnoser.get_cached_report( "web", item={"domain": domain}, warn_if_no_cache=False) or {} if not dnsrecords or not httpreachable: raise YunohostError('certmanager_domain_not_diagnosed_yet', domain=domain) # Check if IP from DNS matches public IP if not dnsrecords.get("status") in [ "SUCCESS", "WARNING" ]: # Warning is for missing IPv6 record which ain't critical for ACME raise YunohostError('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain) # Check if domain seems to be accessible through HTTP? if not httpreachable.get("status") == "SUCCESS": raise YunohostError('certmanager_domain_http_not_working', domain=domain)
def get_ips_checked(self): outgoing_ipversions = [] outgoing_ips = [] ipv4 = Diagnoser.get_cached_report("ip", {"test": "ipv4"}) or {} if ipv4.get("status") == "SUCCESS": outgoing_ipversions.append(4) global_ipv4 = ipv4.get("data", {}).get("global", {}) if global_ipv4: outgoing_ips.append(global_ipv4) if settings_get("smtp.allow_ipv6"): ipv6 = Diagnoser.get_cached_report("ip", {"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": outgoing_ipversions.append(6) global_ipv6 = ipv6.get("data", {}).get("global", {}) if global_ipv6: outgoing_ips.append(global_ipv6) return (outgoing_ipversions, outgoing_ips)
def ipv6_is_important_for_this_domain(): dnsrecords = Diagnoser.get_cached_report( "dnsrecords", item={ "domain": domain, "category": "basic" }) or {} AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") return AAAA_status in ["OK", "WRONG"]
def _prepare_certificate_signing_request(domain, key_file, output_folder): from OpenSSL import crypto # lazy loading this module for performance reasons # Init a request csr = crypto.X509Req() # Set the domain csr.get_subject().CN = domain from yunohost.domain import domain_list # For "parent" domains, include xmpp-upload subdomain in subject alternate names if domain in domain_list(exclude_subdomains=True)["domains"]: subdomain = "xmpp-upload." + domain xmpp_records = ( Diagnoser.get_cached_report( "dnsrecords", item={"domain": domain, "category": "xmpp"} ).get("data") or {} ) if xmpp_records.get("CNAME:xmpp-upload") == "OK": csr.add_extensions( [ crypto.X509Extension( "subjectAltName".encode("utf8"), False, ("DNS:" + subdomain).encode("utf8"), ) ] ) else: logger.warning( m18n.n( "certmanager_warning_subdomain_dns_record", subdomain=subdomain, domain=domain, ) ) # Set the key with open(key_file, "rt") as f: key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) csr.set_pubkey(key) # Sign the request csr.sign(key, "sha256") # Save the request in tmp folder csr_file = output_folder + domain + ".csr" logger.debug("Saving to %s.", csr_file) with open(csr_file, "wb") as f: f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr))
def check_ehlo(self): """ Check the server is reachable from outside and it's the good one This check is ran on IPs we could used to send mail. """ for ipversion in self.ipversions: try: r = Diagnoser.remote_diagnosis("check-smtp", data={}, ipversion=ipversion) except Exception as e: yield dict( meta={ "test": "mail_ehlo", "reason": "remote_server_failed", "ipversion": ipversion, }, data={"error": str(e)}, status="WARNING", summary="diagnosis_mail_ehlo_could_not_diagnose", details=["diagnosis_mail_ehlo_could_not_diagnose_details"], ) continue if r["status"] != "ok": summary = r["status"].replace("error_smtp_", "diagnosis_mail_ehlo_") yield dict( meta={ "test": "mail_ehlo", "ipversion": ipversion }, data={}, status="ERROR", summary=summary, details=[summary + "_details"], ) elif r["helo"] != self.ehlo_domain: yield dict( meta={ "test": "mail_ehlo", "ipversion": ipversion }, data={ "wrong_ehlo": r["helo"], "right_ehlo": self.ehlo_domain }, status="ERROR", summary="diagnosis_mail_ehlo_wrong", details=["diagnosis_mail_ehlo_wrong_details"], )
def run(self): # TODO: report a warning if port 53 or 5353 is exposed to the outside world... # This dict is something like : # { 80: "nginx", # 25: "postfix", # 443: "nginx" # ... } ports = {} services = _get_services() for service, infos in services.items(): for port in infos.get("needs_exposed_ports", []): ports[port] = service ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} if ipv4.get("status") == "SUCCESS": ipversions.append(4) # To be discussed: we could also make this check dependent on the # existence of an AAAA record... ipv6 = Diagnoser.get_cached_report("ip", item={"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": ipversions.append(6) # Fetch test result for each relevant IP version results = {} for ipversion in ipversions: try: r = Diagnoser.remote_diagnosis('check-ports', data={'ports': ports.keys()}, ipversion=ipversion) results[ipversion] = r["ports"] except Exception as e: yield dict( meta={ "reason": "remote_diagnosis_failed", "ipversion": ipversion }, data={"error": str(e)}, status="WARNING", summary="diagnosis_ports_could_not_diagnose", details=["diagnosis_ports_could_not_diagnose_details"]) continue ipversions = results.keys() if not ipversions: return for port, service in sorted(ports.items()): port = str(port) category = services[service].get("category", "[?]") # If both IPv4 and IPv6 (if applicable) are good if all(results[ipversion].get(port) is True for ipversion in ipversions): yield dict(meta={"port": port}, data={ "service": service, "category": category }, status="SUCCESS", summary="diagnosis_ports_ok", details=["diagnosis_ports_needed_by"]) # If both IPv4 and IPv6 (if applicable) are failed elif all(results[ipversion].get(port) is not True for ipversion in ipversions): yield dict(meta={"port": port}, data={ "service": service, "category": category }, status="ERROR", summary="diagnosis_ports_unreachable", details=[ "diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip" ]) # If only IPv4 is failed or only IPv6 is failed (if applicable) else: passed, failed = (4, 6) if results[4].get(port) is True else (6, 4) # Failing in ipv4 is critical. # If we failed in IPv6 but there's in fact no AAAA record # It's an acceptable situation and we shall not report an # error # If any AAAA record is set, IPv6 is important... def ipv6_is_important(): dnsrecords = Diagnoser.get_cached_report( "dnsrecords") or {} return any(record["data"].get("AAAA:@") in ["OK", "WRONG"] for record in dnsrecords.get("items", [])) if failed == 4 or ipv6_is_important(): yield dict(meta={"port": port}, data={ "service": service, "category": category, "passed": passed, "failed": failed }, status="ERROR", summary="diagnosis_ports_partially_unreachable", details=[ "diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip" ]) # So otherwise we report a success # And in addition we report an info about the failure in IPv6 # *with a different meta* (important to avoid conflicts when # fetching the other info...) else: yield dict(meta={"port": port}, data={ "service": service, "category": category }, status="SUCCESS", summary="diagnosis_ports_ok", details=["diagnosis_ports_needed_by"]) yield dict(meta={ "test": "ipv6", "port": port }, data={ "service": service, "category": category, "passed": passed, "failed": failed }, status="INFO", summary="diagnosis_ports_partially_unreachable", details=[ "diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip" ])
def ipv6_is_important(): dnsrecords = Diagnoser.get_cached_report( "dnsrecords") or {} return any(record["data"].get("AAAA:@") in ["OK", "WRONG"] for record in dnsrecords.get("items", []))
def test_http(self, domains, ipversions): results = {} for ipversion in ipversions: try: r = Diagnoser.remote_diagnosis('check-http', data={ 'domains': domains, "nonce": self.nonce }, ipversion=ipversion) results[ipversion] = r["http"] except Exception as e: yield dict( meta={ "reason": "remote_diagnosis_failed", "ipversion": ipversion }, data={"error": str(e)}, status="WARNING", summary="diagnosis_http_could_not_diagnose", details=["diagnosis_http_could_not_diagnose_details"]) continue ipversions = results.keys() if not ipversions: return for domain in domains: # If both IPv4 and IPv6 (if applicable) are good if all(results[ipversion][domain]["status"] == "ok" for ipversion in ipversions): if 4 in ipversions: self.do_hairpinning_test = True yield dict(meta={"domain": domain}, status="SUCCESS", summary="diagnosis_http_ok") # If both IPv4 and IPv6 (if applicable) are failed elif all(results[ipversion][domain]["status"] != "ok" for ipversion in ipversions): detail = results[4 if 4 in ipversions else 6][domain]["status"] yield dict(meta={"domain": domain}, status="ERROR", summary="diagnosis_http_unreachable", details=[ detail.replace("error_http_check", "diagnosis_http") ]) # If only IPv4 is failed or only IPv6 is failed (if applicable) else: passed, failed = ( 4, 6) if results[4][domain]["status"] == "ok" else (6, 4) detail = results[failed][domain]["status"] # Failing in ipv4 is critical. # If we failed in IPv6 but there's in fact no AAAA record # It's an acceptable situation and we shall not report an # error def ipv6_is_important_for_this_domain(): dnsrecords = Diagnoser.get_cached_report( "dnsrecords", item={ "domain": domain, "category": "basic" }) or {} AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") return AAAA_status in ["OK", "WRONG"] if failed == 4 or ipv6_is_important_for_this_domain(): yield dict(meta={"domain": domain}, data={ "passed": passed, "failed": failed }, status="ERROR", summary="diagnosis_http_partially_unreachable", details=[ detail.replace("error_http_check", "diagnosis_http") ]) # So otherwise we report a success (note that this info is # later used to know that ACME challenge is doable) # # And in addition we report an info about the failure in IPv6 # *with a different meta* (important to avoid conflicts when # fetching the other info...) else: self.do_hairpinning_test = True yield dict(meta={"domain": domain}, status="SUCCESS", summary="diagnosis_http_ok") yield dict(meta={ "test": "ipv6", "domain": domain }, data={ "passed": passed, "failed": failed }, status="INFO", summary="diagnosis_http_partially_unreachable", details=[ detail.replace("error_http_check", "diagnosis_http") ])
def run(self): all_domains = domain_list()["domains"] domains_to_check = [] for domain in all_domains: # If the diagnosis location ain't defined, can't do diagnosis, # probably because nginx conf manually modified... nginx_conf = "/etc/nginx/conf.d/%s.conf" % domain if ".well-known/ynh-diagnosis/" not in read_file(nginx_conf): yield dict( meta={"domain": domain}, status="WARNING", summary="diagnosis_http_nginx_conf_not_up_to_date", details=[ "diagnosis_http_nginx_conf_not_up_to_date_details" ]) else: domains_to_check.append(domain) self.nonce = ''.join( random.choice("0123456789abcedf") for i in range(16)) os.system("rm -rf /tmp/.well-known/ynh-diagnosis/") os.system("mkdir -p /tmp/.well-known/ynh-diagnosis/") os.system("touch /tmp/.well-known/ynh-diagnosis/%s" % self.nonce) if not domains_to_check: return # To perform hairpinning test, we gotta make sure that port forwarding # is working and therefore we'll do it only if at least one ipv4 domain # works. self.do_hairpinning_test = False ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} if ipv4.get("status") == "SUCCESS": ipversions.append(4) # To be discussed: we could also make this check dependent on the # existence of an AAAA record... ipv6 = Diagnoser.get_cached_report("ip", item={"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": ipversions.append(6) for item in self.test_http(domains_to_check, ipversions): yield item # If at least one domain is correctly exposed to the outside, # attempt to diagnose hairpinning situations. On network with # hairpinning issues, the server may be correctly exposed on the # outside, but from the outside, it will be as if the port forwarding # was not configured... Hence, calling for example # "curl --head the.global.ip" will simply timeout... if self.do_hairpinning_test: global_ipv4 = ipv4.get("data", {}).get("global", None) if global_ipv4: try: requests.head("http://" + global_ipv4, timeout=5) except requests.exceptions.Timeout: yield dict( meta={"test": "hairpinning"}, status="WARNING", summary="diagnosis_http_hairpinning_issue", details=["diagnosis_http_hairpinning_issue_details"]) except: # Well I dunno what to do if that's another exception # type... That'll most probably *not* be an hairpinning # issue but something else super weird ... pass