diff options
author | HampusM <hampus@hampusmat.com> | 2022-09-09 20:03:02 +0200 |
---|---|---|
committer | HampusM <hampus@hampusmat.com> | 2022-09-09 20:04:54 +0200 |
commit | db42316544c65951c7781357ec5aaba1b9abb8ab (patch) | |
tree | 13fd19efb80cf29f054c1760e8580f4379ce6a6a /src/auth.rs | |
parent | 8a02d3386d4ce0b58de943fcf42bd072af1e0b42 (diff) |
refactor: make AuthPromptHandler use the Hyper web server
Diffstat (limited to 'src/auth.rs')
-rw-r--r-- | src/auth.rs | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..4af4997 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,154 @@ +//! Deezer API authentication. +use std::convert::Infallible; +use std::error::Error; +use std::fmt::Display; +use std::net::ToSocketAddrs; +use std::time::Duration; + +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Request, Response, Server}; +use serde::Deserialize; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; +use tokio::{select, spawn}; + +use crate::errors::auth::AuthPromptHandlerError; + +const AUTH_URL: &str = "https://connect.deezer.com/oauth/auth.php"; + +/// A Deezer access token. +#[derive(Debug, Clone)] +pub struct AccessToken +{ + /// The access token. + pub access_token: String, + + /// The duration until the access token expires. + pub expires: Duration, +} + +/// A Deezer authentication code. +#[derive(Debug, Deserialize, Clone)] +pub struct AuthCode(String); + +impl Display for AuthCode +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + self.0.fmt(f) + } +} + +/// A Deezer authentication prompt handler. +pub struct AuthPromptHandler +{ + /// URL to the Deezer authentication prompt. + pub auth_prompt_url: String, + + /// Handle for the running authentication prompt handler. + /// + /// Finishes when a single authentication has occurred. + pub handler: JoinHandle<Result<AuthCode, Box<dyn Error + Send + Sync>>>, +} + +impl AuthPromptHandler +{ + /// Runs a web server that handles a Deezer authentication prompt. + /// + /// The argument `address` must be in the same domain as the domain you defined when + /// you created the application with the ID `app_id`. + /// + /// # Errors + /// Will return Err if the web server created is unable to bind to `address`. + pub async fn run( + app_id: u32, + address: String, + port: u16, + uri_scheme: String, + ) -> Result<Self, AuthPromptHandlerError> + { + let (done_tx, mut done_rx) = mpsc::channel::<AuthCode>(1); + + let addr = format!("{}:{}", address, port) + .to_socket_addrs() + .map_err(|_| AuthPromptHandlerError::InvalidAddress)? + .next() + .map_or_else(|| Err(AuthPromptHandlerError::InvalidAddress), Ok)?; + + let make_service = make_service_fn(move |_| { + let done_tx_clone = done_tx.clone(); + + let service = + service_fn(move |req| handle_auth_code(done_tx_clone.clone(), req)); + + async move { Ok::<_, Infallible>(service) } + }); + + let server = Server::bind(&addr).serve(make_service); + + let handle = spawn(async move { + let opt_auth_code = select! { + result = server => { + result.map(|_| None) + }, + auth_code = async { + done_rx.recv().await + } => Ok(auth_code) + }?; + + Ok(opt_auth_code.map_or_else(|| Err("No auth code was received"), Ok)?) + }); + + Ok(Self { + auth_prompt_url: format!( + "{}?app_id={}&redirect_uri={}://{}:{}&perms=basic_access", + AUTH_URL, app_id, uri_scheme, address, port + ), + handler: handle, + }) + } +} + +#[derive(Debug, Deserialize)] +struct AuthCodeQuery +{ + error_reason: Option<String>, + code: Option<AuthCode>, +} + +async fn handle_auth_code( + done_tx: mpsc::Sender<AuthCode>, + request: Request<Body>, +) -> Result<Response<Body>, String> +{ + let query = serde_urlencoded::from_str::<AuthCodeQuery>( + request.uri().query().map_or_else(|| "", |query| query), + ) + .map_err(|err| format!("Invalid query. {}", err))?; + + if let Some(error_reason) = &query.error_reason { + return Ok(Response::builder().status(401).body(Body::from(format!( + "Error: No authentication code was retrieved. Reason: {}\n\nYou can close this tab", + error_reason + ))).unwrap()); + } + + let auth_code = match &query.code { + Some(auth_code) => auth_code, + None => { + return Ok(Response::builder() + .status(400) + .body(Body::from("Error: No authentication code was retrieved.")) + .unwrap()); + } + }; + + done_tx.send(auth_code.clone()).await.unwrap(); + + Ok(Response::builder() + .status(200) + .body(Body::from( + "Authentication code was successfully retrieved.\n\nYou can close this tab", + )) + .unwrap()) +} |