league_client/
client.rs

1//! Client creates a http_client and a socket connection to the LCU server.
2//!
3//! ```rust
4//! use league_client;
5//!
6//! async fn create_connection() -> Result<league_client::Client, league_client::Error> {
7//!     let client = league_client::Client::builder()?.insecure(true).build()?;
8//!     Ok(client)
9//! }
10//! ```
11
12use std::process;
13
14use base64::prelude::*;
15use tungstenite::client::IntoClientRequest;
16
17use super::{Error, LCResult as Result};
18
19#[derive(Default, Debug)]
20pub struct ClientBuilder {
21    token: String,
22    port: String,
23    insecure: bool,
24}
25
26impl ClientBuilder {
27    /// Attempts to look for the LeagueClientUx process.
28    ///
29    /// - Uses ps and grep if you're in the linux family.
30    /// - Uses wmic if on the windows family.
31    ///
32    /// If it finds it, it will grab the token and port from the args.
33    /// Set insecure to true to avoid having to pass in the riot key.
34    pub fn from_process() -> Result<Self> {
35        let processes = from_process("LeagueClientUx").ok_or(Error::AppNotRunning)?;
36        let process = processes.get(0).ok_or(Error::AppNotRunning)?;
37        let (token, port) = parse_process(process)?;
38
39        Ok(Self {
40            token,
41            port,
42            ..Default::default()
43        })
44    }
45
46    /// Skip cert check.
47    pub fn insecure(mut self, value: bool) -> Self {
48        self.insecure = value;
49        self
50    }
51
52    /// Consumes the builder and returns a [Client]
53    pub fn build(self) -> Result<Client> {
54        let basic = self.auth();
55        let http_client = self.reqwest_client()?;
56        let connector = crate::connector::Connector::builder()
57            .insecure(self.insecure)
58            .build()?;
59
60        let addr = format!("127.0.0.1:{}", self.port);
61
62        Ok(Client {
63            basic,
64            connector,
65            addr,
66            http: http_client,
67        })
68    }
69
70    fn auth(&self) -> String {
71        let auth = format!("riot:{}", self.token);
72        format!("Basic {}", BASE64_STANDARD.encode(auth))
73    }
74
75    fn reqwest_client(&self) -> Result<reqwest::Client> {
76        let mut headers = reqwest::header::HeaderMap::new();
77        let mut auth = reqwest::header::HeaderValue::from_str(&self.auth())
78            .map_err(|e| Error::HttpClientCreation(e.to_string()))?;
79        auth.set_sensitive(true);
80
81        headers.insert(reqwest::header::AUTHORIZATION, auth);
82
83        let mut client_builder = reqwest::Client::builder().default_headers(headers);
84
85        if self.insecure {
86            client_builder = client_builder.danger_accept_invalid_certs(true);
87        }
88
89        client_builder
90            .build()
91            .map_err(|e| Error::HttpClientCreation(e.to_string()))
92    }
93}
94
95pub struct Client {
96    basic: String,
97    connector: crate::connector::Connector,
98    http: reqwest::Client,
99
100    pub addr: String,
101}
102
103impl Client {
104    pub fn builder() -> Result<ClientBuilder> {
105        ClientBuilder::from_process()
106    }
107
108    /// Connect to the LCU client. Returns a socket connection aliased as [Connected](`crate::connector::Connected`).
109    pub async fn connect_to_socket(&self) -> Result<crate::connector::Connected> {
110        let mut req = format!("wss://{}", &self.addr)
111            .into_client_request()
112            .map_err(|e| Error::WebsocketRequest(e.to_string()))?;
113
114        let auth = self.basic.clone();
115        let headers = req.headers_mut();
116
117        headers.insert(
118            "authorization",
119            auth.parse()
120                .map_err(|_| Error::WebsocketRequest("failed to createa an auth header".into()))?,
121        );
122
123        self.connector.connect(req).await
124    }
125
126    /// Gives back a copy of the reqwest client. [Read More](https://docs.rs/reqwest/latest/reqwest/)
127    pub fn http_client(&self) -> reqwest::Client {
128        self.http.clone()
129    }
130}
131
132#[cfg(target_family = "unix")]
133fn from_process(process: &str) -> Option<Vec<String>> {
134    let ps = process::Command::new("ps")
135        .args(["x", "-A", "-o args"])
136        .stdout(process::Stdio::piped())
137        .spawn()
138        .ok()?;
139
140    let mut grep = process::Command::new("grep");
141    grep.arg(process).stdin(ps.stdout?);
142
143    let output = String::from_utf8(grep.output().ok()?.stdout).ok()?;
144    let lines = output.lines();
145
146    let lines: Vec<String> = lines
147        .filter(|x| x.contains("--app-port") && x.contains("--remoting-auth-token"))
148        .map(String::from)
149        .collect();
150
151    Some(lines)
152}
153
154#[cfg(target_family = "windows")]
155fn from_process(process: &str) -> Option<Vec<String>> {
156    let wmic = process::Command::new("WMIC")
157        .args(["path", "win32_process", "get", "Caption,Commandline"])
158        .stdout(process::Stdio::piped())
159        .spawn()
160        .ok()?;
161
162    let process_exe = format!("{}.exe", process);
163
164    let mut findstr = process::Command::new("findstr");
165    findstr.args(["/R", &process_exe]).stdin(wmic.stdout?);
166
167    let output = String::from_utf8(findstr.output().ok()?.stdout).ok()?;
168    let lines = output.lines();
169
170    let lines: Vec<String> = lines
171        .filter(|x| x.contains("--app-port") && x.contains("--remoting-auth-token"))
172        .map(String::from)
173        .collect();
174
175    Some(lines)
176}
177
178fn parse_process(value: &str) -> Result<(String, String)> {
179    let re = regex::Regex::new(r#"--remoting-auth-token="?([\w]*)"?.*--app-port="?([0-9]*)"?"#)
180        .or(Err(Error::AppNotRunning))?;
181    let caps = re.captures(value);
182    let caps = caps.ok_or(Error::AppNotRunning)?;
183
184    let token: String = caps
185        .get(1)
186        .ok_or(Error::AppNotRunning)?
187        .as_str()
188        .to_string();
189    let port: String = caps
190        .get(2)
191        .ok_or(Error::AppNotRunning)?
192        .as_str()
193        .to_string();
194
195    Ok((token, port))
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn client_from_string() {
204        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"#;
205
206        let (token, port) = parse_process(example).expect("usable client");
207        assert_eq!(port, "12345".to_string());
208        assert_eq!(token, "token".to_string())
209    }
210}