diff --git a/bootstrap/bootstrap/bootstrap.py b/bootstrap/bootstrap/bootstrap.py index db1e7094fd..1eb28933bb 100755 --- a/bootstrap/bootstrap/bootstrap.py +++ b/bootstrap/bootstrap/bootstrap.py @@ -10,8 +10,11 @@ import docker import requests +import urllib3 from loguru import logger +urllib3.disable_warnings() + class Bootstrapper: @@ -24,7 +27,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() @@ -37,7 +42,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}" @@ -45,12 +52,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]: @@ -63,21 +73,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", } @@ -121,7 +141,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 @@ -129,7 +151,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"]) @@ -144,7 +168,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})") @@ -191,7 +217,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: @@ -238,7 +266,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 @@ -250,7 +281,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) + 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 @@ -300,7 +335,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): diff --git a/core/frontend/src/components/wizard/Wizard.vue b/core/frontend/src/components/wizard/Wizard.vue index 9466dfb50c..3f2aa53a74 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..5d4be427a1 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: @@ -139,6 +160,147 @@ 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 + 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 + 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 = [ + 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( + [ + "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 + """ + # 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.now(datetime.timezone.utc).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 ) -> AsyncServiceInfo: @@ -287,6 +449,13 @@ 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 must not start or end with a hyphen, nor contain consecutive hyphens + hostname_regex = re.compile(r"^(?!-)[A-Za-z0-9-]+(? 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 enabled") +@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..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 diff --git a/core/services/helper/main.py b/core/services/helper/main.py index 926da6932a..7f699862c2 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 181c355844..bbd55985d0 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..c98696db2e --- /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 ssl http2; # IPv4 + listen [::]:443 ssl http2; # 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; + } +}