diff options
author | Vika <vika@fireburn.ru> | 2025-04-09 23:31:02 +0300 |
---|---|---|
committer | Vika <vika@fireburn.ru> | 2025-04-09 23:31:57 +0300 |
commit | 8826d9446e6c492db2243b9921e59ce496027bef (patch) | |
tree | 63738aa9001cb73b11cb0e974e93129bcdf1adbb /src/login.rs | |
parent | 519cadfbb298f50cbf819dde757037ab56e2863e (diff) | |
download | kittybox-8826d9446e6c492db2243b9921e59ce496027bef.tar.zst |
cargo fmt
Change-Id: I80e81ebba3f0cdf8c094451c9fe3ee4126b8c888
Diffstat (limited to 'src/login.rs')
-rw-r--r-- | src/login.rs | 266 |
1 files changed, 189 insertions, 77 deletions
diff --git a/src/login.rs b/src/login.rs index eaa787c..3038d9c 100644 --- a/src/login.rs +++ b/src/login.rs @@ -1,10 +1,25 @@ use std::{borrow::Cow, str::FromStr}; +use axum::{ + extract::{FromRef, Query, State}, + http::HeaderValue, + response::IntoResponse, + Form, +}; +use axum_extra::{ + extract::{ + cookie::{self, Cookie}, + Host, SignedCookieJar, + }, + headers::HeaderMapExt, + TypedHeader, +}; use futures_util::FutureExt; -use axum::{extract::{FromRef, Query, State}, http::HeaderValue, response::IntoResponse, Form}; -use axum_extra::{extract::{Host, cookie::{self, Cookie}, SignedCookieJar}, headers::HeaderMapExt, TypedHeader}; -use hyper::{header::{CACHE_CONTROL, LOCATION}, StatusCode}; -use kittybox_frontend_renderer::{Template, LoginPage, LogoutPage}; +use hyper::{ + header::{CACHE_CONTROL, LOCATION}, + StatusCode, +}; +use kittybox_frontend_renderer::{LoginPage, LogoutPage, Template}; use kittybox_indieauth::{AuthorizationResponse, Error, GrantType, PKCEVerifier, Scope, Scopes}; use sha2::Digest; @@ -13,14 +28,13 @@ use crate::database::Storage; /// Show a login page. async fn get<S: Storage + Send + Sync + 'static>( State(db): State<S>, - Host(host): Host + Host(host): Host, ) -> impl axum::response::IntoResponse { let hcard_url: url::Url = format!("https://{}/", host).parse().unwrap(); let (blogname, channels) = tokio::join!( db.get_setting::<crate::database::settings::SiteName>(&hcard_url) - .map(Result::unwrap_or_default), - + .map(Result::unwrap_or_default), db.get_channels(&hcard_url).map(|i| i.unwrap_or_default()) ); ( @@ -34,14 +48,15 @@ async fn get<S: Storage + Send + Sync + 'static>( blog_name: blogname.as_ref(), feeds: channels, user: None, - content: LoginPage {}.to_string() - }.to_string() + content: LoginPage {}.to_string(), + } + .to_string(), ) } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] struct LoginForm { - url: url::Url + url: url::Url, } /// Accept login and start the IndieAuth dance. @@ -60,10 +75,12 @@ async fn post( .expires(None) .secure(true) .http_only(true) - .build() + .build(), ); - let client_id: url::Url = format!("https://{}/.kittybox/login/client_metadata", host).parse().unwrap(); + let client_id: url::Url = format!("https://{}/.kittybox/login/client_metadata", host) + .parse() + .unwrap(); let redirect_uri = { let mut uri = client_id.clone(); uri.set_path("/.kittybox/login/finish"); @@ -71,11 +88,15 @@ async fn post( }; let indieauth_state = kittybox_indieauth::AuthorizationRequest { response_type: kittybox_indieauth::ResponseType::Code, - client_id, redirect_uri, + client_id, + redirect_uri, state: kittybox_indieauth::State::new(), - code_challenge: kittybox_indieauth::PKCEChallenge::new(&code_verifier, kittybox_indieauth::PKCEMethod::S256), + code_challenge: kittybox_indieauth::PKCEChallenge::new( + &code_verifier, + kittybox_indieauth::PKCEMethod::S256, + ), scope: Some(Scopes::new(vec![Scope::Profile])), - me: Some(form.url.clone()) + me: Some(form.url.clone()), }; // Fetch the user's homepage, determine their authorization endpoint @@ -89,8 +110,9 @@ async fn post( tracing::error!("Error fetching homepage: {:?}", err); return ( StatusCode::BAD_REQUEST, - format!("couldn't fetch your homepage: {}", err) - ).into_response() + format!("couldn't fetch your homepage: {}", err), + ) + .into_response(); } }; @@ -106,22 +128,27 @@ async fn post( // .collect::<Vec<axum_extra::headers::Link>>(); // // todo!("parse Link: headers") - + let body = match response.text().await { Ok(body) => match microformats::from_html(&body, form.url) { Ok(mf2) => mf2, - Err(err) => return ( - StatusCode::BAD_REQUEST, - format!("error while parsing your homepage with mf2: {}", err) - ).into_response() + Err(err) => { + return ( + StatusCode::BAD_REQUEST, + format!("error while parsing your homepage with mf2: {}", err), + ) + .into_response() + } }, - Err(err) => return ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("error while fetching your homepage: {}", err) - ).into_response() + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("error while fetching your homepage: {}", err), + ) + .into_response() + } }; - let mut iss: Option<url::Url> = None; let mut authorization_endpoint = match body .rels @@ -139,10 +166,22 @@ async fn post( Ok(metadata) => { iss = Some(metadata.issuer); metadata.authorization_endpoint - }, - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't parse your oauth2 metadata: {}", err)).into_response() + } + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("couldn't parse your oauth2 metadata: {}", err), + ) + .into_response() + } }, - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't fetch your oauth2 metadata: {}", err)).into_response() + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("couldn't fetch your oauth2 metadata: {}", err), + ) + .into_response() + } }, None => match body .rels @@ -151,13 +190,17 @@ async fn post( .map(|v| v.as_slice()) .unwrap_or_default() .first() - .cloned() { - Some(authorization_endpoint) => authorization_endpoint, - None => return ( + .cloned() + { + Some(authorization_endpoint) => authorization_endpoint, + None => { + return ( StatusCode::BAD_REQUEST, - "no authorization endpoint was found on your homepage." - ).into_response() + "no authorization endpoint was found on your homepage.", + ) + .into_response() } + }, }; cookies = cookies.add( @@ -166,7 +209,7 @@ async fn post( .expires(None) .secure(true) .http_only(true) - .build() + .build(), ); if let Some(iss) = iss { @@ -176,7 +219,7 @@ async fn post( .expires(None) .secure(true) .http_only(true) - .build() + .build(), ); } @@ -186,7 +229,7 @@ async fn post( .expires(None) .secure(true) .http_only(true) - .build() + .build(), ); authorization_endpoint @@ -194,9 +237,12 @@ async fn post( .extend_pairs(indieauth_state.as_query_pairs().iter()); tracing::debug!("Forwarding user to {}", authorization_endpoint); - (StatusCode::FOUND, [ - ("Location", authorization_endpoint.to_string()), - ], cookies).into_response() + ( + StatusCode::FOUND, + [("Location", authorization_endpoint.to_string())], + cookies, + ) + .into_response() } /// Accept the return of the IndieAuth dance. Set a cookie for the @@ -208,7 +254,9 @@ async fn callback( State(http): State<reqwest_middleware::ClientWithMiddleware>, State(session_store): State<crate::SessionStore>, ) -> axum::response::Response { - let client_id: url::Url = format!("https://{}/.kittybox/login/client_metadata", host).parse().unwrap(); + let client_id: url::Url = format!("https://{}/.kittybox/login/client_metadata", host) + .parse() + .unwrap(); let redirect_uri = { let mut uri = client_id.clone(); uri.set_path("/.kittybox/login/finish"); @@ -218,7 +266,8 @@ async fn callback( let me: url::Url = cookie_jar.get("me").unwrap().value().parse().unwrap(); let code_verifier: PKCEVerifier = cookie_jar.get("code_verifier").unwrap().value().into(); - let authorization_endpoint: url::Url = cookie_jar.get("authorization_endpoint") + let authorization_endpoint: url::Url = cookie_jar + .get("authorization_endpoint") .and_then(|v| v.value().parse().ok()) .unwrap(); match cookie_jar.get("iss").and_then(|c| c.value().parse().ok()) { @@ -232,24 +281,59 @@ async fn callback( code: response.code, client_id, redirect_uri, - code_verifier, + code_verifier, }; - tracing::debug!("POSTing {:?} to authorization endpoint {}", grant_request, authorization_endpoint); - let res = match http.post(authorization_endpoint) + tracing::debug!( + "POSTing {:?} to authorization endpoint {}", + grant_request, + authorization_endpoint + ); + let res = match http + .post(authorization_endpoint) .form(&grant_request) .header(reqwest::header::ACCEPT, "application/json") .send() .await { - Ok(res) if res.status().is_success() => match res.json::<kittybox_indieauth::GrantResponse>().await { - Ok(grant) => grant, - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, [(CACHE_CONTROL, "no-store")], format!("error parsing authorization endpoint response: {}", err)).into_response() - }, + Ok(res) if res.status().is_success() => { + match res.json::<kittybox_indieauth::GrantResponse>().await { + Ok(grant) => grant, + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CACHE_CONTROL, "no-store")], + format!("error parsing authorization endpoint response: {}", err), + ) + .into_response() + } + } + } Ok(res) => match res.json::<Error>().await { - Ok(err) => return (StatusCode::BAD_REQUEST, [(CACHE_CONTROL, "no-store")], err.to_string()).into_response(), - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, [(CACHE_CONTROL, "no-store")], format!("error parsing indieauth error: {}", err)).into_response() + Ok(err) => { + return ( + StatusCode::BAD_REQUEST, + [(CACHE_CONTROL, "no-store")], + err.to_string(), + ) + .into_response() + } + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CACHE_CONTROL, "no-store")], + format!("error parsing indieauth error: {}", err), + ) + .into_response() + } + }, + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CACHE_CONTROL, "no-store")], + format!("error redeeming authorization code: {}", err), + ) + .into_response() } - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, [(CACHE_CONTROL, "no-store")], format!("error redeeming authorization code: {}", err)).into_response() }; let profile = match res { @@ -265,19 +349,28 @@ async fn callback( let uuid = uuid::Uuid::new_v4(); session_store.write().await.insert(uuid, session); let cookies = cookie_jar - .add(Cookie::build(("session_id", uuid.to_string())) - .expires(None) - .secure(true) - .http_only(true) - .path("/") - .build() + .add( + Cookie::build(("session_id", uuid.to_string())) + .expires(None) + .secure(true) + .http_only(true) + .path("/") + .build(), ) .remove("authorization_endpoint") .remove("me") .remove("iss") .remove("code_verifier"); - (StatusCode::FOUND, [(LOCATION, HeaderValue::from_static("/")), (CACHE_CONTROL, HeaderValue::from_static("no-store"))], dbg!(cookies)).into_response() + ( + StatusCode::FOUND, + [ + (LOCATION, HeaderValue::from_static("/")), + (CACHE_CONTROL, HeaderValue::from_static("no-store")), + ], + dbg!(cookies), + ) + .into_response() } /// Show the form necessary for logout. If JS is enabled, @@ -288,32 +381,42 @@ async fn callback( /// stupid enough to execute JS and send a POST request though, that's /// on the crawler. async fn logout_page() -> impl axum::response::IntoResponse { - (StatusCode::OK, [("Content-Type", "text/html")], Template { - title: "Signing out...", - blog_name: "Kittybox", - feeds: vec![], - user: None, - content: LogoutPage {}.to_string() - }.to_string()) + ( + StatusCode::OK, + [("Content-Type", "text/html")], + Template { + title: "Signing out...", + blog_name: "Kittybox", + feeds: vec![], + user: None, + content: LogoutPage {}.to_string(), + } + .to_string(), + ) } /// Erase the necessary cookies for login and invalidate the session. async fn logout( mut cookies: SignedCookieJar, - State(session_store): State<crate::SessionStore> -) -> (StatusCode, [(&'static str, &'static str); 1], SignedCookieJar) { - if let Some(id) = cookies.get("session_id") + State(session_store): State<crate::SessionStore>, +) -> ( + StatusCode, + [(&'static str, &'static str); 1], + SignedCookieJar, +) { + if let Some(id) = cookies + .get("session_id") .and_then(|c| uuid::Uuid::parse_str(c.value_trimmed()).ok()) { session_store.write().await.remove(&id); } - cookies = cookies.remove("me") + cookies = cookies + .remove("me") .remove("iss") .remove("authorization_endpoint") .remove("code_verifier") .remove("session_id"); - (StatusCode::FOUND, [("Location", "/")], cookies) } @@ -343,7 +446,7 @@ async fn client_metadata<S: Storage + Send + Sync + 'static>( }; if let Some(cached) = cached { if cached.precondition_passes(&etag) { - return StatusCode::NOT_MODIFIED.into_response() + return StatusCode::NOT_MODIFIED.into_response(); } } let client_uri: url::Url = format!("https://{}/", host).parse().unwrap(); @@ -356,7 +459,13 @@ async fn client_metadata<S: Storage + Send + Sync + 'static>( let mut metadata = kittybox_indieauth::ClientMetadata::new(client_id, client_uri).unwrap(); - metadata.client_name = Some(storage.get_setting::<crate::database::settings::SiteName>(&metadata.client_uri).await.unwrap_or_default().0); + metadata.client_name = Some( + storage + .get_setting::<crate::database::settings::SiteName>(&metadata.client_uri) + .await + .unwrap_or_default() + .0, + ); metadata.grant_types = Some(vec![GrantType::AuthorizationCode]); // We don't request anything more than the profile scope. metadata.scope = Some(Scopes::new(vec![Scope::Profile])); @@ -368,15 +477,18 @@ async fn client_metadata<S: Storage + Send + Sync + 'static>( // identity providers, or json to match newest spec let mut response = metadata.into_response(); // Indicate to upstream caches this endpoint does different things depending on the Accept: header. - response.headers_mut().append("Vary", HeaderValue::from_static("Accept")); + response + .headers_mut() + .append("Vary", HeaderValue::from_static("Accept")); // Cache this metadata for an hour. - response.headers_mut().append("Cache-Control", HeaderValue::from_static("max-age=600")); + response + .headers_mut() + .append("Cache-Control", HeaderValue::from_static("max-age=600")); response.headers_mut().typed_insert(etag); response } - /// Produce a router for all of the above. pub fn router<St, S>() -> axum::routing::Router<St> where |