mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Add a static method to call remote diagnosis and supports ipv4-only or ipv6-only check
This commit is contained in:
parent
8e46b536dc
commit
7f3cc33487
3 changed files with 110 additions and 54 deletions
|
@ -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",
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
Loading…
Add table
Reference in a new issue