From a40975839fe04a5d4cdaa47ea7747dea166046b4 Mon Sep 17 00:00:00 2001
From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com>
Date: Mon, 16 Jun 2025 19:03:51 -0700
Subject: [PATCH 1/8] Rebased TLS support
---
bootstrap/bootstrap/bootstrap.py | 3 +-
.../frontend/src/components/wizard/Wizard.vue | 17 +
core/frontend/src/store/beacon.ts | 33 ++
core/services/beacon/main.py | 185 ++++++++++-
core/services/beacon/settings.py | 18 ++
core/services/helper/main.py | 3 +-
core/start-blueos-core | 9 +-
core/tools/nginx/nginx.conf.template | 284 +++++++++++++++++
core/tools/nginx/nginx_tls.conf.template | 298 ++++++++++++++++++
9 files changed, 843 insertions(+), 7 deletions(-)
create mode 100644 core/tools/nginx/nginx.conf.template
create mode 100644 core/tools/nginx/nginx_tls.conf.template
diff --git a/bootstrap/bootstrap/bootstrap.py b/bootstrap/bootstrap/bootstrap.py
index db1e7094fd..5297ccc580 100755
--- a/bootstrap/bootstrap/bootstrap.py
+++ b/bootstrap/bootstrap/bootstrap.py
@@ -10,6 +10,7 @@
import docker
import requests
+import urllib3
from loguru import logger
@@ -250,7 +251,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"]:
if not self.version_chooser_is_online:
self.version_chooser_is_online = True
diff --git a/core/frontend/src/components/wizard/Wizard.vue b/core/frontend/src/components/wizard/Wizard.vue
index 9466dfb50c..fa649f8deb 100644
--- a/core/frontend/src/components/wizard/Wizard.vue
+++ b/core/frontend/src/components/wizard/Wizard.vue
@@ -131,6 +131,7 @@
+
this.setTLS(),
+ message: undefined,
+ done: false,
+ skip: false,
+ started: false,
+ },
{
title: 'Set vehicle image',
summary: 'Set image to be used for vehicle thumbnail',
@@ -599,6 +611,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(() => 'Failed 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 b3639f54cb..562b928d63 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
+
fetchAvailableDomainsTask = new OneMoreTime(
{ delay: 5000 },
)
@@ -61,6 +63,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
@@ -248,6 +256,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 (isBackendOffline(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 ee4447466d..1d91a47965 100755
--- a/core/services/beacon/main.py
+++ b/core/services/beacon/main.py
@@ -1,10 +1,17 @@
#! /usr/bin/env python3
import argparse
import asyncio
+import datetime
import itertools
import logging
+import os
import pathlib
+import re
+import shlex
+import shutil
+import signal
import socket
+import subprocess
from typing import Any, Dict, List, Optional
import psutil
@@ -20,11 +27,19 @@
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"
+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:
def __init__(self, ip_version: IPVersion, interface: str, interface_name: str) -> None:
@@ -79,7 +94,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()
@@ -96,8 +111,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]:
@@ -125,6 +140,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:
@@ -138,6 +159,146 @@ 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:
+ # 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
+ 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(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
+ 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)
+ self.manager.settings.use_tls = enable_tls
+ self.manager.save()
+
+ def generate_cert(self) -> None:
+ """
+ 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("IP:192.168.2.2")
+ alt_names.append("IP:192.168.3.1")
+
+ # shell out to openssl to get the cert
+ try:
+ 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}"),
+ "-addext",
+ shlex.quote(f"subjectAltName={','.join(alt_names)}"),
+ ],
+ shell=False,
+ )
+ except subprocess.CalledProcessError as ex:
+ raise SystemError("Unable to generate certificates") from ex
+
+ def generate_new_nginx_config(
+ 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
+ """
+ # 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(
+ os.path.join(BLUEOS_TOOLS_NGINX_PATH, "nginx_tls.conf.template"), config_path, follow_symlinks=False
+ )
+ else:
+ 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 = os.path.join(NGINX_ROOT_PATH, "nginx.conf.ondeck")) -> bool:
+ """
+ Returns true if the nginx config file is valid
+ """
+ try:
+ 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
+ return False
+
+ def nginx_promote_config(
+ self,
+ 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:
+ """
+ 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)
+
+ # 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(NGINX_PID_PATH):
+ raise SystemError("No nginx master PID found")
+ with open(NGINX_PID_PATH, "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
@@ -287,6 +448,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)
@@ -319,6 +484,18 @@ def get_ip(request: Request) -> Any:
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/services/beacon/settings.py b/core/services/beacon/settings.py
index 286c5fdbe4..bb38661b49 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,20 @@ 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
diff --git a/core/services/helper/main.py b/core/services/helper/main.py
index 926da6932a..0f9ff57f03 100755
--- a/core/services/helper/main.py
+++ b/core/services/helper/main.py
@@ -207,6 +207,7 @@ class Helper:
SKIP_PORTS: Set[int] = {
22, # SSH
80, # BlueOS
+ 443, # BlueoS TLS
5201, # Iperf
6021, # Mavlink Camera Manager's WebRTC signaller
7000, # Major Tom does not have a public API yet
@@ -683,7 +684,7 @@ async def root() -> HTMLResponse:
return HTMLResponse(content=html_content, status_code=200)
-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")
async def main() -> None:
diff --git a/core/start-blueos-core b/core/start-blueos-core
index cbb2830de6..5c63b1fd35 100755
--- a/core/start-blueos-core
+++ b/core/start-blueos-core
@@ -98,6 +98,13 @@ mkdir -p /usr/blueos/userdata/settings
find /usr/blueos/userdata -type d -exec chmod a+rw {} \;
find /usr/blueos/userdata -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,
@@ -130,7 +137,7 @@ SERVICES=(
'ping',0,"nice -19 $RUN_AS_REGULAR_USER_BEGIN $SERVICES_PATH/ping/main.py $RUN_AS_REGULAR_USER_END"
'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"
)
diff --git a/core/tools/nginx/nginx.conf.template b/core/tools/nginx/nginx.conf.template
new file mode 100644
index 0000000000..58f05580a2
--- /dev/null
+++ b/core/tools/nginx/nginx.conf.template
@@ -0,0 +1,284 @@
+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;
+
+ # Cache
+ proxy_cache_path /var/cache/nginx keys_zone=ourcache:10m levels=1:2 max_size=1g inactive=30d;
+
+ ##
+ # 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; # IPv4
+ listen [::]:2770; # IPv6
+
+ location / {
+ rewrite ^/(.*)$ http://$host redirect;
+ }
+ }
+
+ server {
+ listen 80; # IPv4
+ listen [::]:80; # IPv6
+
+ 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 ~ ^/cache/(.*) {
+ resolver 8.8.8.8 ipv6=off;
+ set $target $1;
+ proxy_cache ourcache;
+ proxy_cache_valid 200 30d;
+ proxy_cache_revalidate on;
+ proxy_cache_use_stale error timeout invalid_header http_500 http_502 http_503 http_504;
+ proxy_pass https://$target;
+ }
+
+ location /ardupilot-manager/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:8000/;
+ }
+
+ location /bag/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:9101/;
+ }
+
+ location /beacon/ {
+ include cors.conf;
+ 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;
+ proxy_pass http://127.0.0.1:27353/;
+ }
+
+ location /cable-guy/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:9090/;
+ }
+
+ location /commander/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:9100/;
+ }
+
+ location /docker/ {
+ limit_except GET {
+ deny all;
+ }
+ proxy_pass http://unix:/var/run/docker.sock:/;
+ }
+
+ location /file-browser/ {
+ proxy_pass http://127.0.0.1:7777/;
+ }
+
+ location /helper/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:81/;
+ }
+
+ location /kraken/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:9134/;
+ }
+
+ location /nmea-injector/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:2748/;
+ }
+
+ location ^~ /logviewer/ {
+ # ^~ makes this a higher priority than locations with regex
+ expires 10d;
+ root /var/www/html;
+ }
+
+ location /mavlink2rest/ {
+ # Hide the header from the upstream application
+ proxy_hide_header Access-Control-Allow-Origin;
+
+ include cors.conf;
+ 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 /webrtc/ws/ {
+ # Hide the header from the upstream application
+ proxy_hide_header Access-Control-Allow-Origin;
+
+ include cors.conf;
+ proxy_pass http://127.0.0.1:6021/;
+ proxy_http_version 1.1;
+ # 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;
+ proxy_pass http://127.0.0.1:6020/;
+ }
+
+ location /network-test/ {
+ include cors.conf;
+ 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;
+ 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/ {
+ 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;
+ 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;
+ proxy_pass http://127.0.0.1:9000/;
+ }
+
+ location /ping/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:9110/;
+ }
+
+ location /zenoh/ {
+ proxy_hide_header Access-Control-Allow-Origin;
+
+ rewrite ^/zenoh(/.*)$ $1 break;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+
+ include cors.conf;
+ proxy_pass http://127.0.0.1:7117/;
+
+ # Required for WebSockets
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+ }
+
+ location /zenoh-api/ {
+ proxy_hide_header Access-Control-Allow-Origin;
+
+ rewrite ^/zenoh-api(/.*)$ $1 break;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+
+ include cors.conf;
+ proxy_pass http://127.0.0.1:7118/;
+
+ # Required for WebSockets
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+ }
+
+ location / {
+ root /home/pi/frontend;
+ try_files $uri $uri/ /index.html;
+ autoindex on;
+ # allow frontend to see files using json
+ autoindex_format json;
+
+ # prevent caching of index.html
+ if ($uri = /index.html) {
+ add_header Last-Modified "";
+ add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
+ }
+ }
+
+ 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/;
+
+ # 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;
+ }
+ include /home/pi/tools/nginx/extensions/*.conf;
+ }
+}
diff --git a/core/tools/nginx/nginx_tls.conf.template b/core/tools/nginx/nginx_tls.conf.template
new file mode 100644
index 0000000000..5e7e2cbab2
--- /dev/null
+++ b/core/tools/nginx/nginx_tls.conf.template
@@ -0,0 +1,298 @@
+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;
+
+ # Cache
+ proxy_cache_path /var/cache/nginx keys_zone=ourcache:10m levels=1:2 max_size=1g inactive=30d;
+
+ ##
+ # 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; # IPv4
+ listen [::]:2770; # IPv6
+
+ location / {
+ rewrite ^/(.*)$ http://$host redirect;
+ }
+ }
+
+ server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+
+ server_name _;
+ return 301 https://$host$request_uri;
+ }
+
+ server {
+ listen 443; # IPv4
+ listen [::]:443; # IPv6
+
+ 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 *;
+
+ # Endpoint used for backend status checks.
+ # It will always return an empty 204 response when online.
+ location = /status {
+ return 204;
+ }
+
+ location ~ ^/cache/(.*) {
+ resolver 8.8.8.8 ipv6=off;
+ set $target $1;
+ proxy_cache ourcache;
+ proxy_cache_valid 200 30d;
+ proxy_cache_revalidate on;
+ proxy_cache_use_stale error timeout invalid_header http_500 http_502 http_503 http_504;
+ proxy_pass https://$target;
+ }
+
+ location /ardupilot-manager/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:8000/;
+ }
+
+ location /bag/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:9101/;
+ }
+
+ location /beacon/ {
+ include cors.conf;
+ 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;
+ proxy_pass http://127.0.0.1:27353/;
+ }
+
+ location /cable-guy/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:9090/;
+ }
+
+ location /commander/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:9100/;
+ }
+
+ location /docker/ {
+ limit_except GET {
+ deny all;
+ }
+ proxy_pass http://unix:/var/run/docker.sock:/;
+ }
+
+ location /file-browser/ {
+ proxy_pass http://127.0.0.1:7777/;
+ }
+
+ location /helper/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:81/;
+ }
+
+ location /kraken/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:9134/;
+ }
+
+ location /nmea-injector/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:2748/;
+ }
+
+ location ^~ /logviewer/ {
+ # ^~ makes this a higher priority than locations with regex
+ expires 10d;
+ root /var/www/html;
+ }
+
+ location /mavlink2rest/ {
+ # Hide the header from the upstream application
+ proxy_hide_header Access-Control-Allow-Origin;
+
+ include cors.conf;
+ 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 /webrtc/ws/ {
+ # Hide the header from the upstream application
+ proxy_hide_header Access-Control-Allow-Origin;
+
+ include cors.conf;
+ proxy_pass http://127.0.0.1:6021/;
+ proxy_http_version 1.1;
+ # 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;
+ proxy_pass http://127.0.0.1:6020/;
+ }
+
+ location /network-test/ {
+ include cors.conf;
+ 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;
+ 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/ {
+ 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;
+ 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;
+ proxy_pass http://127.0.0.1:9000/;
+ }
+
+ location /ping/ {
+ include cors.conf;
+ proxy_pass http://127.0.0.1:9110/;
+ }
+
+ location /zenoh/ {
+ proxy_hide_header Access-Control-Allow-Origin;
+
+ rewrite ^/zenoh(/.*)$ $1 break;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+
+ include cors.conf;
+ proxy_pass http://127.0.0.1:7117/;
+
+ # Required for WebSockets
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+ }
+
+ location /zenoh-api/ {
+ proxy_hide_header Access-Control-Allow-Origin;
+
+ rewrite ^/zenoh-api(/.*)$ $1 break;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+
+ include cors.conf;
+ proxy_pass http://127.0.0.1:7118/;
+
+ # Required for WebSockets
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+ }
+
+ location / {
+ root /home/pi/frontend;
+ try_files $uri $uri/ /index.html;
+ autoindex on;
+ # allow frontend to see files using json
+ autoindex_format json;
+
+ # prevent caching of index.html
+ if ($uri = /index.html) {
+ add_header Last-Modified "";
+ add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
+ }
+ }
+
+ 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/;
+
+ # 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;
+ }
+ include /home/pi/tools/nginx/extensions/*.conf;
+ }
+}
From 73cfd2c80cb69047b3ebfc3d7352f1b42680eea4 Mon Sep 17 00:00:00 2001
From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com>
Date: Wed, 18 Jun 2025 12:02:15 -0700
Subject: [PATCH 2/8] Format beacon code
---
core/services/beacon/main.py | 2 +-
core/services/beacon/settings.py | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py
index 1d91a47965..fe59991426 100755
--- a/core/services/beacon/main.py
+++ b/core/services/beacon/main.py
@@ -159,7 +159,7 @@ 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:
# TODO: return what's in settings or assume no...this may change in the future
return self.manager.settings.use_tls or False
diff --git a/core/services/beacon/settings.py b/core/services/beacon/settings.py
index bb38661b49..6f9999278b 100644
--- a/core/services/beacon/settings.py
+++ b/core/services/beacon/settings.py
@@ -202,6 +202,7 @@ def migrate(self, data: Dict[str, Any]) -> None:
data["VERSION"] = SettingsV4.VERSION
+
class SettingsV5(SettingsV4):
VERSION = 5
use_tls = BooleanField()
From 69e8870224335f20ca5b5a147563e78b612c93d7 Mon Sep 17 00:00:00 2001
From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com>
Date: Wed, 18 Jun 2025 12:04:52 -0700
Subject: [PATCH 3/8] Formwat wizard code
---
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 fa649f8deb..3f2aa53a74 100644
--- a/core/frontend/src/components/wizard/Wizard.vue
+++ b/core/frontend/src/components/wizard/Wizard.vue
@@ -131,7 +131,7 @@
-
+
Date: Fri, 20 Jun 2025 14:45:19 -0700
Subject: [PATCH 4/8] Fix nginx config in TLS mode
---
core/tools/nginx/nginx_tls.conf.template | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/core/tools/nginx/nginx_tls.conf.template b/core/tools/nginx/nginx_tls.conf.template
index 5e7e2cbab2..c98696db2e 100644
--- a/core/tools/nginx/nginx_tls.conf.template
+++ b/core/tools/nginx/nginx_tls.conf.template
@@ -53,8 +53,8 @@ http {
}
server {
- listen 443; # IPv4
- listen [::]:443; # IPv6
+ listen 443 ssl http2; # IPv4
+ listen [::]:443 ssl http2; # IPv6
server_name _;
ssl_certificate blueos.crt;
From 454e1197ce6b10ae3a4c139fb060328cb4a1d2cc Mon Sep 17 00:00:00 2001
From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com>
Date: Fri, 20 Jun 2025 14:50:06 -0700
Subject: [PATCH 5/8] Prevent logging TLS warnings in bootstrap
---
bootstrap/bootstrap/bootstrap.py | 69 ++++++++++++++++++++++++--------
1 file changed, 53 insertions(+), 16 deletions(-)
diff --git a/bootstrap/bootstrap/bootstrap.py b/bootstrap/bootstrap/bootstrap.py
index 5297ccc580..5d598f4780 100755
--- a/bootstrap/bootstrap/bootstrap.py
+++ b/bootstrap/bootstrap/bootstrap.py
@@ -14,6 +14,9 @@
from loguru import logger
+urllib3.disable_warnings()
+
+
class Bootstrapper:
DEFAULT_FILE_PATH = pathlib.Path("/bootstrap/startup.json.default")
@@ -25,7 +28,9 @@ class Bootstrapper:
SETTINGS_NAME_CORE = "core"
core_last_response_time = time.monotonic()
- def __init__(self, client: docker.DockerClient, low_level_api: docker.APIClient = None) -> None:
+ def __init__(
+ self, client: docker.DockerClient, low_level_api: docker.APIClient = None
+ ) -> None:
self.version_chooser_is_online = False
self.client: docker.DockerClient = client
self.core_last_response_time = time.monotonic()
@@ -38,7 +43,9 @@ def __init__(self, client: docker.DockerClient, low_level_api: docker.APIClient
def overwrite_config_file_with_defaults() -> None:
"""Overwrites the config file with the default configuration"""
try:
- os.makedirs(pathlib.Path(Bootstrapper.DOCKER_CONFIG_FILE_PATH).parent, exist_ok=True)
+ os.makedirs(
+ pathlib.Path(Bootstrapper.DOCKER_CONFIG_FILE_PATH).parent, exist_ok=True
+ )
except Exception as exception:
raise RuntimeError(
f"Failed to create folder for configuration file: {Bootstrapper.DOCKER_CONFIG_FILE_PATH}"
@@ -46,12 +53,15 @@ def overwrite_config_file_with_defaults() -> None:
try:
shutil.copy(
- Bootstrapper.DOCKER_CONFIG_FILE_PATH, Bootstrapper.DOCKER_CONFIG_FILE_PATH.with_suffix(".json.bak")
+ Bootstrapper.DOCKER_CONFIG_FILE_PATH,
+ Bootstrapper.DOCKER_CONFIG_FILE_PATH.with_suffix(".json.bak"),
)
except FileNotFoundError:
# we don't mind if the file is already there
pass
- shutil.copy(Bootstrapper.DEFAULT_FILE_PATH, Bootstrapper.DOCKER_CONFIG_FILE_PATH)
+ shutil.copy(
+ Bootstrapper.DEFAULT_FILE_PATH, Bootstrapper.DOCKER_CONFIG_FILE_PATH
+ )
@staticmethod
def read_config_file() -> Dict[str, Any]:
@@ -64,21 +74,31 @@ def read_config_file() -> Dict[str, Any]:
# Tries to open the current file
config = {}
try:
- with open(Bootstrapper.DOCKER_CONFIG_FILE_PATH, encoding="utf-8") as config_file:
+ with open(
+ Bootstrapper.DOCKER_CONFIG_FILE_PATH, encoding="utf-8"
+ ) as config_file:
config = json.load(config_file)
- assert Bootstrapper.SETTINGS_NAME_CORE in config, "missing core entry in startup.json"
+ assert (
+ Bootstrapper.SETTINGS_NAME_CORE in config
+ ), "missing core entry in startup.json"
necessary_keys = ["image", "tag", "binds", "privileged", "network"]
for key in necessary_keys:
- assert key in config[Bootstrapper.SETTINGS_NAME_CORE], f"missing key in json file: {key}"
+ assert (
+ key in config[Bootstrapper.SETTINGS_NAME_CORE]
+ ), f"missing key in json file: {key}"
except Exception as error:
- logger.error(f"unable to read startup.json file ({error}), reverting to defaults...")
+ logger.error(
+ f"unable to read startup.json file ({error}), reverting to defaults..."
+ )
# Copy defaults over and read again
Bootstrapper.overwrite_config_file_with_defaults()
with open(Bootstrapper.DEFAULT_FILE_PATH, encoding="utf-8") as config_file:
config = json.load(config_file)
- config[Bootstrapper.SETTINGS_NAME_CORE]["binds"][str(Bootstrapper.HOST_CONFIG_PATH)] = {
+ config[Bootstrapper.SETTINGS_NAME_CORE]["binds"][
+ str(Bootstrapper.HOST_CONFIG_PATH)
+ ] = {
"bind": str(Bootstrapper.DOCKER_CONFIG_PATH),
"mode": "rw",
}
@@ -122,7 +142,9 @@ def pull(self, component_name: str) -> None:
try:
self.client.images.pull(f"{image_name}:{tag}")
except Exception as exception:
- logger.warning(f"Failed to pull image ({image_name}:{tag}): {exception}")
+ logger.warning(
+ f"Failed to pull image ({image_name}:{tag}): {exception}"
+ )
return
# if there is ncurses support, proceed with it
@@ -130,7 +152,9 @@ def pull(self, component_name: str) -> None:
# map each id to a line
id_line: Dict[str, int] = {}
try:
- for line in self.low_level_api.pull(f"{image_name}:{tag}", stream=True, decode=True):
+ for line in self.low_level_api.pull(
+ f"{image_name}:{tag}", stream=True, decode=True
+ ):
if len(line.keys()) == 1 and "status" in line:
# in some cases there is only "status", print that on the last line
screen.addstr(lines, 0, line["status"])
@@ -145,7 +169,9 @@ def pull(self, component_name: str) -> None:
current_line = id_line[layer_id]
if "progress" in line:
progress = line["progress"]
- screen.addstr(current_line, 0, f"[{layer_id}]\t({status})\t{progress}")
+ screen.addstr(
+ current_line, 0, f"[{layer_id}]\t({status})\t{progress}"
+ )
else:
screen.addstr(current_line, 0, f"[{layer_id}]\t({status})")
@@ -192,7 +218,9 @@ def start(self, component_name: str) -> bool:
try:
self.pull(component_name)
except docker.errors.NotFound:
- warn(f"Image {image_name}:{image_version} not found, reverting to default...")
+ warn(
+ f"Image {image_name}:{image_version} not found, reverting to default..."
+ )
self.overwrite_config_file_with_defaults()
return False
except docker.errors.APIError as error:
@@ -239,7 +267,10 @@ def is_running(self, component: str) -> bool:
bool: True if the chosen container is running
"""
try:
- return any(container.name.endswith(component) for container in self.client.containers.list())
+ return any(
+ container.name.endswith(component)
+ for container in self.client.containers.list()
+ )
except Exception as exception:
logger.warning(f"Could not list containers: {exception}")
return False
@@ -251,7 +282,11 @@ 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, verify=False)
+ response = requests.get(
+ "http://localhost/version-chooser/v1.0/version/current",
+ timeout=10,
+ verify=False,
+ )
if Bootstrapper.SETTINGS_NAME_CORE in response.json()["repository"]:
if not self.version_chooser_is_online:
self.version_chooser_is_online = True
@@ -301,7 +336,9 @@ def run(self) -> None:
# Version choose failed, time to restarted core
self.core_last_response_time = time.monotonic()
- logger.warning("Core has not responded in 5 minutes, resetting to factory...")
+ logger.warning(
+ "Core has not responded in 5 minutes, resetting to factory..."
+ )
self.overwrite_config_file_with_defaults()
try:
if self.start(image):
From 8e2b89a6d0a376874bc4041de14080384d2dafb0 Mon Sep 17 00:00:00 2001
From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com>
Date: Fri, 20 Jun 2025 14:54:48 -0700
Subject: [PATCH 6/8] Fix isort warning
---
bootstrap/bootstrap/bootstrap.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/bootstrap/bootstrap/bootstrap.py b/bootstrap/bootstrap/bootstrap.py
index 5d598f4780..1eb28933bb 100755
--- a/bootstrap/bootstrap/bootstrap.py
+++ b/bootstrap/bootstrap/bootstrap.py
@@ -13,7 +13,6 @@
import urllib3
from loguru import logger
-
urllib3.disable_warnings()
From c15073c63ae3c8e0057a2d2bebce3579489a8afd Mon Sep 17 00:00:00 2001
From: Matt <54611367+matt-bathyscope@users.noreply.github.com>
Date: Fri, 20 Jun 2025 17:39:15 -0500
Subject: [PATCH 7/8] Apply suggestions from code review
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
---
core/services/beacon/main.py | 36 +++++++++++++++++++-----------------
core/services/helper/main.py | 2 +-
2 files changed, 20 insertions(+), 18 deletions(-)
diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py
index fe59991426..1c78e11495 100755
--- a/core/services/beacon/main.py
+++ b/core/services/beacon/main.py
@@ -176,8 +176,10 @@ def set_enable_tls(self, enable_tls: bool) -> None:
# bounce nginx
self.nginx_promote_config(keep_backup=True)
# remove old cert
- os.unlink(TLS_CERT_PATH)
- os.unlink(TLS_KEY_PATH)
+ if os.path.exists(TLS_CERT_PATH):
+ os.unlink(TLS_CERT_PATH)
+ if os.path.exists(TLS_KEY_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
@@ -198,13 +200,13 @@ def generate_cert(self) -> None:
"""
# 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("IP:192.168.2.2")
- alt_names.append("IP:192.168.3.1")
-
+ alt_names = [
+ f"DNS:{current_hostname}",
+ f"DNS:{current_hostname}-wifi",
+ f"DNS:{current_hostname}-hotspot",
+ "IP:192.168.2.2",
+ "IP:192.168.3.1",
+ ]
# shell out to openssl to get the cert
try:
subprocess.check_call(
@@ -270,16 +272,15 @@ def nginx_promote_config(
"""
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")
+ # ensure new config exists
if not os.path.isfile(new_config_path):
raise FileNotFoundError("New config not found")
+ # old config may not exist (first-time setup), so do not raise if missing
if keep_backup:
shutil.copyfile(
config_path,
- f"{config_path}_backup_{datetime.datetime.utcnow().strftime('%Y%m%d_%H%M%S')}",
+ f"{config_path}_backup_{datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%d_%H%M%S')}",
follow_symlinks=False,
)
@@ -449,9 +450,10 @@ def get_services() -> Any:
@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")
+ # Hostname must not start or end with a hyphen, nor contain consecutive hyphens
+ hostname_regex = re.compile(r"^(?!-)[A-Za-z0-9-]+(? bool:
return beacon.get_enable_tls()
-@app.post("/use_tls", summary="Set whether TLS should be enbabled")
+@app.post("/use_tls", summary="Set whether TLS should be enabled")
@version(1, 0)
def set_enable_tls(enable_tls: bool) -> Any:
return beacon.set_enable_tls(enable_tls)
diff --git a/core/services/helper/main.py b/core/services/helper/main.py
index 0f9ff57f03..7f699862c2 100755
--- a/core/services/helper/main.py
+++ b/core/services/helper/main.py
@@ -207,7 +207,7 @@ class Helper:
SKIP_PORTS: Set[int] = {
22, # SSH
80, # BlueOS
- 443, # BlueoS TLS
+ 443, # BlueOS TLS
5201, # Iperf
6021, # Mavlink Camera Manager's WebRTC signaller
7000, # Major Tom does not have a public API yet
From c88f244ddfc3c7cc51479fb5dd70798a55ce529d Mon Sep 17 00:00:00 2001
From: Matt Fowler <54611367+matt-bathyscope@users.noreply.github.com>
Date: Fri, 20 Jun 2025 15:49:47 -0700
Subject: [PATCH 8/8] Fix formatting issue from Sourcery
---
core/services/beacon/main.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/core/services/beacon/main.py b/core/services/beacon/main.py
index 1c78e11495..5d4be427a1 100755
--- a/core/services/beacon/main.py
+++ b/core/services/beacon/main.py
@@ -453,7 +453,9 @@ def set_hostname(hostname: str) -> Any:
# Hostname must not start or end with a hyphen, nor contain consecutive hyphens
hostname_regex = re.compile(r"^(?!-)[A-Za-z0-9-]+(?