eve-esi-api 0.0.4

This library provides an authentication to Eve-esi API and some endpoints to call.
Documentation
use log::debug;

use oauth2::basic::{BasicClient, BasicErrorResponseType, BasicTokenType};
use oauth2::reqwest::async_http_client;
use oauth2::{
    AuthUrl, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields,
    PkceCodeChallenge, RedirectUrl, RevocationErrorResponseType, Scope, StandardErrorResponse,
    StandardRevocableToken, StandardTokenIntrospectionResponse, StandardTokenResponse,
    TokenResponse, TokenUrl,
};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::PathBuf;
use std::time::Duration;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use tokio::io::BufReader;
use tokio::io::{AsyncBufReadExt, AsyncReadExt};
use tokio::net::TcpListener;
use url::Url;

use crate::errors::EveEsiError;

use crate::Result;

type Oauth2Client = Client<
    StandardErrorResponse<BasicErrorResponseType>,
    StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
    BasicTokenType,
    StandardTokenIntrospectionResponse<EmptyExtraTokenFields, BasicTokenType>,
    StandardRevocableToken,
    StandardErrorResponse<RevocationErrorResponseType>,
>;

/// Authenticate to Eve Esi Api
///
pub(crate) struct Oauth2Authent {
    pub(super) token_path: Option<PathBuf>,
    pub(super) authent_token: Option<Authent>,
    pub(super) user_agent: String,
    pub(super) scopes: Vec<Scope>,
    pub(super) verify_url: String, // https://login.eveonline.com/oauth/verif
    pub(super) auth_url: String,   // https://login.eveonline.com/v2/oauth/authorize
    pub(super) token_url: String,  // https://login.eveonline.com/v2/oauth/token
    pub(super) callback_url: String, //http://localhost:8569/callback
    pub(super) client_id: String,
    pub(super) client_secret: String,
}

#[derive(Serialize, Deserialize)]
pub struct Authent {
    pub token: StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Verify {
    #[serde(rename = "CharacterID")]
    pub character_id: usize,
    #[serde(rename = "CharacterName")]
    pub character_name: String,
}

impl Oauth2Authent {
    /// entry point
    pub async fn authenticate(&mut self) -> Result<()> {
        match self.handle_existing_token().await {
            Ok(()) => {
                if self.need_refresh(24)? {
                    let _ = self.refresh_token().await;
                }
                Ok(())
            }
            Err(EveEsiError::NoTokenFound) | Err(EveEsiError::ScopesChanged) => {
                self.connect().await?;
                self.serialize_token().await?;
                Ok(())
            }
            other => other,
        }
    }
    /// Serialize token from mem to file
    async fn serialize_token(&self) -> Result<()> {
        if let Some(path) = &self.token_path {
            let mut file = File::create(path).await?;
            Ok(file
                .write_all(serde_json::to_string(&self.authent_token)?.as_bytes())
                .await?)
        } else {
            Ok(())
        }
    }

    /// Deserialize token from file to mem
    async fn deserialize_token(&mut self) -> Result<()> {
        if let Some(path) = &self.token_path {
            let mut file = File::open(path).await?;

            let mut buf = String::new();
            file.read_to_string(&mut buf).await?;
            self.authent_token = serde_json::from_str(buf.as_str())?;
            Ok(())
        } else {
            Ok(())
        }
    }

    /// Verify token in mem
    pub async fn verify(&self) -> Result<Verify> {
        let token = self.get_token()?.unwrap();
        let client = reqwest::Client::builder()
            .user_agent(self.user_agent.as_str())
            .build()?;

        let r = client
            .get(self.verify_url.as_str())
            .bearer_auth(token.token.access_token().secret())
            .send()
            .await?;

        let text_response = r.error_for_status()?.text().await?;

        Ok(serde_json::from_str(text_response.as_str())?)
    }

    /// Refresh token in mem
    async fn refresh_token(&mut self) -> Result<()> {
        let token = self.get_token()?.unwrap();
        let client = self.build_client()?;
        let token = client
            .exchange_refresh_token(token.token.refresh_token().unwrap())
            .request_async(async_http_client)
            .await?;
        self.authent_token = Some(Authent { token });
        Ok(())
    }

    /// Get token in mem, or Err if not in mem
    fn get_token(&self) -> Result<Option<&Authent>> {
        if let Some(t) = &self.authent_token {
            Ok(Some(t))
        } else {
            Err(EveEsiError::NoTokenFound)
        }
    }

    fn build_client(&self) -> Result<Oauth2Client> {
        // let id = env!("CLIENT_ID", "CLIENT_ID empty").to_string();
        // let secret = env!("CLIENT_SECRET", "CLIENT_SECRET empty").to_string();

        let client_id = ClientId::new(self.client_id.clone());
        let client_secret = ClientSecret::new(self.client_secret.clone());
        let auth_url = AuthUrl::new(self.auth_url.clone()).unwrap();
        let token_url = TokenUrl::new(self.token_url.clone()).unwrap();

        Ok(
            BasicClient::new(client_id, Some(client_secret), auth_url, Some(token_url))
                .set_redirect_uri(RedirectUrl::new(self.callback_url.clone()).unwrap()),
        )
    }

    /// Connect: get a fresh token through a full OAuth2 authentication
    async fn connect(&mut self) -> Result<()> {
        let client = self.build_client()?;
        let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

        // Generate the full authorization URL.
        let (auth_url, csrf_state) = client
            .authorize_url(CsrfToken::new_random)
            .add_scopes(self.scopes.clone())
            // Set the PKCE code challenge.
            .set_pkce_challenge(pkce_challenge)
            .url();

        println!("Browse to: {}", auth_url);

        // A very naive implementation of the redirect server.
        let listener = TcpListener::bind("127.0.0.1:8569").await.unwrap();

        if let Ok((mut stream, _)) = listener.accept().await {
            let code;
            let state;
            {
                let mut reader = BufReader::new(&mut stream);

                let mut request_line = String::new();
                reader.read_line(&mut request_line).await.unwrap();

                let redirect_url = request_line.split_whitespace().nth(1).unwrap();
                let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();

                let code_pair = url
                    .query_pairs()
                    .find(|pair| {
                        let (key, _) = pair;
                        key == "code"
                    })
                    .unwrap();

                let (_, value) = code_pair;
                code = AuthorizationCode::new(value.into_owned());

                let state_pair = url
                    .query_pairs()
                    .find(|pair| {
                        let (key, _) = pair;
                        key == "state"
                    })
                    .unwrap();

                let (_, value) = state_pair;
                state = CsrfToken::new(value.into_owned());
            }

            let message = "Go back to your terminal :)";
            let response = format!(
                "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
                message.len(),
                message
            );
            stream.write_all(response.as_bytes()).await.unwrap();

            debug!("EVE returned the following code:\n{}\n", code.secret());
            debug!(
                "EVE returned the following state:\n{} (expected `{}`)\n",
                state.secret(),
                csrf_state.secret()
            );

            // Exchange the code with a token.
            let token_res = client
                .exchange_code(code)
                .set_pkce_verifier(pkce_verifier)
                .request_async(async_http_client)
                .await?;

            debug!("EVE returned the following token:\n{:?}\n", token_res);

            self.authent_token = Some(Authent { token: token_res });
            Ok(())
        } else {
            // The server will terminate itself after collecting the first code.
            Err(EveEsiError::NoTokenFound)
        }
    }

    /// Exit point : Consumes the thing to extract the token
    pub fn get_authent(self) -> Result<Authent> {
        if let Some(auth) = self.authent_token {
            Ok(auth)
        } else {
            Err(EveEsiError::NoTokenFound)
        }
    }

    /// Delete the serialized token
    pub async fn _delete_token(&self) -> Result<()> {
        if let Some(path) = &self.token_path {
            Ok(std::fs::remove_file(path)?)
        } else {
            Ok(())
        }
    }

    fn get_token_scopes(&self) -> Result<Option<&Vec<Scope>>> {
        Ok(self.get_token()?.unwrap().token.scopes())
    }

    fn get_token_expires_in(&self) -> Result<Option<Duration>> {
        Ok(self.get_token()?.unwrap().token.expires_in())
    }

    fn need_refresh(&self, hours: u64) -> Result<bool> {
        if let Some(duration) = self.get_token_expires_in()? {
            if duration > Duration::from_secs(60 * 60 * hours) {
                return Ok(true);
            }
        }
        Ok(false)
    }

    fn have_scopes_changed(&self) -> Result<bool> {
        let existant = self.get_token_scopes()?;
        let required = Some(&self.scopes);

        let existant_set: Option<HashSet<&Scope>> = existant.map(|v| HashSet::from_iter(v.iter()));
        let requiret_set = required.map(|v| HashSet::from_iter(v.iter()));

        Ok(existant_set == requiret_set)
    }

    async fn handle_existing_token(&mut self) -> Result<()> {
        // do we have a token in memory ?
        if self.authent_token.is_some() {
            self.verify().await?;
        }
        // first try local stored token :
        else if self.deserialize_token().await.is_ok() {
            if let Ok(v) = self.verify().await {
                debug!("logged as {:#?}", v);
            } else {
                // try refreshing
                if self.refresh_token().await.is_err() {
                    return Err(EveEsiError::NoTokenFound);
                }
            }
        } else {
            return Err(EveEsiError::NoTokenFound);
        }
        if self.have_scopes_changed()? {
            return Err(EveEsiError::ScopesChanged);
        }
        Ok(())
    }
}