From 83f26434859774aa6b37643a37ca70272e6ac768 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Sun, 16 Jun 2024 04:34:01 -0700 Subject: [PATCH 01/21] TLS support in nginx --- .../frontend/src/components/wizard/Wizard.vue | 17 ++ core/frontend/src/store/beacon.ts | 33 +++ core/services/beacon/main.py | 134 +++++++++ core/tools/nginx/nginx.conf | 1 + core/tools/nginx/nginx.conf.template | 259 ++++++++++++++++++ core/tools/nginx/nginx_tls.conf.template | 259 ++++++++++++++++++ 6 files changed, 703 insertions(+) create mode 100644 core/tools/nginx/nginx.conf.template create mode 100644 core/tools/nginx/nginx_tls.conf.template diff --git a/core/frontend/src/components/wizard/Wizard.vue b/core/frontend/src/components/wizard/Wizard.vue index 89efa30c6e..3774fc93ea 100644 --- a/core/frontend/src/components/wizard/Wizard.vue +++ b/core/frontend/src/components/wizard/Wizard.vue @@ -128,6 +128,7 @@
+
, @@ -454,6 +457,15 @@ export default Vue.extend({ skip: false, started: false, }, + { + title: 'Set TLS', + summary: `Enable TLS for the BlueOS web server: ${this.enable_tls}`, + promise: () => this.setTLS(), + message: undefined, + done: false, + skip: false, + started: false, + }, { title: 'Set vehicle image', summary: 'Set image to be used for vehicle thumbnail', @@ -595,6 +607,11 @@ export default Vue.extend({ .then(() => undefined) .catch(() => 'Failed to set custom vehicle name') }, + async setTLS(): Promise { + return beacon.setTLS(this.enable_tls) + .then(() => undefined) + .catch(() => 'False to change TLS configuration') + }, async disableWifiHotspot(): Promise { return back_axios({ method: 'post', diff --git a/core/frontend/src/store/beacon.ts b/core/frontend/src/store/beacon.ts index 98bee7befc..d00cdc3872 100644 --- a/core/frontend/src/store/beacon.ts +++ b/core/frontend/src/store/beacon.ts @@ -37,6 +37,8 @@ class BeaconStore extends VuexModule { vehicle_name = '' + use_tls = false + // eslint-disable-next-line @Mutation private _setHostname(hostname: string): void { @@ -49,6 +51,12 @@ class BeaconStore extends VuexModule { this.vehicle_name = vehicle_name } + // eslint-disable-next-line + @Mutation + private _setUseTLS(use_tls: boolean): void { + this.use_tls = use_tls + } + @Mutation setAvailableDomains(domains: Domain[]): void { this.available_domains = domains @@ -236,6 +244,31 @@ class BeaconStore extends VuexModule { } }, 1000) } + + @Action + async setTLS(enable_tls: boolean): Promise { + return back_axios({ + method: 'post', + url: `${this.API_URL}/use_tls`, + timeout: 5000, + params: { + enable_tls: enable_tls, + }, + }) + .then(() => { + // eslint-disable-next-line + this._setUseTLS(enable_tls) + return true + }) + .catch((error) => { + if (error === backend_offline_error) { + return false + } + const message = `Could not set TLS option: ${error.response?.data ?? error.message}.` + notifier.pushError('BEACON_SET_TLS_FAIL', message, true) + return false + }) + } } export { BeaconStore } diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py index 4c4fee9b84..bae426f82d 100755 --- a/core/services/beacon/main.py +++ b/core/services/beacon/main.py @@ -5,6 +5,11 @@ import logging import pathlib import socket +import subprocess +import os +import shutil +import shlex +import datetime from typing import Any, Dict, List, Optional import psutil @@ -24,6 +29,7 @@ SERVICE_NAME = "beacon" +TLS_CERT_PATH = "" class AsyncRunner: def __init__(self, ip_version: IPVersion, interface: str, interface_name: str) -> None: @@ -132,7 +138,123 @@ def set_vehicle_name(self, name: str) -> None: def get_vehicle_name(self) -> str: return self.manager.settings.vehicle_name or "BlueROV2" + + def get_enable_tls(self) -> bool: + # return what's in settings or assume no...this may change in the future + return self.manager.settings.enable_tls or False + + def set_enable_tls(self, enable_tls: bool) -> None: + # handle enabling/disabling tls + if not enable_tls and self.get_enable_tls(): + # tls is currently enabled and we need to disable + # change nginx config + self.generate_new_nginx_config(use_tls=False) + # validate config + if not self.nginx_config_is_valid(): + raise SystemError("Unable to validate staged Nginx config") + # bounce nginx + self.nginx_promote_config(keep_backup=True) + # remove old cert + os.unlink("/home/pi/tools/nginx/blueos.crt") + os.unlink("/home/pi/tools/nginx/blueos.key") + elif enable_tls and not self.get_enable_tls(): + # tls is currently disabled and we need to enable + # generate cert + self.generate_cert() + # change nginx config + self.generate_new_nginx_config(use_tls=True) + # validate config + if not self.nginx_config_is_valid(): + raise SystemError("Unable to validate staged Nginx config") + # bounce nginx + self.nginx_promote_config(keep_backup=True) + + def generate_cert(self) -> bool: + ''' + Generates the TLS certificate for the current vehicle hostname and stores in persistent storage + ''' + # get the hostname + current_hostname = self.get_hostname() + alt_names = [] + alt_names.append(f"DNS:{current_hostname}") + alt_names.append(f"DNS:{current_hostname}-wifi") + alt_names.append(f"DNS:{current_hostname}-hotspot") + alt_names.append(f"IP:192.168.2.2") + + # shell out to openssl to get the cert + try: + subprocess.check_call([ + "openssl", "req", "-x509", + "-newkey", "rsa:4096", + "-sha256", + "-days", "1825", + "-nodes", + "-keyout", f"/home/pi/tools/nginx/blueos.key", + "-out", f"/home/pi/tools/nginx/blueos.crt", + "-subj", shlex.quote(f"/CN={self.DEFAULT_HOSTNAME}"), + "-addtext", shlex.quote(f"subjectAltName={','.join(alt_names)}") + ], shell=True) + except subprocess.CalledProcessError: + raise SystemError("Unable to generate certificates") + + def generate_new_nginx_config(self, + config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck", + use_tls: bool = False): + ''' + Generates a new nginx config file at the path specified + ''' + # use the templates for simplicity now + # TODO: the user may have changed the config, so we should parse and update as needed + if use_tls: + shutil.copy("/home/pi/tools/nginx/nginx_tls.conf.template", config_path, follow_symlinks=False) + else: + shutil.copy("/home/pi/tools/nginx/nginx.conf.template", config_path, follow_symlinks=False) + + + def nginx_config_is_valid(self, config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck") -> bool: + ''' + Returns true if the nginx config file is valid + ''' + try: + subprocess.check_call([ + "nginx", "-t", "-c", config_path + ], shell=True) + return True + except subprocess.CalledProcessError: + # got a non-zero return code indicating the config was not valid + return False + + def nginx_promote_config(self, + config_path: str = "/home/pi/tools/nginx/nginx.conf", + new_config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck", + keep_backup: bool = False): + ''' + Moves the file at new_config_path to config_path and bounces nginx, optionally keeping a backup of config_path + ''' + # do both files exist + if not os.path.exists(config_path): + raise FileNotFoundError("Old config not found") + if not os.path.isfile(new_config_path): + raise FileNotFoundError("New config not found") + + if keep_backup: + shutil.copyfile(config_path, + f"{config_path}_backup_{datetime.datetime.utcnow().strftime('%Y%m%d_%H%M%S')}", + follow_symlinks=False) + + # move it + os.unlink(config_path) + os.rename(new_config_path, config_path) + + # restart nginx + try: + subprocess.check_call([ + "systemctl", "restart", "nginx" + ], shell=True) + except subprocess.CalledProcessError: + raise SystemError("Unable to restart nginx") + def create_async_service_infos( self, interface: str, service_name: str, domain_name: str, ip: str ) -> AsyncServiceInfo: @@ -311,6 +433,18 @@ def get_ip(request: Request) -> Any: except KeyError: # We're not going through Nginx for some reason return IpInfo(client_ip=request.scope["client"][0], interface_ip=request.scope["server"][0]) + + +@app.get("/use_tls", summary="Get whether TLS should be enabled") +@version(1, 0) +def get_enable_tls() -> bool: + return beacon.get_enable_tls() + + +@app.post("/use_tls", summary="Set whether TLS should be enbabled") +@version(1, 0) +def set_enable_tls(enable_tls: bool) -> Any: + return beacon.set_enable_tls(enable_tls) app = VersionedFastAPI(app, version="1.0.0", prefix_format="/v{major}.{minor}", enable_latest=True) diff --git a/core/tools/nginx/nginx.conf b/core/tools/nginx/nginx.conf index 46f30aff49..1d57049d34 100644 --- a/core/tools/nginx/nginx.conf +++ b/core/tools/nginx/nginx.conf @@ -42,6 +42,7 @@ http { server { listen 80; + listen [::]:80; add_header Access-Control-Allow-Origin *; diff --git a/core/tools/nginx/nginx.conf.template b/core/tools/nginx/nginx.conf.template new file mode 100644 index 0000000000..1d57049d34 --- /dev/null +++ b/core/tools/nginx/nginx.conf.template @@ -0,0 +1,259 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; + +events { + worker_connections 768; + # multi_accept on; +} + +http { + client_max_body_size 2G; + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + # Add 10min timeout if we get stuck while processing something like docker images and firmware upload + proxy_read_timeout 600s; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Redirect legacy companion port (Companion 0.0.X) to our new homepage + server{ + listen 2770; + + location / { + rewrite ^/(.*)$ http://$host redirect; + } + } + + server { + listen 80; + listen [::]:80; + + add_header Access-Control-Allow-Origin *; + + # Endpoint used for backend status checks. + # It will always return an empty 204 response when online. + location /status { + return 204; + } + + location /ardupilot-manager { + include cors.conf; + rewrite ^/ardupilot-manager$ /ardupilot-manager/ redirect; + rewrite ^/ardupilot-manager/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8000; + } + + location /bag { + include cors.conf; + rewrite ^/bag$ /bag/ redirect; + rewrite ^/bag/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9101; + } + + location /beacon { + include cors.conf; + rewrite ^/beacon$ /beacon/ redirect; + rewrite ^/beacon/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9111; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Interface-Ip $server_addr; + } + + location /bridget { + include cors.conf; + rewrite ^/bridget$ /bridget/ redirect; + rewrite ^/bridget/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:27353; + } + + location /cable-guy { + include cors.conf; + rewrite ^/cable-guy$ /cable-guy/ redirect; + rewrite ^/cable-guy/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9090; + } + + location /commander { + include cors.conf; + rewrite ^/commander$ /commander/ redirect; + rewrite ^/commander/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9100; + } + + location /docker { + limit_except GET { + deny all; + } + rewrite ^/docker$ /docker/ redirect; + rewrite ^/docker/(.*)$ /$1 break; + proxy_pass http://unix:/var/run/docker.sock:/; + } + + location /file-browser { + rewrite ^/file-browser$ /file-browser/ redirect; + rewrite ^/file-browser/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:7777; + } + + location /helper { + include cors.conf; + rewrite ^/helper$ /helper/ redirect; + rewrite ^/helper/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:81; + } + + location /kraken { + include cors.conf; + rewrite ^/kraken$ /kraken/ redirect; + rewrite ^/kraken/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9134; + } + + location /nmea-injector { + include cors.conf; + rewrite ^/nmea-injector$ /nmea-injector/ redirect; + rewrite ^/nmea-injector/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:2748; + } + + location ^~ /logviewer { + # ^~ makes this a higher priority than locations with regex + expires 10d; + root /home/pi/tools; + } + + location /mavlink2rest { + # Hide the header from the upstream application + proxy_hide_header Access-Control-Allow-Origin; + + include cors.conf; + rewrite ^/mavlink2rest$ /mavlink2rest/ redirect; + rewrite ^/mavlink2rest/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:6040; + # next two lines are required for websockets + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location /mavlink-camera-manager { + include cors.conf; + rewrite ^/mavlink-camera-manager$ /mavlink-camera-manager/ redirect; + rewrite ^/mavlink-camera-manager/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:6020; + } + + location /network-test { + include cors.conf; + rewrite ^/network-test$ /network-test/ redirect; + rewrite ^/network-test/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9120; + # next two lines are required for websockets + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location /system-information { + include cors.conf; + rewrite ^/system-information$ /system-information/ redirect; + rewrite ^/system-information/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:6030; + # next two lines are required for websockets + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location /terminal { + rewrite ^/terminal$ /terminal/ redirect; + rewrite ^/terminal/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8088; + # next two lines are required for websockets + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location /version-chooser { + include cors.conf; + rewrite ^/version-chooser$ /version-chooser/ redirect; + rewrite ^/version-chooser/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8081; + proxy_buffering off; + expires -1; + add_header Cache-Control no-store; + } + + location /wifi-manager { + include cors.conf; + rewrite ^/wifi-manager$ /wifi-manager/ redirect; + rewrite ^/wifi-manager/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9000; + } + + location /ping { + include cors.conf; + rewrite ^/ping$ /ping/ redirect; + rewrite ^/ping/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9110; + } + + location / { + root /home/pi/frontend; + try_files $uri $uri/ /index.html; + autoindex on; + # allow frontend to see files using json + autoindex_format json; + } + + location /assets/ { + root /home/pi/frontend; + try_files $uri $uri/; + autoindex on; + add_header Cache-Control "public, max-age=604800"; + } + + location ~ ^/upload(/.*)$ { + client_max_body_size 100M; + alias /usr/blueos$1; + + # Change the access permissions of the uploaded file + dav_access group:rw all:r; + create_full_put_path on; + + # Configure the allowed HTTP methods + dav_methods PUT DELETE MKCOL COPY MOVE; + } + + location /userdata { + root /usr/blueos; + autoindex on; + # use json as it is easily consumed by the frontend + # users already have access through the file browser + autoindex_format json; + # disable cache to improve developer experience + # this should have very little impact for users + expires -1; + add_header Cache-Control no-store; + add_header Access-Control-Allow-Origin *; + } + + # Helper to redirect services to their port + location ~ ^/redirect-port/(?\d+) { + return 301 $scheme://$host:$port; + } + } +} diff --git a/core/tools/nginx/nginx_tls.conf.template b/core/tools/nginx/nginx_tls.conf.template new file mode 100644 index 0000000000..1d57049d34 --- /dev/null +++ b/core/tools/nginx/nginx_tls.conf.template @@ -0,0 +1,259 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; + +events { + worker_connections 768; + # multi_accept on; +} + +http { + client_max_body_size 2G; + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + # Add 10min timeout if we get stuck while processing something like docker images and firmware upload + proxy_read_timeout 600s; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Redirect legacy companion port (Companion 0.0.X) to our new homepage + server{ + listen 2770; + + location / { + rewrite ^/(.*)$ http://$host redirect; + } + } + + server { + listen 80; + listen [::]:80; + + add_header Access-Control-Allow-Origin *; + + # Endpoint used for backend status checks. + # It will always return an empty 204 response when online. + location /status { + return 204; + } + + location /ardupilot-manager { + include cors.conf; + rewrite ^/ardupilot-manager$ /ardupilot-manager/ redirect; + rewrite ^/ardupilot-manager/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8000; + } + + location /bag { + include cors.conf; + rewrite ^/bag$ /bag/ redirect; + rewrite ^/bag/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9101; + } + + location /beacon { + include cors.conf; + rewrite ^/beacon$ /beacon/ redirect; + rewrite ^/beacon/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9111; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Interface-Ip $server_addr; + } + + location /bridget { + include cors.conf; + rewrite ^/bridget$ /bridget/ redirect; + rewrite ^/bridget/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:27353; + } + + location /cable-guy { + include cors.conf; + rewrite ^/cable-guy$ /cable-guy/ redirect; + rewrite ^/cable-guy/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9090; + } + + location /commander { + include cors.conf; + rewrite ^/commander$ /commander/ redirect; + rewrite ^/commander/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9100; + } + + location /docker { + limit_except GET { + deny all; + } + rewrite ^/docker$ /docker/ redirect; + rewrite ^/docker/(.*)$ /$1 break; + proxy_pass http://unix:/var/run/docker.sock:/; + } + + location /file-browser { + rewrite ^/file-browser$ /file-browser/ redirect; + rewrite ^/file-browser/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:7777; + } + + location /helper { + include cors.conf; + rewrite ^/helper$ /helper/ redirect; + rewrite ^/helper/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:81; + } + + location /kraken { + include cors.conf; + rewrite ^/kraken$ /kraken/ redirect; + rewrite ^/kraken/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9134; + } + + location /nmea-injector { + include cors.conf; + rewrite ^/nmea-injector$ /nmea-injector/ redirect; + rewrite ^/nmea-injector/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:2748; + } + + location ^~ /logviewer { + # ^~ makes this a higher priority than locations with regex + expires 10d; + root /home/pi/tools; + } + + location /mavlink2rest { + # Hide the header from the upstream application + proxy_hide_header Access-Control-Allow-Origin; + + include cors.conf; + rewrite ^/mavlink2rest$ /mavlink2rest/ redirect; + rewrite ^/mavlink2rest/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:6040; + # next two lines are required for websockets + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location /mavlink-camera-manager { + include cors.conf; + rewrite ^/mavlink-camera-manager$ /mavlink-camera-manager/ redirect; + rewrite ^/mavlink-camera-manager/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:6020; + } + + location /network-test { + include cors.conf; + rewrite ^/network-test$ /network-test/ redirect; + rewrite ^/network-test/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9120; + # next two lines are required for websockets + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location /system-information { + include cors.conf; + rewrite ^/system-information$ /system-information/ redirect; + rewrite ^/system-information/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:6030; + # next two lines are required for websockets + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location /terminal { + rewrite ^/terminal$ /terminal/ redirect; + rewrite ^/terminal/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8088; + # next two lines are required for websockets + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location /version-chooser { + include cors.conf; + rewrite ^/version-chooser$ /version-chooser/ redirect; + rewrite ^/version-chooser/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8081; + proxy_buffering off; + expires -1; + add_header Cache-Control no-store; + } + + location /wifi-manager { + include cors.conf; + rewrite ^/wifi-manager$ /wifi-manager/ redirect; + rewrite ^/wifi-manager/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9000; + } + + location /ping { + include cors.conf; + rewrite ^/ping$ /ping/ redirect; + rewrite ^/ping/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9110; + } + + location / { + root /home/pi/frontend; + try_files $uri $uri/ /index.html; + autoindex on; + # allow frontend to see files using json + autoindex_format json; + } + + location /assets/ { + root /home/pi/frontend; + try_files $uri $uri/; + autoindex on; + add_header Cache-Control "public, max-age=604800"; + } + + location ~ ^/upload(/.*)$ { + client_max_body_size 100M; + alias /usr/blueos$1; + + # Change the access permissions of the uploaded file + dav_access group:rw all:r; + create_full_put_path on; + + # Configure the allowed HTTP methods + dav_methods PUT DELETE MKCOL COPY MOVE; + } + + location /userdata { + root /usr/blueos; + autoindex on; + # use json as it is easily consumed by the frontend + # users already have access through the file browser + autoindex_format json; + # disable cache to improve developer experience + # this should have very little impact for users + expires -1; + add_header Cache-Control no-store; + add_header Access-Control-Allow-Origin *; + } + + # Helper to redirect services to their port + location ~ ^/redirect-port/(?\d+) { + return 301 $scheme://$host:$port; + } + } +} From 7d1213323d550421b89d3dbbd9275a6618f70692 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Sun, 16 Jun 2024 04:39:40 -0700 Subject: [PATCH 02/21] Fix linting issues --- core/frontend/src/components/wizard/Wizard.vue | 2 +- core/services/beacon/main.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/frontend/src/components/wizard/Wizard.vue b/core/frontend/src/components/wizard/Wizard.vue index 3774fc93ea..a5c8bd570c 100644 --- a/core/frontend/src/components/wizard/Wizard.vue +++ b/core/frontend/src/components/wizard/Wizard.vue @@ -128,7 +128,7 @@
- +
Date: Sun, 16 Jun 2024 04:53:09 -0700 Subject: [PATCH 03/21] Fix more linting --- core/services/beacon/main.py | 103 +++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 46 deletions(-) diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py index 1398e0a848..e62b7bc7be 100755 --- a/core/services/beacon/main.py +++ b/core/services/beacon/main.py @@ -29,7 +29,9 @@ SERVICE_NAME = "beacon" -TLS_CERT_PATH = "" +TLS_CERT_PATH = "/home/pi/tools/nginx/blueos.crt" +TLS_KEY_PATH = "/home/pi/tools/nginx/blueos.key" + class AsyncRunner: def __init__(self, ip_version: IPVersion, interface: str, interface_name: str) -> None: @@ -138,11 +140,11 @@ def set_vehicle_name(self, name: str) -> None: def get_vehicle_name(self) -> str: return self.manager.settings.vehicle_name or "BlueROV2" - + def get_enable_tls(self) -> bool: # return what's in settings or assume no...this may change in the future return self.manager.settings.enable_tls or False - + def set_enable_tls(self, enable_tls: bool) -> None: # handle enabling/disabling tls if not enable_tls and self.get_enable_tls(): @@ -155,8 +157,8 @@ def set_enable_tls(self, enable_tls: bool) -> None: # bounce nginx self.nginx_promote_config(keep_backup=True) # remove old cert - os.unlink("/home/pi/tools/nginx/blueos.crt") - os.unlink("/home/pi/tools/nginx/blueos.key") + os.unlink(TLS_CERT_PATH) + os.unlink(TLS_KEY_PATH) elif enable_tls and not self.get_enable_tls(): # tls is currently disabled and we need to enable # generate cert @@ -170,9 +172,9 @@ def set_enable_tls(self, enable_tls: bool) -> None: self.nginx_promote_config(keep_backup=True) def generate_cert(self) -> bool: - ''' + """ Generates the TLS certificate for the current vehicle hostname and stores in persistent storage - ''' + """ # get the hostname current_hostname = self.get_hostname() alt_names = [] @@ -183,78 +185,87 @@ def generate_cert(self) -> bool: # shell out to openssl to get the cert try: - subprocess.check_call([ - "openssl", "req", "-x509", - "-newkey", "rsa:4096", - "-sha256", - "-days", "1825", - "-nodes", - "-keyout", f"/home/pi/tools/nginx/blueos.key", - "-out", f"/home/pi/tools/nginx/blueos.crt", - "-subj", shlex.quote(f"/CN={self.DEFAULT_HOSTNAME}"), - "-addtext", shlex.quote(f"subjectAltName={','.join(alt_names)}") - ], shell=True) + subprocess.check_call( + [ + "openssl", + "req", + "-x509", + "-newkey", + "rsa:4096", + "-sha256", + "-days", + "1825", + "-nodes", + "-keyout", + TLS_KEY_PATH, + "-out", + TLS_CERT_PATH, + "-subj", + shlex.quote(f"/CN={self.DEFAULT_HOSTNAME}"), + "-addtext", + shlex.quote(f"subjectAltName={','.join(alt_names)}"), + ], + shell=True, + ) except subprocess.CalledProcessError: raise SystemError("Unable to generate certificates") - def generate_new_nginx_config(self, - config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck", - use_tls: bool = False): - ''' + def generate_new_nginx_config( + self, config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck", use_tls: bool = False + ): + """ Generates a new nginx config file at the path specified - ''' + """ # use the templates for simplicity now # TODO: the user may have changed the config, so we should parse and update as needed if use_tls: shutil.copy("/home/pi/tools/nginx/nginx_tls.conf.template", config_path, follow_symlinks=False) else: shutil.copy("/home/pi/tools/nginx/nginx.conf.template", config_path, follow_symlinks=False) - def nginx_config_is_valid(self, config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck") -> bool: - ''' + """ Returns true if the nginx config file is valid - ''' + """ try: - subprocess.check_call([ - "nginx", "-t", "-c", config_path - ], shell=True) + subprocess.check_call(["nginx", "-t", "-c", config_path], shell=True) return True except subprocess.CalledProcessError: # got a non-zero return code indicating the config was not valid return False - - def nginx_promote_config(self, - config_path: str = "/home/pi/tools/nginx/nginx.conf", - new_config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck", - keep_backup: bool = False): - ''' + + def nginx_promote_config( + self, + config_path: str = "/home/pi/tools/nginx/nginx.conf", + new_config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck", + keep_backup: bool = False, + ): + """ Moves the file at new_config_path to config_path and bounces nginx, optionally keeping a backup of config_path - ''' + """ # do both files exist if not os.path.exists(config_path): raise FileNotFoundError("Old config not found") if not os.path.isfile(new_config_path): raise FileNotFoundError("New config not found") - + if keep_backup: - shutil.copyfile(config_path, - f"{config_path}_backup_{datetime.datetime.utcnow().strftime('%Y%m%d_%H%M%S')}", - follow_symlinks=False) + shutil.copyfile( + config_path, + f"{config_path}_backup_{datetime.datetime.utcnow().strftime('%Y%m%d_%H%M%S')}", + follow_symlinks=False, + ) # move it os.unlink(config_path) os.rename(new_config_path, config_path) - + # restart nginx try: - subprocess.check_call([ - "systemctl", "restart", "nginx" - ], shell=True) + subprocess.check_call(["systemctl", "restart", "nginx"], shell=True) except subprocess.CalledProcessError: raise SystemError("Unable to restart nginx") - def create_async_service_infos( self, interface: str, service_name: str, domain_name: str, ip: str ) -> AsyncServiceInfo: @@ -433,7 +444,7 @@ def get_ip(request: Request) -> Any: except KeyError: # We're not going through Nginx for some reason return IpInfo(client_ip=request.scope["client"][0], interface_ip=request.scope["server"][0]) - + @app.get("/use_tls", summary="Get whether TLS should be enabled") @version(1, 0) From 3e0f51ea6a02d61c43b92990eeb64c3b69cd41ea Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Sun, 16 Jun 2024 04:57:28 -0700 Subject: [PATCH 04/21] Fix linting issues --- core/services/beacon/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py index e62b7bc7be..e7029db18e 100755 --- a/core/services/beacon/main.py +++ b/core/services/beacon/main.py @@ -181,7 +181,7 @@ def generate_cert(self) -> bool: alt_names.append(f"DNS:{current_hostname}") alt_names.append(f"DNS:{current_hostname}-wifi") alt_names.append(f"DNS:{current_hostname}-hotspot") - alt_names.append(f"IP:192.168.2.2") + alt_names.append("IP:192.168.2.2") # shell out to openssl to get the cert try: From 388489a6e49b5d24dc50ceca498777acfdeeb5aa Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Sun, 16 Jun 2024 05:04:25 -0700 Subject: [PATCH 05/21] Change exception type --- core/services/beacon/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py index e7029db18e..3377801826 100755 --- a/core/services/beacon/main.py +++ b/core/services/beacon/main.py @@ -207,8 +207,8 @@ def generate_cert(self) -> bool: ], shell=True, ) - except subprocess.CalledProcessError: - raise SystemError("Unable to generate certificates") + except subprocess.CalledProcessError as ex: + raise SystemError("Unable to generate certificates") from ex def generate_new_nginx_config( self, config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck", use_tls: bool = False @@ -263,8 +263,8 @@ def nginx_promote_config( # restart nginx try: subprocess.check_call(["systemctl", "restart", "nginx"], shell=True) - except subprocess.CalledProcessError: - raise SystemError("Unable to restart nginx") + except subprocess.CalledProcessError as ex: + raise SystemError("Unable to restart nginx") from ex def create_async_service_infos( self, interface: str, service_name: str, domain_name: str, ip: str From 25316f497f4e0fc8bbbe3f4db100292d633701e8 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Sun, 16 Jun 2024 05:10:44 -0700 Subject: [PATCH 06/21] Annotate functions --- core/services/beacon/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py index 3377801826..61eb0be3e2 100755 --- a/core/services/beacon/main.py +++ b/core/services/beacon/main.py @@ -171,7 +171,7 @@ def set_enable_tls(self, enable_tls: bool) -> None: # bounce nginx self.nginx_promote_config(keep_backup=True) - def generate_cert(self) -> bool: + def generate_cert(self) -> None: """ Generates the TLS certificate for the current vehicle hostname and stores in persistent storage """ @@ -212,7 +212,7 @@ def generate_cert(self) -> bool: def generate_new_nginx_config( self, config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck", use_tls: bool = False - ): + ) -> None: """ Generates a new nginx config file at the path specified """ @@ -239,7 +239,7 @@ def nginx_promote_config( config_path: str = "/home/pi/tools/nginx/nginx.conf", new_config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck", keep_backup: bool = False, - ): + ) -> None: """ Moves the file at new_config_path to config_path and bounces nginx, optionally keeping a backup of config_path """ From 4ac1f9d15ea390e9c279b1e8653aa5f92e475bfc Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Sun, 16 Jun 2024 06:05:49 -0700 Subject: [PATCH 07/21] Add new settings for beacon --- core/services/beacon/main.py | 8 ++++---- core/services/beacon/settings.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py index 61eb0be3e2..eb53d6b5a1 100755 --- a/core/services/beacon/main.py +++ b/core/services/beacon/main.py @@ -24,7 +24,7 @@ from zeroconf import IPVersion from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf -from settings import ServiceTypes, SettingsV4 +from settings import ServiceTypes, SettingsV5 from typedefs import InterfaceType, IpInfo, MdnsEntry SERVICE_NAME = "beacon" @@ -86,7 +86,7 @@ class Beacon: def __init__(self) -> None: self.runners: Dict[str, AsyncRunner] = {} try: - self.manager = Manager(SERVICE_NAME, SettingsV4) + self.manager = Manager(SERVICE_NAME, SettingsV5) except Exception as e: logger.warning(f"failed to load configuration file ({e}), loading defaults") self.load_default_settings() @@ -103,8 +103,8 @@ def load_default_settings(self) -> None: current_folder = pathlib.Path(__file__).parent.resolve() default_settings_file = current_folder / "default-settings.json" logger.debug("loading settings from ", default_settings_file) - self.manager = Manager(SERVICE_NAME, SettingsV4, load=False) - self.manager.settings = self.manager.load_from_file(SettingsV4, default_settings_file) + self.manager = Manager(SERVICE_NAME, SettingsV5, load=False) + self.manager.settings = self.manager.load_from_file(SettingsV5, default_settings_file) self.manager.save() def load_service_types(self) -> Dict[str, ServiceTypes]: diff --git a/core/services/beacon/settings.py b/core/services/beacon/settings.py index 286c5fdbe4..6f9999278b 100644 --- a/core/services/beacon/settings.py +++ b/core/services/beacon/settings.py @@ -7,6 +7,7 @@ from commonwealth.settings import settings from loguru import logger from pykson import ( + BooleanField, IntegerField, JsonObject, ListField, @@ -200,3 +201,21 @@ def migrate(self, data: Dict[str, Any]) -> None: super().migrate(data) data["VERSION"] = SettingsV4.VERSION + + +class SettingsV5(SettingsV4): + VERSION = 5 + use_tls = BooleanField() + + def __init__(self, *args: str, **kwargs: int) -> None: + super().__init__(*args, **kwargs) + self.VERSION = SettingsV5.VERSION + + def migrate(self, data: Dict[str, Any]) -> None: + if data["VERSION"] == SettingsV5.VERSION: + return + + if data["VERSION"] < SettingsV5.VERSION: + super().migrate(data) + + data["VERSION"] = SettingsV5.VERSION From d967602043f781f9777f2fa7c19df44aeef9b3f6 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Sun, 16 Jun 2024 07:37:16 -0700 Subject: [PATCH 08/21] Use correct settings --- .github/workflows/test-and-deploy.yml | 2 +- core/services/beacon/main.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 872984cb65..f6919a0aaf 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -83,7 +83,7 @@ jobs: matrix: docker: [bootstrap, core] project: [blueos] - platforms: ["linux/arm/v7,linux/arm64/v8,linux/amd64"] + platforms: ["linux/arm64"] steps: - name: Checkout uses: actions/checkout@v3 diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py index eb53d6b5a1..0f68fb9923 100755 --- a/core/services/beacon/main.py +++ b/core/services/beacon/main.py @@ -142,8 +142,8 @@ def get_vehicle_name(self) -> str: return self.manager.settings.vehicle_name or "BlueROV2" def get_enable_tls(self) -> bool: - # return what's in settings or assume no...this may change in the future - return self.manager.settings.enable_tls or False + # TODO: return what's in settings or assume no...this may change in the future + return self.manager.settings.use_tls or False def set_enable_tls(self, enable_tls: bool) -> None: # handle enabling/disabling tls @@ -170,6 +170,8 @@ def set_enable_tls(self, enable_tls: bool) -> None: raise SystemError("Unable to validate staged Nginx config") # bounce nginx self.nginx_promote_config(keep_backup=True) + self.manager.settings.use_tls = enable_tls + self.manager.save() def generate_cert(self) -> None: """ From b45f085ddd1499eddaeac8667453ffde6d701d42 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Sun, 16 Jun 2024 08:51:04 -0700 Subject: [PATCH 09/21] Build for all platforms again --- .github/workflows/test-and-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index f6919a0aaf..872984cb65 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -83,7 +83,7 @@ jobs: matrix: docker: [bootstrap, core] project: [blueos] - platforms: ["linux/arm64"] + platforms: ["linux/arm/v7,linux/arm64/v8,linux/amd64"] steps: - name: Checkout uses: actions/checkout@v3 From f5476df817f9c2f1d5b32cd404f65db155fe21a1 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Sun, 16 Jun 2024 10:55:18 -0700 Subject: [PATCH 10/21] Move nginx config to peristent storage --- core/services/beacon/main.py | 31 +++++++++++++++++-------------- core/start-blueos-core | 9 ++++++++- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py index 0f68fb9923..8cccf6952e 100755 --- a/core/services/beacon/main.py +++ b/core/services/beacon/main.py @@ -8,6 +8,7 @@ import pathlib import shlex import shutil +import signal import socket import subprocess from typing import Any, Dict, List, Optional @@ -29,8 +30,8 @@ SERVICE_NAME = "beacon" -TLS_CERT_PATH = "/home/pi/tools/nginx/blueos.crt" -TLS_KEY_PATH = "/home/pi/tools/nginx/blueos.key" +TLS_CERT_PATH = "/etc/blueos/nginx/blueos.crt" +TLS_KEY_PATH = "/etc/blueos/nginx/blueos.key" class AsyncRunner: @@ -204,33 +205,34 @@ def generate_cert(self) -> None: TLS_CERT_PATH, "-subj", shlex.quote(f"/CN={self.DEFAULT_HOSTNAME}"), - "-addtext", + "-addext", shlex.quote(f"subjectAltName={','.join(alt_names)}"), ], - shell=True, + shell=False, ) except subprocess.CalledProcessError as ex: raise SystemError("Unable to generate certificates") from ex def generate_new_nginx_config( - self, config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck", use_tls: bool = False + self, config_path: str = "/etc/blueos/nginx/nginx.conf.ondeck", use_tls: bool = False ) -> None: """ Generates a new nginx config file at the path specified """ # use the templates for simplicity now + # also, the templates are in core's tools directory but the live config lives in /etc/blueos/nginx # TODO: the user may have changed the config, so we should parse and update as needed if use_tls: shutil.copy("/home/pi/tools/nginx/nginx_tls.conf.template", config_path, follow_symlinks=False) else: shutil.copy("/home/pi/tools/nginx/nginx.conf.template", config_path, follow_symlinks=False) - def nginx_config_is_valid(self, config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck") -> bool: + def nginx_config_is_valid(self, config_path: str = "/etc/blueos/nginx/nginx.conf.ondeck") -> bool: """ Returns true if the nginx config file is valid """ try: - subprocess.check_call(["nginx", "-t", "-c", config_path], shell=True) + subprocess.check_call(["nginx", "-t", "-c", config_path], shell=False) return True except subprocess.CalledProcessError: # got a non-zero return code indicating the config was not valid @@ -238,8 +240,8 @@ def nginx_config_is_valid(self, config_path: str = "/home/pi/tools/nginx/nginx.c def nginx_promote_config( self, - config_path: str = "/home/pi/tools/nginx/nginx.conf", - new_config_path: str = "/home/pi/tools/nginx/nginx.conf.ondeck", + config_path: str = "/etc/blueos/nginx/nginx.conf", + new_config_path: str = "/etc/blueos/nginx/nginx.conf.ondeck", keep_backup: bool = False, ) -> None: """ @@ -262,11 +264,12 @@ def nginx_promote_config( os.unlink(config_path) os.rename(new_config_path, config_path) - # restart nginx - try: - subprocess.check_call(["systemctl", "restart", "nginx"], shell=True) - except subprocess.CalledProcessError as ex: - raise SystemError("Unable to restart nginx") from ex + # reload nginx config by getting the PID of the master process and sending a SIGHUP + if not os.path.exists("/run/nginx.pid"): + raise SystemError("No nginx master PID found") + with open("/run/nginx.pid", "r", encoding="utf-8") as pidf: + nginx_pid = int(pidf.read()) + os.kill(nginx_pid, signal.SIGHUP) def create_async_service_infos( self, interface: str, service_name: str, domain_name: str, ip: str diff --git a/core/start-blueos-core b/core/start-blueos-core index aa6a0190ef..727d765a62 100755 --- a/core/start-blueos-core +++ b/core/start-blueos-core @@ -93,6 +93,13 @@ mkdir -p /usr/blueos/userdata/settings find /usr/blueos -type d -exec chmod a+rw {} \; find /usr/blueos -type f -exec chmod a+rw {} \; +# copy nginx configs over from $TOOLS_PATH to persistent storage if we don't already have one +if [ ! -d "/etc/blueos/nginx" ]; then + mkdir -p /etc/blueos/nginx + cp $TOOLS_PATH/nginx/nginx.conf /etc/blueos/nginx/nginx.conf + cp $TOOLS_PATH/nginx/cors.conf /etc/blueos/nginx/cors.conf +fi + # These services have priority because they do the fundamental for the vehicle to work, # and by initializing them first we reduce the time users have to wait to control the vehicle. # From tests with QGC and Pi3, the reboot time was ~1min42s when not using this strategy, @@ -124,7 +131,7 @@ SERVICES=( 'ping',0,"nice -19 $RUN_AS_REGULAR_USER $SERVICES_PATH/ping/main.py" 'user_terminal',0,"cat /etc/motd" 'ttyd',250,'nice -19 ttyd -p 8088 sh -c "/usr/bin/tmux attach -t user_terminal || /usr/bin/tmux new -s user_terminal"' - 'nginx',250,"nice -18 nginx -g \"daemon off;\" -c $TOOLS_PATH/nginx/nginx.conf" + 'nginx',250,"nice -18 nginx -g \"daemon off;\" -c /etc/blueos/nginx/nginx.conf" 'log_zipper',250,"nice -20 $SERVICES_PATH/log_zipper/main.py '/shortcuts/system_logs/\\\\*\\\\*/\\\\*.log' --max-age-minutes 60" 'bag_of_holding',250,"$SERVICES_PATH/bag_of_holding/main.py" ) From 6220003be93b58d1e525c603b184f1033c8c1b4e Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Sun, 16 Jun 2024 12:27:01 -0700 Subject: [PATCH 11/21] Allow self-signed cert for localhost --- bootstrap/bootstrap/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/bootstrap/bootstrap.py b/bootstrap/bootstrap/bootstrap.py index 388104795e..481283ebb6 100755 --- a/bootstrap/bootstrap/bootstrap.py +++ b/bootstrap/bootstrap/bootstrap.py @@ -226,7 +226,7 @@ def is_version_chooser_online(self) -> bool: bool: True if version chooser is online, False otherwise. """ try: - response = requests.get("http://localhost/version-chooser/v1.0/version/current", timeout=10) + response = requests.get("http://localhost/version-chooser/v1.0/version/current", timeout=10, verify=False) if Bootstrapper.SETTINGS_NAME_CORE in response.json()["repository"]: return True except Exception as e: From ac1a821e3e20dcd26c72c77681036abe4a6b7921 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Sun, 16 Jun 2024 12:52:17 -0700 Subject: [PATCH 12/21] Add TLS config template --- core/tools/nginx/nginx_tls.conf.template | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/core/tools/nginx/nginx_tls.conf.template b/core/tools/nginx/nginx_tls.conf.template index 1d57049d34..124f7227d9 100644 --- a/core/tools/nginx/nginx_tls.conf.template +++ b/core/tools/nginx/nginx_tls.conf.template @@ -41,8 +41,22 @@ http { } server { - listen 80; - listen [::]:80; + listen 80 default_server; + listen [::]:80 default_server; + + server_name _; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name _; + ssl_certificate blueos.crt; + ssl_certificate_key blueos.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; add_header Access-Control-Allow-Origin *; From 53fff462964a9de928409a3ab878f48f458cc886 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:26:05 -0700 Subject: [PATCH 13/21] Add USB-OTG IP to cert alt name --- core/services/beacon/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py index 8cccf6952e..a0abd60aef 100755 --- a/core/services/beacon/main.py +++ b/core/services/beacon/main.py @@ -185,6 +185,7 @@ def generate_cert(self) -> None: alt_names.append(f"DNS:{current_hostname}-wifi") alt_names.append(f"DNS:{current_hostname}-hotspot") alt_names.append("IP:192.168.2.2") + alt_names.append("IP:192.168.3.1") # shell out to openssl to get the cert try: From 7764b7cb5693f300fc2ff8540dc8bd90d05f2ee7 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:27:35 -0700 Subject: [PATCH 14/21] Fix typo in error message --- core/frontend/src/components/wizard/Wizard.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/frontend/src/components/wizard/Wizard.vue b/core/frontend/src/components/wizard/Wizard.vue index 2ad6657c1f..418c10a24b 100644 --- a/core/frontend/src/components/wizard/Wizard.vue +++ b/core/frontend/src/components/wizard/Wizard.vue @@ -614,7 +614,7 @@ export default Vue.extend({ async setTLS(): Promise { return beacon.setTLS(this.enable_tls) .then(() => undefined) - .catch(() => 'False to change TLS configuration') + .catch(() => 'Failed to change TLS configuration') }, async disableWifiHotspot(): Promise { return back_axios({ From b01afcbe8807b19e340a94cc280dd7e4862bba96 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:46:50 -0700 Subject: [PATCH 15/21] Regenerate cert on hostname change --- core/services/beacon/main.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py index a0abd60aef..ca2157666a 100755 --- a/core/services/beacon/main.py +++ b/core/services/beacon/main.py @@ -6,6 +6,7 @@ import logging import os import pathlib +import re import shlex import shutil import signal @@ -128,6 +129,12 @@ def set_hostname(self, hostname: str) -> None: case InterfaceType.HOTSPOT: interface.domain_names = [f"{hostname}-hotspot"] self.manager.save() + # if the hostname is changed and we have TLS enabled we need to regenerate the cert + if self.get_enable_tls(): + os.unlink(TLS_KEY_PATH) + os.unlink(TLS_CERT_PATH) + self.generate_cert() + self.reload_nginx_config() def get_hostname(self) -> str: try: @@ -266,6 +273,12 @@ def nginx_promote_config( os.rename(new_config_path, config_path) # reload nginx config by getting the PID of the master process and sending a SIGHUP + self.reload_nginx_config() + + def reload_nginx_config(self) -> None: + """ + Sends a SIGHUP to the nginx master process to trigger a reload of the running config + """ if not os.path.exists("/run/nginx.pid"): raise SystemError("No nginx master PID found") with open("/run/nginx.pid", "r", encoding="utf-8") as pidf: @@ -420,6 +433,10 @@ def get_services() -> Any: @app.post("/hostname", summary="Set the hostname for mDNS.") @version(1, 0) def set_hostname(hostname: str) -> Any: + # beacon.ts has a regex to validate hostname format, but we should check here too + hostname_regex = re.compile(r"^[a-zA-Z0-9-]+$") + if not hostname_regex.match(hostname): + raise ValueError("Invalid characters in hostname") return beacon.set_hostname(hostname) From 4218817f21e29ea1bdeaeff5bd9c15f8087daa1f Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:55:57 -0700 Subject: [PATCH 16/21] Refactor to minimize hardcoded paths --- core/services/beacon/main.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py index ca2157666a..d4d9254494 100755 --- a/core/services/beacon/main.py +++ b/core/services/beacon/main.py @@ -31,8 +31,13 @@ SERVICE_NAME = "beacon" -TLS_CERT_PATH = "/etc/blueos/nginx/blueos.crt" -TLS_KEY_PATH = "/etc/blueos/nginx/blueos.key" +NGINX_ROOT_PATH = "/etc/blueos/nginx" +NGINX_PID_PATH = "/run/nginx.pid" # this is defined in the nginx config +TLS_CERT_PATH = os.path.join(NGINX_ROOT_PATH, "blueos.crt") +TLS_KEY_PATH = os.path.join(NGINX_ROOT_PATH, "blueos.key") + +BLUEOS_TOOLS_PATH = "/home/pi/tools" +BLUEOS_TOOLS_NGINX_PATH = os.path.join(BLUEOS_TOOLS_PATH, "nginx") class AsyncRunner: @@ -222,7 +227,7 @@ def generate_cert(self) -> None: raise SystemError("Unable to generate certificates") from ex def generate_new_nginx_config( - self, config_path: str = "/etc/blueos/nginx/nginx.conf.ondeck", use_tls: bool = False + self, config_path: str = os.path.join(NGINX_ROOT_PATH, "nginx.conf.ondeck"), use_tls: bool = False ) -> None: """ Generates a new nginx config file at the path specified @@ -231,11 +236,15 @@ def generate_new_nginx_config( # also, the templates are in core's tools directory but the live config lives in /etc/blueos/nginx # TODO: the user may have changed the config, so we should parse and update as needed if use_tls: - shutil.copy("/home/pi/tools/nginx/nginx_tls.conf.template", config_path, follow_symlinks=False) + shutil.copy( + os.path.join(BLUEOS_TOOLS_NGINX_PATH, "nginx_tls.conf.template"), config_path, follow_symlinks=False + ) else: - shutil.copy("/home/pi/tools/nginx/nginx.conf.template", config_path, follow_symlinks=False) + shutil.copy( + os.path.join(BLUEOS_TOOLS_NGINX_PATH, "nginx.conf.template"), config_path, follow_symlinks=False + ) - def nginx_config_is_valid(self, config_path: str = "/etc/blueos/nginx/nginx.conf.ondeck") -> bool: + def nginx_config_is_valid(self, config_path: str = os.path.join(NGINX_ROOT_PATH, "nginx.conf.ondeck")) -> bool: """ Returns true if the nginx config file is valid """ @@ -248,8 +257,8 @@ def nginx_config_is_valid(self, config_path: str = "/etc/blueos/nginx/nginx.conf def nginx_promote_config( self, - config_path: str = "/etc/blueos/nginx/nginx.conf", - new_config_path: str = "/etc/blueos/nginx/nginx.conf.ondeck", + config_path: str = os.path.join(NGINX_ROOT_PATH, "nginx.conf"), + new_config_path: str = os.path.join(NGINX_ROOT_PATH, "nginx.conf.ondeck"), keep_backup: bool = False, ) -> None: """ @@ -279,9 +288,9 @@ def reload_nginx_config(self) -> None: """ Sends a SIGHUP to the nginx master process to trigger a reload of the running config """ - if not os.path.exists("/run/nginx.pid"): + if not os.path.exists(NGINX_PID_PATH): raise SystemError("No nginx master PID found") - with open("/run/nginx.pid", "r", encoding="utf-8") as pidf: + with open(NGINX_PID_PATH, "r", encoding="utf-8") as pidf: nginx_pid = int(pidf.read()) os.kill(nginx_pid, signal.SIGHUP) From 5f86ecc849b4d65ce25c367dc28a7a8a658919f4 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:58:17 -0700 Subject: [PATCH 17/21] Update Helper to use the new nginx path --- core/services/helper/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/services/helper/main.py b/core/services/helper/main.py index 8134de2d2a..00945abeaf 100755 --- a/core/services/helper/main.py +++ b/core/services/helper/main.py @@ -552,7 +552,7 @@ async def periodic() -> None: enable_latest=True, ) -port_to_service_map: Dict[int, str] = parse_nginx_file("/home/pi/tools/nginx/nginx.conf") +port_to_service_map: Dict[int, str] = parse_nginx_file("/etc/blueos/nginx/nginx.conf") if __name__ == "__main__": loop = asyncio.new_event_loop() From e44af9834c758ae981d0de3e293a9304cb3c87fa Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:48:00 -0700 Subject: [PATCH 18/21] Exclude 443 from service discovery --- core/services/helper/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/services/helper/main.py b/core/services/helper/main.py index 00945abeaf..cb24515b58 100755 --- a/core/services/helper/main.py +++ b/core/services/helper/main.py @@ -195,6 +195,7 @@ class Helper: SKIP_PORTS: Set[int] = { 22, # SSH 80, # BlueOS + 443, # BlueOS TLS 6021, # Mavlink Camera Manager's WebRTC signaller 8554, # Mavlink Camera Manager's RTSP server 5777, # ardupilot-manager's Mavlink TCP Server From 75fbdbb31f5f1a041617faa73e25587e48c70dcf Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Tue, 13 Aug 2024 20:50:00 -0700 Subject: [PATCH 19/21] Fix whitespace --- core/services/helper/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/services/helper/main.py b/core/services/helper/main.py index cb24515b58..afda701d0e 100755 --- a/core/services/helper/main.py +++ b/core/services/helper/main.py @@ -195,7 +195,7 @@ class Helper: SKIP_PORTS: Set[int] = { 22, # SSH 80, # BlueOS - 443, # BlueOS TLS + 443, # BlueOS TLS 6021, # Mavlink Camera Manager's WebRTC signaller 8554, # Mavlink Camera Manager's RTSP server 5777, # ardupilot-manager's Mavlink TCP Server From 74e7f0c30c26fecdb4cfb30f2d7c57f9916a8795 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:17:56 -0700 Subject: [PATCH 20/21] Disable urllib3 warnings for TLS --- bootstrap/bootstrap/bootstrap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bootstrap/bootstrap/bootstrap.py b/bootstrap/bootstrap/bootstrap.py index 481283ebb6..ccd54e4758 100755 --- a/bootstrap/bootstrap/bootstrap.py +++ b/bootstrap/bootstrap/bootstrap.py @@ -11,6 +11,7 @@ import docker import requests from loguru import logger +import urllib3 class Bootstrapper: @@ -248,6 +249,7 @@ def remove(self, container: str) -> None: def run(self) -> None: """Runs the bootstrapper""" logger.info(f"Starting bootstrap {self.bootstrap_version()}") + urllib3.disable_warnings() while True: time.sleep(5) for image in self.read_config_file(): From b0700fa73e4a42ca5789dc08cffcfcd8315b5041 Mon Sep 17 00:00:00 2001 From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:23:56 -0700 Subject: [PATCH 21/21] Reorder imports --- bootstrap/bootstrap/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/bootstrap/bootstrap.py b/bootstrap/bootstrap/bootstrap.py index ccd54e4758..65b5ffbe11 100755 --- a/bootstrap/bootstrap/bootstrap.py +++ b/bootstrap/bootstrap/bootstrap.py @@ -10,8 +10,8 @@ import docker import requests -from loguru import logger import urllib3 +from loguru import logger class Bootstrapper: