#[cfg(feature = "puuid")]
use crate::enums::affinity::Affinity;
use crate::errors::response_error::RequestError;
#[cfg(feature = "puuid")]
use crate::response_types::pas_token::PasToken;
#[cfg(feature = "puuid")]
use crate::response_types::xmpp_server_address::XMPPServerAddress;
use crate::utils::functions::set_query_args;
#[cfg(feature = "puuid")]
use crate::utils::functions::{decode_jwt, DecodeJWTError};
use crate::utils::network::http_client::HttpClient;
use hyper::body::{Bytes, HttpBody};
use hyper::{Body, Request, Uri};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::str::FromStr;
use time::{Duration, OffsetDateTime};
use url::Url;
#[derive(Debug, Clone)]
pub struct CredentialsManager {
access_token: Option<String>,
entitlements_token: Option<String>,
#[cfg(feature = "puuid")]
pas_token: Option<String>,
#[cfg(feature = "puuid")]
affinity: Option<Affinity>,
#[cfg(feature = "puuid")]
xmpp_server_address: Option<(String, u16)>,
client_version: Option<String>,
reauth_cookies: Option<Vec<String>>,
exires_at: Option<OffsetDateTime>,
}
const ENTITLEMENTS_HEADER_KEY: &str = "X-Riot-Entitlements-JWT";
const CLIENT_VERSION_HEADER_KEY: &str = "X-Riot-ClientVersion";
const CLIENT_PLATFORM_HEADER_KEY: &str = "X-Riot-ClientPlatform";
const CLIENT_PLATFORM_HEADER_VALUE: &str = "ew0KCSJwbGF0Zm9ybVR5cGUiOiAiUEMiLA0KCSJwbGF0Zm9ybU9TIjogIldpbmRvd3MiLA0KCSJwbGF0Zm9ybU9TVmVyc2lvbiI6ICIxMC4wLjE5MDQyLjEuMjU2LjY0Yml0IiwNCgkicGxhdGZvcm1DaGlwc2V0IjogIlVua25vd24iDQp9";
const AUTHORIZATION_HEADER_KEY: &str = "Authorization";
const COOKIES_PREPARE_DATA: &str = r#"
{
"client_id": "play-valorant-web-prod",
"nonce": "1",
"redirect_uri": "https://playvalorant.com/opt_in",
"response_type": "token id_token"
}
"#;
const USER_AGENT_RSO: &str =
"RiotClient/62.0.1.4852117.4789131 rso-auth (Windows;10;;Professional, x64)";
#[derive(Debug, Serialize, Deserialize)]
struct AccessTokenRequestBody {
r#type: String,
username: String,
password: String,
remember: bool,
language: Option<String>,
region: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct AccessTokenResponseBody {
r#type: String,
response: AccessTokenResponse,
country: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct EntitlementsTokenResponseBody {
entitlements_token: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct AccessTokenResponse {
mode: String,
parameters: AccessTokenResponseParameters,
}
#[derive(Debug, Serialize, Deserialize)]
struct AccessTokenResponseParameters {
r#uri: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct VersionResponseBody {
status: u32,
data: VersionResponseData,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct VersionResponseData {
manifest_id: String,
branch: String,
build_version: String,
engine_version: String,
riot_client_version: String,
riot_client_build: String,
build_date: String,
}
#[cfg(feature = "puuid")]
const XMPP_PORT: u16 = 5223;
impl CredentialsManager {
pub fn new() -> CredentialsManager {
CredentialsManager {
access_token: None,
entitlements_token: None,
#[cfg(feature = "puuid")]
pas_token: None,
#[cfg(feature = "puuid")]
affinity: None,
#[cfg(feature = "puuid")]
xmpp_server_address: None,
client_version: None,
reauth_cookies: None,
exires_at: None,
}
}
pub(super) async fn get_auth_headers(&self) -> Result<HashMap<String, String>, &str> {
let access_token = match self.access_token.as_ref() {
Some(access_token) => access_token,
None => return Err("No access token found"),
};
let entitlements_token = match self.entitlements_token.as_ref() {
Some(entitlements_token) => entitlements_token,
None => return Err("No entitlements token found"),
};
let client_version = match self.client_version.as_ref() {
Some(client_version) => client_version,
None => return Err("No client version found"),
};
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert(
AUTHORIZATION_HEADER_KEY.to_string(),
format!("Bearer {}", access_token),
);
headers.insert(
ENTITLEMENTS_HEADER_KEY.to_string(),
entitlements_token.to_string(),
);
headers.insert(
CLIENT_VERSION_HEADER_KEY.to_string(),
client_version.to_string(),
);
headers.insert(
CLIENT_PLATFORM_HEADER_KEY.to_string(),
CLIENT_PLATFORM_HEADER_VALUE.to_string(),
);
Ok(headers)
}
pub async fn authenticate<T: HttpClient>(
&mut self,
http_client: &T,
username: &str,
password: &str,
) -> Result<&str, RequestError> {
let cookies = self.prepare_cookies(http_client).await?;
let (access_token, reauth_cookies) = self
.fetch_access_token(http_client, username, password, &cookies)
.await?;
self.access_token = Some(access_token.clone());
self.reauth_cookies = Some(reauth_cookies.clone());
let entitlements_token = self
.fetch_entitlements_token(http_client, &access_token, &cookies)
.await?;
self.entitlements_token = Some(entitlements_token.clone());
#[cfg(feature = "puuid")]
{
let pas_token = self.fetch_pas_token(http_client, &access_token).await?;
self.pas_token = Some(pas_token.clone());
let affinity = self
.get_affinity()
.map_err(|e| RequestError::ParseError(format!("Failed to get affinity: {}", e)))?;
self.affinity = Some(affinity);
let xmpp_server_address = self
.fetch_xmpp_server_address(
http_client,
&affinity,
&access_token,
&entitlements_token,
)
.await?;
self.xmpp_server_address = Some(xmpp_server_address);
}
let client_version = self.get_client_version(http_client).await?;
self.client_version = Some(client_version.clone());
self.exires_at = Some(OffsetDateTime::now_utc() + Duration::minutes(30));
Ok("Success")
}
async fn prepare_cookies<T: HttpClient>(
&self,
http_client: &T,
) -> Result<Vec<String>, RequestError> {
let request = hyper::Request::builder()
.method("POST")
.header("Content-Type", "application/json")
.header("Accept-Encoding", "gzip, deflate, br")
.header("Host", "auth.riotgames.com")
.header("User-Agent", USER_AGENT_RSO)
.uri(Uri::from_static(
"https://auth.riotgames.com/api/v1/authorization",
))
.body(hyper::Body::from(COOKIES_PREPARE_DATA))
.map_err(RequestError::from)?;
let mut response = http_client.request(request).await?;
let status = response.status().as_u16();
if status != 200 {
let body: Bytes = match response.body_mut().data().await {
Some(Ok(body)) => body,
_ => Err(RequestError::from("Error while reading body"))?,
};
return Err(RequestError::from((status, body)));
}
let cookies: Vec<String> = response
.headers()
.get_all(hyper::header::SET_COOKIE)
.into_iter()
.filter_map(|h| h.to_str().ok().map(|s| s.to_string()))
.collect();
Ok(cookies)
}
pub fn get_access_token(&self) -> Option<String> {
self.access_token.clone()
}
async fn fetch_access_token<T: HttpClient>(
&self,
http_client: &T,
username: &str,
password: &str,
cookies: &[String],
) -> Result<(String, Vec<String>), RequestError> {
let body = serde_json::to_string(&AccessTokenRequestBody {
r#type: "auth".to_string(),
username: username.to_string(),
password: password.to_string(),
remember: false,
region: None,
language: Some("eu_US".to_string()),
})?;
let mut request = hyper::Request::builder()
.method("PUT")
.header("Content-Type", "application/json")
.header("Accept-Encoding", "identity")
.header("Host", "auth.riotgames.com")
.header("User-Agent", USER_AGENT_RSO)
.uri(Uri::from_static(
"https://auth.riotgames.com/api/v1/authorization",
))
.body(hyper::Body::from(body))?;
Self::set_cookies(cookies, &mut request);
let response = http_client.request(request).await?;
let reauth_cookies: &Vec<String> = &response
.headers()
.get_all(hyper::header::SET_COOKIE)
.into_iter()
.filter_map(|h| h.to_str().ok().map(|s| s.to_string()))
.collect();
let body = hyper::body::to_bytes(response.into_body()).await?;
let access_token_response: AccessTokenResponseBody = serde_json::from_slice(&body)?;
assert_eq!(access_token_response.r#type, "response");
assert_eq!(access_token_response.response.mode, "fragment");
let uri = access_token_response.response.parameters.uri;
let url = Url::from_str(uri.as_str()).map_err(RequestError::from)?;
let access_token = Self::extract_access_token(&url)?;
Ok((access_token, reauth_cookies.clone()))
}
pub fn get_entitlements_token(&self) -> Option<String> {
self.entitlements_token.clone()
}
pub async fn fetch_entitlements_token<T: HttpClient>(
&self,
http_client: &T,
access_token: &str,
cookies: &[String],
) -> Result<String, RequestError> {
let mut request = hyper::Request::builder()
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", access_token))
.uri(Uri::from_static(
"https://entitlements.auth.riotgames.com/api/token/v1",
))
.body(Body::empty())?;
Self::set_cookies(cookies, &mut request);
let response = http_client.request(request).await?;
let body = hyper::body::to_bytes(response.into_body()).await?;
let entitlements_token_response: EntitlementsTokenResponseBody =
serde_json::from_slice(&body)?;
Ok(entitlements_token_response.entitlements_token)
}
#[cfg(feature = "puuid")]
pub fn get_pas_token(&self) -> Option<String> {
self.pas_token.clone()
}
#[cfg(feature = "puuid")]
pub fn get_affinity(&self) -> Result<Affinity, DecodeJWTError> {
let pas_token = self.get_pas_token().ok_or(DecodeJWTError::InvalidBase64)?;
let decoded_token: PasToken = decode_jwt(&pas_token)?;
Ok(decoded_token.affinity)
}
#[cfg(feature = "puuid")]
pub async fn fetch_pas_token<T: HttpClient>(
&self,
http_client: &T,
access_token: &str,
) -> Result<String, RequestError> {
let request = hyper::Request::builder()
.method("GET")
.header("Authorization", format!("Bearer {}", access_token))
.uri(Uri::from_static(
"https://riot-geo.pas.si.riotgames.com/pas/v1/service/chat",
))
.body(Body::empty())?;
let response = http_client.request(request).await?;
let body = hyper::body::to_bytes(response.into_body()).await?;
String::from_utf8(body.to_vec())
.map_err(|_| RequestError::ParseError("Invalid UTF-8".to_string()))
}
#[cfg(feature = "puuid")]
pub fn get_xmpp_server_address(&self) -> Option<(String, u16)> {
self.xmpp_server_address.clone()
}
#[cfg(feature = "puuid")]
pub async fn fetch_xmpp_server_address<T: HttpClient>(
&mut self,
http_client: &T,
affinity: &Affinity,
access_token: &str,
entitlements_token: &str,
) -> Result<(String, u16), RequestError> {
let affinity_str = affinity.to_string();
let mut url = Url::from_str("https://clientconfig.rpg.riotgames.com/api/v1/config/player")
.map_err(|_| RequestError::Request("Error while parsing url".to_string()))?;
let mut query_args: HashMap<&str, &str> = HashMap::new();
query_args.insert("os", "windows");
query_args.insert("region", affinity_str.as_str());
query_args.insert("app", "Riot%20Client");
query_args.insert("version", "39.0.0.3949982");
query_args.insert("patchline", "KeystoneFoundationLiveWin");
set_query_args(&mut url, query_args.into_iter());
let uri = Uri::from_str(url.as_str())
.map_err(|_| RequestError::Request("Error while parsing url".to_string()))?;
let request = hyper::Request::builder()
.method("GET")
.header(AUTHORIZATION_HEADER_KEY, format!("Bearer {}", access_token))
.header(ENTITLEMENTS_HEADER_KEY, entitlements_token)
.uri(uri)
.body(Body::empty())?;
let response = http_client.request(request).await?;
let body = hyper::body::to_bytes(response.into_body()).await?;
let xmpp_server_address_response: XMPPServerAddress = serde_json::from_slice(&body)?;
let server_address = xmpp_server_address_response
.chat_affinities
.get(affinity)
.ok_or(RequestError::ParseError("No chat affinity".to_string()))?;
let server_address = server_address.clone();
Ok((server_address, XMPP_PORT))
}
async fn get_client_version<T: HttpClient>(
&self,
http_client: &T,
) -> Result<String, RequestError> {
let request = Request::builder()
.uri("https://valorant-api.com/v1/version")
.method("GET")
.body(Body::empty())?;
let response = http_client.request(request).await?;
let body = hyper::body::to_bytes(response.into_body()).await?;
let version_response: VersionResponseBody = serde_json::from_slice(&body)?;
Ok(version_response.data.riot_client_version)
}
pub async fn refresh_access_token<T: HttpClient>(
&mut self,
http_client: &T,
) -> Result<(), RequestError> {
if let Some(expires_at) = self.exires_at {
if expires_at > OffsetDateTime::now_utc() {
return Ok(());
}
}
let reauth_cookies = match self.reauth_cookies {
Some(ref reauth_cookies) => reauth_cookies,
None => return Err(RequestError::ParseError("No reauth cookies".to_string())),
};
let query_args: Value = serde_json::from_str(COOKIES_PREPARE_DATA)?;
let query_args = query_args.as_object().ok_or(RequestError::ParseError(
"Failed to parse query args".to_string(),
))?;
let query_args = query_args
.iter()
.map(|(k, v)| (k.to_string(), v.as_str().unwrap_or("").to_string()))
.collect::<Vec<(String, String)>>();
let mut url = Url::from_str("https://auth.riotgames.com/authorize")?;
set_query_args(&mut url, query_args.into_iter());
let uri = Uri::from_str(url.as_str()).map_err(|e| RequestError::Request(e.to_string()))?;
let mut request = hyper::Request::builder()
.method("GET")
.header("Content-Type", "application/json")
.uri(uri)
.body(Body::empty())?;
Self::set_cookies(reauth_cookies, &mut request);
let response = http_client.request(request).await?;
let body = hyper::body::to_bytes(response.into_body()).await?;
let body = String::from_utf8_lossy(&body);
let redirect_uri_start =
body.find("https://playvalorant.com/opt_in")
.ok_or(RequestError::Request(
"Could not find redirect_uri".to_string(),
))?;
let redirect_uri = body.get(redirect_uri_start..).ok_or(RequestError::Request(
"Could not find redirect_uri".to_string(),
))?;
let redirect_url =
Url::from_str(redirect_uri).map_err(|e| RequestError::Request(e.to_string()))?;
let access_token = Self::extract_access_token(&redirect_url)?;
self.access_token = Some(access_token);
Ok(())
}
fn extract_access_token(url: &Url) -> Result<String, RequestError> {
let fragment = match url.fragment() {
Some(fragment) => fragment,
None => return Err(RequestError::ParseError("No fragment".to_string())),
};
let args: HashMap<String, String> = fragment
.split('&')
.filter_map(|arg| {
let mut split = arg.split('=');
let key = match split.next() {
Some(key) => key.to_string(),
None => return None,
};
let value = match split.next() {
Some(value) => value.to_string(),
None => return None,
};
Some((key, value))
})
.collect();
if !args.contains_key("access_token") {
return Err(RequestError::ParseError(
"No access token in fragment".to_string(),
));
}
let access_token = match args.get("access_token").map(|s| s.to_string()) {
Some(access_token) => access_token,
None => return Err(RequestError::ParseError("No access token".to_string())),
};
Ok(access_token)
}
fn set_cookies(cookies: &[String], request: &mut Request<Body>) {
for cookie in cookies {
let header_value = hyper::header::HeaderValue::from_str(cookie);
let header_value = match header_value {
Ok(header_value) => header_value,
Err(_) => continue,
};
request
.headers_mut()
.append(hyper::header::COOKIE, header_value);
}
}
}
impl Default for CredentialsManager {
fn default() -> Self {
Self::new()
}
}
unsafe impl Send for CredentialsManager {}
unsafe impl Sync for CredentialsManager {}