//! Deezer API authentication. use std::convert::Infallible; 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>, } 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 { let (done_tx, mut done_rx) = mpsc::channel::(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) }?; opt_auth_code .map_or_else(|| Err(AuthPromptHandlerError::NoAuthCodeReceived), 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, code: Option, } async fn handle_auth_code( done_tx: mpsc::Sender, request: Request, ) -> Result, String> { let query = serde_urlencoded::from_str::( 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()) }