Example #1
0
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)
Example #2
0
    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)
Example #3
0
                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"]
Example #4
0
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))
Example #5
0
    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"],
                )
Example #6
0
    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"
                               ])
Example #7
0
 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", []))
Example #8
0
    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")
                               ])
Example #9
0
    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