Add a static method to call remote diagnosis and supports ipv4-only or ipv6-only check

This commit is contained in:
Alexandre Aubin 2020-04-16 02:51:29 +02:00
parent 8e46b536dc
commit 7f3cc33487
3 changed files with 110 additions and 54 deletions

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
import os import os
import requests
from yunohost.diagnosis import Diagnoser from yunohost.diagnosis import Diagnoser
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
@ -27,25 +26,16 @@ class PortsDiagnoser(Diagnoser):
ports[port] = service ports[port] = service
try: try:
r = requests.post('https://diagnosis.yunohost.org/check-ports', json={'ports': ports.keys()}, timeout=30) r = Diagnoser.remote_diagnosis('check-ports',
if r.status_code not in [200, 400, 418]: data={'ports': ports.keys()},
raise Exception("Bad response from the server https://diagnosis.yunohost.org/check-ports : %s - %s" % (str(r.status_code), r.content)) ipversion=4)
r = r.json() results = r["ports"]
if "status" not in r.keys():
raise Exception("Bad syntax for response ? Raw json: %s" % str(r))
elif r["status"] == "error":
if "content" in r.keys():
raise Exception(r["content"])
else:
raise Exception("Bad syntax for response ? Raw json: %s" % str(r))
elif r["status"] != "ok" or "ports" not in r.keys() or not isinstance(r["ports"], dict):
raise Exception("Bad syntax for response ? Raw json: %s" % str(r))
except Exception as e: except Exception as e:
raise YunohostError("diagnosis_ports_could_not_diagnose", error=e) raise YunohostError("diagnosis_ports_could_not_diagnose", error=e)
for port, service in sorted(ports.items()): for port, service in sorted(ports.items()):
category = services[service].get("category", "[?]") category = services[service].get("category", "[?]")
if r["ports"].get(str(port), None) is not True: if results.get(str(port), None) is not True:
yield dict(meta={"port": str(port)}, yield dict(meta={"port": str(port)},
data={"service": service, "category": category}, data={"service": service, "category": category},
status="ERROR", status="ERROR",

View file

@ -4,10 +4,14 @@ import os
import random import random
import requests import requests
from moulinette.utils.filesystem import read_file
from yunohost.diagnosis import Diagnoser from yunohost.diagnosis import Diagnoser
from yunohost.domain import domain_list from yunohost.domain import domain_list
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
DIAGNOSIS_SERVER = "diagnosis.yunohost.org"
class WebDiagnoser(Diagnoser): class WebDiagnoser(Diagnoser):
@ -17,52 +21,42 @@ class WebDiagnoser(Diagnoser):
def run(self): def run(self):
nonce_digits = "0123456789abcedf"
at_least_one_domain_ok = False
all_domains = domain_list()["domains"] all_domains = domain_list()["domains"]
domains_to_check = []
for domain in all_domains: for domain in all_domains:
# If the diagnosis location ain't defined, can't do diagnosis, # If the diagnosis location ain't defined, can't do diagnosis,
# probably because nginx conf manually modified... # probably because nginx conf manually modified...
nginx_conf = "/etc/nginx/conf.d/%s.conf" % domain nginx_conf = "/etc/nginx/conf.d/%s.conf" % domain
if os.system("grep -q '^.*location .*/.well-known/ynh-diagnosis/' %s" % nginx_conf) != 0: if ".well-known/ynh-diagnosis/" not in read_file(nginx_conf):
yield dict(meta={"domain": domain}, yield dict(meta={"domain": domain},
status="WARNING", status="WARNING",
summary="diagnosis_http_nginx_conf_not_up_to_date", summary="diagnosis_http_nginx_conf_not_up_to_date",
details=["diagnosis_http_nginx_conf_not_up_to_date_details"]) details=["diagnosis_http_nginx_conf_not_up_to_date_details"])
nonce = ''.join(random.choice(nonce_digits) 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" % nonce)
try:
r = requests.post('https://diagnosis.yunohost.org/check-http', json={'domain': domain, "nonce": nonce}, timeout=30)
if r.status_code not in [200, 400, 418]:
raise Exception("Bad response from the server https://diagnosis.yunohost.org/check-http : %s - %s" % (str(r.status_code), r.content))
r = r.json()
if "status" not in r.keys():
raise Exception("Bad syntax for response ? Raw json: %s" % str(r))
elif r["status"] == "error" and ("code" not in r.keys() or not r["code"].startswith("error_http_check_")):
if "content" in r.keys():
raise Exception(r["content"])
else:
raise Exception("Bad syntax for response ? Raw json: %s" % str(r))
except Exception as e:
raise YunohostError("diagnosis_http_could_not_diagnose", error=e)
if r["status"] == "ok":
at_least_one_domain_ok = True
yield dict(meta={"domain": domain},
status="SUCCESS",
summary="diagnosis_http_ok")
else: else:
detail = r["code"].replace("error_http_check", "diagnosis_http") if "code" in r else "diagnosis_http_unknown_error" domains_to_check.append(domain)
yield dict(meta={"domain": domain},
status="ERROR", self.nonce = ''.join(random.choice("0123456789abcedf") for i in range(16))
summary="diagnosis_http_unreachable", os.system("rm -rf /tmp/.well-known/ynh-diagnosis/")
details=[detail]) 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
ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {}
if ipv4.get("status") == "SUCCESS":
for item in self.test_http(domains_to_check, ipversion=4):
yield item
ipv6 = Diagnoser.get_cached_report("ip", item={"test": "ipv6"}) or {}
if ipv6.get("status") == "SUCCESS":
for item in self.test_http(domains_to_check, ipversion=6):
yield item
# If at least one domain is correctly exposed to the outside, # If at least one domain is correctly exposed to the outside,
# attempt to diagnose hairpinning situations. On network with # attempt to diagnose hairpinning situations. On network with
@ -70,13 +64,12 @@ class WebDiagnoser(Diagnoser):
# outside, but from the outside, it will be as if the port forwarding # outside, but from the outside, it will be as if the port forwarding
# was not configured... Hence, calling for example # was not configured... Hence, calling for example
# "curl --head the.global.ip" will simply timeout... # "curl --head the.global.ip" will simply timeout...
if at_least_one_domain_ok: if self.do_hairpinning_test:
ipv4 = Diagnoser.get_cached_report_item("ip", {"test": "ipv4"}) global_ipv4 = ipv4.get("data", {}).get("global", None)
global_ipv4 = ipv4.get("data", {}).get("global", {})
if global_ipv4: if global_ipv4:
try: try:
requests.head("http://" + global_ipv4, timeout=5) requests.head("http://" + global_ipv4, timeout=5)
except requests.exceptions.Timeout as e: except requests.exceptions.Timeout:
yield dict(meta={"test": "hairpinning"}, yield dict(meta={"test": "hairpinning"},
status="WARNING", status="WARNING",
summary="diagnosis_http_hairpinning_issue", summary="diagnosis_http_hairpinning_issue",
@ -87,6 +80,33 @@ class WebDiagnoser(Diagnoser):
# issue but something else super weird ... # issue but something else super weird ...
pass pass
def test_http(self, domains, ipversion):
try:
r = Diagnoser.remote_diagnosis('check-http',
data={'domains': domains,
"nonce": self.nonce},
ipversion=ipversion)
results = r["http"]
except Exception as e:
raise YunohostError("diagnosis_http_could_not_diagnose", error=e)
assert set(results.keys()) == set(domains)
for domain, result in results.items():
if result["status"] == "ok":
if ipversion == 4:
self.do_hairpinning_test = True
yield dict(meta={"domain": domain},
status="SUCCESS",
summary="diagnosis_http_ok")
else:
yield dict(meta={"domain": domain},
status="ERROR",
summary="diagnosis_http_unreachable",
details=[result["status"].replace("error_http_check", "diagnosis_http")])
def main(args, env, loggers): def main(args, env, loggers):
return WebDiagnoser(args, env, loggers).diagnose() return WebDiagnoser(args, env, loggers).diagnose()

View file

@ -27,6 +27,8 @@
import re import re
import os import os
import time import time
import requests
import socket
from moulinette import m18n, msettings from moulinette import m18n, msettings
from moulinette.utils import log from moulinette.utils import log
@ -39,6 +41,7 @@ logger = log.getActionLogger('yunohost.diagnosis')
DIAGNOSIS_CACHE = "/var/cache/yunohost/diagnosis/" DIAGNOSIS_CACHE = "/var/cache/yunohost/diagnosis/"
DIAGNOSIS_CONFIG_FILE = '/etc/yunohost/diagnosis.yml' DIAGNOSIS_CONFIG_FILE = '/etc/yunohost/diagnosis.yml'
DIAGNOSIS_SERVER = "diagnosis.yunohost.org"
def diagnosis_list(): def diagnosis_list():
all_categories_names = [h for h, _ in _list_diagnosis_categories()] all_categories_names = [h for h, _ in _list_diagnosis_categories()]
@ -492,6 +495,49 @@ class Diagnoser():
if "details" in item: if "details" in item:
item["details"] = [m18n_(info) for info in item["details"]] item["details"] = [m18n_(info) for info in item["details"]]
@staticmethod
def remote_diagnosis(uri, data, ipversion, timeout=30):
# Monkey patch socket.getaddrinfo to force request() to happen in ipv4
# or 6 ...
# Inspired by https://stackoverflow.com/a/50044152
old_getaddrinfo = socket.getaddrinfo
def getaddrinfo_ipv4_only(*args, **kwargs):
responses = old_getaddrinfo(*args, **kwargs)
return [response
for response in responses
if response[0] == socket.AF_INET]
def getaddrinfo_ipv6_only(*args, **kwargs):
responses = old_getaddrinfo(*args, **kwargs)
return [response
for response in responses
if response[0] == socket.AF_INET6]
if ipversion == 4:
socket.getaddrinfo = getaddrinfo_ipv4_only
elif ipversion == 6:
socket.getaddrinfo = getaddrinfo_ipv6_only
url = 'https://%s/%s' % (DIAGNOSIS_SERVER, uri)
try:
r = requests.post(url, json=data, timeout=timeout)
finally:
socket.getaddrinfo = old_getaddrinfo
if r.status_code not in [200, 400]:
raise Exception("Bad response from diagnosis server.\nURL: %s\nStatus code: %s\nMessage: %s" % (url, r.status_code, r.content))
if r.status_code == 400:
raise Exception("Diagnosis request was refused: %s" % r.content)
try:
r = r.json()
except Exception as e:
raise Exception("Failed to parse json from diagnosis server response.\nError: %s\nOriginal content: %s" % (e, r.content))
return r
def _list_diagnosis_categories(): def _list_diagnosis_categories():
hooks_raw = hook_list("diagnosis", list_by="priority", show_info=True)["hooks"] hooks_raw = hook_list("diagnosis", list_by="priority", show_info=True)["hooks"]