From a3f58ccc392317b734f9f901e74ac3694c07b9da Mon Sep 17 00:00:00 2001 From: antifallobst Date: Fri, 24 Nov 2023 01:28:05 +0100 Subject: [PATCH] feat: added forwarding of '/admin/backup' calls to the admin worker --- Cargo.lock | 306 +++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 3 + src/api/admin/calls.rs | 95 +++++++++++++ src/api/admin/mod.rs | 1 + src/api/mod.rs | 6 +- src/main.rs | 3 +- src/tokens.rs | 2 +- 7 files changed, 401 insertions(+), 15 deletions(-) create mode 100644 src/api/admin/calls.rs create mode 100644 src/api/admin/mod.rs diff --git a/Cargo.lock b/Cargo.lock index be7eefa..790b3c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -589,6 +589,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -832,6 +842,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -853,9 +878,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" @@ -881,30 +906,42 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1085,6 +1122,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.8.0" @@ -1103,6 +1151,43 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.57" @@ -1521,6 +1606,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nerdcult_api" version = "0.1.0" @@ -1532,14 +1635,17 @@ dependencies = [ "base64 0.21.3", "chrono", "env_logger", + "futures-util", "libinjection", "log", "mail-send", "regex", + "reqwest", "serde", "sha2", "sqlx", "tokio", + "tokio-stream", "uuid", ] @@ -1626,6 +1732,32 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openssl" +version = "0.10.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "openssl-probe" version = "0.1.5" @@ -1634,9 +1766,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.91" +version = "0.9.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" +checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" dependencies = [ "cc", "libc", @@ -1894,6 +2026,46 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64 0.21.3", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -2034,6 +2206,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2050,6 +2231,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.18" @@ -2460,6 +2664,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.7.1" @@ -2586,6 +2811,16 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.23.4" @@ -2616,6 +2851,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -2632,6 +2868,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.37" @@ -2718,6 +2960,12 @@ dependencies = [ "webpki-roots 0.22.6", ] +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + [[package]] name = "typenum" version = "1.16.0" @@ -2795,6 +3043,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -2832,6 +3089,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.87" @@ -2861,6 +3130,19 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +[[package]] +name = "wasm-streams" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.64" diff --git a/Cargo.toml b/Cargo.toml index a91feec..d05cc6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] tokio = { version = "1.29", features = ["macros", "rt-multi-thread", "sync"] } +tokio-stream = { version = "0.1.3", features = ["sync"] } anyhow = "1.0" actix-web = "4" serde = { version = "1.0.183", features = ["derive"] } @@ -22,3 +23,5 @@ mail-send = "0.4.0" regex = "1.9.3" argon2 = "0.5.2" base64 = "0.21.3" +reqwest = { version = "0.11.22", features = ["stream"] } +futures-util = "0.3.29" diff --git a/src/api/admin/calls.rs b/src/api/admin/calls.rs new file mode 100644 index 0000000..d72e8ac --- /dev/null +++ b/src/api/admin/calls.rs @@ -0,0 +1,95 @@ +use crate::accounts::{Account, AccountFlags}; +use crate::api::ApiState; +use crate::tokens::AuthToken; +use actix_web::{error, http::Method, web, Error, HttpRequest, HttpResponse}; +use actix_web_httpauth::extractors::bearer::BearerAuth; +use anyhow::Result; +use futures_util::StreamExt as _; +use tokio::sync::mpsc; +use tokio_stream::wrappers::UnboundedReceiverStream; + +pub async fn backup_forward( + req: HttpRequest, + mut payload: web::Payload, + method: Method, + data: web::Data, + auth: BearerAuth, +) -> Result { + let token = match AuthToken::check(&data.pool, auth.token()) + .await + .map_err(|e| { + log::error!("Failed to forward admin request: {e}"); + error::ErrorInternalServerError("") + })? { + Some(t) => t, + None => return Ok(HttpResponse::Unauthorized().finish()), + }; + + let account = Account::from_id(&data.pool, token.account.id()).await.map_err(|e| { + log::error!("Failed to forward admin request: {e}"); + error::ErrorInternalServerError("") + })?.ok_or_else(|| { + log::error!("Failed to forward admin request: the authorized account was not found, but the token was valid"); + error::ErrorInternalServerError("") + })?; + + if !account.has_flag(AccountFlags::Admin) { + return Ok(HttpResponse::Forbidden().finish()); + } + + // using http here is fine, because this call stays on the host + let url = format!( + "http://{}/{}", + std::env::var("ADMIN_WORKER_HOST").map_err(|e| { + log::error!("Failed to forward admin request: {e}"); + error::ErrorInternalServerError("") + })?, + req.uri() + .to_string() + .split_once("/admin/") + .ok_or_else(|| { + log::error!("Failed to forward admin request: failed to split URI"); + error::ErrorInternalServerError("URI splitting error") + })? + .1 + ); + + let client = reqwest::Client::new(); + + let (tx, rx) = mpsc::unbounded_channel(); + + actix_web::rt::spawn(async move { + while let Some(chunk) = payload.next().await { + tx.send(chunk).unwrap(); + } + }); + + let forwarded_req = client + .request(method, url) + .body(reqwest::Body::wrap_stream(UnboundedReceiverStream::new(rx))) + .header( + "Authorization", + format!( + "Bearer {}", + std::env::var("ADMIN_WORKER_TOKEN").map_err(|e| { + log::error!("Failed to forward admin request: {e}"); + error::ErrorInternalServerError("") + })?, + ), + ) + .header("Content-Type", "application/json"); + + let res = forwarded_req.send().await.map_err(|e| { + log::error!("Failed to forward admin request: {e}"); + error::ErrorInternalServerError("forwarding error") + })?; + + let mut client_resp = HttpResponse::build(res.status()); + // Remove `Connection` as per + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection#Directives + for (header_name, header_value) in res.headers().iter().filter(|(h, _)| *h != "connection") { + client_resp.insert_header((header_name.clone(), header_value.clone())); + } + + Ok(client_resp.streaming(res.bytes_stream())) +} diff --git a/src/api/admin/mod.rs b/src/api/admin/mod.rs new file mode 100644 index 0000000..09075e5 --- /dev/null +++ b/src/api/admin/mod.rs @@ -0,0 +1 @@ +pub mod calls; diff --git a/src/api/mod.rs b/src/api/mod.rs index 1ed1433..580f757 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,4 +1,5 @@ mod account; +mod admin; mod project; mod user; @@ -7,7 +8,7 @@ use anyhow::Result; use log::info; use sqlx::postgres::PgPool; -struct ApiState { +pub struct ApiState { pool: PgPool, } @@ -98,6 +99,9 @@ pub async fn start(port: u16, pool: PgPool) -> Result<()> { .service(project::calls::info) .service(project::calls::delete) .service(user::calls::info) + .service( + web::scope("/admin/backup").default_service(web::to(admin::calls::backup_forward)), + ) .app_data(web::Data::new(ApiState { pool: pool.clone() })) }) .bind(("0.0.0.0", port))? diff --git a/src/main.rs b/src/main.rs index 67609c3..6dab1c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,10 +18,11 @@ async fn main() -> Result<()> { check_envs(vec![ "DATABASE_URL", - "SMTP_PASSWORD", "SMTP_HOST_URL", "SMTP_USER", "SMTP_PASSWORD", + "ADMIN_WORKER_HOST", + "ADMIN_WORKER_TOKEN", ])?; let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()).await?; diff --git a/src/tokens.rs b/src/tokens.rs index 0c418fc..9446d92 100644 --- a/src/tokens.rs +++ b/src/tokens.rs @@ -47,7 +47,7 @@ impl AuthToken { Ok(token) } - pub async fn check(pool: &PgPool, alphanumeric_token: &String) -> Result> { + pub async fn check(pool: &PgPool, alphanumeric_token: &str) -> Result> { let query_result = sqlx::query_as!( Self, r#"SELECT * FROM AuthTokens WHERE token = $1;"#,