diff options
author | Vika <vika@fireburn.ru> | 2021-12-06 20:49:58 +0300 |
---|---|---|
committer | Vika <vika@fireburn.ru> | 2021-12-06 21:00:02 +0300 |
commit | c02037628e36e71e528b3c76ffb88dc103d73228 (patch) | |
tree | e0d78a61af2f6e1ca7effcca1722f0da31293c05 /src | |
parent | 9a9776230ce8d12d305ca8db19cc76f20ae40926 (diff) | |
download | kittybox-c02037628e36e71e528b3c76ffb88dc103d73228.tar.zst |
Make rustfmt and clippy happy
Diffstat (limited to 'src')
-rw-r--r-- | src/bin/kittybox_database_converter.rs | 4 | ||||
-rw-r--r-- | src/bin/pyindieblog_to_kittybox.rs | 2 | ||||
-rw-r--r-- | src/database/file/mod.rs | 24 | ||||
-rw-r--r-- | src/frontend/login.rs | 235 | ||||
-rw-r--r-- | src/frontend/mod.rs | 11 | ||||
-rw-r--r-- | src/lib.rs | 6 | ||||
-rw-r--r-- | src/main.rs | 2 | ||||
-rw-r--r-- | src/micropub/get.rs | 4 | ||||
-rw-r--r-- | src/micropub/post.rs | 15 |
9 files changed, 184 insertions, 119 deletions
diff --git a/src/bin/kittybox_database_converter.rs b/src/bin/kittybox_database_converter.rs index 4dcd2ab..bc355c9 100644 --- a/src/bin/kittybox_database_converter.rs +++ b/src/bin/kittybox_database_converter.rs @@ -58,7 +58,7 @@ async fn convert_from_redis<S: Storage>(from: String, new_storage: S) -> anyhow: Ok(settings) => { for (k, v) in settings.iter() { if let Err(e) = storage - .set_setting(k, &user, v) + .set_setting(k, user, v) .await .with_context(|| format!("Failed setting {} for {}", k, user)) { @@ -72,7 +72,7 @@ async fn convert_from_redis<S: Storage>(from: String, new_storage: S) -> anyhow: } } - return Ok(()); + Ok(()) } #[async_std::main] diff --git a/src/bin/pyindieblog_to_kittybox.rs b/src/bin/pyindieblog_to_kittybox.rs index 303ca56..38590c3 100644 --- a/src/bin/pyindieblog_to_kittybox.rs +++ b/src/bin/pyindieblog_to_kittybox.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Context, Result}; -use redis; + use redis::AsyncCommands; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/src/database/file/mod.rs b/src/database/file/mod.rs index d556f46..ee7d30b 100644 --- a/src/database/file/mod.rs +++ b/src/database/file/mod.rs @@ -88,7 +88,7 @@ mod tests { } fn url_to_path(root: &Path, url: &str) -> PathBuf { - url_to_relative_path(url).to_path(root).to_path_buf() + url_to_relative_path(url).to_path(root) } fn url_to_relative_path(url: &str) -> relative_path::RelativePathBuf { @@ -321,11 +321,13 @@ impl Storage for FileStorage { let orig = path.clone(); spawn_blocking::<_, Result<()>>(move || { // We're supposed to have a parent here. - let basedir = link.parent().ok_or(StorageError::new( - ErrorKind::Backend, - "Failed to calculate parent directory when creating a symlink", - ))?; - let relative = path_relative_from(&orig, &basedir).unwrap(); + let basedir = link.parent().ok_or_else(|| { + StorageError::new( + ErrorKind::Backend, + "Failed to calculate parent directory when creating a symlink", + ) + })?; + let relative = path_relative_from(&orig, basedir).unwrap(); println!("{:?} - {:?} = {:?}", &orig, &basedir, &relative); println!("Created a symlink at {:?}", &link); let symlink_result; @@ -374,7 +376,7 @@ impl Storage for FileStorage { let mut content = String::new(); guard.read_to_string(&mut content).await?; let mut channels: Vec<super::MicropubChannel>; - if content.len() > 0 { + if !content.is_empty() { channels = serde_json::from_str(&content)?; } else { channels = Vec::default(); @@ -385,7 +387,7 @@ impl Storage for FileStorage { name: post["properties"]["name"][0] .as_str() .map(|s| s.to_string()) - .unwrap_or_else(|| String::default()), + .unwrap_or_else(String::default), }); guard.seek(std::io::SeekFrom::Start(0)).await?; guard.set_len(0).await?; @@ -438,7 +440,7 @@ impl Storage for FileStorage { let mut content = String::new(); (&mut &*guard).read_to_string(&mut content).await?; // This should not happen, but if it does, let's handle it gracefully instead of failing. - if content.len() == 0 { + if content.is_empty() { return Ok(vec![]); } let channels: Vec<super::MicropubChannel> = serde_json::from_str(&content)?; @@ -541,7 +543,7 @@ impl Storage for FileStorage { // it might come with a performance hit and/or memory usage inflation settings .get(setting) - .map(|s| s.clone()) + .cloned() .ok_or_else(|| StorageError::new(ErrorKind::Backend, "Setting not set")) } @@ -572,7 +574,7 @@ impl Storage for FileStorage { log::debug!("Locked. Writing."); let mut content = String::new(); guard.read_to_string(&mut content).await?; - let mut settings: HashMap<String, String> = if content.len() == 0 { + let mut settings: HashMap<String, String> = if content.is_empty() { HashMap::default() } else { serde_json::from_str(&content)? 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 diff --git a/src/lib.rs b/src/lib.rs index eb915c2..2b4d1cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,10 +75,10 @@ where app.with( tide::sessions::SessionMiddleware::new( tide::sessions::CookieStore::new(), - &app.state().cookie_secret.as_bytes() + app.state().cookie_secret.as_bytes(), ) - .with_cookie_name("kittybox_session") - .without_save_unchanged() + .with_cookie_name("kittybox_session") + .without_save_unchanged(), ); app } diff --git a/src/main.rs b/src/main.rs index 4f5f9ed..79e0cf5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,7 +63,7 @@ async fn main() -> Result<(), std::io::Error> { let cookie_secret: String = match env::var("COOKIE_SECRET").ok() { Some(value) => value, None => { - if let Some(filename) = env::var("COOKIE_SECRET_FILE").ok() { + if let Ok(filename) = env::var("COOKIE_SECRET_FILE") { use async_std::io::ReadExt; let mut file = async_std::fs::File::open(filename).await?; diff --git a/src/micropub/get.rs b/src/micropub/get.rs index 9732281..718714a 100644 --- a/src/micropub/get.rs +++ b/src/micropub/get.rs @@ -24,7 +24,7 @@ where match &*query.q { "config" => { let channels: Vec<MicropubChannel>; - match backend.get_channels(&user.me.as_str()).await { + match backend.get_channels(user.me.as_str()).await { Ok(chans) => channels = chans, Err(err) => return Ok(err.into()) } @@ -36,7 +36,7 @@ where }, "channel" => { let channels: Vec<MicropubChannel>; - match backend.get_channels(&user.me.as_str()).await { + match backend.get_channels(user.me.as_str()).await { Ok(chans) => channels = chans, Err(err) => return Ok(err.into()) } diff --git a/src/micropub/post.rs b/src/micropub/post.rs index c465a6f..de2b162 100644 --- a/src/micropub/post.rs +++ b/src/micropub/post.rs @@ -255,7 +255,7 @@ pub async fn new_post<S: Storage>( || channel == vcards_channel || channel == food_channel { - if let Err(err) = create_feed(storage, &uid, &channel, &user).await { + if let Err(err) = create_feed(storage, &uid, &channel, user).await { return error_json!( 500, "database_error", @@ -313,7 +313,7 @@ async fn create_feed( }, "children": [uid] }), - &user, + user, ); storage.put_post(&feed, user.me.as_str()).await } @@ -483,8 +483,9 @@ async fn post_process_new_post<S: Storage>( // TODO: Replace this function once the MF2 parser is ready // A compliant parser's output format includes rels, // we could just find a Webmention one in there - let pattern = easy_scraper::Pattern::new(r#"<link href="{{url}}" rel="webmention">"#) - .expect("Pattern for webmentions couldn't be parsed"); + let pattern = + easy_scraper::Pattern::new(r#"<link href="{{url}}" rel="webmention">"#) + .expect("Pattern for webmentions couldn't be parsed"); let matches = pattern.matches(&body); if matches.is_empty() { return None; @@ -580,7 +581,7 @@ async fn process_json<S: Storage>( "You're not allowed to delete someone else's posts." ); } - if let Err(error) = req.state().storage.delete_post(&url).await { + if let Err(error) = req.state().storage.delete_post(url).await { return Ok(error.into()); } Ok(Response::builder(200).build()) @@ -602,7 +603,7 @@ async fn process_json<S: Storage>( "You're not allowed to delete someone else's posts." ); } - if let Err(error) = req.state().storage.update_post(&url, body.clone()).await { + if let Err(error) = req.state().storage.update_post(url, body.clone()).await { Ok(error.into()) } else { Ok(Response::builder(204).build()) @@ -679,7 +680,7 @@ async fn process_form<S: Storage>( "You're not allowed to delete someone else's posts." ); } - if let Err(error) = req.state().storage.delete_post(&url).await { + if let Err(error) = req.state().storage.delete_post(url).await { return error_json!(500, "database_error", error); } return Ok(Response::builder(200).build()); |