//! Client creates a http_client and a socket connection to the LCU server.
//!
//! ```rust
//! use league_client;
//!
//! async fn create_connection() -> Result<league_client::Client, league_client::Error> {
//! let client = league_client::Client::builder()?.insecure(true).build()?;
//! Ok(client)
//! }
//! ```
use std::process;
use base64::prelude::*;
use tungstenite::client::IntoClientRequest;
use super::{Error, LCResult as Result};
#[derive(Default, Debug)]
pub struct ClientBuilder {
token: String,
port: String,
insecure: bool,
}
impl ClientBuilder {
/// Attempts to look for the LeagueClientUx process.
///
/// - Uses ps and grep if you're in the linux family.
/// - Uses wmic if on the windows family.
///
/// If it finds it, it will grab the token and port from the args.
/// Set insecure to true to avoid having to pass in the riot key.
pub fn from_process() -> Result<Self> {
let processes = from_process("LeagueClientUx").ok_or(Error::AppNotRunning)?;
let process = processes.get(0).ok_or(Error::AppNotRunning)?;
let (token, port) = parse_process(process)?;
Ok(Self {
token,
port,
..Default::default()
})
}
/// Skip cert check.
pub fn insecure(mut self, value: bool) -> Self {
self.insecure = value;
self
}
/// Consumes the builder and returns a [Client]
pub fn build(self) -> Result<Client> {
let basic = self.auth();
let http_client = self.reqwest_client()?;
let connector = crate::connector::Connector::builder()
.insecure(self.insecure)
.build()?;
let addr = format!("127.0.0.1:{}", self.port);
Ok(Client {
basic,
connector,
addr,
http: http_client,
})
}
fn auth(&self) -> String {
let auth = format!("riot:{}", self.token);
format!("Basic {}", BASE64_STANDARD.encode(auth))
}
fn reqwest_client(&self) -> Result<reqwest::Client> {
let mut headers = reqwest::header::HeaderMap::new();
let mut auth = reqwest::header::HeaderValue::from_str(&self.auth())
.map_err(|e| Error::HttpClientCreation(e.to_string()))?;
auth.set_sensitive(true);
headers.insert(reqwest::header::AUTHORIZATION, auth);
let mut client_builder = reqwest::Client::builder().default_headers(headers);
if self.insecure {
client_builder = client_builder.danger_accept_invalid_certs(true);
}
client_builder
.build()
.map_err(|e| Error::HttpClientCreation(e.to_string()))
}
}
pub struct Client {
basic: String,
connector: crate::connector::Connector,
http: reqwest::Client,
pub addr: String,
}
impl Client {
pub fn builder() -> Result<ClientBuilder> {
ClientBuilder::from_process()
}
/// Connect to the LCU client. Returns a socket connection aliased as [Connected](`crate::connector::Connected`).
pub async fn connect_to_socket(&self) -> Result<crate::connector::Connected> {
let mut req = format!("wss://{}", &self.addr)
.into_client_request()
.map_err(|e| Error::WebsocketRequest(e.to_string()))?;
let auth = self.basic.clone();
let headers = req.headers_mut();
headers.insert(
"authorization",
auth.parse()
.map_err(|_| Error::WebsocketRequest("failed to createa an auth header".into()))?,
);
self.connector.connect(req).await
}
/// Gives back a copy of the reqwest client. [Read More](https://docs.rs/reqwest/latest/reqwest/)
pub fn http_client(&self) -> reqwest::Client {
self.http.clone()
}
}
#[cfg(target_family = "unix")]
fn from_process(process: &str) -> Option<Vec<String>> {
let ps = process::Command::new("ps")
.args(["x", "-A", "-o args"])
.stdout(process::Stdio::piped())
.spawn()
.ok()?;
let mut grep = process::Command::new("grep");
grep.arg(process).stdin(ps.stdout?);
let output = String::from_utf8(grep.output().ok()?.stdout).ok()?;
let lines = output.lines();
let lines: Vec<String> = lines
.filter(|x| x.contains("--app-port") && x.contains("--remoting-auth-token"))
.map(String::from)
.collect();
Some(lines)
}
#[cfg(target_family = "windows")]
fn from_process(process: &str) -> Option<Vec<String>> {
let wmic = process::Command::new("WMIC")
.args(["path", "win32_process", "get", "Caption,Commandline"])
.stdout(process::Stdio::piped())
.spawn()
.ok()?;
let process_exe = format!("{}.exe", process);
let mut findstr = process::Command::new("findstr");
findstr.args(["/R", &process_exe]).stdin(wmic.stdout?);
let output = String::from_utf8(findstr.output().ok()?.stdout).ok()?;
let lines = output.lines();
let lines: Vec<String> = lines
.filter(|x| x.contains("--app-port") && x.contains("--remoting-auth-token"))
.map(String::from)
.collect();
Some(lines)
}
fn parse_process(value: &str) -> Result<(String, String)> {
let re = regex::Regex::new(r#"--remoting-auth-token="?([\w]*)"?.*--app-port="?([0-9]*)"?"#)
.or(Err(Error::AppNotRunning))?;
let caps = re.captures(value);
let caps = caps.ok_or(Error::AppNotRunning)?;
let token: String = caps
.get(1)
.ok_or(Error::AppNotRunning)?
.as_str()
.to_string();
let port: String = caps
.get(2)
.ok_or(Error::AppNotRunning)?
.as_str()
.to_string();
Ok((token, port))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_from_string() {
let example = r#"/Applications/League of Legends.app/Contents/LoL/League of Legends.app/Contents/MacOS/LeagueClientUx --riotclient-auth-token=token --riotclient-app-port=12345 --no-rads --disable-self-update --region=NA --locale=en_US --client-config-url=https://clientconfig.rpg.riotgames.com --riotgamesapi-standalone --riotgamesapi-settings=token --rga-lite --remoting-auth-token=token --app-port=12345 --install-directory=/Applications/League of Legends.app/Contents/LoL --app-name=LeagueClient --ux-name=LeagueClientUx --ux-helper-name=LeagueClientUxHelper --log-dir=LeagueClient Logs --crash-reporting=crashpad --crash-environment=NA1 --app-log-file-path=/Applications/League of Legends.app/Contents/LoL/Logs/LeagueClient Logs/2024-03-09T14-52-20_5736_LeagueClient.log --app-pid=5736 --output-base-dir=/Applications/League of Legends.app/Contents/LoL --no-proxy-server --ignore-certificate-errors"#;
let (token, port) = parse_process(example).expect("usable client");
assert_eq!(port, "12345".to_string());
assert_eq!(token, "token".to_string())
}
}