diff options
Diffstat (limited to 'src/components/signin.rs')
-rw-r--r-- | src/components/signin.rs | 251 |
1 files changed, 152 insertions, 99 deletions
diff --git a/src/components/signin.rs b/src/components/signin.rs index f2b5313..972ab43 100644 --- a/src/components/signin.rs +++ b/src/components/signin.rs @@ -2,7 +2,9 @@ use gettextrs::*; use std::cell::RefCell; use adw::prelude::*; -use kittybox_indieauth::{AuthorizationRequest, AuthorizationResponse, Error as IndieauthError, GrantResponse, Metadata}; +use kittybox_indieauth::{ + AuthorizationRequest, AuthorizationResponse, Error as IndieauthError, GrantResponse, Metadata, +}; use relm4::prelude::*; use soup::prelude::{ServerExt, ServerExtManual, SessionExt}; @@ -33,7 +35,7 @@ pub struct SignIn { state: kittybox_indieauth::State, code_verifier: kittybox_indieauth::PKCEVerifier, micropub_uri: Option<glib::Uri>, - metadata: Option<Metadata> + metadata: Option<Metadata>, } #[derive(Debug, thiserror::Error)] @@ -67,7 +69,10 @@ pub enum Input { Callback(Result<AuthorizationResponse, Error>), } -pub async fn get_metadata(http: soup::Session, url: glib::Uri) -> Result<(Metadata, glib::Uri), Error> { +pub async fn get_metadata( + http: soup::Session, + url: glib::Uri, +) -> Result<(Metadata, glib::Uri), Error> { // Fire off a speculative request at the well-known URI. This could // improve UX by parallelizing how we query the user's website. // @@ -75,15 +80,13 @@ pub async fn get_metadata(http: soup::Session, url: glib::Uri) -> Result<(Metada // RECOMMENDED though optional according to IndieAuth specification // ยงย 4.1.1, so we could use that if it's there to speed up the // process. - let metadata = relm4::spawn_local( - SignIn::well_known_metadata(http.clone(), url.clone()) - ); + let metadata = relm4::spawn_local(SignIn::well_known_metadata(http.clone(), url.clone())); let msg = soup::Message::from_uri("GET", &url); let body = http.send_future(&msg, glib::Priority::DEFAULT).await?; let mf2 = microformats::from_reader( std::io::BufReader::new(body.into_read()), - url.to_string().parse().unwrap() + url.to_string().parse().unwrap(), )?; let rels = mf2.rels.by_rels(); @@ -98,7 +101,7 @@ pub async fn get_metadata(http: soup::Session, url: glib::Uri) -> Result<(Metada // The new versions are superior by providing more features that // were previously proprietary extensions, and are more clearer in // general. - return Err(Error::MetadataNotFound) + return Err(Error::MetadataNotFound); }; let micropub_uri = if let Some(url) = rels @@ -108,26 +111,33 @@ pub async fn get_metadata(http: soup::Session, url: glib::Uri) -> Result<(Metada { glib::Uri::parse(url.as_str(), glib::UriFlags::NONE).unwrap() } else { - return Err(Error::MicropubLinkNotFound) + return Err(Error::MicropubLinkNotFound); }; if let Ok(Some(metadata)) = metadata.await { Ok((metadata, micropub_uri)) } else { let msg = soup::Message::from_uri("GET", &metadata_url); - msg.request_headers().unwrap().append("Accept", "application/json"); - match http.send_and_read_future(&msg, glib::Priority::DEFAULT).await { + msg.request_headers() + .unwrap() + .append("Accept", "application/json"); + match http + .send_and_read_future(&msg, glib::Priority::DEFAULT) + .await + { Ok(body) if msg.status() == soup::Status::Ok => { let metadata = serde_json::from_slice(&body)?; Ok((metadata, micropub_uri)) - }, + } Ok(_) => Err(Error::MetadataEndpointFailed(msg.status())), Err(err) => Err(err.into()), } } } -fn callback_handler(sender: AsyncComponentSender<SignIn>) -> impl Fn(&soup::Server, &soup::ServerMessage, &str, std::collections::HashMap<&str, &str>) { +fn callback_handler( + sender: AsyncComponentSender<SignIn>, +) -> impl Fn(&soup::Server, &soup::ServerMessage, &str, std::collections::HashMap<&str, &str>) { move |server, msg, _, _| { let server = ObjectExt::downgrade(server); let sender = sender.clone(); @@ -148,7 +158,7 @@ fn callback_handler(sender: AsyncComponentSender<SignIn>) -> impl Fn(&soup::Serv msg.set_response( Some("text/plain; charset=\"utf-8\""), soup::MemoryUse::Static, - gettext("Thank you! This window can now be closed.").as_bytes() + gettext("Thank you! This window can now be closed.").as_bytes(), ); msg.connect_finished(move |_| { sender.input(Input::Callback(Ok(response.take().unwrap()))); @@ -157,7 +167,7 @@ fn callback_handler(sender: AsyncComponentSender<SignIn>) -> impl Fn(&soup::Serv soup::prelude::ServerExt::disconnect(&server); } }); - }, + } Err(err) => { msg.set_status(400, soup::Status::phrase(400).as_deref()); if let Ok(err) = serde_urlencoded::from_str::<IndieauthError>(q.as_str()) { @@ -191,15 +201,21 @@ impl SignIn { kittybox_indieauth::Scopes::new(vec![ kittybox_indieauth::Scope::Profile, kittybox_indieauth::Scope::Create, - kittybox_indieauth::Scope::Media + kittybox_indieauth::Scope::Media, ]) } - fn bail_out(&mut self, widgets: &mut <Self as AsyncComponent>::Widgets, sender: AsyncComponentSender<Self>, err: Error) { - widgets.toasts.add_toast(adw::Toast::builder() - .title(err.to_string()) - .priority(adw::ToastPriority::High) - .build() + fn bail_out( + &mut self, + widgets: &mut <Self as AsyncComponent>::Widgets, + sender: AsyncComponentSender<Self>, + err: Error, + ) { + widgets.toasts.add_toast( + adw::Toast::builder() + .title(err.to_string()) + .priority(adw::ToastPriority::High) + .build(), ); // Reset all the state for the component for security reasons. self.busy_guard = None; @@ -212,31 +228,45 @@ impl SignIn { } async fn well_known_metadata(http: soup::Session, url: glib::Uri) -> Option<Metadata> { - let well_known = url.parse_relative( - Self::WELL_KNOWN_METADATA_ENDPOINT_PATH, - glib::UriFlags::NONE - ).unwrap(); + let well_known = url + .parse_relative( + Self::WELL_KNOWN_METADATA_ENDPOINT_PATH, + glib::UriFlags::NONE, + ) + .unwrap(); // Speculatively check for metadata at the well-known path let msg = soup::Message::from_uri("GET", &well_known); - msg.request_headers().unwrap().append("Accept", "application/json"); - match http.send_and_read_future(&msg, glib::Priority::DEFAULT).await { - Ok(body) if msg.status() == soup::Status::Ok => { - match serde_json::from_slice(&body) { - Ok(metadata) => { - log::info!("Speculative metadata request successful: {:#?}", metadata); - Some(metadata) - }, - Err(err) => { - log::warn!("Parsing OAuth2 metadata from {} failed: {}", well_known, err); - None - } + msg.request_headers() + .unwrap() + .append("Accept", "application/json"); + match http + .send_and_read_future(&msg, glib::Priority::DEFAULT) + .await + { + Ok(body) if msg.status() == soup::Status::Ok => match serde_json::from_slice(&body) { + Ok(metadata) => { + log::info!("Speculative metadata request successful: {:#?}", metadata); + Some(metadata) + } + Err(err) => { + log::warn!( + "Parsing OAuth2 metadata from {} failed: {}", + well_known, + err + ); + None } }, Ok(_) => { - log::warn!("Speculative request to {} returned {:?} ({})", well_known, msg.status(), msg.reason_phrase().unwrap()); + log::warn!( + "Speculative request to {} returned {:?} ({})", + well_known, + msg.status(), + msg.reason_phrase().unwrap() + ); None - }, + } Err(err) => { log::warn!("Speculative request to {} failed: {}", well_known, err); @@ -344,7 +374,13 @@ impl AsyncComponent for SignIn { std::future::ready(AsyncComponentParts { model, widgets }) } - async fn update_with_view(&mut self, widgets: &mut Self::Widgets, msg: Self::Input, sender: AsyncComponentSender<Self>, _root: &Self::Root) { + async fn update_with_view( + &mut self, + widgets: &mut Self::Widgets, + msg: Self::Input, + sender: AsyncComponentSender<Self>, + _root: &Self::Root, + ) { match msg { Input::Start => { self.busy_guard = Some(relm4::main_adw_application().mark_busy()); @@ -356,30 +392,25 @@ impl AsyncComponent for SignIn { None => { self.me_buffer.insert_text(0, "https://"); url = self.me_buffer.text().into(); - }, + } Some(scheme) => { if scheme != "https" && scheme != "http" { - return self.bail_out( - widgets, sender, - Error::WrongScheme - ); + return self.bail_out(widgets, sender, Error::WrongScheme); } - }, + } } let url = match glib::Uri::parse(url.as_str(), glib::UriFlags::SCHEME_NORMALIZE) { Ok(url) => url, Err(err) => { - return self.bail_out( - widgets, sender, - err.into() - ); - }, + return self.bail_out(widgets, sender, err.into()); + } }; - let (metadata, micropub_uri) = match get_metadata(self.http.clone(), url.clone()).await { - Ok((metadata, micropub_uri)) => (metadata, micropub_uri), - Err(err) => return self.bail_out(widgets, sender, err) - }; + let (metadata, micropub_uri) = + match get_metadata(self.http.clone(), url.clone()).await { + Ok((metadata, micropub_uri)) => (metadata, micropub_uri), + Err(err) => return self.bail_out(widgets, sender, err), + }; let auth_request = AuthorizationRequest { response_type: kittybox_indieauth::ResponseType::Code, @@ -387,15 +418,17 @@ impl AsyncComponent for SignIn { redirect_uri: REDIRECT_URI.parse().unwrap(), state: self.state.clone(), code_challenge: kittybox_indieauth::PKCEChallenge::new( - &self.code_verifier, kittybox_indieauth::PKCEMethod::S256 + &self.code_verifier, + kittybox_indieauth::PKCEMethod::S256, ), scope: Some(Self::scopes()), - me: Some(url.to_str().parse().unwrap()) + me: Some(url.to_str().parse().unwrap()), }; let auth_url = { let mut url = metadata.authorization_endpoint.clone(); - url.query_pairs_mut().extend_pairs(auth_request.as_query_pairs()); + url.query_pairs_mut() + .extend_pairs(auth_request.as_query_pairs()); url }; @@ -407,40 +440,57 @@ impl AsyncComponent for SignIn { server.add_handler(None, callback_handler(sender.clone())); match server.listen_local(60000, soup::ServerListenOptions::empty()) { Ok(()) => server, - Err(err) => return self.bail_out(widgets, sender, err.into()) + Err(err) => return self.bail_out(widgets, sender, err.into()), } }); - if let Err(err) = gtk::UriLauncher::new(auth_url.as_str()).launch_future( - None::<&adw::ApplicationWindow> - ).await { - return self.bail_out(widgets, sender, err.into()) + if let Err(err) = gtk::UriLauncher::new(auth_url.as_str()) + .launch_future(None::<&adw::ApplicationWindow>) + .await + { + return self.bail_out(widgets, sender, err.into()); }; self.busy_guard = None; self.update_view(widgets, sender); - }, + } Input::Callback(Ok(res)) => { // Immediately drop the event if we didn't take a server. - if self.callback_server.take().is_none() { return; } + if self.callback_server.take().is_none() { + return; + } self.busy_guard = Some(relm4::main_adw_application().mark_busy()); let metadata = self.metadata.take().unwrap(); let micropub_uri = self.micropub_uri.take().unwrap(); if res.state != self.state { - return self.bail_out(widgets, sender, IndieauthError { - kind: kittybox_indieauth::ErrorKind::InvalidRequest, - msg: Some(gettext("state doesn't match what we remember, ceremony aborted")), - error_uri: None, - }.into()) + return self.bail_out( + widgets, + sender, + IndieauthError { + kind: kittybox_indieauth::ErrorKind::InvalidRequest, + msg: Some(gettext( + "state doesn't match what we remember, ceremony aborted", + )), + error_uri: None, + } + .into(), + ); } if res.iss != metadata.issuer { - return self.bail_out(widgets, sender, IndieauthError { - kind: kittybox_indieauth::ErrorKind::InvalidRequest, - msg: Some(gettext("issuer doesn't match what we remember, ceremony aborted")), - error_uri: None, - }.into()) + return self.bail_out( + widgets, + sender, + IndieauthError { + kind: kittybox_indieauth::ErrorKind::InvalidRequest, + msg: Some(gettext( + "issuer doesn't match what we remember, ceremony aborted", + )), + error_uri: None, + } + .into(), + ); } let code = res.code; @@ -450,21 +500,28 @@ impl AsyncComponent for SignIn { redirect_uri: REDIRECT_URI.parse().unwrap(), code_verifier: std::mem::replace( &mut self.code_verifier, - kittybox_indieauth::PKCEVerifier::new() - ) + kittybox_indieauth::PKCEVerifier::new(), + ), }; - let url = glib::Uri::parse(metadata.token_endpoint.as_str(), glib::UriFlags::NONE).unwrap(); + let url = glib::Uri::parse(metadata.token_endpoint.as_str(), glib::UriFlags::NONE) + .unwrap(); let msg = soup::Message::from_uri("POST", &url); let headers = msg.request_headers().unwrap(); headers.append("Accept", "application/json"); msg.set_request_body_from_bytes( Some("application/x-www-form-urlencoded"), Some(&glib::Bytes::from_owned( - serde_urlencoded::to_string(token_grant).unwrap().into_bytes() - )) + serde_urlencoded::to_string(token_grant) + .unwrap() + .into_bytes(), + )), ); - match self.http.send_and_read_future(&msg, glib::Priority::DEFAULT).await { + match self + .http + .send_and_read_future(&msg, glib::Priority::DEFAULT) + .await + { Ok(body) if msg.status() == soup::Status::Ok => { match serde_json::from_slice::<GrantResponse>(&body) { Ok(GrantResponse::ProfileUrl(_)) => unreachable!(), @@ -476,16 +533,16 @@ impl AsyncComponent for SignIn { state: _, expires_in, profile, - refresh_token + refresh_token, }) => { let _ = sender.output(Output { - me: glib::Uri::parse(me.as_str(), glib::UriFlags::NONE).unwrap(), + me: glib::Uri::parse(me.as_str(), glib::UriFlags::NONE) + .unwrap(), scope: scope.unwrap_or_else(Self::scopes), micropub: micropub_uri, - userinfo: metadata.userinfo_endpoint - .map(|u| glib::Uri::parse( - u.as_str(), glib::UriFlags::NONE - ).unwrap()), + userinfo: metadata.userinfo_endpoint.map(|u| { + glib::Uri::parse(u.as_str(), glib::UriFlags::NONE).unwrap() + }), access_token, refresh_token, expires_in: expires_in.map(std::time::Duration::from_secs), @@ -493,22 +550,18 @@ impl AsyncComponent for SignIn { }); self.busy_guard = None; self.update_view(widgets, sender); - }, + } Err(err) => self.bail_out(widgets, sender, err.into()), } + } + Ok(body) => match serde_json::from_slice::<IndieauthError>(&body) { + Ok(err) => self.bail_out(widgets, sender, err.into()), + Err(err) => self.bail_out(widgets, sender, err.into()), }, - Ok(body) => { - match serde_json::from_slice::<IndieauthError>(&body) { - Ok(err) => self.bail_out(widgets, sender, err.into()), - Err(err) => self.bail_out(widgets, sender, err.into()) - } - }, - Err(err) => self.bail_out(widgets, sender, err.into()) + Err(err) => self.bail_out(widgets, sender, err.into()), } - }, - Input::Callback(Err(err)) => { - self.bail_out(widgets, sender, err) - }, + } + Input::Callback(Err(err)) => self.bail_out(widgets, sender, err), } } |