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: