From c02037628e36e71e528b3c76ffb88dc103d73228 Mon Sep 17 00:00:00 2001 From: Vika Date: Mon, 6 Dec 2021 20:49:58 +0300 Subject: Make rustfmt and clippy happy --- src/frontend/login.rs | 235 +++++++++++++++++++++++++++++++------------------- 1 file changed, 147 insertions(+), 88 deletions(-) (limited to 'src/frontend/login.rs') diff --git a/src/frontend/login.rs b/src/frontend/login.rs index 09fa75f..1c7c662 100644 --- a/src/frontend/login.rs +++ b/src/frontend/login.rs @@ -1,15 +1,15 @@ -use std::convert::TryInto; -use std::str::FromStr; +use http_types::Mime; use log::{debug, error}; use rand::Rng; -use http_types::Mime; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::convert::TryInto; +use std::str::FromStr; use tide::{Request, Response, Result}; -use serde::{Serialize, Deserialize}; -use sha2::{Sha256, Digest}; -use crate::frontend::{FrontendError, IndiewebEndpoints}; -use crate::{ApplicationState, database::Storage}; use crate::frontend::templates::Template; +use crate::frontend::{FrontendError, IndiewebEndpoints}; +use crate::{database::Storage, ApplicationState}; markup::define! { LoginPage { @@ -34,30 +34,36 @@ pub async fn form(req: Request>) -> Result { let storage = &req.state().storage; let authorization_endpoint = req.state().authorization_endpoint.to_string(); let token_endpoint = req.state().token_endpoint.to_string(); - let blog_name = storage.get_setting("site_name", &owner).await.unwrap_or_else(|_| "Kitty Box!".to_string()); + let blog_name = storage + .get_setting("site_name", &owner) + .await + .unwrap_or_else(|_| "Kitty Box!".to_string()); let feeds = storage.get_channels(&owner).await.unwrap_or_default(); Ok(Response::builder(200) - .body(Template { - title: "Sign in with IndieAuth", - blog_name: &blog_name, - endpoints: IndiewebEndpoints { - authorization_endpoint, - token_endpoint, - webmention: None, - microsub: None - }, - feeds, - user: req.session().get("user"), - content: LoginPage {}.to_string(), - }.to_string()) - .content_type("text/html; charset=utf-8") - .build()) + .body( + Template { + title: "Sign in with IndieAuth", + blog_name: &blog_name, + endpoints: IndiewebEndpoints { + authorization_endpoint, + token_endpoint, + webmention: None, + microsub: None, + }, + feeds, + user: req.session().get("user"), + content: LoginPage {}.to_string(), + } + .to_string(), + ) + .content_type("text/html; charset=utf-8") + .build()) } #[derive(Serialize, Deserialize)] struct LoginForm { - url: String + url: String, } #[derive(Serialize, Deserialize)] @@ -67,21 +73,19 @@ struct IndieAuthClientState { /// The user's initial "me" value. me: String, /// Authorization endpoint used. - authorization_endpoint: String + authorization_endpoint: String, } - #[derive(Serialize, Deserialize)] struct IndieAuthRequestParams { response_type: String, // can only have "code". TODO make an enum client_id: String, // always a URL. TODO consider making a URL redirect_uri: surf::Url, // callback URI for IndieAuth - state: String, // CSRF protection, should include randomness and be passed through - code_challenge: String, // base64-encoded PKCE challenge + state: String, // CSRF protection, should include randomness and be passed through + code_challenge: String, // base64-encoded PKCE challenge code_challenge_method: String, // usually "S256". TODO make an enum - scope: Option, // oAuth2 scopes to grant, - me: surf::Url, // User's entered profile URL - + scope: Option, // oAuth2 scopes to grant, + me: surf::Url, // User's entered profile URL } /// Handle login requests. Find the IndieAuth authorization endpoint and redirect to it. @@ -91,16 +95,22 @@ pub async fn handler(mut req: Request>) -> Resul return Err(FrontendError::with_code(400, "Use the login form, Luke.").into()); } if content_type.unwrap() != Mime::from_str("application/x-www-form-urlencoded").unwrap() { - return Err(FrontendError::with_code(400, "Login form results must be a urlencoded form").into()); + return Err( + FrontendError::with_code(400, "Login form results must be a urlencoded form").into(), + ); } let form = req.body_form::().await?; // FIXME check if it returns 400 or 500 on error let homepage_uri = surf::Url::parse(&form.url)?; let http = &req.state().http_client; - + let mut fetch_response = http.get(&homepage_uri).send().await?; if fetch_response.status() != 200 { - return Err(FrontendError::with_code(500, "Error fetching your authorization endpoint. Check if your website's okay.").into()); + return Err(FrontendError::with_code( + 500, + "Error fetching your authorization endpoint. Check if your website's okay.", + ) + .into()); } let mut authorization_endpoint: Option = None; @@ -123,14 +133,18 @@ pub async fn handler(mut req: Request>) -> Resul || trimmed == "rel=authorization_endpoint" { if let Ok(endpoint) = homepage_uri.join(uri) { - debug!("Found authorization endpoint {} for user {}", endpoint, homepage_uri.as_str()); + debug!( + "Found authorization endpoint {} for user {}", + endpoint, + homepage_uri.as_str() + ); authorization_endpoint = Some(endpoint); break; } } } } - }, + } None => continue, } } @@ -139,64 +153,93 @@ pub async fn handler(mut req: Request>) -> Resul // bring out the big guns and parse HTML to find it. if authorization_endpoint.is_none() { let body = fetch_response.body_string().await?; - let pattern = easy_scraper::Pattern::new(r#""#) - .expect("Cannot parse the pattern for authorization_endpoint"); + let pattern = + easy_scraper::Pattern::new(r#""#) + .expect("Cannot parse the pattern for authorization_endpoint"); let matches = pattern.matches(&body); debug!("Matches for authorization_endpoint in HTML: {:?}", matches); if !matches.is_empty() { if let Ok(endpoint) = homepage_uri.join(&matches[0]["url"]) { - debug!("Found authorization endpoint {} for user {}", endpoint, homepage_uri.as_str()); + debug!( + "Found authorization endpoint {} for user {}", + endpoint, + homepage_uri.as_str() + ); authorization_endpoint = Some(endpoint) } } }; // If even after this the authorization endpoint is still not found, bail out. if authorization_endpoint.is_none() { - error!("Couldn't find authorization_endpoint for {}", homepage_uri.as_str()); - return Err(FrontendError::with_code(400, "Your website doesn't support the IndieAuth protocol.").into()); + error!( + "Couldn't find authorization_endpoint for {}", + homepage_uri.as_str() + ); + return Err(FrontendError::with_code( + 400, + "Your website doesn't support the IndieAuth protocol.", + ) + .into()); } let mut authorization_endpoint: surf::Url = authorization_endpoint.unwrap(); let mut rng = rand::thread_rng(); - let state: String = data_encoding::BASE64URL.encode(serde_urlencoded::to_string(IndieAuthClientState { - nonce: (0..8).map(|_| { + let state: String = data_encoding::BASE64URL.encode( + serde_urlencoded::to_string(IndieAuthClientState { + nonce: (0..8) + .map(|_| { + let idx = rng.gen_range(0..INDIEAUTH_PKCE_CHARSET.len()); + INDIEAUTH_PKCE_CHARSET[idx] as char + }) + .collect(), + me: homepage_uri.to_string(), + authorization_endpoint: authorization_endpoint.to_string(), + })? + .as_bytes(), + ); + // PKCE code generation + let code_verifier: String = (0..128) + .map(|_| { let idx = rng.gen_range(0..INDIEAUTH_PKCE_CHARSET.len()); INDIEAUTH_PKCE_CHARSET[idx] as char - }).collect(), - me: homepage_uri.to_string(), - authorization_endpoint: authorization_endpoint.to_string() - })?.as_bytes()); - // PKCE code generation - let code_verifier: String = (0..128).map(|_| { - let idx = rng.gen_range(0..INDIEAUTH_PKCE_CHARSET.len()); - INDIEAUTH_PKCE_CHARSET[idx] as char - }).collect(); + }) + .collect(); let mut hasher = Sha256::new(); hasher.update(code_verifier.as_bytes()); let code_challenge: String = data_encoding::BASE64URL.encode(&hasher.finalize()); - authorization_endpoint.set_query(Some(&serde_urlencoded::to_string(IndieAuthRequestParams { - response_type: "code".to_string(), - client_id: req.url().origin().ascii_serialization(), - redirect_uri: req.url().join("login/callback")?, - state: state.clone(), code_challenge, - code_challenge_method: "S256".to_string(), - scope: Some("profile".to_string()), - me: homepage_uri, - })?)); + authorization_endpoint.set_query(Some(&serde_urlencoded::to_string( + IndieAuthRequestParams { + response_type: "code".to_string(), + client_id: req.url().origin().ascii_serialization(), + redirect_uri: req.url().join("login/callback")?, + state: state.clone(), + code_challenge, + code_challenge_method: "S256".to_string(), + scope: Some("profile".to_string()), + me: homepage_uri, + }, + )?)); let cookies = vec![ - format!(r#"indieauth_state="{}"; Same-Site: None; Secure; Max-Age: 600"#, state), - format!(r#"indieauth_code_verifier="{}"; Same-Site: None; Secure; Max-Age: 600"#, code_verifier) + format!( + r#"indieauth_state="{}"; Same-Site: None; Secure; Max-Age: 600"#, + state + ), + format!( + r#"indieauth_code_verifier="{}"; Same-Site: None; Secure; Max-Age: 600"#, + code_verifier + ), ]; - let cookie_header = cookies.iter().map(|i| -> http_types::headers::HeaderValue { - (i as &str).try_into().unwrap() - }).collect::>(); - + let cookie_header = cookies + .iter() + .map(|i| -> http_types::headers::HeaderValue { (i as &str).try_into().unwrap() }) + .collect::>(); + Ok(Response::builder(302) - .header("Location", authorization_endpoint.to_string()) - .header("Set-Cookie", &*cookie_header) - .build()) + .header("Location", authorization_endpoint.to_string()) + .header("Set-Cookie", &*cookie_header) + .build()) } const INDIEAUTH_PKCE_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ @@ -211,12 +254,12 @@ struct IndieAuthCallbackResponse { #[allow(dead_code)] error_uri: Option, // This needs to be further decoded to receive state back and will always be present - state: String + state: String, } impl IndieAuthCallbackResponse { fn is_successful(&self) -> bool { - !self.code.is_none() + self.code.is_some() } } @@ -226,7 +269,7 @@ struct IndieAuthCodeRedeem { code: String, client_id: String, redirect_uri: String, - code_verifier: String + code_verifier: String, } #[derive(Serialize, Deserialize)] @@ -234,7 +277,7 @@ struct IndieWebProfile { name: Option, url: Option, email: Option, - photo: Option + photo: Option, } #[derive(Serialize, Deserialize)] @@ -243,7 +286,7 @@ struct IndieAuthResponse { scope: Option, access_token: Option, token_type: Option, - profile: Option + profile: Option, } /// Handle IndieAuth parameters, fetch the final h-card and redirect the user to the homepage. @@ -252,41 +295,57 @@ pub async fn callback(mut req: Request>) -> Resu let http: &surf::Client = &req.state().http_client; let origin = req.url().origin().ascii_serialization(); - if req.cookie("indieauth_state").unwrap().value() != ¶ms.state { + if req.cookie("indieauth_state").unwrap().value() != params.state { return Err(FrontendError::with_code(400, "The state doesn't match. A possible CSRF attack was prevented. Please try again later.").into()); } - let state: IndieAuthClientState = serde_urlencoded::from_bytes( - &data_encoding::BASE64URL.decode(params.state.as_bytes())? - )?; + let state: IndieAuthClientState = + serde_urlencoded::from_bytes(&data_encoding::BASE64URL.decode(params.state.as_bytes())?)?; if !params.is_successful() { - return Err(FrontendError::with_code(400, &format!("The authorization endpoint indicated a following error: {:?}: {:?}", ¶ms.error, ¶ms.error_description)).into()) + return Err(FrontendError::with_code( + 400, + &format!( + "The authorization endpoint indicated a following error: {:?}: {:?}", + ¶ms.error, ¶ms.error_description + ), + ) + .into()); } let authorization_endpoint = surf::Url::parse(&state.authorization_endpoint).unwrap(); - let mut code_response = http.post(authorization_endpoint) + let mut code_response = http + .post(authorization_endpoint) .body_string(serde_urlencoded::to_string(IndieAuthCodeRedeem { grant_type: "authorization_code".to_string(), code: params.code.unwrap().to_string(), client_id: origin.to_string(), redirect_uri: origin + "/login/callback", - code_verifier: req.cookie("indieauth_code_verifier").unwrap().value().to_string() + code_verifier: req + .cookie("indieauth_code_verifier") + .unwrap() + .value() + .to_string(), })?) .header("Content-Type", "application/x-www-form-urlencoded") .header("Accept", "application/json") - .send().await?; + .send() + .await?; if code_response.status() != 200 { - return Err(FrontendError::with_code(code_response.status(), &format!("Authorization endpoint returned an error when redeeming the code: {}", code_response.body_string().await?)).into()); + return Err(FrontendError::with_code( + code_response.status(), + &format!( + "Authorization endpoint returned an error when redeeming the code: {}", + code_response.body_string().await? + ), + ) + .into()); } let json: IndieAuthResponse = code_response.body_json().await?; - drop(http); let session = req.session_mut(); session.insert("user", &json.me)?; // TODO redirect to the page user came from - Ok(Response::builder(302) - .header("Location", "/") - .build()) + Ok(Response::builder(302).header("Location", "/").build()) } -- cgit 1.4.1