From fcd4b4538a7692fd042a2ab3de53c05067a3443e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arma=C3=ABl=20Gu=C3=A9neau?= Date: Thu, 2 Jan 2025 11:54:29 +0100 Subject: [PATCH] web framework: switch to axum --- Cargo.lock | 230 ++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- README.md | 1 + src/main.rs | 141 ++++++++++++++++-------------- src/workers.rs | 2 +- 5 files changed, 182 insertions(+), 194 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6297ef..b4916ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "actix-codec" @@ -55,14 +55,11 @@ dependencies = [ "ahash", "base64 0.22.1", "bitflags 2.6.0", - "brotli", "bytes", "bytestring", "derive_more", "encoding_rs", - "flate2", "futures-core", - "h2 0.3.26", "http 0.2.12", "httparse", "httpdate", @@ -78,17 +75,6 @@ dependencies = [ "tokio", "tokio-util", "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", ] [[package]] @@ -100,7 +86,6 @@ dependencies = [ "bytestring", "cfg-if", "http 0.2.12", - "regex", "regex-lite", "serde", "tracing", @@ -162,18 +147,15 @@ checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" dependencies = [ "actix-codec", "actix-http", - "actix-macros", "actix-router", "actix-rt", "actix-server", "actix-service", "actix-utils", - "actix-web-codegen", "ahash", "bytes", "bytestring", "cfg-if", - "cookie", "derive_more", "encoding_rs", "futures-core", @@ -185,7 +167,6 @@ dependencies = [ "mime", "once_cell", "pin-project-lite", - "regex", "regex-lite", "serde", "serde_json", @@ -196,18 +177,6 @@ dependencies = [ "url", ] -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "addr2line" version = "0.24.2" @@ -245,21 +214,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -673,6 +627,60 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +dependencies = [ + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -743,27 +751,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "brotli" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bstr" version = "1.11.1" @@ -817,8 +804,6 @@ version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ - "jobserver", - "libc", "shlex", ] @@ -884,17 +869,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -1151,16 +1125,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "flate2" -version = "1.0.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1212,10 +1176,10 @@ name = "forgery" version = "0.1.0" dependencies = [ "actix-files", - "actix-web", "anyhow", "aws-config", "aws-sdk-s3", + "axum", "forgejo-api", "include_dir", "lazy_static", @@ -1534,6 +1498,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1847,15 +1812,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.76" @@ -1970,6 +1926,12 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -2655,6 +2617,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" version = "1.0.18" @@ -2770,6 +2738,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3184,6 +3162,28 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -3802,31 +3802,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zstd" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index c9f420d..8293515 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ url = "2" anyhow = "1" serde_json = "1" rand = "0.8" -actix-web = "4" +axum = { version = "0.8", features = ["form"] } tera = "1" lazy_static = "1" actix-files = "0.6" diff --git a/README.md b/README.md index 653bd93..e50adc6 100644 --- a/README.md +++ b/README.md @@ -57,4 +57,5 @@ Environment variables read when `STORAGE_BACKEND=s3`: could not be locked, but delete the account after the grace period even if the email could not be sent…) - auth: add support for connecting to the forge using oauth? +- allow customizing the address & port on which to listen - improve error handling diff --git a/src/main.rs b/src/main.rs index 59395b7..9942761 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,12 @@ -use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer, Responder}; use anyhow::{anyhow, Context}; +use axum::{ + extract::{OriginalUri, Path, Query, State}, + http::{header, StatusCode}, + response::{Html, IntoResponse}, + routing::get, + Form, + Router, +}; use forgejo_api::{Auth, Forgejo}; use lazy_static::lazy_static; use rand::prelude::*; @@ -277,13 +284,12 @@ fn approx_score(score: f32) -> ApproxScore { } } -#[get("/")] -async fn index( - data: web::Data, - q: web::Query, - req: HttpRequest, -) -> impl Responder { - eprintln!("GET {}", req.uri()); +async fn get_index( + State(data): State>, + Query(q): Query, + OriginalUri(uri): OriginalUri, +) -> Html { + eprintln!("GET {}", uri); let db = &data.db; @@ -340,16 +346,16 @@ async fn index( eprintln!("rendering template..."); let page = TEMPLATES.render("index.html", &context).unwrap(); eprintln!("done"); - HttpResponse::Ok().body(page) + Html::from(page) } async fn post_classified( - data: web::Data, - form: web::Form>, - req: HttpRequest, + State(data): State>, + Form(form): Form>, + OriginalUri(uri): OriginalUri, overwrite: bool, -) -> impl Responder { - eprintln!("POST {}", req.uri()); +) -> Result { + eprintln!("POST {}", uri); let updates: Vec<(UserId, bool)> = form .iter() @@ -366,45 +372,34 @@ async fn post_classified( ) .await; - let res = storage::store_db(&data.storage, &data.db).await; + storage::store_db(&data.storage, &data.db).await?; eprintln!("done"); - match res { - Ok(()) => HttpResponse::SeeOther() - .insert_header(("Location", req.uri().to_string())) - .finish(), - Err(e) => { - HttpResponse::InternalServerError().body(format!("Internal server error:\n\n{e}")) - } - } + Ok((StatusCode::SEE_OTHER, [(header::LOCATION, uri.to_string())], ())) } -#[post("/")] async fn post_classified_index( - data: web::Data, - form: web::Form>, - req: HttpRequest, -) -> impl Responder { - post_classified(data, form, req, false).await + data: State>, + uri: OriginalUri, + form: Form>, +) -> impl IntoResponse { + post_classified(data, form, uri, false).await } -#[post("/classified")] async fn post_classified_edit( - data: web::Data, - form: web::Form>, - req: HttpRequest, -) -> impl Responder { - post_classified(data, form, req, true).await + data: State>, + uri: OriginalUri, + form: Form>, +) -> impl IntoResponse { + post_classified(data, form, uri, true).await } -#[get("/classified")] -async fn classified( - data: web::Data, - _q: web::Query, - req: HttpRequest, -) -> impl Responder { - eprintln!("GET {}", req.uri()); +async fn get_classified( + State(data): State>, + OriginalUri(uri): OriginalUri, +) -> Html { + eprintln!("GET {}", uri); let db = &data.db; let mut users: Vec<(UserId, UserData, f32, bool)> = db.with_userdb(|udb| { @@ -427,23 +422,21 @@ async fn classified( eprintln!("rendering template..."); let page = TEMPLATES.render("classified.html", &context).unwrap(); eprintln!("done"); - HttpResponse::Ok().body(page) + Html::from(page) } const STATIC_DIR: include_dir::Dir = include_dir::include_dir!("static"); -#[get("/static/{filename:.*}")] -async fn static_(req: HttpRequest) -> impl Responder { - eprintln!("GET {}", req.uri()); +async fn get_static_(Path(filename): Path, OriginalUri(uri): OriginalUri) -> impl IntoResponse { + eprintln!("GET {}", uri); - let path: String = req.match_info().query("filename").parse().unwrap(); - match STATIC_DIR.get_file(path) { - None => HttpResponse::NotFound().body("404 Not found"), - Some(page) => HttpResponse::Ok().body(page.contents()), + match STATIC_DIR.get_file(filename) { + None => (StatusCode::NOT_FOUND, "404 Not found").into_response(), + Some(page) => page.contents().into_response(), } } -#[actix_web::main] +#[tokio::main] async fn main() -> anyhow::Result<()> { eprintln!("Eval templates"); let _ = *TEMPLATES; @@ -455,7 +448,7 @@ async fn main() -> anyhow::Result<()> { eprintln!("Load users and repos"); let db = load_db(&storage, &forge).await?; - let st = web::Data::new(AppState { + let shared_state = Arc::new(AppState { db: db.clone(), storage: storage.clone(), forge: forge.clone(), @@ -489,17 +482,16 @@ async fn main() -> anyhow::Result<()> { println!("Listening on http://127.0.0.1:8080"); - let webserver = HttpServer::new(move || { - App::new() - .app_data(st.clone()) - .service(static_) - .service(index) - .service(classified) - .service(post_classified_index) - .service(post_classified_edit) - }) - .bind(("127.0.0.1", 8080))? - .run(); + let app = Router::new() + .route("/", get(get_index).post(post_classified_index)) + .route("/classified", get(get_classified).post(post_classified_edit)) + .route("/static/{*filename}", get(get_static_)) + .with_state(shared_state); + + let webserver = { + let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap(); + axum::serve(listener, app) + }; tokio::select! { _ = workers.join_all() => { @@ -514,3 +506,26 @@ async fn main() -> anyhow::Result<()> { Ok(()) } + +// setup to allow turning anyhow errors into axum "internal server error" responses + +struct AppError(anyhow::Error); + +impl IntoResponse for AppError { + fn into_response(self) -> axum::response::Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Internal server error:\n{}", self.0), + ) + .into_response() + } +} + +impl From for AppError +where + E: Into, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} diff --git a/src/workers.rs b/src/workers.rs index 223cbf1..4648d06 100644 --- a/src/workers.rs +++ b/src/workers.rs @@ -203,7 +203,7 @@ pub async fn try_lock_and_notify_user( let user = u.userdata(user_id).unwrap(); (user.login.clone(), user.email.clone(), u.is_spam(user_id)) }); - let is_spam = match is_spam{ + let is_spam = match is_spam { Some(IsSpam::Spam { classified_at, locked,