diff options
Diffstat (limited to 'src/frontend')
-rw-r--r-- | src/frontend/login.rs | 235 | ||||
-rw-r--r-- | src/frontend/mod.rs | 11 |
2 files changed, 154 insertions, 92 deletions
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<S: Storage>(req: Request<ApplicationState<S>>) -> 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<String>, // oAuth2 scopes to grant, - me: surf::Url, // User's entered profile URL - + scope: Option<String>, // 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<S: Storage>(mut req: Request<ApplicationState<S>>) -> 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::<LoginForm>().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<surf::Url> = None; @@ -123,14 +133,18 @@ pub async fn handler<S: Storage>(mut req: Request<ApplicationState<S>>) -> 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<S: Storage>(mut req: Request<ApplicationState<S>>) -> 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#"<link rel="authorization_endpoint" href="{{url}}">"#) - .expect("Cannot parse the pattern for authorization_endpoint"); + let pattern = + easy_scraper::Pattern::new(r#"<link rel="authorization_endpoint" href="{{url}}">"#) + .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::<Vec<_>>(); - + let cookie_header = cookies + .iter() + .map(|i| -> http_types::headers::HeaderValue { (i as &str).try_into().unwrap() }) + .collect::<Vec<_>>(); + 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<String>, // 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<String>, url: Option<String>, email: Option<String>, - photo: Option<String> + photo: Option<String>, } #[derive(Serialize, Deserialize)] @@ -243,7 +286,7 @@ struct IndieAuthResponse { scope: Option<String>, access_token: Option<String>, token_type: Option<String>, - profile: Option<IndieWebProfile> + profile: Option<IndieWebProfile>, } /// Handle IndieAuth parameters, fetch the final h-card and redirect the user to the homepage. @@ -252,41 +295,57 @@ pub async fn callback<S: Storage>(mut req: Request<ApplicationState<S>>) -> 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()) } diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index 76114c5..c0452f3 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -33,11 +33,14 @@ struct FrontendError { code: StatusCode, } impl FrontendError { - pub fn with_code<C>(code: C, msg: &str) -> Self where C: TryInto<StatusCode> { + pub fn with_code<C>(code: C, msg: &str) -> Self + where + C: TryInto<StatusCode>, + { Self { msg: msg.to_string(), source: None, - code: code.try_into().unwrap_or_else(|_| StatusCode::InternalServerError), + code: code.try_into().unwrap_or(StatusCode::InternalServerError), } } pub fn msg(&self) -> &str { @@ -188,7 +191,7 @@ pub async fn onboarding_receiver<S: Storage>(mut req: Request<ApplicationState<S log::debug!("Creating feeds..."); for feed in body.feeds { - if &feed.name == "" || &feed.slug == "" { + if feed.name.is_empty() || feed.slug.is_empty() { continue; }; log::debug!("Creating feed {} with slug {}", &feed.name, &feed.slug); @@ -408,7 +411,7 @@ where ) -> Result { let authorization_endpoint = request.state().authorization_endpoint.to_string(); let token_endpoint = request.state().token_endpoint.to_string(); - let owner = request.url().origin().ascii_serialization().clone() + "/"; + let owner = request.url().origin().ascii_serialization() + "/"; let site_name = &request .state() .storage |