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>,
>;
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, pub(super) auth_url: String, pub(super) token_url: String, pub(super) callback_url: String, 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 {
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,
}
}
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(())
}
}
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(())
}
}
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())?)
}
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(())
}
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 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()),
)
}
async fn connect(&mut self) -> Result<()> {
let client = self.build_client()?;
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let (auth_url, csrf_state) = client
.authorize_url(CsrfToken::new_random)
.add_scopes(self.scopes.clone())
.set_pkce_challenge(pkce_challenge)
.url();
println!("Browse to: {}", auth_url);
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()
);
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 {
Err(EveEsiError::NoTokenFound)
}
}
pub fn get_authent(self) -> Result<Authent> {
if let Some(auth) = self.authent_token {
Ok(auth)
} else {
Err(EveEsiError::NoTokenFound)
}
}
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<()> {
if self.authent_token.is_some() {
self.verify().await?;
}
else if self.deserialize_token().await.is_ok() {
if let Ok(v) = self.verify().await {
debug!("logged as {:#?}", v);
} else {
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(())
}
}