From 0fc546962e7999d0a6a235102982c561304b270c Mon Sep 17 00:00:00 2001 From: Stephen Chavez Date: Wed, 9 Nov 2022 05:30:26 +0000 Subject: [PATCH] Http proxy (#1772) * adding skeleton for new http_proxy. * more stuff yay * more stuff yay * more stuff * more stuff * "working" poc * more stuff * more stuff * fix mored stuff * working proxy * moved to handler style for requests * clean up * cleaning stuff up * more stuff * refactoring code * more changes * refactoring handle * refactored code * more stuff * Co-authored-by: J M * Co-authored-by: J M * more stuff * more stuff * working main ui handle * Implement old code to handler in static server * Feat/long running (#1676) * feat: Start the long running container * feat: Long running docker, running, stoping, and uninstalling * feat: Just make the folders that we would like to mount. * fix: Uninstall not working * chore: remove some logging * feat: Smarter cleanup * feat: Wait for start * wip: Need to kill * chore: Remove the bad tracing * feat: Stopping the long running processes without killing the long running * Mino Feat: Change the Manifest To have a new type (#1736) * Add build-essential to README.md (#1716) Update README.md * write image to sparse-aware archive format (#1709) * fix: Add modification to the max_user_watches (#1695) * fix: Add modification to the max_user_watches * chore: Move to initialization * [Feat] follow logs (#1714) * tail logs * add cli * add FE * abstract http to shared * batch new logs * file download for logs * fix modal error when no config Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: BluJ * Update README.md (#1728) * fix build for patch-db client for consistency (#1722) * fix cli install (#1720) * highlight instructions if not viewed (#1731) * wip: * [ ] Fix the build (dependencies:634 map for option) * fix: Cargo build * fix: Long running wasn't starting * fix: uninstall works Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * chore: Fix a dbg! * chore: Make the commands of the docker-inject do inject instead of exec * chore: Fix compile mistake * chore: Change to use simpler Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * remove recovered services and drop reordering feature (#1829) * chore: Convert the migration to use receipt. (#1842) * feat: remove ionic storage (#1839) * feat: remove ionic storage * grayscal when disconncted, rename local storage service for clarity * remove storage from package lock * update patchDB Co-authored-by: Matt Hill * update patch DB * workring http server * Feat/community marketplace (#1790) * add community marketplace * Update embassy-mock-api.service.ts * expect ui/marketplace to be undefined * possible undefined from getpackage * fix marketplace pages * rework marketplace infrastructure * fix bugs Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> * remove unwrap * cleanup code * d * more stuff * fix: make `shared` module independent of `config.js` (#1870) * cert stuff WIP * MORE CERT STUFF * more stuff * more stuff * more stuff * abstract service fn * almost ssl * fix ssl export * Feat/long running (#1676) * feat: Start the long running container * feat: Long running docker, running, stoping, and uninstalling * feat: Just make the folders that we would like to mount. * fix: Uninstall not working * chore: remove some logging * feat: Smarter cleanup * feat: Wait for start * wip: Need to kill * chore: Remove the bad tracing * feat: Stopping the long running processes without killing the long running * Mino Feat: Change the Manifest To have a new type (#1736) * Add build-essential to README.md (#1716) Update README.md * write image to sparse-aware archive format (#1709) * fix: Add modification to the max_user_watches (#1695) * fix: Add modification to the max_user_watches * chore: Move to initialization * [Feat] follow logs (#1714) * tail logs * add cli * add FE * abstract http to shared * batch new logs * file download for logs * fix modal error when no config Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: BluJ * Update README.md (#1728) * fix build for patch-db client for consistency (#1722) * fix cli install (#1720) * highlight instructions if not viewed (#1731) * wip: * [ ] Fix the build (dependencies:634 map for option) * fix: Cargo build * fix: Long running wasn't starting * fix: uninstall works Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * chore: Fix a dbg! * chore: Make the commands of the docker-inject do inject instead of exec * chore: Fix compile mistake * chore: Change to use simpler Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * remove recovered services and drop reordering feature (#1829) * chore: Convert the migration to use receipt. (#1842) * feat: remove ionic storage (#1839) * feat: remove ionic storage * grayscal when disconncted, rename local storage service for clarity * remove storage from package lock * update patchDB Co-authored-by: Matt Hill * update patch DB * Feat/community marketplace (#1790) * add community marketplace * Update embassy-mock-api.service.ts * expect ui/marketplace to be undefined * possible undefined from getpackage * fix marketplace pages * rework marketplace infrastructure * fix bugs Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> * fix: make `shared` module independent of `config.js` (#1870) * fix: small fix for marketplace header styles (#1873) * feat: setup publishing of share and marketplace packages (#1874) * inlcude marketplace in linter * fix npm publish scrips and bump versions of libs * feat: add assets to published packages and fix peerDeps versions (#1875) * bump peer dep * fix: add assets to published paths (#1876) * allow ca download over lan (#1877) * only desaturate when logged in and not fully * Feature/multi platform (#1866) * wip * wip * wip * wip * wip * wip * remove debian dir * lazy env and git hash * remove env and git hash on clean * don't leave project dir * use docker for native builds * start9 rust * correctly mount registry * remove systemd config * switch to /usr/bin * disable sound for now * wip * change disk list * multi-arch images * multi-arch system images * default aarch64 * edition 2021 * dynamic wifi interface name * use wifi interface from config * bugfixes * add beep based sound * wip * wip * wip * separate out raspberry pi specific files * fixes * use new initramfs always * switch journald conf to sed script * fixes * fix permissions * talking about kernel modules not scripts * fix * fix * switch to MBR * install to /usr/lib * fixes * fixes * fixes * fixes * add media config to cfg path * fixes * fixes * fixes * raspi image fixes * fix test * fix workflow * sync boot partition * gahhhhh * more stuff * remove restore warning and better messaging for backup/restore (#1881) * Update READMEs (#1885) * docs * fix host key generation * debugging eos with tokio console * fix recursive default * build improvements (#1886) * build improvements * no workdir * kiosk fully working * setup profile prefs * Feat/js long running (#1879) * wip: combining the streams * chore: Testing locally * chore: Fix some lint * Feat/long running (#1676) * feat: Start the long running container * feat: Long running docker, running, stoping, and uninstalling * feat: Just make the folders that we would like to mount. * fix: Uninstall not working * chore: remove some logging * feat: Smarter cleanup * feat: Wait for start * wip: Need to kill * chore: Remove the bad tracing * feat: Stopping the long running processes without killing the long running * Mino Feat: Change the Manifest To have a new type (#1736) * Add build-essential to README.md (#1716) Update README.md * write image to sparse-aware archive format (#1709) * fix: Add modification to the max_user_watches (#1695) * fix: Add modification to the max_user_watches * chore: Move to initialization * [Feat] follow logs (#1714) * tail logs * add cli * add FE * abstract http to shared * batch new logs * file download for logs * fix modal error when no config Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: BluJ * Update README.md (#1728) * fix build for patch-db client for consistency (#1722) * fix cli install (#1720) * highlight instructions if not viewed (#1731) * wip: * [ ] Fix the build (dependencies:634 map for option) * fix: Cargo build * fix: Long running wasn't starting * fix: uninstall works Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * chore: Fix a dbg! * chore: Make the commands of the docker-inject do inject instead of exec * chore: Fix compile mistake * chore: Change to use simpler Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * wip: making the mananger create * wip: Working on trying to make the long running docker container command * Feat/long running (#1676) * feat: Start the long running container * feat: Long running docker, running, stoping, and uninstalling * feat: Just make the folders that we would like to mount. * fix: Uninstall not working * chore: remove some logging * feat: Smarter cleanup * feat: Wait for start * wip: Need to kill * chore: Remove the bad tracing * feat: Stopping the long running processes without killing the long running * Mino Feat: Change the Manifest To have a new type (#1736) * Add build-essential to README.md (#1716) Update README.md * write image to sparse-aware archive format (#1709) * fix: Add modification to the max_user_watches (#1695) * fix: Add modification to the max_user_watches * chore: Move to initialization * [Feat] follow logs (#1714) * tail logs * add cli * add FE * abstract http to shared * batch new logs * file download for logs * fix modal error when no config Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: BluJ * Update README.md (#1728) * fix build for patch-db client for consistency (#1722) * fix cli install (#1720) * highlight instructions if not viewed (#1731) * wip: * [ ] Fix the build (dependencies:634 map for option) * fix: Cargo build * fix: Long running wasn't starting * fix: uninstall works Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * chore: Fix a dbg! * chore: Make the commands of the docker-inject do inject instead of exec * chore: Fix compile mistake * chore: Change to use simpler Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * feat: Use the long running feature in the manager * remove recovered services and drop reordering feature (#1829) * wip: Need to get the initial docker command running? * chore: Add in the new procedure for the docker. * feat: Get the system to finally run long * wip: Added the command inserter to the docker persistance * wip: Added the command inserter to the docker persistance * Feat/long running (#1676) * feat: Start the long running container * feat: Long running docker, running, stoping, and uninstalling * feat: Just make the folders that we would like to mount. * fix: Uninstall not working * chore: remove some logging * feat: Smarter cleanup * feat: Wait for start * wip: Need to kill * chore: Remove the bad tracing * feat: Stopping the long running processes without killing the long running * Mino Feat: Change the Manifest To have a new type (#1736) * Add build-essential to README.md (#1716) Update README.md * write image to sparse-aware archive format (#1709) * fix: Add modification to the max_user_watches (#1695) * fix: Add modification to the max_user_watches * chore: Move to initialization * [Feat] follow logs (#1714) * tail logs * add cli * add FE * abstract http to shared * batch new logs * file download for logs * fix modal error when no config Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: BluJ * Update README.md (#1728) * fix build for patch-db client for consistency (#1722) * fix cli install (#1720) * highlight instructions if not viewed (#1731) * wip: * [ ] Fix the build (dependencies:634 map for option) * fix: Cargo build * fix: Long running wasn't starting * fix: uninstall works Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * chore: Fix a dbg! * chore: Make the commands of the docker-inject do inject instead of exec * chore: Fix compile mistake * chore: Change to use simpler Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * remove recovered services and drop reordering feature (#1829) * chore: Convert the migration to use receipt. (#1842) * feat: remove ionic storage (#1839) * feat: remove ionic storage * grayscal when disconncted, rename local storage service for clarity * remove storage from package lock * update patchDB Co-authored-by: Matt Hill * update patchDB * feat: Move the run_command into the js * Feat/long running (#1676) * feat: Start the long running container * feat: Long running docker, running, stoping, and uninstalling * feat: Just make the folders that we would like to mount. * fix: Uninstall not working * chore: remove some logging * feat: Smarter cleanup * feat: Wait for start * wip: Need to kill * chore: Remove the bad tracing * feat: Stopping the long running processes without killing the long running * Mino Feat: Change the Manifest To have a new type (#1736) * Add build-essential to README.md (#1716) Update README.md * write image to sparse-aware archive format (#1709) * fix: Add modification to the max_user_watches (#1695) * fix: Add modification to the max_user_watches * chore: Move to initialization * [Feat] follow logs (#1714) * tail logs * add cli * add FE * abstract http to shared * batch new logs * file download for logs * fix modal error when no config Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: BluJ * Update README.md (#1728) * fix build for patch-db client for consistency (#1722) * fix cli install (#1720) * highlight instructions if not viewed (#1731) * wip: * [ ] Fix the build (dependencies:634 map for option) * fix: Cargo build * fix: Long running wasn't starting * fix: uninstall works Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * chore: Fix a dbg! * chore: Make the commands of the docker-inject do inject instead of exec * chore: Fix compile mistake * chore: Change to use simpler Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill * remove recovered services and drop reordering feature (#1829) * chore: Convert the migration to use receipt. (#1842) * feat: remove ionic storage (#1839) * feat: remove ionic storage * grayscal when disconncted, rename local storage service for clarity * remove storage from package lock * update patchDB Co-authored-by: Matt Hill * update patch DB * chore: Change the error catching for the long running to try all * Feat/community marketplace (#1790) * add community marketplace * Update embassy-mock-api.service.ts * expect ui/marketplace to be undefined * possible undefined from getpackage * fix marketplace pages * rework marketplace infrastructure * fix bugs Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> * WIP: Fix the build, needed to move around creation of exec * wip: Working on solving why there is a missing end. * fix: make `shared` module independent of `config.js` (#1870) * feat: Add in the kill and timeout * feat: Get the run to actually work. * chore: Add when/ why/ where comments * feat: Convert inject main to use exec main. * Fix: Ability to stop services * wip: long running js main * feat: Kill for the main * Fix * fix: Fix the build for x86 * wip: Working on changes * wip: Working on trying to kill js * fix: Testing for slow * feat: Test that the new manifest works * chore: Try and fix build? * chore: Fix? the build * chore: Fix the long input dies and never restarts * build improvements * no workdir * fix: Architecture for long running * chore: Fix and remove the docker inject * chore: Undo the changes to the kiosk mode * fix: Remove the it from the prod build * fix: Start issue * fix: The compat build * chore: Add in the conditional compilation again for the missing impl * chore: Change to aux * chore: Remove the aux for now * chore: Add some documentation to docker container Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill Co-authored-by: Alex Inkin * use old resolv.conf until systemd is on * update patchdb * update patch db submodule * no x11 wrapper config * working poc * fixing misc stuff * switch patchdb to next * Feat/update tab (#1865) * implement updates tab for viewing all updates from all marketplaces in one place * remove auto-check-updates * feat: implement updates page (#1888) * feat: implement updates page * chore: comments * better styling in update tab * rework marketplace service (#1891) * rework marketplace service * remove unneeded ? * fix: refactor marketplace to cache requests Co-authored-by: waterplea Co-authored-by: Alex Inkin * misc fixes (#1894) * changing hostname stuff * changes * move marketplace settings into marketplace tab (#1895) * move marketplace settings into marketplace tab * Update frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> * bump marketplace version * removing oldd code * working service proxy * fqdn struct wwip * new types for ssl proxy * restructure restore.rs and embassyd.rs * adding dbg * debugging proxy handlers * add lots of debugging for the svc handler removal bug * debugging * remove extra code * fixing proxy and removing old debug code * finalizing proxy code to serve the setup ui and diag ui * final new eos http proxy * remove uneeded trace error * remove extra file * not needed flags * clean up * Fix/debug (#1909) chore: Use debug by default" * chore: Fix on the rsync not having stdout. (#1911) * install wizard project (#1893) * install wizard project * reboot endpoint * Update frontend/projects/install-wizard/src/app/pages/home/home.page.ts Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> * Update frontend/projects/install-wizard/src/app/pages/home/home.page.ts Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> * Update frontend/projects/install-wizard/src/app/pages/home/home.page.ts Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> * update build * fix build * backend portion * increase image size * loaded * dont auto resize * fix install wizard * use localhost if still in setup mode * fix compat Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Aiden McClelland * fix kiosk * integrate install wizard * fix build typo * no nginx * fix build * remove nginx stuff from build * fixes Co-authored-by: Stephen Chavez Co-authored-by: J M <2364004+Blu-J@users.noreply.github.com> Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill Co-authored-by: Alex Inkin --- backend/Cargo.lock | 2 + backend/Cargo.toml | 6 +- backend/embassy-init.service | 2 +- backend/src/bin/embassy-init.rs | 193 +++----- backend/src/bin/embassyd.rs | 228 ++------- backend/src/context/diagnostic.rs | 59 +-- backend/src/context/install.rs | 28 +- backend/src/context/rpc.rs | 67 +-- backend/src/context/setup.rs | 22 +- backend/src/error.rs | 22 +- backend/src/hostname.rs | 31 ++ backend/src/lib.rs | 1 - backend/src/net/cert_resolver.rs | 216 +++++++++ .../src/net/embassy_service_http_server.rs | 173 +++++++ backend/src/net/mod.rs | 203 ++------ backend/src/net/net_controller.rs | 274 +++++++++++ backend/src/net/net_utils.rs | 122 +++++ backend/src/net/nginx.conf.template | 19 - backend/src/net/nginx.rs | 254 ---------- backend/src/net/proxy_controller.rs | 384 +++++++++++++++ backend/src/net/ssl.rs | 60 +-- backend/src/net/static_server.rs | 458 ++++++++++++++++++ backend/src/net/vhost_controller.rs | 82 ++++ backend/src/net/ws_server.rs | 94 ++++ backend/src/nginx/diagnostic-ui.conf | 35 -- backend/src/nginx/main-ui.conf.template | 119 ----- backend/src/nginx/setup-wizard.conf | 29 -- backend/src/os_install.rs | 17 +- backend/src/static_server.rs | 195 -------- backend/src/util/config.rs | 1 + backend/src/volume.rs | 2 +- build/lib/conflicts | 4 +- build/lib/depends | 4 +- build/lib/scripts/enable-kiosk | 2 +- build/lib/scripts/postinst | 7 +- libs/models/src/id.rs | 2 +- libs/models/src/interface_id.rs | 2 +- system-images/compat/Cargo.lock | 121 +++-- 38 files changed, 2130 insertions(+), 1410 deletions(-) create mode 100644 backend/src/net/cert_resolver.rs create mode 100644 backend/src/net/embassy_service_http_server.rs create mode 100644 backend/src/net/net_controller.rs create mode 100644 backend/src/net/net_utils.rs delete mode 100644 backend/src/net/nginx.conf.template delete mode 100644 backend/src/net/nginx.rs create mode 100644 backend/src/net/proxy_controller.rs create mode 100644 backend/src/net/static_server.rs create mode 100644 backend/src/net/vhost_controller.rs create mode 100644 backend/src/net/ws_server.rs delete mode 100644 backend/src/nginx/diagnostic-ui.conf delete mode 100644 backend/src/nginx/main-ui.conf.template delete mode 100644 backend/src/nginx/setup-wizard.conf delete mode 100644 backend/src/static_server.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 81835bb5d..fa2f71579 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1188,6 +1188,7 @@ dependencies = [ "base64ct", "basic-cookies", "bollard", + "bytes", "chrono", "ciborium", "clap 3.2.23", @@ -1256,6 +1257,7 @@ dependencies = [ "tar", "thiserror", "tokio", + "tokio-rustls", "tokio-stream", "tokio-tar", "tokio-tungstenite", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index ba99e9b3c..11eb0a077 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -58,6 +58,7 @@ base64 = "0.13.0" base64ct = "1.5.1" basic-cookies = "0.1.4" bollard = "0.13.0" +bytes = "1" chrono = { version = "0.4.19", features = ["serde"] } clap = "3.2.8" color-eyre = "0.6.1" @@ -114,7 +115,7 @@ regex = "1.6.0" reqwest = { version = "0.11.11", features = ["stream", "json", "socks"] } reqwest_cookie_store = "0.4.0" rpassword = "7.0.0" -rpc-toolkit = "0.2.1" +rpc-toolkit = "0.2.2" rust-argon2 = "1.0.0" scopeguard = "1.1" # because avahi-sys fucks your shit up serde = { version = "1.0.139", features = ["derive", "rc"] } @@ -135,10 +136,11 @@ sqlx = { version = "0.6.0", features = [ stderrlog = "0.5.3" tar = "0.4.38" thiserror = "1.0.31" -tokio = { version = "1.19.2", features = ["full"] } +tokio = { version = "1.21.2", features = ["full"] } tokio-stream = { version = "0.1.9", features = ["io-util", "sync"] } tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" } tokio-tungstenite = { version = "0.17.1", features = ["native-tls"] } +tokio-rustls = "0.23.4" tokio-util = { version = "0.7.3", features = ["io"] } torut = "0.2.1" tracing = "0.1.35" diff --git a/backend/embassy-init.service b/backend/embassy-init.service index 7a9b9ce66..e7753b63c 100644 --- a/backend/embassy-init.service +++ b/backend/embassy-init.service @@ -2,7 +2,7 @@ Description=Embassy Init After=network.target Requires=network.target -Wants=avahi-daemon.service nginx.service tor.service +Wants=avahi-daemon.service tor.service [Service] Type=oneshot diff --git a/backend/src/bin/embassy-init.rs b/backend/src/bin/embassy-init.rs index 0483bf83b..6484afbe9 100644 --- a/backend/src/bin/embassy-init.rs +++ b/backend/src/bin/embassy-init.rs @@ -7,106 +7,90 @@ use embassy::context::{DiagnosticContext, InstallContext, SetupContext}; use embassy::disk::fsck::RepairStrategy; use embassy::disk::main::DEFAULT_PASSWORD; use embassy::disk::REPAIR_DISK_PATH; +use embassy::hostname::get_current_ip; use embassy::init::STANDBY_MODE_PATH; -use embassy::middleware::cors::cors; -use embassy::middleware::diagnostic::diagnostic; +use embassy::net::embassy_service_http_server::EmbassyServiceHTTPServer; #[cfg(feature = "avahi")] use embassy::net::mdns::MdnsController; +use embassy::net::net_utils::ResourceFqdn; +use embassy::net::static_server::{ + diag_ui_file_router, install_ui_file_router, setup_ui_file_router, +}; use embassy::shutdown::Shutdown; use embassy::sound::CHIME; use embassy::util::logger::EmbassyLogger; use embassy::util::Invoke; use embassy::{Error, ErrorKind, ResultExt}; -use http::StatusCode; -use rpc_toolkit::rpc_server; use tokio::process::Command; use tracing::instrument; -fn status_fn(_: i32) -> StatusCode { - StatusCode::OK -} - #[instrument] async fn setup_or_init(cfg_path: Option) -> Result<(), Error> { if tokio::fs::metadata("/cdrom").await.is_ok() { #[cfg(feature = "avahi")] - let _mdns = MdnsController::init(); - tokio::fs::write( - "/etc/nginx/sites-available/default", - include_str!("../nginx/install-wizard.conf"), - ) - .await - .with_ctx(|_| { - ( - embassy::ErrorKind::Filesystem, - "/etc/nginx/sites-available/default", - ) - })?; - Command::new("systemctl") - .arg("reload") - .arg("nginx") - .invoke(embassy::ErrorKind::Nginx) - .await?; + let _mdns = MdnsController::init().await?; + let ctx = InstallContext::init(cfg_path).await?; + + let embassy_ip = dbg!(get_current_ip(ctx.ethernet_interface.to_owned()).await?); + let embassy_ip_fqdn: ResourceFqdn = embassy_ip.parse()?; + let embassy_fqdn: ResourceFqdn = "pureos.local".parse()?; + + let install_ui_handler = install_ui_file_router(ctx.clone()).await?; + + let mut install_http_server = + EmbassyServiceHTTPServer::new([0, 0, 0, 0].into(), 80, None).await?; + install_http_server + .add_svc_handler_mapping(embassy_ip_fqdn, install_ui_handler.clone()) + .await?; + install_http_server + .add_svc_handler_mapping(embassy_fqdn, install_ui_handler) + .await?; + tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this CHIME.play().await?; - rpc_server!({ - command: embassy::install_api, - context: ctx.clone(), - status: status_fn, - middleware: [ - cors, - ] - }) - .with_graceful_shutdown({ - let mut shutdown = ctx.shutdown.subscribe(); - async move { - shutdown.recv().await.expect("context dropped"); - } - }) - .await - .with_kind(embassy::ErrorKind::Network)?; + + ctx.shutdown + .subscribe() + .recv() + .await + .expect("context dropped"); + install_http_server.shutdown.send(()).unwrap(); + Command::new("reboot") + .invoke(embassy::ErrorKind::Unknown) + .await?; } else if tokio::fs::metadata("/media/embassy/config/disk.guid") .await .is_err() { #[cfg(feature = "avahi")] - let _mdns = MdnsController::init(); - tokio::fs::write( - "/etc/nginx/sites-available/default", - include_str!("../nginx/setup-wizard.conf"), - ) - .await - .with_ctx(|_| { - ( - embassy::ErrorKind::Filesystem, - "/etc/nginx/sites-available/default", - ) - })?; - Command::new("systemctl") - .arg("reload") - .arg("nginx") - .invoke(embassy::ErrorKind::Nginx) - .await?; + let _mdns = MdnsController::init().await?; + let ctx = SetupContext::init(cfg_path).await?; + + let embassy_ip = get_current_ip(ctx.ethernet_interface.to_owned()).await?; + let embassy_ip_fqdn: ResourceFqdn = embassy_ip.parse()?; + let embassy_fqdn: ResourceFqdn = "embassy.local".parse()?; + + let setup_ui_handler = setup_ui_file_router(ctx.clone()).await?; + + let mut setup_http_server = + EmbassyServiceHTTPServer::new([0, 0, 0, 0].into(), 80, None).await?; + setup_http_server + .add_svc_handler_mapping(embassy_ip_fqdn, setup_ui_handler.clone()) + .await?; + setup_http_server + .add_svc_handler_mapping(embassy_fqdn, setup_ui_handler) + .await?; + tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this CHIME.play().await?; - rpc_server!({ - command: embassy::setup_api, - context: ctx.clone(), - status: status_fn, - middleware: [ - cors, - ] - }) - .with_graceful_shutdown({ - let mut shutdown = ctx.shutdown.subscribe(); - async move { - shutdown.recv().await.expect("context dropped"); - } - }) - .await - .with_kind(embassy::ErrorKind::Network)?; + ctx.shutdown + .subscribe() + .recv() + .await + .expect("context dropped"); + setup_http_server.shutdown.send(()).unwrap(); } else { let cfg = RpcContextConfig::load(cfg_path).await?; let guid_string = tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy @@ -178,23 +162,8 @@ async fn inner_main(cfg_path: Option) -> Result, Error tracing::debug!("{}", e.source); embassy::sound::BEETHOVEN.play().await?; #[cfg(feature = "avahi")] - let _mdns = MdnsController::init(); - tokio::fs::write( - "/etc/nginx/sites-available/default", - include_str!("../nginx/diagnostic-ui.conf"), - ) - .await - .with_ctx(|_| { - ( - embassy::ErrorKind::Filesystem, - "/etc/nginx/sites-available/default", - ) - })?; - Command::new("systemctl") - .arg("reload") - .arg("nginx") - .invoke(embassy::ErrorKind::Nginx) - .await?; + let _mdns = MdnsController::init().await?; + let ctx = DiagnosticContext::init( cfg_path, if tokio::fs::metadata("/media/embassy/config/disk.guid") @@ -213,31 +182,25 @@ async fn inner_main(cfg_path: Option) -> Result, Error e, ) .await?; - let mut shutdown_recv = ctx.shutdown.subscribe(); - rpc_server!({ - command: embassy::diagnostic_api, - context: ctx.clone(), - status: status_fn, - middleware: [ - cors, - diagnostic, - ] - }) - .with_graceful_shutdown({ - let mut shutdown = ctx.shutdown.subscribe(); - async move { - shutdown.recv().await.expect("context dropped"); - } - }) - .await - .with_kind(embassy::ErrorKind::Network)?; - Ok::<_, Error>( - shutdown_recv - .recv() - .await - .with_kind(embassy::ErrorKind::Network)?, - ) + let embassy_ip = get_current_ip(ctx.ethernet_interface.to_owned()).await?; + let embassy_ip_fqdn: ResourceFqdn = embassy_ip.parse()?; + let embassy_fqdn: ResourceFqdn = "embassy.local".parse()?; + + let diag_ui_handler = diag_ui_file_router(ctx.clone()).await?; + + let mut diag_http_server = + EmbassyServiceHTTPServer::new([0, 0, 0, 0].into(), 80, None).await?; + diag_http_server + .add_svc_handler_mapping(embassy_ip_fqdn, diag_ui_handler.clone()) + .await?; + diag_http_server + .add_svc_handler_mapping(embassy_fqdn, diag_ui_handler) + .await?; + + let shutdown = ctx.shutdown.subscribe().recv().await.unwrap(); + diag_http_server.shutdown.send(()).unwrap(); + Ok(shutdown) } .await } else { diff --git a/backend/src/bin/embassyd.rs b/backend/src/bin/embassyd.rs index 2ca188de8..2d32ede0f 100644 --- a/backend/src/bin/embassyd.rs +++ b/backend/src/bin/embassyd.rs @@ -4,41 +4,25 @@ use std::time::Duration; use color_eyre::eyre::eyre; use embassy::context::{DiagnosticContext, RpcContext}; -use embassy::core::rpc_continuations::RequestGuid; -use embassy::db::subscribe; -use embassy::middleware::auth::auth; -use embassy::middleware::cors::cors; -use embassy::middleware::db::db as db_middleware; -use embassy::middleware::diagnostic::diagnostic; + +use embassy::hostname::get_current_ip; +use embassy::net::embassy_service_http_server::EmbassyServiceHTTPServer; #[cfg(feature = "avahi")] use embassy::net::mdns::MdnsController; +use embassy::net::net_controller::NetController; +use embassy::net::net_utils::ResourceFqdn; +use embassy::net::static_server::diag_ui_file_router; use embassy::net::tor::tor_health_check; use embassy::shutdown::Shutdown; use embassy::system::launch_metrics_task; +use embassy::util::daemon; use embassy::util::logger::EmbassyLogger; -use embassy::util::{daemon, Invoke}; -use embassy::{static_server, Error, ErrorKind, ResultExt}; +use embassy::{Error, ErrorKind, ResultExt}; use futures::{FutureExt, TryFutureExt}; use reqwest::{Client, Proxy}; -use rpc_toolkit::hyper::{Body, Response, Server, StatusCode}; -use rpc_toolkit::rpc_server; -use tokio::process::Command; use tokio::signal::unix::signal; use tracing::instrument; -fn status_fn(_: i32) -> StatusCode { - StatusCode::OK -} - -fn err_to_500(e: Error) -> Response { - tracing::error!("{}", e); - tracing::debug!("{:?}", e); - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .unwrap() -} - #[instrument] async fn inner_main(cfg_path: Option) -> Result, Error> { let (rpc_ctx, shutdown) = { @@ -52,6 +36,8 @@ async fn inner_main(cfg_path: Option) -> Result, Error ), ) .await?; + NetController::setup_embassy_ui(rpc_ctx.clone()).await?; + let mut shutdown_recv = rpc_ctx.shutdown.subscribe(); let sig_handler_ctx = rpc_ctx.clone(); @@ -67,7 +53,7 @@ async fn inner_main(cfg_path: Option) -> Result, Error .map(|s| { async move { signal(*s) - .expect(&format!("register {:?} handler", s)) + .unwrap_or_else(|_| panic!("register {:?} handler", s)) .recv() .await } @@ -82,31 +68,11 @@ async fn inner_main(cfg_path: Option) -> Result, Error .expect("send shutdown signal"); }); - let mut db = rpc_ctx.db.handle(); - let receipts = embassy::context::rpc::RpcSetNginxReceipts::new(&mut db).await?; - embassy::hostname::sync_hostname(&mut db, &receipts.hostname_receipts).await?; - - rpc_ctx.set_nginx_conf(&mut db, receipts).await?; - drop(db); - let auth = auth(rpc_ctx.clone()); - let db_middleware = db_middleware(rpc_ctx.clone()); - let ctx = rpc_ctx.clone(); - let server = rpc_server!({ - command: embassy::main_api, - context: ctx, - status: status_fn, - middleware: [ - cors, - auth, - db_middleware, - ] - }) - .with_graceful_shutdown({ - let mut shutdown = rpc_ctx.shutdown.subscribe(); - async move { - shutdown.recv().await.expect("context dropped"); - } - }); + { + let mut db = rpc_ctx.db.handle(); + let receipts = embassy::context::rpc::RpcSetHostNameReceipts::new(&mut db).await?; + embassy::hostname::sync_hostname(&mut db, &receipts.hostname_receipts).await?; + } let metrics_ctx = rpc_ctx.clone(); let metrics_task = tokio::spawn(async move { @@ -116,105 +82,6 @@ async fn inner_main(cfg_path: Option) -> Result, Error .await }); - let ws_ctx = rpc_ctx.clone(); - let ws_server = { - let builder = Server::bind(&ws_ctx.bind_ws); - - let make_svc = ::rpc_toolkit::hyper::service::make_service_fn(move |_| { - let ctx = ws_ctx.clone(); - async move { - Ok::<_, ::rpc_toolkit::hyper::Error>(::rpc_toolkit::hyper::service::service_fn( - move |req| { - let ctx = ctx.clone(); - async move { - tracing::debug!("Request to {}", req.uri().path()); - match req.uri().path() { - "/ws/db" => { - Ok(subscribe(ctx, req).await.unwrap_or_else(err_to_500)) - } - path if path.starts_with("/ws/rpc/") => { - match RequestGuid::from( - path.strip_prefix("/ws/rpc/").unwrap(), - ) { - None => { - tracing::debug!("No Guid Path"); - Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::empty()) - } - Some(guid) => { - match ctx.get_ws_continuation_handler(&guid).await { - Some(cont) => match cont(req).await { - Ok(r) => Ok(r), - Err(e) => Response::builder() - .status( - StatusCode::INTERNAL_SERVER_ERROR, - ) - .body(Body::from(format!("{}", e))), - }, - _ => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::empty()), - } - } - } - } - path if path.starts_with("/rest/rpc/") => { - match RequestGuid::from( - path.strip_prefix("/rest/rpc/").unwrap(), - ) { - None => { - tracing::debug!("No Guid Path"); - Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::empty()) - } - Some(guid) => { - match ctx.get_rest_continuation_handler(&guid).await - { - None => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::empty()), - Some(cont) => match cont(req).await { - Ok(r) => Ok(r), - Err(e) => Response::builder() - .status( - StatusCode::INTERNAL_SERVER_ERROR, - ) - .body(Body::from(format!("{}", e))), - }, - } - } - } - } - _ => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::empty()), - } - } - }, - )) - } - }); - builder.serve(make_svc) - } - .with_graceful_shutdown({ - let mut shutdown = rpc_ctx.shutdown.subscribe(); - async move { - shutdown.recv().await.expect("context dropped"); - } - }); - - let file_server_ctx = rpc_ctx.clone(); - let file_server = { - static_server::init(file_server_ctx, { - let mut shutdown = rpc_ctx.shutdown.subscribe(); - async move { - shutdown.recv().await.expect("context dropped"); - } - }) - }; - let tor_health_ctx = rpc_ctx.clone(); let tor_client = Client::builder() .proxy( @@ -240,21 +107,12 @@ async fn inner_main(cfg_path: Option) -> Result, Error embassy::sound::CHIME.play().await?; futures::try_join!( - server - .map_err(|e| Error::new(e, ErrorKind::Network)) - .map_ok(|_| tracing::debug!("RPC Server Shutdown")), metrics_task .map_err(|e| Error::new( eyre!("{}", e).wrap_err("Metrics daemon panicked!"), ErrorKind::Unknown )) .map_ok(|_| tracing::debug!("Metrics daemon Shutdown")), - ws_server - .map_err(|e| Error::new(e, ErrorKind::Network)) - .map_ok(|_| tracing::debug!("WebSocket Server Shutdown")), - file_server - .map_err(|e| Error::new(e, ErrorKind::Network)) - .map_ok(|_| tracing::debug!("Static File Server Shutdown")), tor_health_daemon .map_err(|e| Error::new( e.wrap_err("Tor Health daemon panicked!"), @@ -309,23 +167,7 @@ fn main() { tracing::debug!("{:?}", e.source); embassy::sound::BEETHOVEN.play().await?; #[cfg(feature = "avahi")] - let _mdns = MdnsController::init(); - tokio::fs::write( - "/etc/nginx/sites-available/default", - include_str!("../nginx/diagnostic-ui.conf"), - ) - .await - .with_ctx(|_| { - ( - embassy::ErrorKind::Filesystem, - "/etc/nginx/sites-available/default", - ) - })?; - Command::new("systemctl") - .arg("reload") - .arg("nginx") - .invoke(embassy::ErrorKind::Nginx) - .await?; + let _mdns = MdnsController::init().await?; let ctx = DiagnosticContext::init( cfg_path, if tokio::fs::metadata("/media/embassy/config/disk.guid") @@ -344,25 +186,25 @@ fn main() { e, ) .await?; + + let embassy_ip = get_current_ip(ctx.ethernet_interface.to_owned()).await?; + let embassy_ip_fqdn: ResourceFqdn = embassy_ip.parse()?; + let embassy_fqdn: ResourceFqdn = "embassy.local".parse()?; + + let diag_ui_handler = diag_ui_file_router(ctx.clone()).await?; + + let mut diag_http_server = + EmbassyServiceHTTPServer::new([0, 0, 0, 0].into(), 80, None).await?; + diag_http_server + .add_svc_handler_mapping(embassy_ip_fqdn, diag_ui_handler.clone()) + .await?; + diag_http_server + .add_svc_handler_mapping(embassy_fqdn, diag_ui_handler) + .await?; + let mut shutdown = ctx.shutdown.subscribe(); - rpc_server!({ - command: embassy::diagnostic_api, - context: ctx.clone(), - status: status_fn, - middleware: [ - cors, - diagnostic, - ] - }) - .with_graceful_shutdown({ - let mut shutdown = ctx.shutdown.subscribe(); - async move { - shutdown.recv().await.expect("context dropped"); - } - }) - .await - .with_kind(embassy::ErrorKind::Network)?; - Ok::<_, Error>(shutdown.recv().await.with_kind(crate::ErrorKind::Unknown)?) + + shutdown.recv().await.with_kind(crate::ErrorKind::Unknown) })() .await } diff --git a/backend/src/context/diagnostic.rs b/backend/src/context/diagnostic.rs index 025179829..0024f8565 100644 --- a/backend/src/context/diagnostic.rs +++ b/backend/src/context/diagnostic.rs @@ -1,4 +1,3 @@ -use std::net::{IpAddr, SocketAddr}; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -6,38 +5,37 @@ use std::sync::Arc; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::Context; use serde::Deserialize; -use tokio::fs::File; use tokio::sync::broadcast::Sender; use tracing::instrument; -use url::Host; use crate::shutdown::Shutdown; -use crate::util::io::from_toml_async_reader; -use crate::util::AsyncFileExt; -use crate::{Error, ResultExt}; +use crate::util::config::load_config_from_paths; +use crate::Error; #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct DiagnosticContextConfig { - pub bind_rpc: Option, + pub ethernet_interface: String, pub datadir: Option, } impl DiagnosticContextConfig { #[instrument(skip(path))] - pub async fn load>(path: Option

) -> Result { - let cfg_path = path - .as_ref() - .map(|p| p.as_ref()) - .unwrap_or(Path::new(crate::util::config::CONFIG_PATH)); - if let Some(f) = File::maybe_open(cfg_path) - .await - .with_ctx(|_| (crate::ErrorKind::Filesystem, cfg_path.display().to_string()))? - { - from_toml_async_reader(f).await - } else { - Ok(Self::default()) - } + pub async fn load + Send + 'static>(path: Option

) -> Result { + tokio::task::spawn_blocking(move || { + load_config_from_paths( + path.as_ref() + .into_iter() + .map(|p| p.as_ref()) + .chain(std::iter::once(Path::new( + crate::util::config::DEVICE_CONFIG_PATH, + ))) + .chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))), + ) + }) + .await + .unwrap() } + pub fn datadir(&self) -> &Path { self.datadir .as_deref() @@ -46,7 +44,7 @@ impl DiagnosticContextConfig { } pub struct DiagnosticContextSeed { - pub bind_rpc: SocketAddr, + pub ethernet_interface: String, pub datadir: PathBuf, pub shutdown: Sender>, pub error: Arc, @@ -57,17 +55,20 @@ pub struct DiagnosticContextSeed { pub struct DiagnosticContext(Arc); impl DiagnosticContext { #[instrument(skip(path))] - pub async fn init>( + pub async fn init + Send + 'static>( path: Option

, disk_guid: Option>, error: Error, ) -> Result { + tracing::error!("Error: {}: Starting diagnostic UI", error); + tracing::debug!("{:?}", error); + let cfg = DiagnosticContextConfig::load(path).await?; let (shutdown, _) = tokio::sync::broadcast::channel(1); Ok(Self(Arc::new(DiagnosticContextSeed { - bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()), + ethernet_interface: cfg.ethernet_interface.clone(), datadir: cfg.datadir().to_owned(), shutdown, disk_guid, @@ -76,17 +77,7 @@ impl DiagnosticContext { } } -impl Context for DiagnosticContext { - fn host(&self) -> Host<&str> { - match self.0.bind_rpc.ip() { - IpAddr::V4(a) => Host::Ipv4(a), - IpAddr::V6(a) => Host::Ipv6(a), - } - } - fn port(&self) -> u16 { - self.0.bind_rpc.port() - } -} +impl Context for DiagnosticContext {} impl Deref for DiagnosticContext { type Target = DiagnosticContextSeed; fn deref(&self) -> &Self::Target { diff --git a/backend/src/context/install.rs b/backend/src/context/install.rs index 7d74b6acb..00268112a 100644 --- a/backend/src/context/install.rs +++ b/backend/src/context/install.rs @@ -1,4 +1,3 @@ -use std::net::{IpAddr, SocketAddr}; use std::ops::Deref; use std::path::Path; use std::sync::Arc; @@ -7,16 +6,14 @@ use rpc_toolkit::Context; use serde::Deserialize; use tokio::sync::broadcast::Sender; use tracing::instrument; -use url::Host; +use crate::os_install::find_eth_iface; use crate::util::config::load_config_from_paths; use crate::Error; #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case")] -pub struct InstallContextConfig { - pub bind_rpc: Option, -} +pub struct InstallContextConfig {} impl InstallContextConfig { #[instrument(skip(path))] pub async fn load + Send + 'static>(path: Option

) -> Result { @@ -25,9 +22,6 @@ impl InstallContextConfig { path.as_ref() .into_iter() .map(|p| p.as_ref()) - .chain(std::iter::once(Path::new( - "/media/embassy/config/config.yaml", - ))) .chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))), ) }) @@ -37,7 +31,7 @@ impl InstallContextConfig { } pub struct InstallContextSeed { - pub bind_rpc: SocketAddr, + pub ethernet_interface: String, pub shutdown: Sender<()>, } @@ -46,26 +40,16 @@ pub struct InstallContext(Arc); impl InstallContext { #[instrument(skip(path))] pub async fn init + Send + 'static>(path: Option

) -> Result { - let cfg = InstallContextConfig::load(path.as_ref().map(|p| p.as_ref().to_owned())).await?; + let _cfg = InstallContextConfig::load(path.as_ref().map(|p| p.as_ref().to_owned())).await?; let (shutdown, _) = tokio::sync::broadcast::channel(1); Ok(Self(Arc::new(InstallContextSeed { - bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()), + ethernet_interface: find_eth_iface().await?, shutdown, }))) } } -impl Context for InstallContext { - fn host(&self) -> Host<&str> { - match self.0.bind_rpc.ip() { - IpAddr::V4(a) => Host::Ipv4(a), - IpAddr::V6(a) => Host::Ipv6(a), - } - } - fn port(&self) -> u16 { - self.0.bind_rpc.port() - } -} +impl Context for InstallContext {} impl Deref for InstallContext { type Target = InstallContextSeed; fn deref(&self) -> &Self::Target { diff --git a/backend/src/context/rpc.rs b/backend/src/context/rpc.rs index 4e295c8b4..cccc40659 100644 --- a/backend/src/context/rpc.rs +++ b/backend/src/context/rpc.rs @@ -1,5 +1,5 @@ use std::collections::{BTreeMap, VecDeque}; -use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -11,12 +11,10 @@ use helpers::to_tmp_path; use patch_db::json_ptr::JsonPointer; use patch_db::{DbHandle, LockReceipt, LockType, PatchDb, Revision}; use reqwest::Url; -use rpc_toolkit::url::Host; use rpc_toolkit::Context; use serde::Deserialize; use sqlx::postgres::PgConnectOptions; use sqlx::PgPool; -use tokio::process::Command; use tokio::sync::{broadcast, oneshot, Mutex, RwLock}; use tracing::instrument; @@ -28,15 +26,14 @@ use crate::init::{init_postgres, pgloader}; use crate::install::cleanup::{cleanup_failed, uninstall, CleanupFailedReceipts}; use crate::manager::ManagerMap; use crate::middleware::auth::HashSessionToken; +use crate::net::net_controller::NetController; use crate::net::tor::os_key; use crate::net::wifi::WpaCli; -use crate::net::NetController; use crate::notifications::NotificationManager; use crate::setup::password_hash; use crate::shutdown::Shutdown; use crate::status::{MainStatus, Status}; use crate::util::config::load_config_from_paths; -use crate::util::Invoke; use crate::{Error, ErrorKind, ResultExt}; #[derive(Debug, Default, Deserialize)] @@ -48,8 +45,6 @@ pub struct RpcContextConfig { pub migration_batch_rows: Option, pub migration_prefetch_rows: Option, pub bind_rpc: Option, - pub bind_ws: Option, - pub bind_static: Option, pub tor_control: Option, pub tor_socks: Option, pub dns_bind: Option>, @@ -65,7 +60,7 @@ impl RpcContextConfig { .into_iter() .map(|p| p.as_ref()) .chain(std::iter::once(Path::new( - "/media/embassy/config/config.yaml", + crate::util::config::DEVICE_CONFIG_PATH, ))) .chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))), ) @@ -123,9 +118,6 @@ pub struct RpcContextSeed { pub os_partitions: OsPartitionInfo, pub wifi_interface: Option, pub ethernet_interface: String, - pub bind_rpc: SocketAddr, - pub bind_ws: SocketAddr, - pub bind_static: SocketAddr, pub datadir: PathBuf, pub disk_guid: Arc, pub db: PatchDb, @@ -182,12 +174,13 @@ impl RpcCleanReceipts { } } -pub struct RpcSetNginxReceipts { +pub struct RpcSetHostNameReceipts { pub hostname_receipts: HostNameReceipt, + #[allow(dead_code)] server_info: LockReceipt, } -impl RpcSetNginxReceipts { +impl RpcSetHostNameReceipts { pub async fn new(db: &'_ mut impl DbHandle) -> Result { let mut locks = Vec::new(); @@ -235,7 +228,7 @@ impl RpcContext { docker.set_timeout(Duration::from_secs(600)); tracing::info!("Connected to Docker"); let net_controller = NetController::init( - ([127, 0, 0, 1], 80).into(), + ([0, 0, 0, 0], 80).into(), crate::net::tor::os_key(&mut secret_store.acquire().await?).await?, base.tor_control .unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))), @@ -244,8 +237,8 @@ impl RpcContext { .map(|v| v.as_slice()) .unwrap_or(&[SocketAddr::from(([127, 0, 0, 1], 53))]), secret_store.clone(), - None, &mut db.handle(), + None, ) .await?; tracing::info!("Initialized Net Controller"); @@ -259,9 +252,6 @@ impl RpcContext { os_partitions: base.os_partitions, wifi_interface: base.wifi_interface.clone(), ethernet_interface: base.ethernet_interface, - bind_rpc: base.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()), - bind_ws: base.bind_ws.unwrap_or(([127, 0, 0, 1], 5960).into()), - bind_static: base.bind_static.unwrap_or(([127, 0, 0, 1], 5961).into()), disk_guid, db, secret_store, @@ -295,39 +285,12 @@ impl RpcContext { Ok(res) } - #[instrument(skip(self, db, receipts))] - pub async fn set_nginx_conf( - &self, - db: &mut Db, - receipts: RpcSetNginxReceipts, - ) -> Result<(), Error> { - tokio::fs::write("/etc/nginx/sites-available/default", { - let info = receipts.server_info.get(db).await?; - format!( - include_str!("../nginx/main-ui.conf.template"), - lan_hostname = info.lan_address.host_str().unwrap(), - tor_hostname = info.tor_address.host_str().unwrap(), - ) - }) - .await - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - "/etc/nginx/sites-available/default", - ) - })?; - Command::new("systemctl") - .arg("reload") - .arg("nginx") - .invoke(crate::ErrorKind::Nginx) - .await?; - Ok(()) - } #[instrument(skip(self))] pub async fn shutdown(self) -> Result<(), Error> { self.managers.empty().await?; self.secret_store.close().await; self.is_closed.store(true, Ordering::SeqCst); + // TODO: shutdown http servers Ok(()) } @@ -461,17 +424,7 @@ impl RpcContext { } } } -impl Context for RpcContext { - fn host(&self) -> Host<&str> { - match self.0.bind_rpc.ip() { - IpAddr::V4(a) => Host::Ipv4(a), - IpAddr::V6(a) => Host::Ipv6(a), - } - } - fn port(&self) -> u16 { - self.0.bind_rpc.port() - } -} +impl Context for RpcContext {} impl Deref for RpcContext { type Target = RpcContextSeed; fn deref(&self) -> &Self::Target { diff --git a/backend/src/context/setup.rs b/backend/src/context/setup.rs index e2a76fbc1..8a66c72f5 100644 --- a/backend/src/context/setup.rs +++ b/backend/src/context/setup.rs @@ -1,4 +1,3 @@ -use std::net::{IpAddr, SocketAddr}; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -14,7 +13,6 @@ use sqlx::PgPool; use tokio::sync::broadcast::Sender; use tokio::sync::RwLock; use tracing::instrument; -use url::Host; use crate::db::model::Database; use crate::disk::OsPartitionInfo; @@ -36,9 +34,9 @@ pub struct SetupResult { #[serde(rename_all = "kebab-case")] pub struct SetupContextConfig { pub os_partitions: OsPartitionInfo, + pub ethernet_interface: String, pub migration_batch_rows: Option, pub migration_prefetch_rows: Option, - pub bind_rpc: Option, pub datadir: Option, } impl SetupContextConfig { @@ -50,7 +48,7 @@ impl SetupContextConfig { .into_iter() .map(|p| p.as_ref()) .chain(std::iter::once(Path::new( - "/media/embassy/config/config.yaml", + crate::util::config::DEVICE_CONFIG_PATH, ))) .chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))), ) @@ -67,10 +65,10 @@ impl SetupContextConfig { pub struct SetupContextSeed { pub os_partitions: OsPartitionInfo, + pub ethernet_interface: String, pub config_path: Option, pub migration_batch_rows: usize, pub migration_prefetch_rows: usize, - pub bind_rpc: SocketAddr, pub shutdown: Sender<()>, pub datadir: PathBuf, /// Used to encrypt for hidding from snoopers for setups create password @@ -98,10 +96,10 @@ impl SetupContext { let datadir = cfg.datadir().to_owned(); Ok(Self(Arc::new(SetupContextSeed { os_partitions: cfg.os_partitions, + ethernet_interface: cfg.ethernet_interface, config_path: path.as_ref().map(|p| p.as_ref().to_owned()), migration_batch_rows: cfg.migration_batch_rows.unwrap_or(25000), migration_prefetch_rows: cfg.migration_prefetch_rows.unwrap_or(100_000), - bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()), shutdown, datadir, current_secret: Arc::new( @@ -161,17 +159,7 @@ impl SetupContext { } } -impl Context for SetupContext { - fn host(&self) -> Host<&str> { - match self.0.bind_rpc.ip() { - IpAddr::V4(a) => Host::Ipv4(a), - IpAddr::V6(a) => Host::Ipv6(a), - } - } - fn port(&self) -> u16 { - self.0.bind_rpc.port() - } -} +impl Context for SetupContext {} impl Deref for SetupContext { type Target = SetupContextSeed; fn deref(&self) -> &Self::Target { diff --git a/backend/src/error.rs b/backend/src/error.rs index 5c11d2440..d250a6bbb 100644 --- a/backend/src/error.rs +++ b/backend/src/error.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use color_eyre::eyre::eyre; +use http::uri::InvalidUri; use models::InvalidId; use patch_db::Revision; use rpc_toolkit::yajrc::RpcError; @@ -22,7 +23,7 @@ pub enum ErrorKind { Utf8 = 13, ParseVersion = 14, IncorrectDisk = 15, - Nginx = 16, + // Nginx = 16, Dependency = 17, ParseS9pk = 18, ParseUrl = 19, @@ -71,6 +72,11 @@ pub enum ErrorKind { HttpRange = 62, ContentLength = 63, BytesError = 64, + InvalidIP = 65, + JoinError = 66, + AsciiError = 67, + NoHost = 68, + SignError = 69, } impl ErrorKind { pub fn as_str(&self) -> &'static str { @@ -91,7 +97,7 @@ impl ErrorKind { Utf8 => "UTF-8 Parse Error", ParseVersion => "Version Parsing Error", IncorrectDisk => "Incorrect Disk", - Nginx => "Nginx Error", + // Nginx => "Nginx Error", Dependency => "Dependency Error", ParseS9pk => "S9PK Parsing Error", ParseUrl => "URL Parsing Error", @@ -140,6 +146,11 @@ impl ErrorKind { HttpRange => "No Support for Web Server HTTP Ranges", ContentLength => "Request has no content length header", BytesError => "Could not get the bytes for this request", + InvalidIP => "Could not parse this IP address", + JoinError => "Join Handle Error", + AsciiError => "Could not parse ascii text", + NoHost => "No Host header ", + SignError => "Signing error", } } } @@ -250,6 +261,13 @@ impl From for Error { Error::new(e, ErrorKind::DiskManagement) } } + +impl From for Error { + fn from(e: InvalidUri) -> Self { + Error::new(eyre!("{}", e), ErrorKind::ParseUrl) + } +} + impl From for RpcError { fn from(e: Error) -> Self { let mut data_object = serde_json::Map::with_capacity(3); diff --git a/backend/src/hostname.rs b/backend/src/hostname.rs index 1290d5ede..c8a492e44 100644 --- a/backend/src/hostname.rs +++ b/backend/src/hostname.rs @@ -1,8 +1,10 @@ use patch_db::DbHandle; use rand::{thread_rng, Rng}; +use sqlx::Connection; use tokio::process::Command; use tracing::instrument; +use crate::context::RpcContext; use crate::util::Invoke; use crate::{Error, ErrorKind}; #[derive(Clone, serde::Deserialize, serde::Serialize, Debug)] @@ -22,6 +24,35 @@ impl Hostname { pub fn lan_address(&self) -> String { format!("https://{}.local", self.0) } + + pub fn local_domain_name(&self) -> String { + format!("{}.local", self.0) + } + pub fn no_dot_host_name(&self) -> String { + self.0.to_owned() + } +} + +pub async fn get_current_ip(eth: String) -> Result { + let cmd = format!(r"ifconfig {} | awk '/inet / {{print $2}}'", eth); + + let out = Command::new("bash") + .arg("-c") + .arg(cmd) + .invoke(ErrorKind::ParseSysInfo) + .await?; + let out_string = String::from_utf8(out)?; + Ok(out_string.trim().to_owned()) +} + +pub async fn get_embassyd_tor_addr(rpc_ctx: RpcContext) -> Result { + let mut secrets_handle = rpc_ctx.secret_store.acquire().await?; + + let mut secrets_tx = secrets_handle.begin().await?; + + let tor_key = crate::net::tor::os_key(&mut secrets_tx).await?; + + Ok(tor_key.public().get_onion_address().to_string()) } pub fn generate_hostname() -> Hostname { diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 876aacafb..8bca91f20 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -43,7 +43,6 @@ pub mod setup; pub mod shutdown; pub mod sound; pub mod ssh; -pub mod static_server; pub mod status; pub mod system; pub mod update; diff --git a/backend/src/net/cert_resolver.rs b/backend/src/net/cert_resolver.rs new file mode 100644 index 000000000..9e2e9a3d2 --- /dev/null +++ b/backend/src/net/cert_resolver.rs @@ -0,0 +1,216 @@ +use std::collections::BTreeMap; +use std::io; +use std::pin::Pin; +use std::str::FromStr; +use std::sync::{Arc, RwLock}; +use std::task::{Context, Poll}; + +use color_eyre::eyre::eyre; +use futures::{ready, Future}; +use hyper::server::accept::Accept; +use hyper::server::conn::{AddrIncoming, AddrStream}; +use openssl::pkey::{PKey, Private}; +use openssl::x509::X509; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio_rustls::rustls::server::ResolvesServerCert; +use tokio_rustls::rustls::sign::{any_supported_type, CertifiedKey}; +use tokio_rustls::rustls::{Certificate, PrivateKey, ServerConfig}; + +use crate::net::net_utils::ResourceFqdn; +use crate::Error; + +enum State { + Handshaking(tokio_rustls::Accept), + Streaming(tokio_rustls::server::TlsStream), +} + +// tokio_rustls::server::TlsStream doesn't expose constructor methods, +// so we have to TlsAcceptor::accept and handshake to have access to it +// TlsStream implements AsyncRead/AsyncWrite handshaking tokio_rustls::Accept first +pub struct TlsStream { + state: State, +} + +impl TlsStream { + fn new(stream: AddrStream, config: Arc) -> TlsStream { + let accept = tokio_rustls::TlsAcceptor::from(config).accept(stream); + TlsStream { + state: State::Handshaking(accept), + } + } +} + +impl AsyncRead for TlsStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut ReadBuf, + ) -> Poll> { + let pin = self.get_mut(); + match pin.state { + State::Handshaking(ref mut accept) => match ready!(Pin::new(accept).poll(cx)) { + Ok(mut stream) => { + let result = Pin::new(&mut stream).poll_read(cx, buf); + pin.state = State::Streaming(stream); + result + } + Err(err) => Poll::Ready(Err(err)), + }, + State::Streaming(ref mut stream) => Pin::new(stream).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for TlsStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let pin = self.get_mut(); + match pin.state { + State::Handshaking(ref mut accept) => match ready!(Pin::new(accept).poll(cx)) { + Ok(mut stream) => { + let result = Pin::new(&mut stream).poll_write(cx, buf); + pin.state = State::Streaming(stream); + result + } + Err(err) => Poll::Ready(Err(err)), + }, + State::Streaming(ref mut stream) => Pin::new(stream).poll_write(cx, buf), + } + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.state { + State::Handshaking(_) => Poll::Ready(Ok(())), + State::Streaming(ref mut stream) => Pin::new(stream).poll_flush(cx), + } + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.state { + State::Handshaking(_) => Poll::Ready(Ok(())), + State::Streaming(ref mut stream) => Pin::new(stream).poll_shutdown(cx), + } + } +} + +impl ResolvesServerCert for EmbassyCertResolver { + fn resolve( + &self, + client_hello: tokio_rustls::rustls::server::ClientHello, + ) -> Option> { + let hostname_raw = client_hello.server_name(); + + match hostname_raw { + Some(hostname_str) => { + + let full_fqdn = match ResourceFqdn::from_str(hostname_str) { + Ok(fqdn) => fqdn, + Err(_) => { + tracing::error!("Error converting {} to fqdn struct", hostname_str); + return None; + } + }; + let lock = self.cert_mapping.read(); + + match lock { + Ok(lock) => lock + .get(&full_fqdn) + .map(|cert_key| Arc::new(cert_key.to_owned())), + Err(err) => { + tracing::error!("resolve fn Error: {}", err); + None + } + } + } + None => None, + } + } +} + +#[derive(Clone, Default)] +pub struct EmbassyCertResolver { + cert_mapping: Arc>>, +} + +impl EmbassyCertResolver { + pub fn new() -> Self { + Self::default() + } + pub async fn add_certificate_to_resolver( + &mut self, + service_resource_fqdn: ResourceFqdn, + package_cert_data: (PKey, Vec), + ) -> Result<(), Error> { + let x509_cert_chain = package_cert_data.1; + let private_keys = package_cert_data + .0 + .private_key_to_der() + .map_err(|err| Error::new(eyre!("err {}", err), crate::ErrorKind::BytesError))?; + + let mut full_rustls_certs = Vec::new(); + for cert in x509_cert_chain.iter() { + let cert = + Certificate(cert.to_der().map_err(|err| { + Error::new(eyre!("err: {}", err), crate::ErrorKind::BytesError) + })?); + + full_rustls_certs.push(cert); + } + + let pre_sign_key = PrivateKey(private_keys); + let actual_sign_key = any_supported_type(&pre_sign_key) + .map_err(|err| Error::new(eyre!("{}", err), crate::ErrorKind::SignError))?; + + let cert_key = CertifiedKey::new(full_rustls_certs, actual_sign_key); + + let mut lock = self + .cert_mapping + .write() + .map_err(|err| Error::new(eyre!("{}", err), crate::ErrorKind::Network))?; + lock.insert(service_resource_fqdn, cert_key); + + Ok(()) + } + + pub async fn remove_cert(&mut self, hostname: ResourceFqdn) -> Result<(), Error> { + let mut lock = self + .cert_mapping + .write() + .map_err(|err| Error::new(eyre!("{}", err), crate::ErrorKind::Network))?; + + lock.remove(&hostname); + + Ok(()) + } +} + +pub struct TlsAcceptor { + config: Arc, + incoming: AddrIncoming, +} + +impl TlsAcceptor { + pub fn new(config: Arc, incoming: AddrIncoming) -> TlsAcceptor { + TlsAcceptor { config, incoming } + } +} + +impl Accept for TlsAcceptor { + type Conn = TlsStream; + type Error = io::Error; + + fn poll_accept( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let pin = self.get_mut(); + match ready!(Pin::new(&mut pin.incoming).poll_accept(cx)) { + Some(Ok(sock)) => Poll::Ready(Some(Ok(TlsStream::new(sock, pin.config.clone())))), + Some(Err(e)) => Poll::Ready(Some(Err(e))), + None => Poll::Ready(None), + } + } +} diff --git a/backend/src/net/embassy_service_http_server.rs b/backend/src/net/embassy_service_http_server.rs new file mode 100644 index 000000000..37f12141d --- /dev/null +++ b/backend/src/net/embassy_service_http_server.rs @@ -0,0 +1,173 @@ +use std::collections::BTreeMap; +use std::net::{IpAddr, SocketAddr}; +use std::sync::Arc; + +use helpers::NonDetachingJoinHandle; +use http::StatusCode; +use hyper::server::conn::AddrIncoming; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Error as HyperError, Response, Server}; +use tokio::sync::oneshot; +use tokio_rustls::rustls::ServerConfig; +use tracing::error; + +use crate::net::cert_resolver::TlsAcceptor; +use crate::net::net_utils::{host_addr_fqdn, ResourceFqdn}; +use crate::net::HttpHandler; +use crate::Error; + +static RES_NOT_FOUND: &[u8] = b"503 Service Unavailable"; +static NO_HOST: &[u8] = b"No host header found"; + +pub struct EmbassyServiceHTTPServer { + pub svc_mapping: Arc>>, + pub shutdown: oneshot::Sender<()>, + pub handle: NonDetachingJoinHandle<()>, + pub ssl_cfg: Option>, +} + +impl EmbassyServiceHTTPServer { + pub async fn new( + listener_addr: IpAddr, + port: u16, + ssl_cfg: Option>, + ) -> Result { + let (tx, rx) = tokio::sync::oneshot::channel::<()>(); + + let listener_socket_addr = SocketAddr::from((listener_addr, port)); + + let server_service_mapping = Arc::new(tokio::sync::RwLock::new(BTreeMap::< + ResourceFqdn, + HttpHandler, + >::new())); + + let server_service_mapping1 = server_service_mapping.clone(); + + let bare_make_service_fn = move || { + let server_service_mapping = server_service_mapping.clone(); + + async move { + Ok::<_, HyperError>(service_fn(move |req| { + let mut server_service_mapping = server_service_mapping.clone(); + + async move { + server_service_mapping = server_service_mapping.clone(); + + let host = host_addr_fqdn(&req); + match host { + Ok(host_uri) => { + let res = { + let mapping = server_service_mapping.read().await; + + let opt_handler = mapping.get(&host_uri).cloned(); + + opt_handler + }; + match res { + Some(opt_handler) => { + let response = opt_handler(req).await; + + match response { + Ok(resp) => Ok::, hyper::Error>(resp), + Err(err) => Ok(respond_hyper_error(err)), + } + } + None => Ok(res_not_found()), + } + } + Err(e) => Ok(no_host_found(e)), + } + } + })) + } + }; + + let inner_ssl_cfg = ssl_cfg.clone(); + let handle = tokio::spawn(async move { + match inner_ssl_cfg { + Some(cfg) => { + let incoming = AddrIncoming::bind(&listener_socket_addr).unwrap(); + + let server = Server::builder(TlsAcceptor::new(cfg, incoming)) + .http1_preserve_header_case(true) + .http1_title_case_headers(true) + .serve(make_service_fn(|_| bare_make_service_fn())) + .with_graceful_shutdown({ + async { + rx.await.ok(); + } + }); + + if let Err(e) = server.await { + error!("Spawning hyper server errorr: {}", e); + } + } + None => { + let server = Server::bind(&listener_socket_addr) + .http1_preserve_header_case(true) + .http1_title_case_headers(true) + .serve(make_service_fn(|_| bare_make_service_fn())) + .with_graceful_shutdown({ + async { + rx.await.ok(); + } + }); + if let Err(e) = server.await { + error!("Spawning hyper server errorr: {}", e); + } + } + }; + }); + + Ok(Self { + svc_mapping: server_service_mapping1, + handle: handle.into(), + shutdown: tx, + ssl_cfg, + }) + } + + pub async fn add_svc_handler_mapping( + &mut self, + fqdn: ResourceFqdn, + svc_handle: HttpHandler, + ) -> Result<(), Error> { + let mut mapping = self.svc_mapping.write().await; + + mapping.insert(fqdn.clone(), svc_handle); + + Ok(()) + } + + pub async fn remove_svc_handler_mapping(&mut self, fqdn: ResourceFqdn) -> Result<(), Error> { + let mut mapping = self.svc_mapping.write().await; + + mapping.remove(&fqdn); + + Ok(()) + } +} + +/// HTTP status code 503 +fn res_not_found() -> Response { + Response::builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .body(RES_NOT_FOUND.into()) + .unwrap() +} + +fn no_host_found(err: Error) -> Response { + let err_txt = format!("{}: Error {}", String::from_utf8_lossy(NO_HOST), err); + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(err_txt.into()) + .unwrap() +} + +fn respond_hyper_error(err: hyper::Error) -> Response { + let err_txt = format!("{}: Error {}", String::from_utf8_lossy(NO_HOST), err); + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(err_txt.into()) + .unwrap() +} diff --git a/backend/src/net/mod.rs b/backend/src/net/mod.rs index c6a9787a0..73d59f9b0 100644 --- a/backend/src/net/mod.rs +++ b/backend/src/net/mod.rs @@ -1,34 +1,31 @@ -use std::net::{Ipv4Addr, SocketAddr}; -use std::path::PathBuf; +use std::collections::BTreeMap; +use std::sync::Arc; -use openssl::pkey::{PKey, Private}; -use openssl::x509::X509; -use patch_db::DbHandle; +use futures::future::BoxFuture; +use hyper::{Body, Client, Error as HyperError, Request, Response}; +use indexmap::IndexSet; use rpc_toolkit::command; -use sqlx::PgPool; -use torut::onion::{OnionAddressV3, TorSecretKeyV3}; -use tracing::instrument; -use self::interface::{Interface, InterfaceId}; -#[cfg(feature = "avahi")] -use self::mdns::MdnsController; -use self::nginx::NginxController; -use self::ssl::SslManager; -use self::tor::TorController; -use crate::hostname::get_hostname; -use crate::net::dns::DnsController; -use crate::net::interface::TorConfig; -use crate::net::nginx::InterfaceMetadata; -use crate::s9pk::manifest::PackageId; +use self::interface::InterfaceId; + +use crate::net::interface::LanPortConfig; + +use crate::util::serde::Port; use crate::Error; pub mod dns; pub mod interface; #[cfg(feature = "avahi")] pub mod mdns; -pub mod nginx; +pub mod embassy_service_http_server; +pub mod net_controller; +pub mod net_utils; +pub mod proxy_controller; pub mod ssl; +pub mod cert_resolver; +pub mod static_server; pub mod tor; +pub mod vhost_controller; pub mod wifi; const PACKAGE_CERT_PATH: &str = "/var/lib/embassy/ssl"; @@ -38,161 +35,23 @@ pub fn net() -> Result<(), Error> { Ok(()) } +#[derive(Default)] +struct PackageNetInfo { + interfaces: BTreeMap, +} +pub struct InterfaceMetadata { + pub fqdn: String, + pub lan_config: BTreeMap, + pub protocols: IndexSet, +} + /// Indicates that the net controller has created the /// SSL keys #[derive(Clone, Copy)] pub struct GeneratedCertificateMountPoint(()); -pub struct NetController { - pub tor: TorController, - #[cfg(feature = "avahi")] - pub mdns: MdnsController, - pub nginx: NginxController, - pub ssl: SslManager, - pub dns: DnsController, -} -impl NetController { - #[instrument(skip(db, handle))] - pub async fn init( - embassyd_addr: SocketAddr, - embassyd_tor_key: TorSecretKeyV3, - tor_control: SocketAddr, - dns_bind: &[SocketAddr], - db: PgPool, - import_root_ca: Option<(PKey, X509)>, - handle: &mut Db, - ) -> Result { - let ssl = match import_root_ca { - None => SslManager::init(db, handle).await, - Some(a) => SslManager::import_root_ca(db, a.0, a.1).await, - }?; +pub type HttpHandler = Arc< + dyn Fn(Request) -> BoxFuture<'static, Result, HyperError>> + Send + Sync, +>; - let hostname_receipts = crate::hostname::HostNameReceipt::new(handle).await?; - let hostname = get_hostname(handle, &hostname_receipts).await?; - Ok(Self { - tor: TorController::init(embassyd_addr, embassyd_tor_key, tor_control).await?, - #[cfg(feature = "avahi")] - mdns: MdnsController::init().await?, - nginx: NginxController::init(PathBuf::from("/etc/nginx"), &ssl, &hostname).await?, - ssl, - dns: DnsController::init(dns_bind).await?, - }) - } - - pub fn ssl_directory_for(pkg_id: &PackageId) -> PathBuf { - PathBuf::from(format!("{}/{}", PACKAGE_CERT_PATH, pkg_id)) - } - - #[instrument(skip(self, interfaces, _generated_certificate))] - pub async fn add<'a, I>( - &self, - pkg_id: &PackageId, - ip: Ipv4Addr, - interfaces: I, - _generated_certificate: GeneratedCertificateMountPoint, - ) -> Result<(), Error> - where - I: IntoIterator + Clone, - for<'b> &'b I: IntoIterator, - { - let interfaces_tor = interfaces - .clone() - .into_iter() - .filter_map(|i| match i.1.tor_config.clone() { - None => None, - Some(cfg) => Some((i.0, cfg, i.2)), - }) - .collect::>(); - let (tor_res, _, nginx_res, _) = tokio::join!( - self.tor.add(pkg_id, ip, interfaces_tor), - { - #[cfg(feature = "avahi")] - let mdns_fut = self.mdns.add( - pkg_id, - interfaces - .clone() - .into_iter() - .map(|(interface_id, _, key)| (interface_id, key)), - ); - #[cfg(not(feature = "avahi"))] - let mdns_fut = futures::future::ready(()); - mdns_fut - }, - { - let interfaces = interfaces - .into_iter() - .filter_map(|(id, interface, tor_key)| match &interface.lan_config { - None => None, - Some(cfg) => Some(( - id, - InterfaceMetadata { - dns_base: OnionAddressV3::from(&tor_key.public()) - .get_address_without_dot_onion(), - lan_config: cfg.clone(), - protocols: interface.protocols.clone(), - }, - )), - }); - self.nginx.add(&self.ssl, pkg_id.clone(), ip, interfaces) - }, - self.dns.add(pkg_id, ip), - ); - tor_res?; - nginx_res?; - - Ok(()) - } - - #[instrument(skip(self, interfaces))] - pub async fn remove + Clone>( - &self, - pkg_id: &PackageId, - ip: Ipv4Addr, - interfaces: I, - ) -> Result<(), Error> { - let (tor_res, _, nginx_res, _) = tokio::join!( - self.tor.remove(pkg_id, interfaces.clone()), - { - #[cfg(feature = "avahi")] - let mdns_fut = self.mdns.remove(pkg_id, interfaces); - #[cfg(not(feature = "avahi"))] - let mdns_fut = futures::future::ready(()); - mdns_fut - }, - self.nginx.remove(pkg_id), - self.dns.remove(pkg_id, ip), - ); - tor_res?; - nginx_res?; - Ok(()) - } - - pub async fn generate_certificate_mountpoint<'a, I>( - &self, - pkg_id: &PackageId, - interfaces: &I, - ) -> Result - where - I: IntoIterator + Clone, - for<'b> &'b I: IntoIterator, - { - tracing::info!("Generating SSL Certificate mountpoints for {}", pkg_id); - let package_path = PathBuf::from(PACKAGE_CERT_PATH).join(pkg_id); - tokio::fs::create_dir_all(&package_path).await?; - for (id, _, key) in interfaces { - let dns_base = OnionAddressV3::from(&key.public()).get_address_without_dot_onion(); - let ssl_path_key = package_path.join(format!("{}.key.pem", id)); - let ssl_path_cert = package_path.join(format!("{}.cert.pem", id)); - let (key, chain) = self.ssl.certificate_for(&dns_base, pkg_id).await?; - tokio::try_join!( - crate::net::ssl::export_key(&key, &ssl_path_key), - crate::net::ssl::export_cert(&chain, &ssl_path_cert) - )?; - } - Ok(GeneratedCertificateMountPoint(())) - } - - pub async fn export_root_ca(&self) -> Result<(PKey, X509), Error> { - self.ssl.export_root_ca().await - } -} +pub type HttpClient = Client; diff --git a/backend/src/net/net_controller.rs b/backend/src/net/net_controller.rs new file mode 100644 index 000000000..2c2754001 --- /dev/null +++ b/backend/src/net/net_controller.rs @@ -0,0 +1,274 @@ +use std::net::{Ipv4Addr, SocketAddr}; +use std::path::PathBuf; +use std::str::FromStr; + +use models::InterfaceId; +use openssl::pkey::{PKey, Private}; +use openssl::x509::X509; +use patch_db::DbHandle; +use sqlx::PgPool; +use torut::onion::{OnionAddressV3, TorSecretKeyV3}; +use tracing::instrument; + +use crate::context::RpcContext; +use crate::hostname::{get_current_ip, get_embassyd_tor_addr, get_hostname, HostNameReceipt}; +use crate::net::dns::DnsController; +use crate::net::interface::{Interface, TorConfig}; +#[cfg(feature = "avahi")] +use crate::net::mdns::MdnsController; +use crate::net::net_utils::ResourceFqdn; +use crate::net::proxy_controller::ProxyController; +use crate::net::ssl::SslManager; +use crate::net::tor::TorController; +use crate::net::{ + GeneratedCertificateMountPoint, HttpHandler, InterfaceMetadata, PACKAGE_CERT_PATH, +}; +use crate::s9pk::manifest::PackageId; +use crate::Error; + +pub struct NetController { + pub tor: TorController, + #[cfg(feature = "avahi")] + pub mdns: MdnsController, + pub proxy: ProxyController, + pub ssl: SslManager, + pub dns: DnsController, +} + +impl NetController { + #[instrument(skip(db, db_handle))] + pub async fn init( + embassyd_addr: SocketAddr, + embassyd_tor_key: TorSecretKeyV3, + tor_control: SocketAddr, + dns_bind: &[SocketAddr], + db: PgPool, + db_handle: &mut Db, + import_root_ca: Option<(PKey, X509)>, + ) -> Result { + let receipts = HostNameReceipt::new(db_handle).await?; + let embassy_host_name = get_hostname(db_handle, &receipts).await?; + let embassy_name = embassy_host_name.local_domain_name(); + + let fqdn_name = ResourceFqdn::from_str(&embassy_name)?; + + let ssl = match import_root_ca { + None => SslManager::init(db.clone(), db_handle).await, + Some(a) => SslManager::import_root_ca(db.clone(), a.0, a.1).await, + }?; + Ok(Self { + tor: TorController::init(embassyd_addr, embassyd_tor_key, tor_control).await?, + #[cfg(feature = "avahi")] + mdns: MdnsController::init().await?, + proxy: ProxyController::init(embassyd_addr, fqdn_name, ssl.clone()).await?, + ssl, + dns: DnsController::init(dns_bind).await?, + }) + } + + pub async fn setup_embassy_ui(rpc_ctx: RpcContext) -> Result<(), Error> { + NetController::setup_embassy_http_ui_handle(rpc_ctx.clone()).await?; + NetController::setup_embassy_https_ui_handle(rpc_ctx.clone()).await?; + + Ok(()) + } + + async fn setup_embassy_https_ui_handle(rpc_ctx: RpcContext) -> Result<(), Error> { + let host_name = rpc_ctx.net_controller.proxy.get_hostname().await; + + let host_name_fqdn: ResourceFqdn = host_name.parse()?; + + let handler: HttpHandler = + crate::net::static_server::main_ui_server_router(rpc_ctx.clone()).await?; + + let eos_pkg_id: PackageId = "embassy".parse().unwrap(); + + if let ResourceFqdn::Uri { + full_uri: _, + root, + tld: _, + } = host_name_fqdn.clone() + { + let root_cert = rpc_ctx + .net_controller + .ssl + .certificate_for(&root, &eos_pkg_id) + .await?; + + rpc_ctx + .net_controller + .proxy + .add_certificate_to_resolver(host_name_fqdn.clone(), root_cert.clone()) + .await?; + + rpc_ctx + .net_controller + .proxy + .add_handle(443, host_name_fqdn.clone(), handler.clone(), true) + .await?; + }; + + // serving ip https is not yet supported + + Ok(()) + } + + async fn setup_embassy_http_ui_handle(rpc_ctx: RpcContext) -> Result<(), Error> { + let host_name = rpc_ctx.net_controller.proxy.get_hostname().await; + let ip = get_current_ip(rpc_ctx.ethernet_interface.to_owned()).await?; + + let embassy_tor_addr = get_embassyd_tor_addr(rpc_ctx.clone()).await?; + + let embassy_tor_fqdn: ResourceFqdn = embassy_tor_addr.parse()?; + + let host_name_fqdn: ResourceFqdn = host_name.parse()?; + let ip_fqdn: ResourceFqdn = ip.parse()?; + + + let handler: HttpHandler = + crate::net::static_server::main_ui_server_router(rpc_ctx.clone()).await?; + + rpc_ctx + .net_controller + .proxy + .add_handle(80, embassy_tor_fqdn.clone(), handler.clone(), false) + .await?; + + rpc_ctx + .net_controller + .proxy + .add_handle(80, host_name_fqdn.clone(), handler.clone(), false) + .await?; + + rpc_ctx + .net_controller + .proxy + .add_handle(80, ip_fqdn.clone(), handler.clone(), false) + .await?; + + Ok(()) + } + + pub fn ssl_directory_for(pkg_id: &PackageId) -> PathBuf { + PathBuf::from(PACKAGE_CERT_PATH).join(pkg_id) + } + + #[instrument(skip(self, interfaces, _generated_certificate))] + pub async fn add<'a, I>( + &self, + pkg_id: &PackageId, + ip: Ipv4Addr, + interfaces: I, + _generated_certificate: GeneratedCertificateMountPoint, + ) -> Result<(), Error> + where + I: IntoIterator + Clone, + for<'b> &'b I: IntoIterator, + { + let interfaces_tor = interfaces + .clone() + .into_iter() + .filter_map(|i| match i.1.tor_config.clone() { + None => None, + Some(cfg) => Some((i.0, cfg, i.2)), + }) + .collect::>(); + let (tor_res, _, proxy_res, _) = tokio::join!( + self.tor.add(pkg_id, ip, interfaces_tor), + { + #[cfg(feature = "avahi")] + let mdns_fut = self.mdns.add( + pkg_id, + interfaces + .clone() + .into_iter() + .map(|(interface_id, _, key)| (interface_id, key)), + ); + + #[cfg(not(feature = "avahi"))] + let mdns_fut = futures::future::ready(()); + mdns_fut + }, + { + let interfaces = + interfaces + .clone() + .into_iter() + .filter_map(|(id, interface, tor_key)| { + interface.lan_config.as_ref().map(|cfg| { + ( + id, + InterfaceMetadata { + fqdn: OnionAddressV3::from(&tor_key.public()) + .get_address_without_dot_onion() + + ".local", + lan_config: cfg.clone(), + protocols: interface.protocols.clone(), + }, + ) + }) + }); + self.proxy + .add_docker_service(pkg_id.clone(), ip, interfaces) + }, + self.dns.add(pkg_id, ip), + ); + tor_res?; + proxy_res?; + + Ok(()) + } + + #[instrument(skip(self, interfaces))] + pub async fn remove + Clone>( + &self, + pkg_id: &PackageId, + ip: Ipv4Addr, + interfaces: I, + ) -> Result<(), Error> { + let (tor_res, _, proxy_res, _) = tokio::join!( + self.tor.remove(pkg_id, interfaces.clone()), + { + #[cfg(feature = "avahi")] + let mdns_fut = self.mdns.remove(pkg_id, interfaces); + #[cfg(not(feature = "avahi"))] + let mdns_fut = futures::future::ready(()); + mdns_fut + }, + self.proxy.remove_docker_service(pkg_id), + self.dns.remove(pkg_id, ip), + ); + tor_res?; + proxy_res?; + Ok(()) + } + + pub async fn generate_certificate_mountpoint<'a, I>( + &self, + pkg_id: &PackageId, + interfaces: &I, + ) -> Result + where + I: IntoIterator + Clone, + for<'b> &'b I: IntoIterator, + { + tracing::info!("Generating SSL Certificate mountpoints for {}", pkg_id); + let package_path = PathBuf::from(PACKAGE_CERT_PATH).join(pkg_id); + tokio::fs::create_dir_all(&package_path).await?; + for (id, _, key) in interfaces { + let dns_base = OnionAddressV3::from(&key.public()).get_address_without_dot_onion(); + let ssl_path_key = package_path.join(format!("{}.key.pem", id)); + let ssl_path_cert = package_path.join(format!("{}.cert.pem", id)); + let (key, chain) = self.ssl.certificate_for(&dns_base, pkg_id).await?; + tokio::try_join!( + crate::net::ssl::export_key(&key, &ssl_path_key), + crate::net::ssl::export_cert(&chain, &ssl_path_cert) + )?; + } + Ok(GeneratedCertificateMountPoint(())) + } + + pub async fn export_root_ca(&self) -> Result<(PKey, X509), Error> { + self.ssl.export_root_ca().await + } +} diff --git a/backend/src/net/net_utils.rs b/backend/src/net/net_utils.rs new file mode 100644 index 000000000..6341f02a7 --- /dev/null +++ b/backend/src/net/net_utils.rs @@ -0,0 +1,122 @@ +use std::fmt; +use std::net::IpAddr; +use std::str::FromStr; + +use color_eyre::eyre::eyre; +use http::{Request, Uri}; +use hyper::Body; + +use crate::Error; + +pub fn host_addr_fqdn(req: &Request) -> Result { + let host = req.headers().get(http::header::HOST); + + match host { + Some(host) => { + let host_str = host + .to_str() + .map_err(|e| Error::new(eyre!("{}", e), crate::ErrorKind::AsciiError))? + .to_string(); + + let host_uri: ResourceFqdn = host_str.parse()?; + + Ok(host_uri) + } + + None => Err(Error::new(eyre!("No Host"), crate::ErrorKind::NoHost)), + } +} + +#[derive(Eq, PartialEq, PartialOrd, Ord, Debug, Clone)] +pub enum ResourceFqdn { + IpAddr(IpAddr), + Uri { + full_uri: String, + root: String, + tld: Tld, + }, +} + +impl fmt::Display for ResourceFqdn { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ResourceFqdn::IpAddr(ip) => { + write!(f, "{}", ip) + } + ResourceFqdn::Uri { + full_uri, + root: _, + tld: _, + } => { + write!(f, "{}", full_uri) + } + } + } +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] +pub enum Tld { + Local, + Onion, + Embassy, +} + +impl fmt::Display for Tld { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Tld::Local => write!(f, ".local"), + Tld::Onion => write!(f, ".onion"), + Tld::Embassy => write!(f, ".embassy"), + } + } +} + +impl FromStr for ResourceFqdn { + type Err = Error; + + fn from_str(input: &str) -> Result { + + if let Ok(ip) = input.parse::() { + return Ok(ResourceFqdn::IpAddr(ip)); + } + + let hostname_split: Vec<&str> = input.split('.').collect(); + + if hostname_split.len() != 2 { + return Err(Error::new( + eyre!("invalid url tld number: add support for tldextract to parse complex urls like blah.domain.co.uk and etc?"), + crate::ErrorKind::ParseUrl, + )); + } + + match hostname_split[1] { + "local" => Ok(ResourceFqdn::Uri { + full_uri: input.to_owned(), + root: hostname_split[0].to_owned(), + tld: Tld::Local, + }), + "embassy" => Ok(ResourceFqdn::Uri { + full_uri: input.to_owned(), + root: hostname_split[0].to_owned(), + tld: Tld::Embassy, + }), + "onion" => Ok(ResourceFqdn::Uri { + full_uri: input.to_owned(), + root: hostname_split[0].to_owned(), + tld: Tld::Onion, + }), + _ => Err(Error::new( + eyre!("Unknown TLD for enum"), + crate::ErrorKind::ParseUrl, + )), + } + } +} + +impl TryFrom for ResourceFqdn { + type Error = Error; + + fn try_from(value: Uri) -> Result { + Self::from_str(&value.to_string()) + } +} diff --git a/backend/src/net/nginx.conf.template b/backend/src/net/nginx.conf.template deleted file mode 100644 index 9909dc4d5..000000000 --- a/backend/src/net/nginx.conf.template +++ /dev/null @@ -1,19 +0,0 @@ -server {{ - listen {listen_args}; - listen [::]:{listen_args_ipv6}; - server_name .{hostname}.local; - {ssl_certificate_line} - {ssl_certificate_key_line} - location / {{ - proxy_pass http://{app_ip}:{internal_port}/; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - client_max_body_size 0; - proxy_request_buffering off; - proxy_buffering off; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - {proxy_redirect_directive} - }} -}} diff --git a/backend/src/net/nginx.rs b/backend/src/net/nginx.rs deleted file mode 100644 index 69d01c5c7..000000000 --- a/backend/src/net/nginx.rs +++ /dev/null @@ -1,254 +0,0 @@ -use std::collections::BTreeMap; -use std::net::Ipv4Addr; -use std::path::{Path, PathBuf}; - -use futures::FutureExt; -use indexmap::IndexSet; -use tokio::sync::Mutex; -use tracing::instrument; - -use super::interface::{InterfaceId, LanPortConfig}; -use super::ssl::SslManager; -use crate::hostname::Hostname; -use crate::s9pk::manifest::PackageId; -use crate::util::serde::Port; -use crate::util::Invoke; -use crate::{Error, ErrorKind, ResultExt}; - -pub struct NginxController { - pub nginx_root: PathBuf, - inner: Mutex, -} -impl NginxController { - pub async fn init( - nginx_root: PathBuf, - ssl_manager: &SslManager, - host_name: &Hostname, - ) -> Result { - Ok(NginxController { - inner: Mutex::new( - NginxControllerInner::init(&nginx_root, ssl_manager, host_name).await?, - ), - nginx_root, - }) - } - pub async fn add>( - &self, - ssl_manager: &SslManager, - package: PackageId, - ipv4: Ipv4Addr, - interfaces: I, - ) -> Result<(), Error> { - self.inner - .lock() - .await - .add(&self.nginx_root, ssl_manager, package, ipv4, interfaces) - .await - } - pub async fn remove(&self, package: &PackageId) -> Result<(), Error> { - self.inner - .lock() - .await - .remove(&self.nginx_root, package) - .await - } -} - -pub struct NginxControllerInner { - interfaces: BTreeMap, -} -impl NginxControllerInner { - #[instrument] - async fn init( - nginx_root: &Path, - ssl_manager: &SslManager, - host_name: &Hostname, - ) -> Result { - let inner = NginxControllerInner { - interfaces: BTreeMap::new(), - }; - // write main ssl key/cert to fs location - let (key, cert) = ssl_manager - .certificate_for(host_name.as_ref(), &"embassy".parse().unwrap()) - .await?; - let ssl_path_key = nginx_root.join(format!("ssl/embassy_main.key.pem")); - let ssl_path_cert = nginx_root.join(format!("ssl/embassy_main.cert.pem")); - tokio::try_join!( - crate::net::ssl::export_key(&key, &ssl_path_key), - crate::net::ssl::export_cert(&cert, &ssl_path_cert), - )?; - Ok(inner) - } - #[instrument(skip(self, interfaces))] - async fn add>( - &mut self, - nginx_root: &Path, - ssl_manager: &SslManager, - package: PackageId, - ipv4: Ipv4Addr, - interfaces: I, - ) -> Result<(), Error> { - let interface_map = interfaces - .into_iter() - .filter(|(_, meta)| { - // don't add nginx stuff for anything we can't connect to over some flavor of http - (meta.protocols.contains("http") || meta.protocols.contains("https")) - // also don't add nginx unless it has at least one exposed port - && meta.lan_config.len() > 0 - }) - .collect::>(); - - for (id, meta) in interface_map.iter() { - for (port, lan_port_config) in meta.lan_config.iter() { - // get ssl certificate chain - let ( - listen_args, - ssl_certificate_line, - ssl_certificate_key_line, - proxy_redirect_directive, - ) = if lan_port_config.ssl { - // these have already been written by the net controller - let package_path = nginx_root.join(format!("ssl/{}", package)); - if tokio::fs::metadata(&package_path).await.is_err() { - tokio::fs::create_dir_all(&package_path) - .await - .with_ctx(|_| { - (ErrorKind::Filesystem, package_path.display().to_string()) - })?; - } - let ssl_path_key = package_path.join(format!("{}.key.pem", id)); - let ssl_path_cert = package_path.join(format!("{}.cert.pem", id)); - let (key, chain) = ssl_manager - .certificate_for(&meta.dns_base, &package) - .await?; - tokio::try_join!( - crate::net::ssl::export_key(&key, &ssl_path_key), - crate::net::ssl::export_cert(&chain, &ssl_path_cert) - )?; - ( - format!("{} ssl", port.0), - format!("ssl_certificate {};", ssl_path_cert.to_str().unwrap()), - format!("ssl_certificate_key {};", ssl_path_key.to_str().unwrap()), - format!("proxy_redirect http://$host/ https://$host/;"), - ) - } else { - ( - format!("{}", port.0), - String::from(""), - String::from(""), - String::from(""), - ) - }; - // write nginx configs - let nginx_conf_path = nginx_root.join(format!( - "sites-available/{}_{}_{}.conf", - package, id, port.0 - )); - tokio::fs::write( - &nginx_conf_path, - format!( - include_str!("nginx.conf.template"), - listen_args = listen_args, - listen_args_ipv6 = listen_args, - hostname = meta.dns_base, - ssl_certificate_line = ssl_certificate_line, - ssl_certificate_key_line = ssl_certificate_key_line, - app_ip = ipv4, - internal_port = lan_port_config.internal, - proxy_redirect_directive = proxy_redirect_directive, - ), - ) - .await - .with_ctx(|_| (ErrorKind::Filesystem, nginx_conf_path.display().to_string()))?; - let sites_enabled_link_path = - nginx_root.join(format!("sites-enabled/{}_{}_{}.conf", package, id, port.0)); - if tokio::fs::metadata(&sites_enabled_link_path).await.is_ok() { - tokio::fs::remove_file(&sites_enabled_link_path).await?; - } - tokio::fs::symlink(&nginx_conf_path, &sites_enabled_link_path) - .await - .with_ctx(|_| (ErrorKind::Filesystem, nginx_conf_path.display().to_string()))?; - } - } - match self.interfaces.get_mut(&package) { - None => { - let info = PackageNetInfo { - interfaces: interface_map, - }; - self.interfaces.insert(package, info); - } - Some(p) => { - p.interfaces.extend(interface_map); - } - }; - - self.hup().await?; - Ok(()) - } - - #[instrument(skip(self))] - async fn remove(&mut self, nginx_root: &Path, package: &PackageId) -> Result<(), Error> { - let removed = self.interfaces.remove(package); - if let Some(net_info) = removed { - for (id, meta) in net_info.interfaces { - for (port, _lan_port_config) in meta.lan_config.iter() { - // remove ssl certificates and nginx configs - let package_path = nginx_root.join(format!("ssl/{}", package)); - let enabled_path = nginx_root - .join(format!("sites-enabled/{}_{}_{}.conf", package, id, port.0)); - let available_path = nginx_root.join(format!( - "sites-available/{}_{}_{}.conf", - package, id, port.0 - )); - let _ = tokio::try_join!( - async { - if tokio::fs::metadata(&package_path).await.is_ok() { - tokio::fs::remove_dir_all(&package_path) - .map(|res| { - res.with_ctx(|_| { - ( - ErrorKind::Filesystem, - package_path.display().to_string(), - ) - }) - }) - .await?; - Ok(()) - } else { - Ok(()) - } - }, - tokio::fs::remove_file(&enabled_path).map(|res| res.with_ctx(|_| ( - ErrorKind::Filesystem, - enabled_path.display().to_string() - ))), - tokio::fs::remove_file(&available_path).map(|res| res.with_ctx(|_| ( - ErrorKind::Filesystem, - available_path.display().to_string() - ))), - )?; - } - } - } - self.hup().await?; - Ok(()) - } - - #[instrument(skip(self))] - async fn hup(&self) -> Result<(), Error> { - let _ = tokio::process::Command::new("systemctl") - .arg("reload") - .arg("nginx") - .invoke(ErrorKind::Nginx) - .await?; - Ok(()) - } -} -struct PackageNetInfo { - interfaces: BTreeMap, -} -pub struct InterfaceMetadata { - pub dns_base: String, - pub lan_config: BTreeMap, - pub protocols: IndexSet, -} diff --git a/backend/src/net/proxy_controller.rs b/backend/src/net/proxy_controller.rs new file mode 100644 index 000000000..d7b944f41 --- /dev/null +++ b/backend/src/net/proxy_controller.rs @@ -0,0 +1,384 @@ +use std::collections::BTreeMap; +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; +use std::sync::Arc; + +use color_eyre::eyre::eyre; +use futures::FutureExt; +use http::{Method, Request, Response}; +use hyper::upgrade::Upgraded; +use hyper::{Body, Error as HyperError}; +use models::{InterfaceId, PackageId}; +use openssl::pkey::{PKey, Private}; +use openssl::x509::X509; +use tokio::net::TcpStream; +use tokio::sync::Mutex; +use tracing::{error, info, instrument}; + +use crate::net::net_utils::{host_addr_fqdn, ResourceFqdn}; +use crate::net::ssl::SslManager; +use crate::net::vhost_controller::VHOSTController; +use crate::net::{HttpClient, HttpHandler, InterfaceMetadata, PackageNetInfo}; +use crate::{Error, ResultExt}; + +pub struct ProxyController { + inner: Mutex, +} + +impl ProxyController { + pub async fn init( + embassyd_socket_addr: SocketAddr, + embassy_fqdn: ResourceFqdn, + ssl_manager: SslManager, + ) -> Result { + Ok(ProxyController { + inner: Mutex::new( + ProxyControllerInner::init(embassyd_socket_addr, embassy_fqdn, ssl_manager).await?, + ), + }) + } + + pub async fn add_docker_service>( + &self, + package: PackageId, + ipv4: Ipv4Addr, + interfaces: I, + ) -> Result<(), Error> { + self.inner + .lock() + .await + .add_docker_service(package, ipv4, interfaces) + .await + } + + pub async fn remove_docker_service(&self, package: &PackageId) -> Result<(), Error> { + self.inner.lock().await.remove_docker_service(package).await + } + + pub async fn add_certificate_to_resolver( + &self, + fqdn: ResourceFqdn, + cert_data: (PKey, Vec), + ) -> Result<(), Error> { + self.inner + .lock() + .await + .add_certificate_to_resolver(fqdn, cert_data) + .await + } + + pub async fn add_handle( + &self, + ext_port: u16, + fqdn: ResourceFqdn, + handler: HttpHandler, + is_ssl: bool, + ) -> Result<(), Error> { + self.inner + .lock() + .await + .add_handle(ext_port, fqdn, handler, is_ssl) + .await + } + + pub async fn get_hostname(&self) -> String { + self.inner.lock().await.get_embassy_hostname() + } + + pub async fn proxy( + client: HttpClient, + req: Request, + ) -> Result, HyperError> { + if Method::CONNECT == req.method() { + // Received an HTTP request like: + // ``` + // CONNECT www.domain.com:443 HTTP/1.1s + // Host: www.domain.com:443 + // Proxy-Connection: Keep-Alive + // ``` + // + // When HTTP method is CONNECT we should return an empty body + // then we can eventually upgrade the connection and talk a new protocol. + // + // Note: only after client received an empty body with STATUS_OK can the + // connection be upgraded, so we can't return a response inside + // `on_upgrade` future. + match host_addr_fqdn(&req) { + Ok(host) => { + tokio::task::spawn(async move { + match hyper::upgrade::on(req).await { + Ok(upgraded) => match host { + ResourceFqdn::IpAddr(ip) => { + if let Err(e) = Self::tunnel(upgraded, ip.to_string()).await { + error!("server io error: {}", e); + }; + } + ResourceFqdn::Uri { + full_uri, + root: _, + tld: _, + } => { + if let Err(e) = + Self::tunnel(upgraded, full_uri.to_string()).await + { + error!("server io error: {}", e); + }; + } + }, + Err(e) => error!("upgrade error: {}", e), + } + }); + + Ok(Response::new(Body::empty())) + } + Err(e) => { + let err_txt = format!("CONNECT host is not socket addr: {:?}", &req.uri()); + let mut resp = Response::new(Body::from(format!( + "CONNECT must be to a socket address: {}: {}", + err_txt, e + ))); + *resp.status_mut() = http::StatusCode::BAD_REQUEST; + + Ok(resp) + } + } + } else { + client.request(req).await + } + } + + // Create a TCP connection to host:port, build a tunnel between the connection and + // the upgraded connection + async fn tunnel(mut upgraded: Upgraded, addr: String) -> std::io::Result<()> { + let mut server = TcpStream::connect(addr).await?; + + let (from_client, from_server) = + tokio::io::copy_bidirectional(&mut upgraded, &mut server).await?; + + info!( + "client wrote {} bytes and received {} bytes", + from_client, from_server + ); + + Ok(()) + } +} +struct ProxyControllerInner { + ssl_manager: SslManager, + vhosts: VHOSTController, + embassyd_fqdn: ResourceFqdn, + docker_interfaces: BTreeMap, + docker_iface_lookups: BTreeMap<(PackageId, InterfaceId), ResourceFqdn>, +} + +impl ProxyControllerInner { + #[instrument] + async fn init( + embassyd_socket_addr: SocketAddr, + embassyd_fqdn: ResourceFqdn, + ssl_manager: SslManager, + ) -> Result { + let inner = ProxyControllerInner { + vhosts: VHOSTController::init(embassyd_socket_addr), + ssl_manager, + embassyd_fqdn, + docker_interfaces: BTreeMap::new(), + docker_iface_lookups: BTreeMap::new(), + }; + + Ok(inner) + } + + async fn add_certificate_to_resolver( + &mut self, + hostname: ResourceFqdn, + cert_data: (PKey, Vec), + ) -> Result<(), Error> { + self.vhosts + .cert_resolver + .add_certificate_to_resolver(hostname, cert_data) + .await + .map_err(|err| { + Error::new( + eyre!("Unable to add ssl cert to the resolver: {}", err), + crate::ErrorKind::Network, + ) + })?; + + Ok(()) + } + + async fn add_package_certificate_to_resolver( + &mut self, + resource_fqdn: ResourceFqdn, + pkg_id: PackageId, + ) -> Result<(), Error> { + let package_cert = match resource_fqdn.clone() { + ResourceFqdn::IpAddr(ip) => { + self.ssl_manager + .certificate_for(&ip.to_string(), &pkg_id) + .await? + } + ResourceFqdn::Uri { + full_uri: _, + root, + tld: _, + } => self.ssl_manager.certificate_for(&root, &pkg_id).await?, + }; + + self.vhosts + .cert_resolver + .add_certificate_to_resolver(resource_fqdn, package_cert) + .await + .map_err(|err| { + Error::new( + eyre!("Unable to add ssl cert to the resolver: {}", err), + crate::ErrorKind::Network, + ) + })?; + + Ok(()) + } + + pub async fn add_handle( + &mut self, + external_svc_port: u16, + fqdn: ResourceFqdn, + svc_handler: HttpHandler, + is_ssl: bool, + ) -> Result<(), Error> { + self.vhosts + .add_server_or_handle(external_svc_port, fqdn, svc_handler, is_ssl) + .await + } + + #[instrument(skip(self, interfaces))] + pub async fn add_docker_service>( + &mut self, + package: PackageId, + docker_ipv4: Ipv4Addr, + interfaces: I, + ) -> Result<(), Error> { + let mut interface_map = interfaces + .into_iter() + .filter(|(_, meta)| { + // don't add stuff for anything we can't connect to over some flavor of http + (meta.protocols.contains("http") || meta.protocols.contains("https")) + // also don't add anything unless it has at least one exposed port + && !meta.lan_config.is_empty() + }) + .collect::>(); + + for (id, meta) in interface_map.iter() { + for (external_svc_port, lan_port_config) in meta.lan_config.iter() { + let full_fqdn = ResourceFqdn::from_str(&meta.fqdn).unwrap(); + + self.docker_iface_lookups + .insert((package.clone(), id.clone()), full_fqdn.clone()); + + self.add_package_certificate_to_resolver(full_fqdn.clone(), package.clone()) + .await?; + + let svc_handler = + Self::create_docker_handle(docker_ipv4.to_string(), lan_port_config.internal) + .await; + + self.add_handle( + external_svc_port.0, + full_fqdn.clone(), + svc_handler, + lan_port_config.ssl, + ) + .await?; + } + } + + let docker_interface = self.docker_interfaces.entry(package.clone()).or_default(); + docker_interface.interfaces.append(&mut interface_map); + + Ok(()) + } + + async fn create_docker_handle(internal_ip: String, port: u16) -> HttpHandler { + let svc_handler: HttpHandler = Arc::new(move |mut req| { + let proxy_addr = internal_ip.clone(); + async move { + let client = HttpClient::new(); + + let uri_string = format!( + "http://{}:{}{}", + proxy_addr, + port, + req.uri() + .path_and_query() + .map(|x| x.as_str()) + .unwrap_or("/") + ); + + let uri = uri_string.parse().unwrap(); + *req.uri_mut() = uri; + + ProxyController::proxy(client, req).await + } + .boxed() + }); + + svc_handler + } + + #[instrument(skip(self))] + pub async fn remove_docker_service(&mut self, package: &PackageId) -> Result<(), Error> { + let mut server_removals: Vec<(u16, InterfaceId)> = Default::default(); + + let net_info = match self.docker_interfaces.get(package) { + Some(a) => a, + None => return Ok(()), + }; + + for (id, meta) in &net_info.interfaces { + for (service_ext_port, _lan_port_config) in meta.lan_config.iter() { + if let Some(server) = self.vhosts.service_servers.get_mut(&service_ext_port.0) { + if let Some(fqdn) = self + .docker_iface_lookups + .get(&(package.clone(), id.clone())) + { + server.remove_svc_handler_mapping(fqdn.to_owned()).await?; + self.vhosts + .cert_resolver + .remove_cert(fqdn.to_owned()) + .await?; + + let mapping = server.svc_mapping.read().await; + + if mapping.is_empty() { + server_removals.push((service_ext_port.0, id.to_owned())); + } + } + } + } + } + + for (port, interface_id) in server_removals { + if let Some(removed_server) = self.vhosts.service_servers.remove(&port) { + removed_server.shutdown.send(()).map_err(|_| { + Error::new( + eyre!("Hyper server did not quit properly"), + crate::ErrorKind::JoinError, + ) + })?; + removed_server + .handle + .await + .with_kind(crate::ErrorKind::JoinError)?; + self.docker_interfaces.remove(&package.clone()); + self.docker_iface_lookups + .remove(&(package.clone(), interface_id)); + } + } + Ok(()) + } + + pub fn get_embassy_hostname(&self) -> String { + self.embassyd_fqdn.to_string() + } +} diff --git a/backend/src/net/ssl.rs b/backend/src/net/ssl.rs index f0730e77b..358301aec 100644 --- a/backend/src/net/ssl.rs +++ b/backend/src/net/ssl.rs @@ -24,7 +24,7 @@ use crate::{Error, ErrorKind, ResultExt}; static CERTIFICATE_VERSION: i32 = 2; // X509 version 3 is actually encoded as '2' in the cert because fuck you. pub const ROOT_CA_STATIC_PATH: &str = "/var/lib/embassy/ssl/root-ca.crt"; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SslManager { store: SslStore, root_cert: X509, @@ -32,7 +32,7 @@ pub struct SslManager { int_cert: X509, } -#[derive(Debug)] +#[derive(Debug, Clone)] struct SslStore { secret_store: PgPool, } @@ -177,7 +177,7 @@ impl SslManager { } Some((key, cert)) => Ok((key, cert)), }?; - // generate static file for download, this will get blown up on embassy restart so it's good to write it on + // generate static file for download, this will gte blown up on embassy restart so it's good to write it on // every ssl manager init tokio::fs::create_dir_all( Path::new(ROOT_CA_STATIC_PATH) @@ -513,57 +513,3 @@ fn make_leaf_cert( let cert = builder.build(); Ok(cert) } - -// #[tokio::test] -// async fn ca_details_persist() -> Result<(), Error> { -// let pool = sqlx::Pool::::connect("postgres::memory:").await?; -// sqlx::migrate!() -// .run(&pool) -// .await -// .with_kind(crate::ErrorKind::Database)?; -// let mgr = SslManager::init(pool.clone()).await?; -// let root_cert0 = mgr.root_cert; -// let int_key0 = mgr.int_key; -// let int_cert0 = mgr.int_cert; -// let mgr = SslManager::init(pool).await?; -// let root_cert1 = mgr.root_cert; -// let int_key1 = mgr.int_key; -// let int_cert1 = mgr.int_cert; -// -// assert_eq!(root_cert0.to_pem()?, root_cert1.to_pem()?); -// assert_eq!( -// int_key0.private_key_to_pem_pkcs8()?, -// int_key1.private_key_to_pem_pkcs8()? -// ); -// assert_eq!(int_cert0.to_pem()?, int_cert1.to_pem()?); -// Ok(()) -// } -// -// #[tokio::test] -// async fn certificate_details_persist() -> Result<(), Error> { -// let pool = sqlx::Pool::::connect("postgres::memory:").await?; -// sqlx::migrate!() -// .run(&pool) -// .await -// .with_kind(crate::ErrorKind::Database)?; -// let mgr = SslManager::init(pool.clone()).await?; -// let package_id = "bitcoind".parse().unwrap(); -// let (key0, cert_chain0) = mgr.certificate_for("start9", &package_id).await?; -// let (key1, cert_chain1) = mgr.certificate_for("start9", &package_id).await?; -// -// assert_eq!( -// key0.private_key_to_pem_pkcs8()?, -// key1.private_key_to_pem_pkcs8()? -// ); -// assert_eq!( -// cert_chain0 -// .iter() -// .map(|cert| cert.to_pem().unwrap()) -// .collect::>>(), -// cert_chain1 -// .iter() -// .map(|cert| cert.to_pem().unwrap()) -// .collect::>>() -// ); -// Ok(()) -// } diff --git a/backend/src/net/static_server.rs b/backend/src/net/static_server.rs new file mode 100644 index 000000000..06c5de980 --- /dev/null +++ b/backend/src/net/static_server.rs @@ -0,0 +1,458 @@ +use std::fs::Metadata; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::UNIX_EPOCH; + +use color_eyre::eyre::eyre; +use digest::Digest; +use futures::FutureExt; +use http::response::Builder; +use hyper::{Body, Method, Request, Response, StatusCode}; + +use rpc_toolkit::rpc_handler; +use tokio::fs::File; +use tokio_util::codec::{BytesCodec, FramedRead}; + +use crate::context::{DiagnosticContext, InstallContext, RpcContext, SetupContext}; +use crate::core::rpc_continuations::RequestGuid; +use crate::db::subscribe; +use crate::install::PKG_PUBLIC_DIR; +use crate::middleware::auth::HasValidSession; +use crate::net::HttpHandler; +use crate::{diagnostic_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt}; + +static NOT_FOUND: &[u8] = b"Not Found"; +static NOT_AUTHORIZED: &[u8] = b"Not Authorized"; + +pub const MAIN_UI_WWW_DIR: &str = "/var/www/html/main"; +pub const SETUP_UI_WWW_DIR: &str = "/var/www/html/setup"; +pub const DIAG_UI_WWW_DIR: &str = "/var/www/html/diagnostic"; +pub const INSTALL_UI_WWW_DIR: &str = "/var/www/html/install"; + +fn status_fn(_: i32) -> StatusCode { + StatusCode::OK +} + +#[derive(Clone)] +pub enum UiMode { + Setup, + Diag, + Install, + Main, +} + +pub async fn setup_ui_file_router(ctx: SetupContext) -> Result { + let handler: HttpHandler = Arc::new(move |req| { + let ctx = ctx.clone(); + + let ui_mode = UiMode::Setup; + async move { + let res = match req.uri().path() { + path if path.starts_with("/rpc/") => { + let rpc_handler = + rpc_handler!({command: setup_api, context: ctx, status: status_fn}); + + rpc_handler(req) + .await + .map_err(|err| Error::new(eyre!("{}", err), crate::ErrorKind::Network)) + } + _ => alt_ui(req, ui_mode).await, + }; + + match res { + Ok(data) => Ok(data), + Err(err) => Ok(server_error(err)), + } + } + .boxed() + }); + + Ok(handler) +} + +pub async fn diag_ui_file_router(ctx: DiagnosticContext) -> Result { + let handler: HttpHandler = Arc::new(move |req| { + let ctx = ctx.clone(); + let ui_mode = UiMode::Diag; + async move { + let res = match req.uri().path() { + path if path.starts_with("/rpc/") => { + let rpc_handler = + rpc_handler!({command: diagnostic_api, context: ctx, status: status_fn}); + + rpc_handler(req) + .await + .map_err(|err| Error::new(eyre!("{}", err), crate::ErrorKind::Network)) + } + _ => alt_ui(req, ui_mode).await, + }; + + match res { + Ok(data) => Ok(data), + Err(err) => Ok(server_error(err)), + } + } + .boxed() + }); + + Ok(handler) +} + +pub async fn install_ui_file_router(ctx: InstallContext) -> Result { + let handler: HttpHandler = Arc::new(move |req| { + let ctx = ctx.clone(); + let ui_mode = UiMode::Install; + async move { + let res = match req.uri().path() { + path if path.starts_with("/rpc/") => { + let rpc_handler = + rpc_handler!({command: install_api, context: ctx, status: status_fn}); + + rpc_handler(req) + .await + .map_err(|err| Error::new(eyre!("{}", err), crate::ErrorKind::Network)) + } + _ => alt_ui(req, ui_mode).await, + }; + + match res { + Ok(data) => Ok(data), + Err(err) => Ok(server_error(err)), + } + } + .boxed() + }); + + Ok(handler) +} + +pub async fn main_ui_server_router(ctx: RpcContext) -> Result { + let handler: HttpHandler = Arc::new(move |req| { + let ctx = ctx.clone(); + + async move { + let res = match req.uri().path() { + path if path.starts_with("/rpc/") => { + let rpc_handler = + rpc_handler!({command: main_api, context: ctx, status: status_fn}); + + rpc_handler(req) + .await + .map_err(|err| Error::new(eyre!("{}", err), crate::ErrorKind::Network)) + } + "/ws/db" => subscribe(ctx, req).await, + path if path.starts_with("/ws/rpc/") => { + match RequestGuid::from(path.strip_prefix("/ws/rpc/").unwrap()) { + None => { + tracing::debug!("No Guid Path"); + Ok::<_, Error>(bad_request()) + } + Some(guid) => match ctx.get_ws_continuation_handler(&guid).await { + Some(cont) => match cont(req).await { + Ok::<_, Error>(r) => Ok::<_, Error>(r), + Err(err) => Ok::<_, Error>(server_error(err)), + }, + _ => Ok::<_, Error>(not_found()), + }, + } + } + path if path.starts_with("/rest/rpc/") => { + match RequestGuid::from(path.strip_prefix("/rest/rpc/").unwrap()) { + None => { + tracing::debug!("No Guid Path"); + Ok::<_, Error>(bad_request()) + } + Some(guid) => match ctx.get_rest_continuation_handler(&guid).await { + None => Ok::<_, Error>(not_found()), + Some(cont) => match cont(req).await { + Ok::<_, Error>(r) => Ok::<_, Error>(r), + Err(e) => Ok::<_, Error>(server_error(e)), + }, + }, + } + } + _ => main_embassy_ui(req, ctx).await, + }; + + match res { + Ok(data) => Ok(data), + Err(err) => Ok(server_error(err)), + } + } + .boxed() + }); + + Ok(handler) +} + +async fn alt_ui(req: Request, ui_mode: UiMode) -> Result, Error> { + let selected_root_dir = match ui_mode { + UiMode::Setup => SETUP_UI_WWW_DIR, + UiMode::Diag => DIAG_UI_WWW_DIR, + UiMode::Install => INSTALL_UI_WWW_DIR, + UiMode::Main => MAIN_UI_WWW_DIR, + }; + + let (request_parts, _body) = req.into_parts(); + match request_parts.uri.path() { + "/" => { + let full_path = PathBuf::from(selected_root_dir).join("index.html"); + + file_send(full_path).await + } + _ => { + match ( + request_parts.method, + request_parts + .uri + .path() + .strip_prefix('/') + .unwrap_or(request_parts.uri.path()) + .split_once('/'), + ) { + (Method::GET, None) => { + let uri_path = request_parts + .uri + .path() + .strip_prefix('/') + .unwrap_or(request_parts.uri.path()); + + let full_path = PathBuf::from(selected_root_dir).join(uri_path); + file_send(full_path).await + } + + (Method::GET, Some((dir, file))) => { + let full_path = PathBuf::from(selected_root_dir).join(dir).join(file); + file_send(full_path).await + } + + _ => Ok(not_found()), + } + } + } +} + +async fn main_embassy_ui(req: Request, ctx: RpcContext) -> Result, Error> { + let selected_root_dir = MAIN_UI_WWW_DIR; + + let (request_parts, _body) = req.into_parts(); + match request_parts.uri.path() { + "/" => { + let full_path = PathBuf::from(selected_root_dir).join("index.html"); + + file_send(full_path).await + } + _ => { + let valid_session = HasValidSession::from_request_parts(&request_parts, &ctx).await; + + match valid_session { + Ok(_valid) => { + match ( + request_parts.method, + request_parts + .uri + .path() + .strip_prefix('/') + .unwrap_or(request_parts.uri.path()) + .split_once('/'), + ) { + (Method::GET, Some(("public", path))) => { + let sub_path = Path::new(path); + if let Ok(rest) = sub_path.strip_prefix("package-data") { + file_send(ctx.datadir.join(PKG_PUBLIC_DIR).join(rest)).await + } else if let Ok(rest) = sub_path.strip_prefix("eos") { + match rest.to_str() { + Some("local.crt") => { + file_send(crate::net::ssl::ROOT_CA_STATIC_PATH).await + } + None => Ok(bad_request()), + _ => Ok(not_found()), + } + } else { + Ok(not_found()) + } + } + (Method::GET, Some(("eos", "local.crt"))) => { + file_send(PathBuf::from(crate::net::ssl::ROOT_CA_STATIC_PATH)).await + } + + (Method::GET, None) => { + let uri_path = request_parts + .uri + .path() + .strip_prefix('/') + .unwrap_or(request_parts.uri.path()); + + let full_path = PathBuf::from(selected_root_dir).join(uri_path); + file_send(full_path).await + } + + (Method::GET, Some((dir, file))) => { + let full_path = PathBuf::from(selected_root_dir).join(dir).join(file); + file_send(full_path).await + } + + _ => Ok(not_found()), + } + } + Err(err) => { + match ( + request_parts.method, + request_parts + .uri + .path() + .strip_prefix('/') + .unwrap_or(request_parts.uri.path()) + .split_once('/'), + ) { + (Method::GET, Some(("public", _path))) => { + un_authorized(err, request_parts.uri.path()) + } + (Method::GET, Some(("eos", "local.crt"))) => { + un_authorized(err, request_parts.uri.path()) + } + (Method::GET, None) => { + let uri_path = request_parts + .uri + .path() + .strip_prefix('/') + .unwrap_or(request_parts.uri.path()); + + let full_path = PathBuf::from(selected_root_dir).join(uri_path); + file_send(full_path).await + } + + (Method::GET, Some((dir, file))) => { + let full_path = PathBuf::from(selected_root_dir).join(dir).join(file); + file_send(full_path).await + } + + _ => Ok(not_found()), + } + } + } + } + } +} + +fn un_authorized(err: Error, path: &str) -> Result, Error> { + tracing::warn!("unauthorized for {} @{:?}", err, path); + tracing::debug!("{:?}", err); + Ok(Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body(NOT_AUTHORIZED.into()) + .unwrap()) +} + +/// HTTP status code 404 +fn not_found() -> Response { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(NOT_FOUND.into()) + .unwrap() +} + +fn server_error(err: Error) -> Response { + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(err.to_string().into()) + .unwrap() +} + +fn bad_request() -> Response { + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::empty()) + .unwrap() +} + +async fn file_send(path: impl AsRef) -> Result, Error> { + // Serve a file by asynchronously reading it by chunks using tokio-util crate. + + let path = path.as_ref(); + + if let Ok(file) = File::open(path).await { + let metadata = file.metadata().await.with_kind(ErrorKind::Filesystem)?; + + match IsNonEmptyFile::new(&metadata, path) { + Some(a) => a, + None => return Ok(not_found()), + }; + + let mut builder = Response::builder().status(StatusCode::OK); + builder = with_e_tag(path, &metadata, builder)?; + builder = with_content_type(path, builder); + builder = with_content_length(&metadata, builder); + let stream = FramedRead::new(file, BytesCodec::new()); + let body = Body::wrap_stream(stream); + return builder.body(body).with_kind(ErrorKind::Network); + } + tracing::debug!("File not found: {:?}", path); + + Ok(not_found()) +} + +struct IsNonEmptyFile(()); +impl IsNonEmptyFile { + fn new(metadata: &Metadata, path: &Path) -> Option { + let length = metadata.len(); + if !metadata.is_file() || length == 0 { + tracing::debug!("File is empty: {:?}", path); + return None; + } + Some(Self(())) + } +} + +fn with_e_tag(path: &Path, metadata: &Metadata, builder: Builder) -> Result { + let modified = metadata.modified().with_kind(ErrorKind::Filesystem)?; + let mut hasher = sha2::Sha256::new(); + hasher.update(format!("{:?}", path).as_bytes()); + hasher.update( + format!( + "{}", + modified + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + ) + .as_bytes(), + ); + let res = hasher.finalize(); + Ok(builder.header( + "ETag", + base32::encode(base32::Alphabet::RFC4648 { padding: false }, res.as_slice()).to_lowercase(), + )) +} +///https://en.wikipedia.org/wiki/Media_type +fn with_content_type(path: &Path, builder: Builder) -> Builder { + let content_type = match path.extension() { + Some(os_str) => match os_str.to_str() { + Some("apng") => "image/apng", + Some("avif") => "image/avif", + Some("flif") => "image/flif", + Some("gif") => "image/gif", + Some("jpg") | Some("jpeg") | Some("jfif") | Some("pjpeg") | Some("pjp") => "image/jpeg", + Some("jxl") => "image/jxl", + Some("png") => "image/png", + Some("svg") => "image/svg+xml", + Some("webp") => "image/webp", + Some("mng") | Some("x-mng") => "image/x-mng", + Some("css") => "text/css", + Some("csv") => "text/csv", + Some("html") => "text/html", + Some("php") => "text/php", + Some("plain") | Some("md") | Some("txt") => "text/plain", + Some("xml") => "text/xml", + Some("js") => "text/javascript", + Some("wasm") => "application/wasm", + None | Some(_) => "text/plain", + }, + None => "text/plain", + }; + builder.header("Content-Type", content_type) +} + +fn with_content_length(metadata: &Metadata, builder: Builder) -> Builder { + builder.header(http::header::CONTENT_LENGTH, metadata.len()) +} diff --git a/backend/src/net/vhost_controller.rs b/backend/src/net/vhost_controller.rs new file mode 100644 index 000000000..5d0a0494f --- /dev/null +++ b/backend/src/net/vhost_controller.rs @@ -0,0 +1,82 @@ +use std::collections::BTreeMap; +use std::net::SocketAddr; +use std::sync::Arc; + +use tokio_rustls::rustls::ServerConfig; +use crate::net::cert_resolver::EmbassyCertResolver; +use crate::net::embassy_service_http_server::{EmbassyServiceHTTPServer}; + +use crate::net::HttpHandler; +use crate::Error; +use crate::net::net_utils::ResourceFqdn; + +pub struct VHOSTController { + pub service_servers: BTreeMap, + pub cert_resolver: EmbassyCertResolver, + embassyd_addr: SocketAddr, +} + +impl VHOSTController { + pub fn init(embassyd_addr: SocketAddr) -> Self { + Self { + embassyd_addr, + service_servers: BTreeMap::new(), + cert_resolver: EmbassyCertResolver::new(), + } + } + + pub fn build_ssl_svr_cfg(&self) -> Result, Error> { + let ssl_cfg = ServerConfig::builder() + .with_safe_default_cipher_suites() + .with_safe_default_kx_groups() + .with_safe_default_protocol_versions() + .unwrap() + .with_no_client_auth() + .with_cert_resolver(Arc::new(self.cert_resolver.clone())); + + Ok(Arc::new(ssl_cfg)) + } + + pub async fn add_server_or_handle( + &mut self, + external_svc_port: u16, + fqdn: ResourceFqdn, + svc_handler: HttpHandler, + is_ssl: bool, + ) -> Result<(), Error> { + if let Some(server) = self.service_servers.get_mut(&external_svc_port) { + server.add_svc_handler_mapping(fqdn, svc_handler).await?; + } else { + self.add_server(is_ssl, external_svc_port, fqdn, svc_handler) + .await?; + } + + Ok(()) + } + + async fn add_server( + &mut self, + is_ssl: bool, + external_svc_port: u16, + fqdn: ResourceFqdn, + svc_handler: HttpHandler, + ) -> Result<(), Error> { + let ssl_cfg = if is_ssl { + Some(self.build_ssl_svr_cfg()?) + } else { + None + }; + + + let mut new_service_server = + EmbassyServiceHTTPServer::new(self.embassyd_addr.ip(), external_svc_port, ssl_cfg) + .await?; + new_service_server + .add_svc_handler_mapping(fqdn.clone(), svc_handler) + .await?; + self.service_servers + .insert(external_svc_port, new_service_server); + + Ok(()) + } +} diff --git a/backend/src/net/ws_server.rs b/backend/src/net/ws_server.rs new file mode 100644 index 000000000..16519c6c8 --- /dev/null +++ b/backend/src/net/ws_server.rs @@ -0,0 +1,94 @@ +use crate::context::RpcContext; + +pub async fn ws_server_handle(rpc_ctx: RpcContext) { + + let ws_ctx = rpc_ctx.clone(); + let ws_server_handle = { + let builder = Server::bind(&ws_ctx.bind_ws); + + let make_svc = ::rpc_toolkit::hyper::service::make_service_fn(move |_| { + let ctx = ws_ctx.clone(); + async move { + Ok::<_, ::rpc_toolkit::hyper::Error>(::rpc_toolkit::hyper::service::service_fn( + move |req| { + let ctx = ctx.clone(); + async move { + tracing::debug!("Request to {}", req.uri().path()); + match req.uri().path() { + "/ws/db" => { + Ok(subscribe(ctx, req).await.unwrap_or_else(err_to_500)) + } + path if path.starts_with("/ws/rpc/") => { + match RequestGuid::from( + path.strip_prefix("/ws/rpc/").unwrap(), + ) { + None => { + tracing::debug!("No Guid Path"); + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::empty()) + } + Some(guid) => { + match ctx.get_ws_continuation_handler(&guid).await { + Some(cont) => match cont(req).await { + Ok(r) => Ok(r), + Err(e) => Response::builder() + .status( + StatusCode::INTERNAL_SERVER_ERROR, + ) + .body(Body::from(format!("{}", e))), + }, + _ => Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()), + } + } + } + } + path if path.starts_with("/rest/rpc/") => { + match RequestGuid::from( + path.strip_prefix("/rest/rpc/").unwrap(), + ) { + None => { + tracing::debug!("No Guid Path"); + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::empty()) + } + Some(guid) => { + match ctx.get_rest_continuation_handler(&guid).await + { + None => Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()), + Some(cont) => match cont(req).await { + Ok(r) => Ok(r), + Err(e) => Response::builder() + .status( + StatusCode::INTERNAL_SERVER_ERROR, + ) + .body(Body::from(format!("{}", e))), + }, + } + } + } + } + _ => Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()), + } + } + }, + )) + } + }); + builder.serve(make_svc) + } + .with_graceful_shutdown({ + let mut shutdown = rpc_ctx.shutdown.subscribe(); + async move { + shutdown.recv().await.expect("context dropped"); + } + }); + +} \ No newline at end of file diff --git a/backend/src/nginx/diagnostic-ui.conf b/backend/src/nginx/diagnostic-ui.conf deleted file mode 100644 index a186c1b10..000000000 --- a/backend/src/nginx/diagnostic-ui.conf +++ /dev/null @@ -1,35 +0,0 @@ - -map $http_upgrade $connection_upgrade { - default upgrade; - '' $http_connection; -} - -server { - listen 80 default_server; - listen [::]:80 default_server; - - root /var/www/html/diagnostic; - - index index.html index.htm index.nginx-debian.html; - - server_name _; - - proxy_buffering off; - proxy_request_buffering off; - proxy_socket_keepalive on; - proxy_http_version 1.1; - proxy_read_timeout 1800; - - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; - - location /rpc/ { - proxy_pass http://127.0.0.1:5959/; - } - - location / { - try_files $uri $uri/ =404; - } -} \ No newline at end of file diff --git a/backend/src/nginx/main-ui.conf.template b/backend/src/nginx/main-ui.conf.template deleted file mode 100644 index 7532c41b5..000000000 --- a/backend/src/nginx/main-ui.conf.template +++ /dev/null @@ -1,119 +0,0 @@ - -map $http_upgrade $connection_upgrade {{ - default upgrade; - '' $http_connection; -}} - -server {{ - listen 443 ssl default_server; - listen [::]:443 ssl default_server; - ssl_certificate /etc/nginx/ssl/embassy_main.cert.pem; - ssl_certificate_key /etc/nginx/ssl/embassy_main.key.pem; - - root /var/www/html/main; - - index index.html index.htm index.nginx-debian.html; - - server_name .{lan_hostname}; - - proxy_buffers 4 512k; - proxy_buffer_size 512k; - proxy_buffering off; - proxy_request_buffering off; - proxy_socket_keepalive on; - proxy_http_version 1.1; - proxy_read_timeout 1800; - - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript image/svg+xml font/tts font/otf font/eot font/openttype application/x-javascript application/xml; - - - location /rpc/ {{ - proxy_pass http://127.0.0.1:5959/; - }} - - location /ws/ {{ - proxy_pass http://127.0.0.1:5960$request_uri; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - }} - - location /rest/ {{ - proxy_pass http://127.0.0.1:5960$request_uri; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - client_max_body_size 0; - }} - - location /public/ {{ - proxy_pass http://127.0.0.1:5961/; - }} - - location / {{ - try_files $uri $uri/ =404; - }} -}} -server {{ - listen 80; - listen [::]:80; - server_name .{lan_hostname}; - return 301 https://$host$request_uri; -}} -server {{ - listen 80 default_server; - listen [::]:80 default_server; - ssl_certificate /etc/nginx/ssl/embassy_main.cert.pem; - ssl_certificate_key /etc/nginx/ssl/embassy_main.key.pem; - - root /var/www/html/main; - - index index.html index.htm index.nginx-debian.html; - - server_name .{tor_hostname}; - - proxy_buffers 4 512k; - proxy_buffer_size 512k; - proxy_buffering off; - proxy_request_buffering off; - proxy_socket_keepalive on; - proxy_http_version 1.1; - proxy_read_timeout 1800; - - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; - - location /rpc/ {{ - proxy_pass http://127.0.0.1:5959/; - }} - - location /ws/ {{ - proxy_pass http://127.0.0.1:5960$request_uri; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - }} - - location /rest/ {{ - proxy_pass http://127.0.0.1:5960$request_uri; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - client_max_body_size 0; - }} - - location /public/ {{ - proxy_pass http://127.0.0.1:5961/; - }} - - location / {{ - try_files $uri $uri/ =404; - }} -}} -server {{ - listen 443 ssl; - listen [::]:443; - server_name .{tor_hostname}; - return 301 http://$host$request_uri; -}} \ No newline at end of file diff --git a/backend/src/nginx/setup-wizard.conf b/backend/src/nginx/setup-wizard.conf deleted file mode 100644 index edc71d897..000000000 --- a/backend/src/nginx/setup-wizard.conf +++ /dev/null @@ -1,29 +0,0 @@ -server { - listen 80 default_server; - listen [::]:80 default_server; - - root /var/www/html/setup; - - index index.html index.htm index.nginx-debian.html; - - server_name _; - - proxy_buffering off; - proxy_request_buffering off; - proxy_socket_keepalive on; - proxy_http_version 1.1; - proxy_read_timeout 1800; - - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; - - location /rpc/ { - proxy_pass http://127.0.0.1:5959/; - } - - location / { - try_files $uri $uri/ =404; - } -} \ No newline at end of file diff --git a/backend/src/os_install.rs b/backend/src/os_install.rs index 4f1ea2623..c670ea75d 100644 --- a/backend/src/os_install.rs +++ b/backend/src/os_install.rs @@ -6,6 +6,7 @@ use rpc_toolkit::command; use serde::{Deserialize, Serialize}; use tokio::process::Command; +use crate::context::InstallContext; use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::ReadWrite; @@ -84,16 +85,6 @@ pub async fn find_eth_iface() -> Result { )) } -// pub struct FDisk { -// child: Child, -// stdin: ChildStdin, -// } -// impl FDisk { -// pub async fn command(&mut self, cmd: &[u8]) -> Result<(), Error> { - -// } -// } - pub fn partition_for(disk: impl AsRef, idx: usize) -> PathBuf { let disk_path = disk.as_ref(); let (root, leaf) = if let (Some(root), Some(leaf)) = ( @@ -306,12 +297,10 @@ pub async fn execute( } #[command(display(display_none))] -pub async fn reboot() -> Result<(), Error> { +pub async fn reboot(#[context] ctx: InstallContext) -> Result<(), Error> { Command::new("sync") .invoke(crate::ErrorKind::Filesystem) .await?; - Command::new("reboot") - .invoke(crate::ErrorKind::Filesystem) - .await?; + ctx.shutdown.send(()).unwrap(); Ok(()) } diff --git a/backend/src/static_server.rs b/backend/src/static_server.rs deleted file mode 100644 index fc86bc51a..000000000 --- a/backend/src/static_server.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::fs::Metadata; -use std::future::Future; -use std::path::{Path, PathBuf}; -use std::time::UNIX_EPOCH; - -use digest::Digest; -use http::response::Builder; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Error as HyperError, Method, Request, Response, Server, StatusCode}; -use tokio::fs::File; -use tokio_util::codec::{BytesCodec, FramedRead}; - -use crate::context::RpcContext; -use crate::install::PKG_PUBLIC_DIR; -use crate::middleware::auth::HasValidSession; -use crate::{Error, ErrorKind, ResultExt}; - -static NOT_FOUND: &[u8] = b"Not Found"; -static NOT_AUTHORIZED: &[u8] = b"Not Authorized"; - -pub fn init( - ctx: RpcContext, - shutdown: impl Future + Send + 'static, -) -> impl Future> { - let addr = ctx.bind_static; - - let make_service = make_service_fn(move |_| { - let ctx = ctx.clone(); - async move { - Ok::<_, HyperError>(service_fn(move |req| { - let ctx = ctx.clone(); - async move { - match file_server_router(req, ctx).await { - Ok(x) => Ok::<_, HyperError>(x), - Err(err) => { - tracing::error!("{:?}", err); - Ok(server_error()) - } - } - } - })) - } - }); - - Server::bind(&addr) - .serve(make_service) - .with_graceful_shutdown(shutdown) -} - -async fn file_server_router(req: Request, ctx: RpcContext) -> Result, Error> { - let (request_parts, _body) = req.into_parts(); - let valid_session = HasValidSession::from_request_parts(&request_parts, &ctx).await; - match ( - valid_session, - request_parts.method, - request_parts - .uri - .path() - .strip_prefix("/") - .unwrap_or(request_parts.uri.path()) - .split_once("/"), - ) { - (Err(error), _, _) => { - tracing::warn!("unauthorized for {} @{:?}", error, request_parts.uri.path()); - tracing::debug!("{:?}", error); - return Ok(Response::builder() - .status(StatusCode::UNAUTHORIZED) - .body(NOT_AUTHORIZED.into()) - .unwrap()); - } - (Ok(valid_session), Method::GET, Some(("package-data", path))) => { - file_send( - valid_session, - &ctx, - ctx.datadir.join(PKG_PUBLIC_DIR).join(path), - ) - .await - } - (Ok(valid_session), Method::GET, Some(("eos", "local.crt"))) => { - file_send( - valid_session, - &ctx, - PathBuf::from(crate::net::ssl::ROOT_CA_STATIC_PATH), - ) - .await - } - _ => Ok(not_found()), - } -} - -/// HTTP status code 404 -fn not_found() -> Response { - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(NOT_FOUND.into()) - .unwrap() -} - -/// HTTP status code 500 -fn server_error() -> Response { - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body("".into()) - .unwrap() -} -async fn file_send( - _valid_session: HasValidSession, - _ctx: &RpcContext, - path: impl AsRef, -) -> Result, Error> { - // Serve a file by asynchronously reading it by chunks using tokio-util crate. - - let path = path.as_ref(); - - if let Ok(file) = File::open(path).await { - let metadata = file.metadata().await.with_kind(ErrorKind::Filesystem)?; - let _is_non_empty = match IsNonEmptyFile::new(&metadata, path) { - Some(a) => a, - None => return Ok(not_found()), - }; - - let mut builder = Response::builder().status(StatusCode::OK); - builder = with_e_tag(path, &metadata, builder)?; - builder = with_content_type(path, builder); - builder = with_content_length(&metadata, builder); - let stream = FramedRead::new(file, BytesCodec::new()); - let body = Body::wrap_stream(stream); - return Ok(builder.body(body).with_kind(ErrorKind::Network)?); - } - tracing::debug!("File not found: {:?}", path); - - Ok(not_found()) -} - -struct IsNonEmptyFile(()); -impl IsNonEmptyFile { - fn new(metadata: &Metadata, path: &Path) -> Option { - let length = metadata.len(); - if !metadata.is_file() || length == 0 { - tracing::debug!("File is empty: {:?}", path); - return None; - } - Some(Self(())) - } -} - -fn with_e_tag(path: &Path, metadata: &Metadata, builder: Builder) -> Result { - let modified = metadata.modified().with_kind(ErrorKind::Filesystem)?; - let mut hasher = sha2::Sha256::new(); - hasher.update(format!("{:?}", path).as_bytes()); - hasher.update( - format!( - "{}", - modified - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() - ) - .as_bytes(), - ); - let res = hasher.finalize(); - Ok(builder.header( - "ETag", - base32::encode(base32::Alphabet::RFC4648 { padding: false }, res.as_slice()).to_lowercase(), - )) -} -///https://en.wikipedia.org/wiki/Media_type -fn with_content_type(path: &Path, builder: Builder) -> Builder { - let content_type = match path.extension() { - Some(os_str) => match os_str.to_str() { - Some("apng") => "image/apng", - Some("avif") => "image/avif", - Some("flif") => "image/flif", - Some("gif") => "image/gif", - Some("jpg") | Some("jpeg") | Some("jfif") | Some("pjpeg") | Some("pjp") => "image/jpeg", - Some("jxl") => "image/jxl", - Some("png") => "image/png", - Some("svg") => "image/svg+xml", - Some("webp") => "image/webp", - Some("mng") | Some("x-mng") => "image/x-mng", - Some("css") => "text/css", - Some("csv") => "text/csv", - Some("html") => "text/html", - Some("php") => "text/php", - Some("plain") | Some("md") | Some("txt") => "text/plain", - Some("xml") => "text/xml", - None | Some(_) => "text/plain", - }, - None => "text/plain", - }; - builder.header("Content-Type", content_type) -} -fn with_content_length(metadata: &Metadata, builder: Builder) -> Builder { - builder.header("Content-Length", metadata.len()) -} diff --git a/backend/src/util/config.rs b/backend/src/util/config.rs index ffdeaa7cc..ddc6f87c8 100644 --- a/backend/src/util/config.rs +++ b/backend/src/util/config.rs @@ -7,6 +7,7 @@ use serde_json::Value; use crate::util::serde::IoFormat; use crate::{Config, Error, ResultExt}; +pub const DEVICE_CONFIG_PATH: &str = "/media/embassy/config/config.yaml"; pub const CONFIG_PATH: &str = "/etc/embassy/config.yaml"; pub const CONFIG_PATH_LOCAL: &str = ".embassy/config.yaml"; diff --git a/backend/src/volume.rs b/backend/src/volume.rs index f243121c3..c18010439 100644 --- a/backend/src/volume.rs +++ b/backend/src/volume.rs @@ -10,7 +10,7 @@ use tracing::instrument; use crate::context::RpcContext; use crate::net::interface::{InterfaceId, Interfaces}; -use crate::net::NetController; +use crate::net::net_controller::NetController; use crate::s9pk::manifest::PackageId; use crate::util::Version; use crate::{Error, ResultExt}; diff --git a/build/lib/conflicts b/build/lib/conflicts index 58d721ef5..536950a42 100644 --- a/build/lib/conflicts +++ b/build/lib/conflicts @@ -1,3 +1,5 @@ openresolv dhcpcd5 -firewalld \ No newline at end of file +firewalld +nginx +nginx-common \ No newline at end of file diff --git a/build/lib/depends b/build/lib/depends index 3307c44d8..fa31db2c6 100644 --- a/build/lib/depends +++ b/build/lib/depends @@ -1,5 +1,4 @@ tor -nginx avahi-daemon avahi-utils iotop @@ -29,4 +28,5 @@ httpdirfs iw squashfs-tools rsync -systemd-timesyncd \ No newline at end of file +systemd-timesyncd +magic-wormhole \ No newline at end of file diff --git a/build/lib/scripts/enable-kiosk b/build/lib/scripts/enable-kiosk index 79350c3ea..441bc6135 100755 --- a/build/lib/scripts/enable-kiosk +++ b/build/lib/scripts/enable-kiosk @@ -25,7 +25,7 @@ user_pref("signon.rememberSignons", false); EOT matchbox-window-manager -use_titlebar yes & if [ -z "$use_https" ]; then - firefox-esr --kiosk http://localhost --profile $PROFILE + firefox-esr --kiosk http://$(hostname).local --profile $PROFILE else firefox-esr --kiosk https://$(hostname).local --profile $PROFILE fi diff --git a/build/lib/scripts/postinst b/build/lib/scripts/postinst index 224a15ea0..24bf3a0fb 100755 --- a/build/lib/scripts/postinst +++ b/build/lib/scripts/postinst @@ -29,7 +29,10 @@ ln -s /usr/share/zoneinfo/Etc/UTC /etc/localtime # switch to systemd-resolved & network-manager echo "#" > /etc/network/interfaces -[ -f /run/systemd/resolve/stub-resolv.conf ] || mkdir -p /run/systemd/resolve && cp /etc/resolv.conf /run/systemd/resolve/stub-resolv.conf +if ! [ -f /run/systemd/resolve/stub-resolv.conf ]; then + mkdir -p /run/systemd/resolve + cp /etc/resolv.conf /run/systemd/resolve/stub-resolv.conf +fi ln -rsf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf cat << EOF > /etc/NetworkManager/NetworkManager.conf [main] @@ -59,8 +62,6 @@ fi sed -i 's/Restart=on-failure/Restart=always/g' /lib/systemd/system/tor@default.service sed -i 's/ExecStart=\/usr\/bin\/dockerd/ExecStart=\/usr\/bin\/dockerd --exec-opt native.cgroupdriver=systemd/g' /lib/systemd/system/docker.service -sed -i '/}/i \ \ \ \ application\/wasm \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ wasm;' /etc/nginx/mime.types -sed -i 's/# server_names_hash_bucket_size 64;/server_names_hash_bucket_size 128;/g' /etc/nginx/nginx.conf sed -i '/\(^\|#\)entries-per-entry-group-max=/c\entries-per-entry-group-max=128' /etc/avahi/avahi-daemon.conf sed -i '/\(^\|#\)Storage=/c\Storage=persistent' /etc/systemd/journald.conf sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf diff --git a/libs/models/src/id.rs b/libs/models/src/id.rs index 79e725b5c..d8b9ae163 100644 --- a/libs/models/src/id.rs +++ b/libs/models/src/id.rs @@ -7,7 +7,7 @@ use crate::invalid_id::InvalidId; pub const SYSTEM_ID: Id<&'static str> = Id("x_system"); -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Id = String>(S); impl> Id { pub fn try_from(value: S) -> Result { diff --git a/libs/models/src/interface_id.rs b/libs/models/src/interface_id.rs index 25dafaff4..168756bac 100644 --- a/libs/models/src/interface_id.rs +++ b/libs/models/src/interface_id.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Deserializer, Serialize}; use crate::Id; -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Default)] pub struct InterfaceId = String>(Id); impl> From> for InterfaceId { fn from(id: Id) -> Self { diff --git a/system-images/compat/Cargo.lock b/system-images/compat/Cargo.lock index 581e7b9e2..c7b9d7069 100644 --- a/system-images/compat/Cargo.lock +++ b/system-images/compat/Cargo.lock @@ -83,12 +83,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - [[package]] name = "arrayvec" version = "0.7.2" @@ -216,6 +210,15 @@ dependencies = [ "itertools 0.9.0", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -254,9 +257,9 @@ checksum = "703642b98a00b3b90513279a8ede3fcfa479c126c5fb46e78f3051522f021403" [[package]] name = "bitvec" -version = "0.19.6" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" +checksum = "5237f00a8c86130a0cc317830e558b966dd7850d48a953d998c813f01a41b527" dependencies = [ "funty", "radium", @@ -271,7 +274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72936ee4afc7f8f736d1c38383b56480b5497b4617b4a77bdbf1d2ababc76127" dependencies = [ "arrayref", - "arrayvec 0.7.2", + "arrayvec", "constant_time_eq", ] @@ -957,6 +960,7 @@ dependencies = [ "base64ct", "basic-cookies", "bollard", + "bytes", "chrono", "ciborium", "clap 3.2.22", @@ -988,9 +992,10 @@ dependencies = [ "lazy_static", "libc", "log", + "mbrman", "models", "nix 0.25.0", - "nom 7.1.1", + "nom", "num", "num_enum", "openssh-keys", @@ -1023,6 +1028,7 @@ dependencies = [ "tar", "thiserror", "tokio", + "tokio-rustls", "tokio-stream", "tokio-tar", "tokio-tungstenite", @@ -1058,13 +1064,12 @@ dependencies = [ [[package]] name = "emver" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed260c4d7efaec031b9c4f6c4d3cf136e3df2bbfe50925800236f5e847f28704" +version = "0.1.7" +source = "git+https://github.com/Start9Labs/emver-rs.git#61cf0bc96711b4d6f3f30df8efef025e0cc02bad" dependencies = [ "either", "fp-core", - "nom 6.1.2", + "nom", "serde", ] @@ -1248,9 +1253,9 @@ dependencies = [ [[package]] name = "funty" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" +checksum = "1847abb9cb65d566acd5942e94aea9c8f547ad02c98e1649326fc0e8910b8b1e" [[package]] name = "futures" @@ -1892,19 +1897,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lexical-core" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" -dependencies = [ - "arrayvec 0.5.2", - "bitflags", - "cfg-if", - "ryu", - "static_assertions", -] - [[package]] name = "libc" version = "0.2.133" @@ -1961,6 +1953,19 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "mbrman" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdee9caaf6ebb0bae8ae1cb6b591e5553b6cf5a34c7ea07bb6f24c1a80619819" +dependencies = [ + "bincode", + "bitvec", + "serde", + "serde-big-array", + "thiserror", +] + [[package]] name = "md-5" version = "0.9.1" @@ -2114,19 +2119,6 @@ dependencies = [ "pin-utils", ] -[[package]] -name = "nom" -version = "6.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" -dependencies = [ - "bitvec", - "funty", - "lexical-core", - "memchr", - "version_check", -] - [[package]] name = "nom" version = "7.1.1" @@ -2744,9 +2736,9 @@ dependencies = [ [[package]] name = "radium" -version = "0.5.3" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" [[package]] name = "radix_trie" @@ -2990,9 +2982,9 @@ dependencies = [ [[package]] name = "rpc-toolkit" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5bfeb75c188f3af65774d5fe92f97dac2cede5e313c643c7a1b82a8e53b0e6" +checksum = "5353673ffd8265292281141560d2b851e4da49e83e2f5e255fd473736d45ee10" dependencies = [ "clap 3.2.22", "futures", @@ -3012,9 +3004,9 @@ dependencies = [ [[package]] name = "rpc-toolkit-macro" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecb48bdaace41cfbb514b3e541ae0fc1ac0fb8283498215ad8a3d22ca2ea5ae5" +checksum = "f8e4b9cb00baf2d61bcd35e98d67dcb760382a3b4540df7e63b38d053c8a7b8b" dependencies = [ "proc-macro2 1.0.44", "rpc-toolkit-macro-internals", @@ -3023,9 +3015,9 @@ dependencies = [ [[package]] name = "rpc-toolkit-macro-internals" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a9e2bae02a2beecad48d87255e51cab941d0c89a2bcee05a03a77803a0a282" +checksum = "d3e2ce21b936feaecdab9c9a8e75b9dca64374ccc11951a58045ad6559b75f42" dependencies = [ "proc-macro2 1.0.44", "quote 1.0.21", @@ -3168,6 +3160,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3323f09a748af288c3dc2474ea6803ee81f118321775bffa3ac8f7e65c5e90e7" +dependencies = [ + "serde", +] + [[package]] name = "serde_bytes" version = "0.11.7" @@ -3471,7 +3472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a" dependencies = [ "itertools 0.10.5", - "nom 7.1.1", + "nom", "unicode_categories", ] @@ -3572,12 +3573,6 @@ dependencies = [ "tokio-rustls", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "stderrlog" version = "0.5.3" @@ -3828,9 +3823,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", "bytes", @@ -3838,7 +3833,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", @@ -4550,9 +4544,12 @@ dependencies = [ [[package]] name = "wyz" -version = "0.2.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" +checksum = "129e027ad65ce1453680623c3fb5163cbf7107bfe1aa32257e7d0e63f9ced188" +dependencies = [ + "tap", +] [[package]] name = "xattr"