#![cfg(feature = "puuid")]
use crate::enums::affinity::Affinity;
use crate::utils::credentials_manager::CredentialsManager;
use crate::utils::network::tlsstream_client::TLSStreamClient;
use crate::utils::network::xmpp_client::{XMPPClient, XMPPReadError};
use std::io;
use std::ops::Deref;
use std::str::FromStr;
use uuid::Uuid;
use xmlparser::{Token, Tokenizer};
#[derive(Debug)]
pub enum PuuidFetcherError {
XMPPSend(io::Error),
XMPPRead(XMPPReadError),
Parse(xmlparser::Error),
MissingToken(String),
UuidParse(uuid::Error),
MissingElement(String),
XMLVersionMismatch,
UnexpectedElement,
MaxRosterSizeReached,
}
pub struct PuuidFetcher<T: TLSStreamClient> {
pub xmpp_client: XMPPClient<T>,
}
impl<T: TLSStreamClient> PuuidFetcher<T> {
pub fn authorize(
&mut self,
credentials_manager: &CredentialsManager,
affinity: Affinity,
) -> Result<(), PuuidFetcherError> {
self.send_stream_init_message(&affinity)?;
self.send_auth_message(credentials_manager)?;
self.send_xmpp_init_message(&affinity)?;
self.send_xmpp_bind_message()?;
self.send_xmpp_session_message()?;
self.send_entitlements_token_message(credentials_manager)?;
Ok(())
}
pub fn get_puuid(&mut self, name: &str, tag: &str) -> Result<Uuid, PuuidFetcherError> {
let (puuid, jid) = self.sent_friend_request(name, tag)?;
let mut remove_retries = 3;
'retry: loop {
let result = self.remove_friend_request(&jid);
match result {
Ok(_) => {
break 'retry;
}
Err(e) => {
if remove_retries == 0 {
return Err(e);
}
remove_retries -= 1;
}
}
}
Ok(puuid)
}
fn sent_friend_request(
&mut self,
name: &str,
tag: &str,
) -> Result<(Uuid, String), PuuidFetcherError> {
let add_friend_message = format!(
"<iq id=\"roster_add_10\" type=\"set\"><query xmlns=\"jabber:iq:riotgames:roster\"><item subscription=\"pending_out\"><id name=\"{}\" tagline=\"{}\"/></item></query></iq>",
name,
tag
);
self.xmpp_client
.send_data(add_friend_message)
.map_err(PuuidFetcherError::XMPPSend)?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
let max_size_reached = Self::has_text(response.deref(), Some("max_roster_size_receiver"));
if max_size_reached.is_ok() {
return Err(PuuidFetcherError::MaxRosterSizeReached);
}
Self::check_element_start(response.first(), "", "iq")?;
Self::has_attribute(response.deref(), "", "from", None)?;
Self::has_attribute(response.deref(), "", "to", None)?;
Self::has_attribute(response.deref(), "", "id", None)?;
Self::has_attribute(response.deref(), "", "type", Some("set"))?;
Self::has_element_start(response.deref(), "", "query")?;
Self::has_attribute(response.deref(), "", "jid", None)?;
Self::has_attribute(response.deref(), "", "puuid", None)?;
Self::has_attribute(response.deref(), "", "subscription", Some("pending_out"))?;
let jid = Self::get_attribute(response.deref(), "", "jid")?;
let puuid = Self::get_attribute(response.deref(), "", "puuid")?;
let puuid = Uuid::from_str(puuid.as_str()).map_err(PuuidFetcherError::UuidParse)?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
Self::check_element_start(response.first(), "", "iq")?;
Self::has_attribute(response.deref(), "", "from", None)?;
Self::has_attribute(response.deref(), "", "to", None)?;
Self::has_attribute(response.deref(), "", "id", Some("roster_add_10"))?;
Self::has_attribute(response.deref(), "", "type", Some("result"))?;
Ok((puuid, jid))
}
fn remove_friend_request(&mut self, jid: &str) -> Result<(), PuuidFetcherError> {
let remove_friend_message = format!(
"<iq id=\"roster_remove_1\" type=\"set\"><query xmlns=\"jabber:iq:riotgames:roster\"><item subscription=\"remove\" jid=\"{}\"></item></query></iq>",
jid
);
self.xmpp_client
.send_data(remove_friend_message)
.map_err(PuuidFetcherError::XMPPSend)?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
Self::check_element_start(response.first(), "", "iq")?;
Self::has_attribute(response.deref(), "", "from", None)?;
Self::has_attribute(response.deref(), "", "to", None)?;
Self::has_attribute(response.deref(), "", "id", None)?;
let r#type = Self::get_attribute(response.deref(), "", "type")?;
if r#type == "result" {
return Ok(());
}
Self::has_element_start(response.deref(), "", "query")?;
Self::has_attribute(response.deref(), "", "jid", None)?;
Self::has_attribute(response.deref(), "", "puuid", None)?;
Self::has_attribute(response.deref(), "", "subscription", Some("remove"))?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
Self::check_element_start(response.first(), "", "iq")?;
Self::has_attribute(response.deref(), "", "from", None)?;
Self::has_attribute(response.deref(), "", "to", None)?;
Self::has_attribute(response.deref(), "", "id", Some("roster_remove_1"))?;
Self::has_attribute(response.deref(), "", "type", Some("result"))?;
Ok(())
}
fn send_stream_init_message(&mut self, affinity: &Affinity) -> Result<(), PuuidFetcherError> {
let stream_init_message = format!(
"<?xml version=\"1.0\"?><stream:stream to=\"{}.pvp.net\" version=\"1.0\" xmlns:stream=\"http://etherx.jabber.org/streams\">",
affinity
);
self.xmpp_client
.send_data(stream_init_message)
.map_err(PuuidFetcherError::XMPPSend)?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
Self::check_init_response(affinity, response)?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
Self::has_element_start(response.deref(), "stream", "features")?;
response
.iter()
.filter_map(|e| match e {
Token::Text { text } => Some(text),
_ => None,
})
.filter(|text| matches!(text.as_str(), "X-Riot-RSO" | "X-Riot-RSO-PAS"))
.count()
.eq(&2)
.then_some(())
.ok_or(PuuidFetcherError::MissingElement("Mechanism".to_string()))?;
Ok(())
}
fn send_auth_message(
&mut self,
credentials_manager: &CredentialsManager,
) -> Result<(), PuuidFetcherError> {
let access_token = match credentials_manager.get_access_token() {
Some(token) => token,
None => return Err(PuuidFetcherError::MissingToken("access_token".to_string())),
};
let pas_token = match credentials_manager.get_pas_token() {
Some(token) => token,
None => return Err(PuuidFetcherError::MissingToken("pas_token".to_string())),
};
let auth_message = format!(
"<auth mechanism=\"X-Riot-RSO-PAS\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"><rso_token>{}</rso_token><pas_token>{}</pas_token></auth>",
access_token,
pas_token
);
self.xmpp_client
.send_data(auth_message)
.map_err(PuuidFetcherError::XMPPSend)?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
Self::check_element_start(response.first(), "", "success")?;
Ok(())
}
fn send_xmpp_init_message(&mut self, affinity: &Affinity) -> Result<(), PuuidFetcherError> {
let init_message = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><stream:stream to=\"{}.pvp.net\" xml:lang=\"en\" version=\"1.0\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\">",
affinity
);
self.xmpp_client
.send_data(init_message)
.map_err(PuuidFetcherError::XMPPSend)?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
Self::check_init_response(affinity, response)?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
Self::has_element_start(response.deref(), "stream", "features")?;
Self::has_element_start(response.deref(), "", "rxep")?;
Self::has_element_start(response.deref(), "", "entitlements")?;
Self::has_element_start(response.deref(), "", "bind")?;
Self::has_element_start(response.deref(), "", "puuid-mode")?;
Self::has_element_start(response.deref(), "", "session")?;
Ok(())
}
fn send_xmpp_bind_message(&mut self) -> Result<(), PuuidFetcherError> {
let xmpp_bind_message = "<iq id=\"_xmpp_bind1\" type=\"set\"><bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"></bind></iq>".to_string();
self.xmpp_client
.send_data(xmpp_bind_message)
.map_err(PuuidFetcherError::XMPPSend)?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
Self::check_element_start(response.first(), "", "iq")?;
Self::has_attribute(response.deref(), "", "id", Some("_xmpp_bind1"))?;
Self::has_attribute(response.deref(), "", "type", Some("result"))?;
Self::has_element_start(response.deref(), "", "bind")?;
Self::has_text(response.deref(), None)?;
Ok(())
}
fn send_xmpp_session_message(&mut self) -> Result<(), PuuidFetcherError> {
let xmpp_session_message = "<iq id=\"_xmpp_session1\" type=\"set\"><session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/></iq>".to_string();
self.xmpp_client
.send_data(xmpp_session_message)
.map_err(PuuidFetcherError::XMPPSend)?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
Self::check_element_start(response.first(), "", "iq")?;
Self::has_attribute(response.deref(), "", "id", Some("_xmpp_session1"))?;
Self::has_attribute(response.deref(), "", "type", Some("result"))?;
Self::has_element_start(response.deref(), "", "session")?;
Self::has_element_start(response.deref(), "", "id")?;
Self::has_attribute(response.deref(), "", "name", None)?;
Self::has_attribute(response.deref(), "", "tagline", None)?;
Self::has_element_start(response.deref(), "", "platforms")?;
Self::has_element_start(response.deref(), "", "riot")?;
Self::has_element_start(response.deref(), "", "ts")?;
Self::has_text(response.deref(), None)?;
Ok(())
}
fn send_entitlements_token_message(
&mut self,
credentials_manager: &CredentialsManager,
) -> Result<(), PuuidFetcherError> {
let entitlements_token = match credentials_manager.get_entitlements_token() {
Some(token) => token,
None => {
return Err(PuuidFetcherError::MissingToken(
"entitlements_token".to_string(),
))
}
};
let entitlements_token_message = format!(
"<iq id=\"xmpp_entitlements_0\" type=\"set\"><entitlements xmlns=\"urn:riotgames:entitlements\"><token xmlns=\"\">{}</token></entitlements></iq>",
entitlements_token
);
self.xmpp_client
.send_data(entitlements_token_message)
.map_err(PuuidFetcherError::XMPPSend)?;
let response = self
.xmpp_client
.read_data()
.map_err(PuuidFetcherError::XMPPRead)?;
let response = Self::parse_response(&response);
Self::check_element_start(response.first(), "", "iq")?;
Self::has_attribute(response.deref(), "", "id", Some("xmpp_entitlements_0"))?;
Self::has_attribute(response.deref(), "", "type", Some("result"))?;
Self::has_element_start(response.deref(), "", "entitlements")?;
Ok(())
}
fn has_element_start(
elements: &[Token],
prefix: &str,
local: &str,
) -> Result<(), PuuidFetcherError> {
for element in elements {
let result = Self::check_element_start(Some(element), prefix, local);
if result.is_ok() {
return result;
}
}
Err(PuuidFetcherError::MissingElement(format!(
"{}:{}",
prefix, local
)))
}
fn check_element_start(
element: Option<&Token>,
prefix: &str,
local: &str,
) -> Result<(), PuuidFetcherError> {
match element {
Some(Token::ElementStart {
prefix: elem_prefix,
local: elem_local,
..
}) => {
if elem_prefix.as_str() != prefix || elem_local.as_str() != local {
return Err(PuuidFetcherError::UnexpectedElement);
}
}
_ => {
return Err(PuuidFetcherError::MissingElement(format!(
"{}:{}",
prefix, local
)))
}
}
Ok(())
}
fn has_attribute(
elements: &[Token],
prefix: &str,
local: &str,
value: Option<&str>,
) -> Result<(), PuuidFetcherError> {
for element in elements {
let result = Self::check_attribute(Some(element), prefix, local, value);
if result.is_ok() {
return result;
}
}
Err(PuuidFetcherError::MissingElement(format!(
"{}:{} = {:?}",
prefix, local, value
)))
}
fn get_attribute(
elements: &[Token],
prefix: &str,
local: &str,
) -> Result<String, PuuidFetcherError> {
for element in elements {
if let Token::Attribute {
prefix: elem_prefix,
local: elem_local,
value: elem_value,
..
} = element
{
if elem_prefix.as_str() == prefix && elem_local.as_str() == local {
return Ok(elem_value.to_string());
}
}
}
Err(PuuidFetcherError::MissingElement(format!(
"{}:{}",
prefix, local
)))
}
fn check_attribute(
element: Option<&Token>,
prefix: &str,
local: &str,
value: Option<&str>,
) -> Result<(), PuuidFetcherError> {
match element {
Some(Token::Attribute {
prefix: elem_prefix,
local: elem_local,
value: elem_value,
..
}) => {
if elem_prefix.as_str() != prefix
|| elem_local.as_str() != local
|| value.is_some_and(|v| elem_value.as_str() != v)
{
return Err(PuuidFetcherError::UnexpectedElement);
}
}
_ => {
return Err(PuuidFetcherError::MissingElement(format!(
"{}:{} = {:?}",
prefix, local, value
)))
}
}
Ok(())
}
fn has_text(elements: &[Token], value: Option<&str>) -> Result<(), PuuidFetcherError> {
for element in elements {
let result = Self::check_text(Some(element), value);
if result.is_ok() {
return result;
}
}
Err(PuuidFetcherError::MissingElement("text".to_string()))
}
fn check_text(element: Option<&Token>, value: Option<&str>) -> Result<(), PuuidFetcherError> {
match element {
Some(Token::Text { text }) => {
if value.is_some_and(|v| text.as_str() != v) {
return Err(PuuidFetcherError::UnexpectedElement);
}
}
_ => return Err(PuuidFetcherError::MissingElement("text".to_string())),
}
Ok(())
}
fn check_init_response(
affinity: &Affinity,
init_parsed: Vec<Token>,
) -> Result<(), PuuidFetcherError> {
match init_parsed.first() {
Some(Token::Declaration { version, .. }) => {
if version.as_str() != "1.0" {
return Err(PuuidFetcherError::XMLVersionMismatch);
}
}
_ => return Err(PuuidFetcherError::MissingElement("declaration".to_string())),
}
match init_parsed.get(1) {
Some(Token::ElementStart { prefix, local, .. }) => {
if prefix.as_str() != "stream" || local.as_str() != "stream" {
return Err(PuuidFetcherError::UnexpectedElement);
}
}
_ => return Err(PuuidFetcherError::MissingElement("stream".to_string())),
}
init_parsed
.iter()
.find(|t| {
if let Token::Attribute { local, value, .. } = t {
local.as_str() == "xmlns" && value.as_str() == "jabber:client"
} else {
false
}
})
.ok_or(PuuidFetcherError::MissingElement(
"xmlns:jabber:client".to_string(),
))?;
init_parsed
.iter()
.find(|t| {
if let Token::Attribute {
prefix,
local,
value,
..
} = t
{
prefix.as_str() == "xmlns"
&& local.as_str() == "stream"
&& value.as_str() == "http://etherx.jabber.org/streams"
} else {
false
}
})
.ok_or(PuuidFetcherError::MissingElement(
"xmlns:stream".to_string(),
))?;
let expected_url = format!("{}.pvp.net", affinity);
init_parsed
.iter()
.find(|t| {
if let Token::Attribute { local, value, .. } = t {
local.as_str() == "from" && value.as_str() == expected_url
} else {
false
}
})
.ok_or(PuuidFetcherError::MissingElement("from".to_string()))?;
Ok(())
}
fn parse_response(response: &str) -> Vec<Token> {
Tokenizer::from(response)
.filter_map(|t| t.ok())
.collect::<Vec<_>>()
}
}
unsafe impl<T: TLSStreamClient> Send for PuuidFetcher<T> {}
unsafe impl<T: TLSStreamClient> Sync for PuuidFetcher<T> {}