summary refs log tree commit diff
path: root/src/components/signin.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/signin.rs')
-rw-r--r--src/components/signin.rs251
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),
         }
     }