diff --git a/Cargo.lock b/Cargo.lock index 1bd44ca..eb3730e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,54 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -263,7 +311,25 @@ dependencies = [ "thiserror", "tokio", "tokio-util", - "toml", + "toml 0.8.2", +] + +[[package]] +name = "binctl" +version = "0.1.0" +dependencies = [ + "axum", + "clap", + "confy", + "dirs", + "exitcode", + "open", + "openidconnect", + "reqwest", + "serde", + "thiserror", + "tokio", + "tokio-util", ] [[package]] @@ -356,6 +422,64 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "confy" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c" +dependencies = [ + "directories", + "serde", + "thiserror", + "toml 0.5.11", +] + [[package]] name = "const-oid" version = "0.9.5" @@ -530,6 +654,47 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -658,6 +823,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "exitcode" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" + [[package]] name = "ff" version = "0.13.0" @@ -742,6 +913,7 @@ dependencies = [ "futures-core", "futures-io", "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -1045,6 +1217,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + [[package]] name = "is-terminal" version = "0.4.9" @@ -1056,6 +1237,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1339,6 +1530,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "open" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfabf1927dce4d6fdf563d63328a0a506101ced3ec780ca2135747336c98cef8" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openidconnect" version = "3.4.0" @@ -1371,6 +1573,12 @@ dependencies = [ "url", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "2.10.1" @@ -1422,11 +1630,17 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pem" version = "1.1.1" @@ -1621,6 +1835,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1630,6 +1853,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + [[package]] name = "regex" version = "1.10.2" @@ -1709,10 +1943,12 @@ dependencies = [ "system-configuration", "tokio", "tokio-rustls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", "winreg", @@ -2300,6 +2536,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.2" @@ -2443,6 +2688,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "version_check" version = "0.9.4" @@ -2530,6 +2781,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 fa02fc0..288cf11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,29 +1,10 @@ -[package] +[workspace] +resolver = "2" +members = [ + "server", + "cli" +] + +[workspace.package] name = "bin" version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -tokio = { version = "1.33", features = ["full"] } -tokio-util = { version="0.7", features = ["io"]} -futures-util = "0.3" -axum = {version="0.6", features=["macros", "headers", "multipart"]} -serde = "1.0" -toml = "0.8" -duration-str = "0.7.0" -render = { git="https://github.com/render-rs/render.rs" } -thiserror = "1.0" -rand = "0.8" -dotenvy = "0.15" -markdown = "0.3" -axum_oidc = {git="https://git2.zettoit.eu/pfz4/axum_oidc"} -log = "0.4" -env_logger = "0.10" - -chacha20 = "0.9" -sha3 = "0.10" -hex = "0.4" -bytes = "1.5" -pin-project-lite = "0.2" diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..c37e72e --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "binctl" +edition = "2021" +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version="4.4", features = ["derive"] } +reqwest = { version="0.11", features = ["rustls-tls", "stream"], default-features=false} +openidconnect = "3.4" +thiserror = "1.0" +serde = { version="1.0", features = [ "derive" ] } +axum = "0.6" +tokio = { version = "1.33", features = ["full"] } +open = "5.0" +tokio-util = { version="0.7.9", features = ["io"]} +dirs = "5.0" +confy = "0.5" +exitcode = "1.1.2" diff --git a/cli/src/auth.rs b/cli/src/auth.rs new file mode 100644 index 0000000..33ea1af --- /dev/null +++ b/cli/src/auth.rs @@ -0,0 +1,174 @@ +use std::sync::Arc; + +use axum::{ + extract::{Query, State}, + response::{Html, IntoResponse}, + routing::get, + Router, +}; +use openidconnect::{ + core::{CoreAuthenticationFlow, CoreClient, CoreErrorResponseType, CoreProviderMetadata}, + reqwest::async_http_client, + AccessTokenHash, AuthorizationCode, ClaimsVerificationError, ClientId, CsrfToken, + DiscoveryError, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, RedirectUrl, + RefreshToken, RequestTokenError, Scope, SigningError, StandardErrorResponse, TokenResponse, +}; +use serde::Deserialize; +use thiserror::Error; +use tokio::sync::mpsc; + +#[derive(Error, Debug)] +pub enum Error { + #[error("url parse error: {:?}", 0)] + UrlParse(#[from] openidconnect::url::ParseError), + + #[error("discovery error: {:?}", 0)] + Discovery(#[from] DiscoveryError>), + + #[error("request token error: {:?}", 0)] + RequestToken( + #[from] + RequestTokenError< + openidconnect::reqwest::Error, + StandardErrorResponse, + >, + ), + + #[error("claims verification error: {:?}", 0)] + ClaimsVerification(#[from] ClaimsVerificationError), + + #[error("signing error: {:?}", 0)] + Signing(#[from] SigningError), + + #[error("server did not return an id token")] + NoIdToken, + + #[error("invalid access token")] + InvalidAccessToken, + + #[error("no response received")] + NoResponse, + + #[error("csrf mismatch")] + CsrfMismatch, +} + +#[derive(Debug, Deserialize)] +struct ResponseData { + pub code: String, + pub state: String, +} + +pub(crate) async fn login( + issuer: &str, + client_id: &str, + scopes: &[String], + refresh_token: &mut Option, +) -> Result { + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(issuer.to_string())?, + async_http_client, + ) + .await?; + + // Create an OpenID Connect client by specifying the client ID, client secret, authorization URL + // and token URL. + let client = CoreClient::from_provider_metadata( + provider_metadata, + ClientId::new(client_id.to_string()), + None, + ) + // Set the URL the user will be redirected to after the authorization process. + .set_redirect_uri(RedirectUrl::new("http://[::1]:8080".to_string())?); + + // Generate a PKCE challenge. + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + if let Some(refresh_token) = refresh_token { + if let Ok(token_response) = client + .exchange_refresh_token(&RefreshToken::new(refresh_token.to_string())) + .request_async(async_http_client) + .await + { + eprintln!("authenticated with oidc provider"); + return Ok(token_response.access_token().secret().clone()); + } + } + + // Generate the full authorization URL. + let mut auth = client.authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ); + + for scope in scopes { + auth = auth.add_scope(Scope::new(scope.to_string())); + } + let (auth_url, csrf_token, nonce) = auth + // Set the PKCE code challenge. + .set_pkce_challenge(pkce_challenge) + .url(); + open::that(auth_url.to_string()).unwrap(); + eprintln!("a browser should have been opened with the url {auth_url}. please login with your oidc provider."); + + let (fuse_tx, mut fuse_rx) = mpsc::channel::(1); + let app = Router::new() + .route("/", get(handle_post)) + .with_state(Arc::new(fuse_tx)); + + let server = axum::Server::bind(&"[::1]:8080".parse().unwrap()).serve(app.into_make_service()); + + let data = tokio::select! { + x = fuse_rx.recv() => { + x + } + _ = server => { + None + } + }; + + let data = data.ok_or(Error::NoResponse)?; + + // match csrf_state + + if *csrf_token.secret() != data.state { + return Err(Error::CsrfMismatch); + } + + let token_response = client + .exchange_code(AuthorizationCode::new(data.code)) + // Set the PKCE code verifier. + .set_pkce_verifier(pkce_verifier) + .request_async(async_http_client) + .await?; + + // Extract the ID token claims after verifying its authenticity and nonce. + let id_token = token_response.id_token().ok_or_else(|| Error::NoIdToken)?; + let claims = id_token.claims(&client.id_token_verifier(), &nonce)?; + + // Verify the access token hash to ensure that the access token hasn't been substituted for + // another user's. + if let Some(expected_access_token_hash) = claims.access_token_hash() { + let actual_access_token_hash = + AccessTokenHash::from_token(token_response.access_token(), &id_token.signing_alg()?)?; + if actual_access_token_hash != *expected_access_token_hash { + return Err(Error::InvalidAccessToken); + } + } + + if let Some(new_refresh_token) = token_response.refresh_token() { + *refresh_token = Some(new_refresh_token.secret().to_string()); + } + + eprintln!("authenticated with oidc provider"); + Ok(token_response.access_token().secret().clone()) +} + +async fn handle_post( + State(fuse_tx): State>>, + Query(data): Query, +) -> impl IntoResponse { + fuse_tx.clone().send(data).await; + Html("Die Anmeldung war erfolgreich. Du kannst dieses Fenster jetzt schließen.") +} diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..9fda8a5 --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,103 @@ +use std::process::ExitCode; + +use clap::Parser; +use reqwest::{Body, Url}; +use serde::{Deserialize, Serialize}; +use tokio::io::stdin; +use tokio_util::io::ReaderStream; + +use crate::auth::login; + +mod auth; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Config { + pub refresh_token: Option, + pub binurl: String, + pub issuer: String, + pub client_id: String, + pub scopes: Vec, +} + +#[derive(Debug, Parser)] +pub struct Args { + #[arg(short, long)] + content_type: Option, + + #[arg(short, long)] + ttl: Option, +} + +impl Default for Config { + fn default() -> Self { + Self { + refresh_token: None, + binurl: "https://bin.zettoit.eu".to_string(), + issuer: "https://auth.zettoit.eu/realms/zettoit".to_string(), + client_id: "binctl".to_string(), + scopes: vec!["zettoit-bin".to_string()], + } + } +} + +#[tokio::main] +async fn main() { + let mut cfg: Config = confy::load("binctl", None).unwrap_or_default(); + + let args = Args::parse(); + let access_token = login( + &cfg.issuer, + &cfg.client_id, + cfg.scopes.as_slice(), + &mut cfg.refresh_token, + ) + .await + .unwrap(); + let mut bin = create_bin(&cfg.binurl, &access_token).await.unwrap(); + eprintln!("created bin at {}. uploading...", bin); + bin.set_query(args.ttl.map(|x| format!("ttl={}", x)).as_deref()); + + upload_to_bin( + bin.as_ref(), + &args + .content_type + .unwrap_or("application/octet-stream".to_string()), + ) + .await + .unwrap(); + + let _ = confy::store("binctl", None, cfg); + bin.set_query(None); + print!("{bin}"); +} + +async fn create_bin(binurl: &str, access_token: &str) -> Result { + let client = reqwest::Client::new(); + + Ok(client + .get(binurl) + .header("Authorization", format!("Bearer {}", access_token)) + .send() + .await? + .url() + .to_owned()) +} + +async fn upload_to_bin(url: &str, content_type: &str) -> Result<(), reqwest::Error> { + let client = reqwest::Client::new(); + + if let Err(error) = client + .post(url) + .header("Content-Type", content_type) + .body(Body::wrap_stream(ReaderStream::new(stdin()))) + .send() + .await? + .text() + .await + { + eprintln!("{:?}", error); + std::process::exit(exitcode::TEMPFAIL); + } + + Ok(()) +} diff --git a/flake.nix b/flake.nix index 4997bb7..c66048e 100644 --- a/flake.nix +++ b/flake.nix @@ -26,20 +26,22 @@ nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" - ] (system: let + ] (system: function system nixpkgs.legacyPackages.${system}); + in rec { + packages = forAllSystems(system: syspkgs: let pkgs = import nixpkgs { inherit system; overlays = [ (import rust-overlay) ]; }; rustToolchain = pkgs.rust-bin.stable.latest.default; - markdownFilter = path: _type: builtins.match ".*md$" path != null; - markdownOrCargo = path: type: (markdownFilter path type) || (craneLib.filterCargoSources path type); - craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; src = pkgs.lib.cleanSourceWith { src = craneLib.path ./.; - filter = markdownOrCargo; + filter = path: type: + (pkgs.lib.hasSuffix "\.md" path) || + (craneLib.filterCargoSources path type) + ; }; nativeBuildInputs = with pkgs; [ rustToolchain pkg-config ]; @@ -52,18 +54,20 @@ bin = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts; + pname = "bin"; }); - in function { - inherit bin pkgs; - }); - in { - packages = forAllSystems({pkgs, bin}: { - inherit bin; + binctl = craneLib.buildPackage (commonArgs // { + inherit cargoArtifacts; + pname = "binctl"; + }); + in { + inherit bin binctl; default = bin; }); - devShells = forAllSystems({pkgs, bin}: pkgs.mkShell { - inputsFrom = bin; + devShells = forAllSystems(system: pkgs: pkgs.mkShell { + inputsFrom = [packages.${system}.bin packages.${system}.binctl]; }); - hydraJobs."build" = forAllSystems({pkgs, bin}: bin); + hydraJobs."bin" = forAllSystems(system: pkgs: packages.${system}.bin); + hydraJobs."binctl" = forAllSystems(system: pkgs: packages.${system}.binctl); }; } diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 0000000..cfd632e --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "bin" +version.workspace = true +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1.33", features = ["full"] } +tokio-util = { version="0.7", features = ["io"]} +futures-util = "0.3" +axum = {version="0.6", features=["macros", "headers", "multipart"]} +serde = "1.0" +toml = "0.8" +duration-str = "0.7.0" +render = { git="https://github.com/render-rs/render.rs" } +thiserror = "1.0" +rand = "0.8" +dotenvy = "0.15" +markdown = "0.3" +axum_oidc = {git="https://git2.zettoit.eu/pfz4/axum_oidc"} +log = "0.4" +env_logger = "0.10" + +chacha20 = "0.9" +sha3 = "0.10" +hex = "0.4" +bytes = "1.5" +pin-project-lite = "0.2" diff --git a/src/error.rs b/server/src/error.rs similarity index 87% rename from src/error.rs rename to server/src/error.rs index a6a6d7a..f74faf7 100644 --- a/src/error.rs +++ b/server/src/error.rs @@ -38,6 +38,12 @@ pub enum Error { #[error("invalid ttl")] InvalidTtl, + + #[error("unauthorized")] + Unauthorized, + + #[error("forbidden")] + Forbidden, } impl IntoResponse for Error { @@ -54,6 +60,8 @@ impl IntoResponse for Error { (StatusCode::BAD_REQUEST, "invalid multipart data").into_response() } Self::InvalidTtl => (StatusCode::BAD_REQUEST, "invalid ttl specified").into_response(), + Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized\n").into_response(), + Self::Forbidden => (StatusCode::FORBIDDEN, "forbidden\n").into_response(), _ => { error!("{:?}", self); (StatusCode::INTERNAL_SERVER_ERROR, "internal server error\n").into_response() diff --git a/src/garbage_collector.rs b/server/src/garbage_collector.rs similarity index 100% rename from src/garbage_collector.rs rename to server/src/garbage_collector.rs diff --git a/src/item_explanation.md b/server/src/item_explanation.md similarity index 100% rename from src/item_explanation.md rename to server/src/item_explanation.md diff --git a/src/main.rs b/server/src/main.rs similarity index 85% rename from src/main.rs rename to server/src/main.rs index 60f1461..296f07e 100644 --- a/src/main.rs +++ b/server/src/main.rs @@ -32,12 +32,12 @@ use chacha20::{ }; use futures_util::StreamExt; use garbage_collector::GarbageCollector; -use log::debug; +use log::{debug, warn}; use render::{html, raw}; use serde::Deserialize; use sha3::{Digest, Sha3_256}; use tokio::{ - fs::File, + fs::{self, File}, io::{AsyncWriteExt, BufReader, BufWriter}, }; use util::{IdSalt, KeySalt}; @@ -147,7 +147,14 @@ async fn main() { let app = Router::new() .route("/", get(get_index)) - .route("/:id", get(get_item).post(post_item).put(post_item)) + .route( + "/:id", + get(get_item) + .post(upload_bin) + .put(upload_bin) + .delete(delete_bin), + ) + .route("/:id/delete", get(delete_bin_interactive).post(delete_bin)) .with_state(state); axum::Server::bind(&"[::]:8080".parse().expect("valid listen address")) .serve(app.into_make_service()) @@ -195,12 +202,69 @@ async fn get_index( ))) } +async fn delete_bin( + Path(phrase): Path, + State(app_state): State, + oidc_extractor: Result, axum_oidc::error::Error>, + jwt_claims: Option>, +) -> HandlerResult { + let subject = match (oidc_extractor, jwt_claims) { + (_, Some(claims)) => claims.sub.to_string(), + (Ok(oidc), None) => oidc.claims.subject().to_string(), + (Err(_), None) => return Err(Error::Unauthorized), + }; + + let phrase = Phrase::from_str(&phrase)?; + let id = Id::from_phrase(&phrase, &app_state.id_salt); + + let metadata_path = format!("{}/{}.toml", app_state.data, id); + let metadata = Metadata::from_file(&metadata_path).await?; + + if metadata.subject != subject { + return Err(Error::Forbidden); + } + + debug!("deleting bin {}", id); + let res_meta = fs::remove_file(&format!("{}/{}.toml", app_state.data, id)).await; + let res_data = fs::remove_file(&format!("{}/{}.dat", app_state.data, id)).await; + + if res_meta.is_err() || res_data.is_err() { + warn!("failed to delete bin {} for manual deletion", id); + } + + Ok("ok\n") +} + +async fn delete_bin_interactive( + _: Path, + _: OidcExtractor, +) -> HandlerResult { + let body = html! { + + + {"zettoit bin"} + + + +
+

{"Confirm Deletion"}

+

{"The bin will be deleted. All data will be permanently lost."}

+
+ +
+
+ + + }; + Ok(Html(body)) +} + #[derive(Deserialize)] pub struct PostQuery { ttl: Option, } -async fn post_item( +async fn upload_bin( Path(phrase): Path, Query(params): Query, State(app_state): State, @@ -308,7 +372,6 @@ async fn post_item( } } -#[debug_handler] async fn get_item( Path(phrase): Path, State(app_state): State, diff --git a/src/metadata.rs b/server/src/metadata.rs similarity index 100% rename from src/metadata.rs rename to server/src/metadata.rs diff --git a/src/util.rs b/server/src/util.rs similarity index 100% rename from src/util.rs rename to server/src/util.rs diff --git a/src/web_util.rs b/server/src/web_util.rs similarity index 100% rename from src/web_util.rs rename to server/src/web_util.rs