diff --git a/Cargo.lock b/Cargo.lock index 0f2419c..1e29d0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -702,9 +702,9 @@ dependencies = [ [[package]] name = "b64-ct" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32261e5a95e234446dd31774ef4305dc164039370fe2622fd3e935a0f2730689" +checksum = "41dd1e07aafe656df62d40e74d4627a0cce26da43b0bec233ac1305207a5a768" [[package]] name = "backtrace" @@ -962,9 +962,13 @@ dependencies = [ "chir-rs-http-api", "clap", "color-eyre", + "dotenvy", "eyre", + "mime", + "mime_guess", "reqwest", "tokio", + "tracing", "tracing-subscriber", ] @@ -1028,10 +1032,13 @@ dependencies = [ "chir-rs-misc", "chrono", "eyre", + "futures", "mime", "rusty_paseto", "sentry-tower", + "serde", "tokio", + "tokio-util", "tower-http 0.6.2", "tracing", "unicode-normalization", @@ -2482,6 +2489,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3064,10 +3081,12 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-rustls 0.26.0", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", "windows-registry", @@ -4170,12 +4189,13 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -4408,6 +4428,12 @@ dependencies = [ "libc", ] +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + [[package]] name = "unicode-bidi" version = "0.3.17" @@ -4626,6 +4652,19 @@ version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.74" diff --git a/Cargo.nix b/Cargo.nix index a543310..cfb535e 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -32,7 +32,7 @@ args@{ ignoreLockHash, }: let - nixifiedLockHash = "7dc55944ae2b0d1b65a004e3be3f5314339c3d2e023e794d54b030b197c5bf87"; + nixifiedLockHash = "51a600afd0b9503f1748762462902306933599d46304da01c1ecdecdb6049697"; workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc; currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock); lockHashIgnored = @@ -1927,7 +1927,7 @@ else inherit profileName; }).out; tokio_util = - (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.12" { + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" { inherit profileName; }).out; }; @@ -2262,15 +2262,15 @@ else }; }); - "registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.2" = + "registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.3" = overridableMkRustCrate (profileName: rec { name = "b64-ct"; - version = "0.1.2"; + version = "0.1.3"; registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; - sha256 = "32261e5a95e234446dd31774ef4305dc164039370fe2622fd3e935a0f2730689"; + sha256 = "41dd1e07aafe656df62d40e74d4627a0cce26da43b0bec233ac1305207a5a768"; }; features = builtins.concatLists [ [ "default" ] @@ -3010,10 +3010,22 @@ else (rustPackages."registry+https://github.com/rust-lang/crates.io-index".color-eyre."0.6.3" { inherit profileName; }).out; + dotenvy = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".dotenvy."0.15.7" { + inherit profileName; + }).out; eyre = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".eyre."0.6.12" { inherit profileName; }).out; + mime = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".mime."0.3.17" { + inherit profileName; + }).out; + mime_guess = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".mime_guess."2.0.5" { + inherit profileName; + }).out; reqwest = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".reqwest."0.12.9" { inherit profileName; @@ -3022,6 +3034,10 @@ else (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.42.0" { inherit profileName; }).out; + tracing = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.41" { + inherit profileName; + }).out; tracing_subscriber = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-subscriber."0.3.19" { inherit profileName; @@ -3164,7 +3180,7 @@ else inherit profileName; }).out; b64_ct = - (rustPackages."registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.2" { + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.3" { inherit profileName; }).out; bincode = @@ -3184,6 +3200,10 @@ else (rustPackages."registry+https://github.com/rust-lang/crates.io-index".eyre."0.6.12" { inherit profileName; }).out; + futures = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.31" { + inherit profileName; + }).out; mime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".mime."0.3.17" { inherit profileName; @@ -3196,10 +3216,18 @@ else (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sentry-tower."0.34.0" { inherit profileName; }).out; + serde = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.215" { + inherit profileName; + }).out; tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.42.0" { inherit profileName; }).out; + tokio_util = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" { + inherit profileName; + }).out; tower_http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-http."0.6.2" { inherit profileName; @@ -3226,7 +3254,6 @@ else [ "axum-core" ] [ "bytes" ] [ "mime" ] - [ "tracing" ] ]; dependencies = { async_trait = @@ -3238,7 +3265,7 @@ else inherit profileName; }).out; b64_ct = - (rustPackages."registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.2" { + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.3" { inherit profileName; }).out; bincode = @@ -5111,6 +5138,7 @@ else [ "async-await" ] [ "async-await-macro" ] [ "channel" ] + [ "default" ] [ "futures-channel" ] [ "futures-io" ] [ "futures-macro" ] @@ -5351,7 +5379,7 @@ else inherit profileName; }).out; tokio_util = - (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.12" { + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" { inherit profileName; }).out; tracing = @@ -5409,7 +5437,7 @@ else inherit profileName; }).out; tokio_util = - (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.12" { + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" { inherit profileName; }).out; tracing = @@ -7269,6 +7297,38 @@ else }; }); + "registry+https://github.com/rust-lang/crates.io-index".mime_guess."2.0.5" = + overridableMkRustCrate + (profileName: rec { + name = "mime_guess"; + version = "2.0.5"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { + inherit name version; + sha256 = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"; + }; + features = builtins.concatLists [ + [ "default" ] + [ "rev-mappings" ] + ]; + dependencies = { + mime = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".mime."0.3.17" { + inherit profileName; + }).out; + unicase = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".unicase."2.8.0" { + inherit profileName; + }).out; + }; + buildDependencies = { + unicase = + (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".unicase."2.8.0" { + profileName = "__noProfile"; + }).out; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".minimal-lexical."0.2.1" = overridableMkRustCrate (profileName: rec { @@ -8815,6 +8875,7 @@ else [ "rustls-tls" ] [ "rustls-tls-webpki-roots" ] [ "rustls-tls-webpki-roots-no-provider" ] + [ "stream" ] ]; dependencies = { base64 = @@ -8929,6 +8990,10 @@ else (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-rustls."0.26.0" { inherit profileName; }).out; + ${if !(hostPlatform.parsed.cpu.name == "wasm32") then "tokio_util" else null} = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" { + inherit profileName; + }).out; tower_service = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.3" { inherit profileName; @@ -8945,6 +9010,10 @@ else (rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen-futures."0.4.47" { inherit profileName; } ).out; + ${if hostPlatform.parsed.cpu.name == "wasm32" then "wasm_streams" else null} = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-streams."0.4.2" { + inherit profileName; + }).out; ${if hostPlatform.parsed.cpu.name == "wasm32" then "web_sys" else null} = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.74" { inherit profileName; @@ -12344,19 +12413,21 @@ else }; }); - "registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.12" = + "registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" = overridableMkRustCrate (profileName: rec { name = "tokio-util"; - version = "0.7.12"; + version = "0.7.13"; registry = "registry+https://github.com/rust-lang/crates.io-index"; src = fetchCratesIo { inherit name version; - sha256 = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"; + sha256 = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"; }; features = builtins.concatLists [ [ "codec" ] + [ "compat" ] [ "default" ] + [ "futures-io" ] [ "io" ] ]; dependencies = { @@ -12368,6 +12439,10 @@ else (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.31" { inherit profileName; }).out; + futures_io = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.31" { + inherit profileName; + }).out; futures_sink = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.31" { inherit profileName; @@ -13030,6 +13105,18 @@ else }; }); + "registry+https://github.com/rust-lang/crates.io-index".unicase."2.8.0" = + overridableMkRustCrate + (profileName: rec { + name = "unicase"; + version = "2.8.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { + inherit name version; + sha256 = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".unicode-bidi."0.3.17" = overridableMkRustCrate (profileName: rec { @@ -13590,6 +13677,40 @@ else }; }); + "registry+https://github.com/rust-lang/crates.io-index".wasm-streams."0.4.2" = + overridableMkRustCrate + (profileName: rec { + name = "wasm-streams"; + version = "0.4.2"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { + inherit name version; + sha256 = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"; + }; + dependencies = { + futures_util = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.31" { + inherit profileName; + }).out; + js_sys = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".js-sys."0.3.74" { + inherit profileName; + }).out; + wasm_bindgen = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen."0.2.97" { + inherit profileName; + }).out; + wasm_bindgen_futures = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen-futures."0.4.47" + { inherit profileName; } + ).out; + web_sys = + (rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.74" { + inherit profileName; + }).out; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.74" = overridableMkRustCrate (profileName: rec { @@ -13612,16 +13733,36 @@ else [ "Headers" ] [ "MessageEvent" ] [ "Performance" ] + [ "QueuingStrategy" ] + [ "ReadableByteStreamController" ] [ "ReadableStream" ] + [ "ReadableStreamByobReader" ] + [ "ReadableStreamByobRequest" ] + [ "ReadableStreamDefaultController" ] + [ "ReadableStreamDefaultReader" ] + [ "ReadableStreamGetReaderOptions" ] + [ "ReadableStreamReadResult" ] + [ "ReadableStreamReaderMode" ] + [ "ReadableStreamType" ] + [ "ReadableWritablePair" ] [ "Request" ] [ "RequestCredentials" ] [ "RequestInit" ] [ "RequestMode" ] [ "Response" ] [ "ServiceWorkerGlobalScope" ] + [ "StreamPipeOptions" ] + [ "TransformStream" ] + [ "TransformStreamDefaultController" ] + [ "Transformer" ] + [ "UnderlyingSink" ] + [ "UnderlyingSource" ] [ "Window" ] [ "Worker" ] [ "WorkerGlobalScope" ] + [ "WritableStream" ] + [ "WritableStreamDefaultController" ] + [ "WritableStreamDefaultWriter" ] [ "default" ] [ "std" ] ]; diff --git a/chir-rs-castore/src/lib.rs b/chir-rs-castore/src/lib.rs index 2d550fe..f9f41a1 100644 --- a/chir-rs-castore/src/lib.rs +++ b/chir-rs-castore/src/lib.rs @@ -95,9 +95,9 @@ impl CaStore { { let mut reader = Box::pin(reader); let string_id = lexicographic_base64::encode(id.to_be_bytes()); - - info!("Starting multipart upload {id}"); let source_fname = format!("temp/{string_id}"); + + /*info!("Starting multipart upload {id}"); let multipart_result = self .client .create_multipart_upload() @@ -163,7 +163,28 @@ impl CaStore { .set_upload_id(multipart_result.upload_id) .send() .await - .context("Completing multipart upload")?; + .context("Completing multipart upload")?;*/ + + let hasher = Arc::new(Mutex::new(Hasher::new())); + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).await?; + let buf = Bytes::from(buf); + let buf2 = buf.clone(); + let hasher2 = Arc::clone(&hasher); + spawn_blocking(move || { + hasher2.blocking_lock().update_rayon(&buf2); + }) + .await?; + self.client + .put_object() + .bucket(&*self.bucket) + .key(&source_fname) + .body(ByteStream::from(buf.to_vec())) + .send() + .await + .context("Uploading file")?; + + let hash = hasher.lock().await.finalize(); let target_fname = lexicographic_base64::encode(hash.as_bytes()); diff --git a/chir-rs-client/Cargo.toml b/chir-rs-client/Cargo.toml index 312678d..6181592 100644 --- a/chir-rs-client/Cargo.toml +++ b/chir-rs-client/Cargo.toml @@ -8,9 +8,11 @@ bincode = "2.0.0-rc.3" chir-rs-http-api = { version = "0.1.0", path = "../chir-rs-http-api" } clap = { version = "4.5.21", features = ["derive"] } color-eyre = "0.6.3" +dotenvy = "0.15.7" eyre = "0.6.12" -reqwest = { version = "0.12.9", default-features = false, features = [ - "rustls-tls", -] } -tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] } +mime = "0.3.17" +mime_guess = "2.0.5" +reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls", "stream"] } +tokio = { version = "1.41.1", features = ["fs", "macros", "rt-multi-thread"] } +tracing = "0.1.41" tracing-subscriber = "0.3.19" diff --git a/chir-rs-client/src/main.rs b/chir-rs-client/src/main.rs index ead0d7b..fbb70fa 100644 --- a/chir-rs-client/src/main.rs +++ b/chir-rs-client/src/main.rs @@ -1,11 +1,15 @@ -use std::collections::HashSet; +use std::{collections::HashSet, future::Future, path::Path, pin::Pin}; use chir_rs_http_api::{ auth::{LoginRequest, PasetoToken, Scope}, errors::APIError, }; use clap::{arg, Parser, Subcommand}; -use eyre::Result; +use eyre::{eyre, Context as _, OptionExt as _, Result}; +use mime_guess::{Mime, MimeGuess}; +use reqwest::Body; +use tokio::join; +use tracing::instrument; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -24,6 +28,18 @@ enum Command { #[arg(short, long)] password: String, }, + Upload { + #[arg(short, long)] + source: String, + #[arg(short, long)] + dest: String, + }, + UploadDir { + #[arg(short, long)] + source: String, + #[arg(short, long)] + dest: String, + }, } async fn login(url: String, username: String, password: String) -> Result<()> { @@ -58,14 +74,76 @@ async fn login(url: String, username: String, password: String) -> Result<()> { Ok(()) } +#[instrument(skip(source))] +async fn upload(url: String, source: impl AsRef, dest: String) -> Result<()> { + let client = reqwest::Client::new(); + let token = std::env::var("CHIR_RS_TOKEN")?; + let file = tokio::fs::File::open(&source).await?; + let res = client + .post(format!("{url}{dest}")) + .header( + "Content-Type", + MimeGuess::from_path(&source) + .first() + .unwrap_or(mime::APPLICATION_OCTET_STREAM) + .to_string(), + ) + .header("Authorization", format!("Bearer {token}")) + .body(Body::from(file)) + .send() + .await?; + if !res.status().is_success() { + let response = res.bytes().await?; + let response: APIError = + bincode::decode_from_slice(&response, bincode::config::standard())?.0; + Err(response).with_context(|| format!("Uploading to {dest}"))?; + } + Ok(()) +} + +#[instrument(skip(source))] +async fn upload_dir(url: String, source: impl AsRef, dest: String) -> Result<()> { + let mut dir = tokio::fs::read_dir(source).await?; + while let Some(ent) = dir.next_entry().await? { + let file_type = ent.file_type().await?; + let file_name_str = ent + .file_name() + .into_string() + .map_err(|_| eyre!("Invalid file name encountered"))?; + let tgt = if dest.is_empty() { + file_name_str.clone() + } else { + format!("{dest}/{file_name_str}") + }; + if file_type.is_dir() { + let sub_fut: Pin>>> = + Box::pin(upload_dir(url.clone(), ent.path(), tgt)); + sub_fut.await?; + continue; + } + if file_name_str == "index.html" { + if dest.is_empty() { + upload(url.clone(), ent.path(), "".to_string()).await?; + } else { + upload(url.clone(), ent.path(), format!("{dest}/")).await?; + } + } + upload(url.clone(), ent.path(), tgt).await?; + } + Ok(()) +} + #[tokio::main] async fn main() -> Result<()> { color_eyre::install().ok(); + dotenvy::dotenv().ok(); tracing_subscriber::fmt::init(); let matches = Args::parse(); match matches.command { Command::Login { username, password } => login(matches.url, username, password).await?, + Command::Upload { source, dest } => upload(matches.url, source, dest).await?, + Command::UploadDir { source, dest } => upload_dir(matches.url, source, dest).await?, } Ok(()) diff --git a/chir-rs-db/src/file.rs b/chir-rs-db/src/file.rs index 1ff7d3a..059d83c 100644 --- a/chir-rs-db/src/file.rs +++ b/chir-rs-db/src/file.rs @@ -165,11 +165,11 @@ impl File { /// Creates a new file /// /// # Errors - /// This function returns an error if a database error occurs when writing, or there is a conflict + /// This function returns an error if a database error occurs when writing #[instrument(skip(db))] pub async fn new(db: &Database, path: &str, mime: &str, hash: &Hash) -> Result<()> { query_as( - r#"INSERT INTO file_map ("file_path", "mime", "b3hash") VALUES ($1, $2, $3) RETURNING *"#, + r#"INSERT INTO file_map ("file_path", "mime", "b3hash") VALUES ($1, $2, $3) ON CONFLICT ("file_path", "mime") DO UPDATE SET "b3hash" = $3 RETURNING *"#, ) .bind(path) .bind(mime) diff --git a/chir-rs-db/src/session.rs b/chir-rs-db/src/session.rs index da37de0..fba7ed6 100644 --- a/chir-rs-db/src/session.rs +++ b/chir-rs-db/src/session.rs @@ -23,7 +23,7 @@ pub async fn expire(db: &Database) -> Result<()> { let oldest_acceptable_id = oldest_acceptable_id.to_be_bytes(); #[expect(clippy::panic, reason = "sqlx moment")] query!( - r#"DELETE FROM "session_scopes" WHERE session_id < $1"#, + r#"DELETE FROM "sessions" WHERE id < $1"#, &oldest_acceptable_id ) .execute(&*db.0) diff --git a/chir-rs-http-api/Cargo.toml b/chir-rs-http-api/Cargo.toml index 4d7fa81..674a22b 100644 --- a/chir-rs-http-api/Cargo.toml +++ b/chir-rs-http-api/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [features] -axum = ["axum-core", "async-trait", "bytes", "mime", "tracing"] +axum = ["axum-core", "async-trait", "bytes", "mime"] [dependencies] async-trait = { version = "0.1.83", optional = true } @@ -18,7 +18,7 @@ http = "1.1.0" mime = { version = "0.3.17", optional = true } serde = { version = "1.0.215", features = ["derive"] } thiserror = "2.0.3" -tracing = { version = "0.1.40", optional = true } +tracing = { version = "0.1.40" } [lints.rust] deprecated-safe = "forbid" diff --git a/chir-rs-http-api/src/auth/mod.rs b/chir-rs-http-api/src/auth/mod.rs index 1d48844..5aad54e 100644 --- a/chir-rs-http-api/src/auth/mod.rs +++ b/chir-rs-http-api/src/auth/mod.rs @@ -4,13 +4,27 @@ use bincode::{Decode, Encode}; use educe::Educe; use eyre::{bail, eyre, Result}; use serde::{Deserialize, Serialize}; -use std::{collections::HashSet, fmt::Debug}; +use std::{ + collections::HashSet, + fmt::{Debug, Display}, +}; /// List of supported scopes for authentication #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, Hash)] pub enum Scope { /// Full scope granted by logging in. Full, + /// The ability to create or update files. + CreateUpdateFile, +} + +impl Display for Scope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Self::Full => write!(f, "Full permissions"), + Self::CreateUpdateFile => write!(f, "Create and update files"), + } + } } impl Scope { @@ -19,6 +33,7 @@ impl Scope { pub const fn to_i64(self) -> i64 { match self { Self::Full => 0, + Self::CreateUpdateFile => 1, } } @@ -29,6 +44,7 @@ impl Scope { pub fn from_i64(id: i64) -> Result { match id { 0 => Ok(Self::Full), + 1 => Ok(Self::CreateUpdateFile), _ => bail!("Invalid scope ID {id}"), } } diff --git a/chir-rs-http-api/src/errors/mod.rs b/chir-rs-http-api/src/errors/mod.rs index 0118e56..1c56fdc 100644 --- a/chir-rs-http-api/src/errors/mod.rs +++ b/chir-rs-http-api/src/errors/mod.rs @@ -3,6 +3,9 @@ use bincode::{Decode, Encode}; use http::StatusCode; use thiserror::Error; +use tracing::error; + +use crate::auth::Scope; /// The main error type #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Error)] @@ -31,7 +34,7 @@ pub enum APIError { #[error("Invalid payload")] PayloadInvalid, /// Returned when the error is unknown - #[error("Unknown Error")] + #[error("Unknown Error {0}")] Unknown(String), /// Returned when there is a database error #[error("Database error: {0}")] @@ -57,6 +60,16 @@ pub enum APIError { /// Invalid session #[error("Invalid session")] InvalidSession, + /// Missing scope + #[error("Missing required scope for request: {0}")] + MissingScope(Scope), +} + +impl From for APIError { + fn from(value: eyre::Report) -> Self { + error!("Error while handling request: {value:?}"); + Self::Unknown(format!("Error while handling request: {value:?}")) + } } impl APIError { @@ -75,7 +88,8 @@ impl APIError { | Self::InvalidAuthorizationHeader(_) | Self::InvalidAuthorizationMethod(_, _) | Self::Unauthorized - | Self::InvalidSession => StatusCode::UNAUTHORIZED, + | Self::InvalidSession + | Self::MissingScope(_) => StatusCode::UNAUTHORIZED, Self::Unknown(_) | Self::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } diff --git a/chir-rs-http/Cargo.toml b/chir-rs-http/Cargo.toml index afa85bc..b224011 100644 --- a/chir-rs-http/Cargo.toml +++ b/chir-rs-http/Cargo.toml @@ -20,10 +20,13 @@ chir-rs-misc = { version = "0.1.0", path = "../chir-rs-misc", features = [ ] } chrono = "0.4.38" eyre = "0.6.12" +futures = "0.3.31" mime = "0.3.17" rusty_paseto = { version = "0.7.1", default-features = false, features = ["batteries_included", "v4_local"] } sentry-tower = { version = "0.34.0", features = ["axum", "axum-matched-path"] } +serde = { version = "1.0.215", features = ["derive"] } tokio = { version = "1.41.1", features = ["fs", "net"] } +tokio-util = { version = "0.7.13", features = ["compat"] } tower-http = { version = "0.6.2", features = ["trace"] } tracing = "0.1.40" unicode-normalization = "0.1.24" diff --git a/chir-rs-http/src/auth/req_auth/auth_header.rs b/chir-rs-http/src/auth/req_auth/auth_header.rs index a00198e..d465250 100644 --- a/chir-rs-http/src/auth/req_auth/auth_header.rs +++ b/chir-rs-http/src/auth/req_auth/auth_header.rs @@ -20,6 +20,23 @@ use crate::AppState; #[derive(Clone, Debug, PartialEq, Eq)] pub struct AuthHeader(pub String, pub HashSet); +impl AuthHeader { + /// Checks whether or not a scope is granted for the session + /// + /// # Errors + /// This function returns an error if the current session does not have a scope granted. + pub fn assert_scope(&self, scope: Scope) -> Result<(), APIError> { + if self.1.contains(&Scope::Full) { + return Ok(()); + } + if self.1.contains(&scope) { + Ok(()) + } else { + Err(APIError::MissingScope(scope)) + } + } +} + #[async_trait] impl FromRequestParts for AuthHeader { type Rejection = APIError; diff --git a/chir-rs-http/src/ca_server/mod.rs b/chir-rs-http/src/ca_server/mod.rs index f037512..1ac9855 100644 --- a/chir-rs-http/src/ca_server/mod.rs +++ b/chir-rs-http/src/ca_server/mod.rs @@ -2,20 +2,24 @@ use axum::{ body::Body, - extract::State, + extract::{Query, State}, http::{ header::{ACCEPT, CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE, ETAG, IF_NONE_MATCH}, - HeaderMap, StatusCode, Uri, + HeaderMap, Request, StatusCode, Uri, }, response::Response, }; use chir_rs_db::file::File; +use chir_rs_http_api::{auth::Scope, errors::APIError}; use chir_rs_misc::lexicographic_base64; use eyre::Context as _; +use futures::TryStreamExt; use mime::MimeIter; +use serde::Deserialize; +use tokio_util::compat::FuturesAsyncReadCompatExt as _; use tracing::{debug, error, info}; -use crate::AppState; +use crate::{auth::req_auth::auth_header::AuthHeader, AppState}; /// Formats an eyre error message #[expect(clippy::expect_used, reason = "fatal error in error handling function")] @@ -153,3 +157,35 @@ pub async fn serve_files( .with_context(|| format!("Serving file for {path}")) .map_err(format_error) } + +/// Creates a static file +/// +/// # Errors +/// +/// This function returns an error if the request fails. +pub async fn create_files( + State(state): State, + session: AuthHeader, + uri: Uri, + headers: HeaderMap, + req: Request, +) -> Result<[u8; 32], APIError> { + session.assert_scope(Scope::CreateUpdateFile)?; + let mime = headers + .get(CONTENT_TYPE) + .ok_or_else(|| APIError::ClientMissingContentType { + expected: "*/*".to_string(), + })? + .to_str() + .map_err(|e| APIError::ClientInvalidContentType { + expected: "*/*".to_string(), + received: format!("{e:?}"), + })?; + let mut stream = + TryStreamExt::map_err(req.into_body().into_data_stream(), std::io::Error::other) + .into_async_read() + .compat(); + let result = state.ca.upload(&mut stream).await?; + File::new(&state.db, uri.path(), mime, &result).await?; + Ok(*result.as_bytes()) +} diff --git a/chir-rs-http/src/lib.rs b/chir-rs-http/src/lib.rs index b7a4396..37a9c6a 100644 --- a/chir-rs-http/src/lib.rs +++ b/chir-rs-http/src/lib.rs @@ -80,7 +80,7 @@ pub async fn main(cfg: Arc, db: Database, castore: CaStore) -> Result<() get(|| async move { metric_handle.render() }), ) .route("/.api/auth/login", post(auth::password_login::login)) - .fallback(get(ca_server::serve_files)) + .fallback(get(ca_server::serve_files).post(ca_server::create_files)) .with_state(AppState { db, ca: castore,