This commit is contained in:
nora 2025-08-02 22:28:42 +02:00
parent 27c8420d61
commit 5cf3f69553
3 changed files with 79 additions and 78 deletions

15
Cargo.lock generated
View file

@ -71,6 +71,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
dependencies = [ dependencies = [
"axum-core", "axum-core",
"axum-macros",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
@ -115,6 +116,17 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "axum-macros"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.75" version = "0.3.75"
@ -1670,6 +1682,7 @@ dependencies = [
"tower", "tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
@ -1792,6 +1805,8 @@ dependencies = [
"rand_core", "rand_core",
"subtle", "subtle",
"tokio", "tokio",
"tower",
"tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]

View file

@ -8,13 +8,7 @@ opt-level = "s"
lto = "thin" lto = "thin"
[dependencies] [dependencies]
axum = { version = "0.8.4", default-features = false, features = [ axum = { version = "0.8.4", default-features = false, features = ["http2", "macros", "multipart", "tokio", "tower-log", "tracing"] }
"http2",
"multipart",
"tokio",
"tower-log",
"tracing",
] }
base64 = "0.22.1" base64 = "0.22.1"
bs58 = "0.5.1" bs58 = "0.5.1"
color-eyre = "0.6.5" color-eyre = "0.6.5"
@ -22,5 +16,7 @@ object_store = { version = "0.12.3", default-features = false, features = ["aws"
rand_core = { version = "0.9.3", features = ["os_rng"] } rand_core = { version = "0.9.3", features = ["os_rng"] }
subtle = { version = "2.6.1", default-features = false } subtle = { version = "2.6.1", default-features = false }
tokio = { version = "1.47.1", features = ["macros", "rt", "net"] } tokio = { version = "1.47.1", features = ["macros", "rt", "net"] }
tower = "0.5.2"
tower-http = { version = "0.6.6", features = ["trace"] }
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }

View file

@ -1,7 +1,8 @@
use axum::{ use axum::{
body::Bytes, body::Bytes,
extract::{DefaultBodyLimit, FromRequestParts, Multipart, State}, extract::{DefaultBodyLimit, Multipart, Request, State},
http::{header, StatusCode}, http::{header, StatusCode},
middleware::Next,
response::{Html, IntoResponse, Redirect, Response}, response::{Html, IntoResponse, Redirect, Response},
routing::{get, post}, routing::{get, post},
Router, Router,
@ -11,6 +12,7 @@ use color_eyre::eyre::{self, bail, Context};
use color_eyre::{eyre::OptionExt, Result}; use color_eyre::{eyre::OptionExt, Result};
use object_store::ObjectStore; use object_store::ObjectStore;
use rand_core::TryRngCore; use rand_core::TryRngCore;
use tower::ServiceBuilder;
use tracing::{error, info, level_filters::LevelFilter}; use tracing::{error, info, level_filters::LevelFilter};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
@ -48,16 +50,23 @@ async fn main() -> Result<()> {
.build() .build()
.wrap_err("failed to build client")?; .wrap_err("failed to build client")?;
let app = Router::new() let state = Config {
.route("/", get(index))
.route("/", post(upload))
.with_state(Config {
username, username,
password, password,
s3_client, s3_client,
}) };
let app = Router::new()
.route("/", get(index))
.route("/", post(upload))
.with_state(state.clone())
.layer(
ServiceBuilder::new()
.layer(tower_http::trace::TraceLayer::new_for_http())
// raise limit to 100MB // raise limit to 100MB
.layer(DefaultBodyLimit::max(100_000_000)); .layer(DefaultBodyLimit::max(100_000_000))
.layer(axum::middleware::from_fn_with_state(state, auth_middleware)),
);
let addr = "0.0.0.0:3050"; let addr = "0.0.0.0:3050";
let listener = tokio::net::TcpListener::bind(addr) let listener = tokio::net::TcpListener::bind(addr)
@ -68,23 +77,11 @@ async fn main() -> Result<()> {
axum::serve(listener, app).await.wrap_err("failed to serve") axum::serve(listener, app).await.wrap_err("failed to serve")
} }
// todo: use middleware async fn index() -> impl IntoResponse {
async fn index(_: Auth) -> impl IntoResponse {
Html(include_str!("../index.html")) Html(include_str!("../index.html"))
} }
async fn upload( async fn upload(State(config): State<Config>, multipart: Multipart) -> Result<Response, Response> {
auth: Auth,
State(config): State<Config>,
multipart: Multipart,
) -> Result<Response, Response> {
if auth.username != config.username {
return Err(reject_auth("invalid username"));
}
if subtle::ConstantTimeEq::ct_ne(auth.password.as_bytes(), config.password.as_bytes()).into() {
return Err(reject_auth("invalid password"));
}
let req = parse_req(multipart).await.map_err(|err| { let req = parse_req(multipart).await.map_err(|err| {
info!(?err, "Bad request for upload"); info!(?err, "Bad request for upload");
(StatusCode::BAD_REQUEST, err.to_string()).into_response() (StatusCode::BAD_REQUEST, err.to_string()).into_response()
@ -203,31 +200,16 @@ async fn parse_req(mut multipart: Multipart) -> Result<UploadRequest> {
}) })
} }
struct Auth { #[axum::debug_middleware]
username: String, async fn auth_middleware(State(config): State<Config>, request: Request, next: Next) -> Response {
password: String, match check_auth(config, request).await {
Ok(request) => next.run(request).await,
Err(err) => err,
}
} }
fn reject_auth(reason: &str) -> Response { async fn check_auth(config: Config, request: Request) -> Result<Request, Response> {
info!("Rejecting request authentication due to {reason}"); let Some(header) = request.headers().get(header::AUTHORIZATION) else {
(
StatusCode::UNAUTHORIZED,
[(
header::WWW_AUTHENTICATE,
"Basic realm=\"upload.files.noratrieb.dev\"",
)],
)
.into_response()
}
impl FromRequestParts<Config> for Auth {
type Rejection = Response;
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
config: &Config,
) -> Result<Self, Self::Rejection> {
let Some(header) = parts.headers.get(header::AUTHORIZATION) else {
return Err(reject_auth("missing authorization header")); return Err(reject_auth("missing authorization header"));
}; };
@ -259,9 +241,17 @@ impl FromRequestParts<Config> for Auth {
return Err(reject_auth("invalid password")); return Err(reject_auth("invalid password"));
} }
Ok(Auth { Ok(request)
username: username.to_owned(), }
password: password.to_owned(),
}) fn reject_auth(reason: &str) -> Response {
} info!("Rejecting request authentication due to {reason}");
(
StatusCode::UNAUTHORIZED,
[(
header::WWW_AUTHENTICATE,
"Basic realm=\"upload.files.noratrieb.dev\"",
)],
)
.into_response()
} }