From a6a7cd83f93415749c39cdef03ef17cd81f6edf0 Mon Sep 17 00:00:00 2001 From: "Loan (LSE)" Date: Fri, 10 Mar 2023 17:41:15 +0000 Subject: [PATCH] [FIX] hw_drivers: HTTPS certificate info on IoT homepage HTTPS certificate IoT issues can be complicated to troubleshoot as the information are not visible/given. This PR aim to share this information on the IoT box homepage. As there is a lot of possible causes for a given problem, a code is used that will be explained/detailed in Odoo's IoT documentation: https://github.com/odoo/documentation/pull/3818 OPW-3227004 closes odoo/odoo#116650 X-original-commit: 8bc6b2d0033676507f95b74b3ae383ab17164203 Signed-off-by: Loan (LSE) Signed-off-by: Sens Loan (lse) --- addons/hw_drivers/controllers/driver.py | 2 +- addons/hw_drivers/main.py | 5 +- addons/hw_drivers/tools/helpers.py | 147 ++++++++++++------ addons/hw_posbox_homepage/controllers/main.py | 4 + addons/hw_posbox_homepage/views/homepage.html | 23 +++ 5 files changed, 134 insertions(+), 47 deletions(-) diff --git a/addons/hw_drivers/controllers/driver.py b/addons/hw_drivers/controllers/driver.py index c563879209a..b82800e55a8 100755 --- a/addons/hw_drivers/controllers/driver.py +++ b/addons/hw_drivers/controllers/driver.py @@ -40,7 +40,7 @@ class DriverController(http.Controller): This route is called when we want to check if certificate is up-to-date Used in cron.daily """ - helpers.check_certificate() + helpers.get_certificate_status() @http.route('/hw_drivers/event', type='json', auth='none', cors='*', csrf=False, save_session=False) def event(self, listener): diff --git a/addons/hw_drivers/main.py b/addons/hw_drivers/main.py index 8e4c46969c4..ab5e4ffc28d 100644 --- a/addons/hw_drivers/main.py +++ b/addons/hw_drivers/main.py @@ -80,7 +80,10 @@ class Manager(Thread): helpers.start_nginx_server() if platform.system() == 'Linux': helpers.check_git_branch() - helpers.check_certificate() + is_certificate_ok, certificate_details = helpers.get_certificate_status() + if not is_certificate_ok: + _logger.warning("An error happened when trying to get the HTTPS certificate: %s", + certificate_details) # We first add the IoT Box to the connected DB because IoT handlers cannot be downloaded if # the identifier of the Box is not found in the DB. So add the Box to the DB. diff --git a/addons/hw_drivers/tools/helpers.py b/addons/hw_drivers/tools/helpers.py index af8b3c483e5..99866148bf4 100644 --- a/addons/hw_drivers/tools/helpers.py +++ b/addons/hw_drivers/tools/helpers.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import datetime +from enum import Enum from importlib import util import platform import io @@ -29,6 +30,13 @@ _logger = logging.getLogger(__name__) # Helper #---------------------------------------------------------- + +class CertificateStatus(Enum): + OK = 1 + NEED_REFRESH = 2 + ERROR = 3 + + class IoTRestart(Thread): """ Thread to restart odoo server in IoT Box when we must return a answer before @@ -73,27 +81,42 @@ def start_nginx_server(): def check_certificate(): """ Check if the current certificate is up to date or not authenticated + :return CheckCertificateStatus """ server = get_odoo_server_url() - if server: - if platform.system() == 'Windows': - path = Path(get_path_nginx()).joinpath('conf/nginx-cert.crt') - elif platform.system() == 'Linux': - path = Path('/etc/ssl/certs/nginx-cert.crt') - if path.exists(): - with path.open('r') as f: - cert = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) - cert_end_date = datetime.datetime.strptime(cert.get_notAfter().decode('utf-8'), "%Y%m%d%H%M%SZ") - datetime.timedelta(days=10) - for key in cert.get_subject().get_components(): - if key[0] == b'CN': - cn = key[1].decode('utf-8') - if cn == 'OdooTempIoTBoxCertificate' or datetime.datetime.now() > cert_end_date: - _logger.info(_('Your certificate %s must be updated') % (cn)) - load_certificate() - else: - _logger.info(_('Your certificate %s is valid until %s') % (cn, cert_end_date)) - else: - load_certificate() + + if not server: + return {"status": CertificateStatus.ERROR, + "error_code": "ERR_IOT_HTTPS_CHECK_NO_SERVER"} + + if platform.system() == 'Windows': + path = Path(get_path_nginx()).joinpath('conf/nginx-cert.crt') + elif platform.system() == 'Linux': + path = Path('/etc/ssl/certs/nginx-cert.crt') + + if not path.exists(): + return {"status": CertificateStatus.NEED_REFRESH} + + try: + with path.open('r') as f: + cert = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) + except EnvironmentError: + _logger.exception("Unable to read certificate file") + return {"status": CertificateStatus.ERROR, + "error_code": "ERR_IOT_HTTPS_CHECK_CERT_READ_EXCEPTION"} + + cert_end_date = datetime.datetime.strptime(cert.get_notAfter().decode('utf-8'), "%Y%m%d%H%M%SZ") - datetime.timedelta(days=10) + for key in cert.get_subject().get_components(): + if key[0] == b'CN': + cn = key[1].decode('utf-8') + if cn == 'OdooTempIoTBoxCertificate' or datetime.datetime.now() > cert_end_date: + message = _('Your certificate %s must be updated') % (cn) + _logger.info(message) + return {"status": CertificateStatus.NEED_REFRESH} + else: + message = _('Your certificate %s is valid until %s') % (cn, cert_end_date) + _logger.info(message) + return {"status": CertificateStatus.OK, "message": message} def check_git_branch(): """ @@ -165,6 +188,27 @@ def save_conf_server(url, token, db_uuid, enterprise_code): write_file('odoo-db-uuid.conf', db_uuid or '') write_file('odoo-enterprise-code.conf', enterprise_code or '') +def get_certificate_status(is_first=True): + """ + Will get the HTTPS certificate details if present. Will load the certificate if missing. + + :param is_first: Use to make sure that the recursion happens only once + :return: (bool, str) + """ + check_certificate_result = check_certificate() + certificateStatus = check_certificate_result["status"] + + if certificateStatus == CertificateStatus.ERROR: + return False, check_certificate_result["error_code"] + + if certificateStatus == CertificateStatus.NEED_REFRESH and is_first: + certificate_process = load_certificate() + if certificate_process is not True: + return False, certificate_process + return get_certificate_status(is_first=False) # recursive call to attempt certificate read + return True, check_certificate_result.get("message", + "The HTTPS certificate was generated correctly") + def get_img_name(): major, minor = get_version().split('.') return 'iotboxv%s_%s.zip' % (major, minor) @@ -228,39 +272,52 @@ def load_certificate(): """ db_uuid = read_file_first_line('odoo-db-uuid.conf') enterprise_code = read_file_first_line('odoo-enterprise-code.conf') - if db_uuid and enterprise_code: - url = 'https://www.odoo.com/odoo-enterprise/iot/x509' - data = { - 'params': { - 'db_uuid': db_uuid, - 'enterprise_code': enterprise_code - } + if not (db_uuid and enterprise_code): + return "ERR_IOT_HTTPS_LOAD_NO_CREDENTIAL" + + url = 'https://www.odoo.com/odoo-enterprise/iot/x509' + data = { + 'params': { + 'db_uuid': db_uuid, + 'enterprise_code': enterprise_code } - urllib3.disable_warnings() - http = urllib3.PoolManager(cert_reqs='CERT_NONE') + } + urllib3.disable_warnings() + http = urllib3.PoolManager(cert_reqs='CERT_NONE', retries=urllib3.Retry(4)) + try: response = http.request( 'POST', url, body = json.dumps(data).encode('utf8'), headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} ) - result = json.loads(response.data.decode('utf8'))['result'] - if result: - write_file('odoo-subject.conf', result['subject_cn']) - if platform.system() == 'Linux': - with writable(): - Path('/etc/ssl/certs/nginx-cert.crt').write_text(result['x509_pem']) - Path('/root_bypass_ramdisks/etc/ssl/certs/nginx-cert.crt').write_text(result['x509_pem']) - Path('/etc/ssl/private/nginx-cert.key').write_text(result['private_key_pem']) - Path('/root_bypass_ramdisks/etc/ssl/private/nginx-cert.key').write_text(result['private_key_pem']) - elif platform.system() == 'Windows': - Path(get_path_nginx()).joinpath('conf/nginx-cert.crt').write_text(result['x509_pem']) - Path(get_path_nginx()).joinpath('conf/nginx-cert.key').write_text(result['private_key_pem']) - time.sleep(3) - if platform.system() == 'Windows': - odoo_restart(0) - elif platform.system() == 'Linux': - start_nginx_server() + except Exception as e: + _logger.exception("An error occurred while trying to reach odoo.com servers.") + return "ERR_IOT_HTTPS_LOAD_REQUEST_EXCEPTION\n\n%s" % e + + if response.status != 200: + return "ERR_IOT_HTTPS_LOAD_REQUEST_STATUS %s\n\n%s" % (response.status, response.reason) + + result = json.loads(response.data.decode('utf8'))['result'] + if not result: + return "ERR_IOT_HTTPS_LOAD_REQUEST_NO_RESULT" + + write_file('odoo-subject.conf', result['subject_cn']) + if platform.system() == 'Linux': + with writable(): + Path('/etc/ssl/certs/nginx-cert.crt').write_text(result['x509_pem']) + Path('/root_bypass_ramdisks/etc/ssl/certs/nginx-cert.crt').write_text(result['x509_pem']) + Path('/etc/ssl/private/nginx-cert.key').write_text(result['private_key_pem']) + Path('/root_bypass_ramdisks/etc/ssl/private/nginx-cert.key').write_text(result['private_key_pem']) + elif platform.system() == 'Windows': + Path(get_path_nginx()).joinpath('conf/nginx-cert.crt').write_text(result['x509_pem']) + Path(get_path_nginx()).joinpath('conf/nginx-cert.key').write_text(result['private_key_pem']) + time.sleep(3) + if platform.system() == 'Windows': + odoo_restart(0) + elif platform.system() == 'Linux': + start_nginx_server() + return True def download_iot_handlers(auto=True): """ diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py index 718718cb9f7..7ee8e5cee60 100644 --- a/addons/hw_posbox_homepage/controllers/main.py +++ b/addons/hw_posbox_homepage/controllers/main.py @@ -76,6 +76,8 @@ class IoTboxHomepage(Home): else: network = 'Not Connected' + is_certificate_ok, certificate_details = helpers.get_certificate_status() + iot_device = [] for device in iot_devices: iot_device.append({ @@ -95,6 +97,8 @@ class IoTboxHomepage(Home): 'network_status': network, 'version': helpers.get_version(), 'system': platform.system(), + 'is_certificate_ok': is_certificate_ok, + 'certificate_details': certificate_details, } @http.route() diff --git a/addons/hw_posbox_homepage/views/homepage.html b/addons/hw_posbox_homepage/views/homepage.html index 70d4c62340f..3dc9f406899 100644 --- a/addons/hw_posbox_homepage/views/homepage.html +++ b/addons/hw_posbox_homepage/views/homepage.html @@ -89,6 +89,11 @@ position: absolute; right: 0; } + .warn-tr { + color: #856404; + background-color: #fff3cd; + border: 2px solid #f3e4ce; + }