1use 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 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 pub fn insecure(mut self, value: bool) -> Self {
48 self.insecure = value;
49 self
50 }
51
52 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 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 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}