1use std::{env, path::PathBuf};
2
3use sysinfo::System;
4
5use crate::{Error, Result};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Credentials {
9 pub port: u16,
10 pub password: String,
11 pub protocol: String,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum CredentialsSource {
16 Auto,
17 Process,
18 DefaultLockfile,
19 Lockfile(PathBuf),
20 LockfileContent(String),
21 Manual(Credentials),
22}
23
24impl Credentials {
25 pub fn new(port: u16, password: impl Into<String>) -> Self {
26 Self {
27 port,
28 password: password.into(),
29 protocol: "https".to_string(),
30 }
31 }
32
33 pub fn base_url(&self) -> String {
34 format!("{}://127.0.0.1:{}", self.protocol, self.port)
35 }
36
37 pub fn websocket_url(&self) -> String {
38 format!("wss://127.0.0.1:{}", self.port)
39 }
40
41 pub async fn discover(source: CredentialsSource) -> Result<Self> {
42 match source {
43 CredentialsSource::Auto => Self::from_process()
44 .or_else(|_| Self::from_default_lockfile_blocking())
45 .or(Err(Error::CredentialsNotFound)),
46 CredentialsSource::Process => Self::from_process(),
47 CredentialsSource::DefaultLockfile => Self::from_default_lockfile().await,
48 CredentialsSource::Lockfile(path) => Self::from_lockfile(path).await,
49 CredentialsSource::LockfileContent(content) => Self::from_lockfile_content(&content),
50 CredentialsSource::Manual(credentials) => Ok(credentials),
51 }
52 }
53
54 pub async fn from_default_lockfile() -> Result<Self> {
55 let path = default_lockfile_path().ok_or(Error::CredentialsNotFound)?;
56 Self::from_lockfile(path).await
57 }
58
59 pub async fn from_lockfile(path: impl Into<PathBuf>) -> Result<Self> {
60 let content = tokio::fs::read_to_string(path.into()).await?;
61 Self::from_lockfile_content(&content)
62 }
63
64 pub fn from_process() -> Result<Self> {
65 let system = System::new_all();
66
67 for process in system.processes().values() {
68 let name = process.name().to_string_lossy();
69 let command_line = process
70 .cmd()
71 .iter()
72 .map(|part| part.to_string_lossy())
73 .collect::<Vec<_>>()
74 .join(" ");
75
76 if !is_league_client_process(&name, &command_line) {
77 continue;
78 }
79
80 if let Some(credentials) = parse_credentials_from_command_line(&command_line) {
81 return Ok(credentials);
82 }
83 }
84
85 Err(Error::CredentialsNotFound)
86 }
87
88 pub fn from_lockfile_content(content: &str) -> Result<Self> {
89 let parts = content.trim().split(':').collect::<Vec<_>>();
90 if parts.len() < 5 {
91 return Err(Error::InvalidLockfile);
92 }
93
94 let port = parts[2].parse().map_err(|_| Error::InvalidLockfile)?;
95 Ok(Self {
96 port,
97 password: parts[3].to_string(),
98 protocol: parts[4].to_string(),
99 })
100 }
101
102 fn from_default_lockfile_blocking() -> Result<Self> {
103 let path = default_lockfile_path().ok_or(Error::CredentialsNotFound)?;
104 let content = std::fs::read_to_string(path)?;
105 Self::from_lockfile_content(&content)
106 }
107}
108
109fn is_league_client_process(name: &str, command_line: &str) -> bool {
110 name.eq_ignore_ascii_case("LeagueClientUx.exe")
111 || name.eq_ignore_ascii_case("LeagueClientUx")
112 || command_line.contains("LeagueClientUx")
113}
114
115fn parse_credentials_from_command_line(command_line: &str) -> Option<Credentials> {
116 let port = argument_value(command_line, "--app-port")?.parse().ok()?;
117 let password = argument_value(command_line, "--remoting-auth-token")?;
118
119 Some(Credentials {
120 port,
121 password,
122 protocol: "https".to_string(),
123 })
124}
125
126fn argument_value(command_line: &str, name: &str) -> Option<String> {
127 let parts = command_line
128 .split_whitespace()
129 .map(|part| part.trim_matches('"'))
130 .collect::<Vec<_>>();
131
132 for (index, part) in parts.iter().enumerate() {
133 if let Some(value) = part.strip_prefix(&format!("{name}=")) {
134 return Some(value.trim_matches('"').to_string());
135 }
136
137 if *part == name {
138 return parts
139 .get(index + 1)
140 .map(|value| value.trim_matches('"').to_string());
141 }
142 }
143
144 None
145}
146
147fn default_lockfile_path() -> Option<PathBuf> {
148 if let Ok(path) = env::var("RUSTY_LCU_LOCKFILE") {
149 return Some(PathBuf::from(path));
150 }
151
152 #[cfg(windows)]
153 {
154 let drive = env::var("SystemDrive").unwrap_or_else(|_| "C:".to_string());
155 Some(PathBuf::from(format!(
156 r"{drive}\Riot Games\League of Legends\lockfile"
157 )))
158 }
159
160 #[cfg(not(windows))]
161 {
162 None
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn parses_lockfile_content() {
172 let credentials =
173 Credentials::from_lockfile_content("LeagueClient:1234:51111:secret:https").unwrap();
174
175 assert_eq!(credentials.port, 51111);
176 assert_eq!(credentials.password, "secret");
177 assert_eq!(credentials.protocol, "https");
178 }
179
180 #[test]
181 fn parses_process_arguments_with_equals() {
182 let credentials = parse_credentials_from_command_line(
183 r#"LeagueClientUx.exe --app-port=51111 --remoting-auth-token=secret"#,
184 )
185 .unwrap();
186
187 assert_eq!(credentials, Credentials::new(51111, "secret"));
188 }
189
190 #[test]
191 fn parses_process_arguments_with_spaces() {
192 let credentials = parse_credentials_from_command_line(
193 r#"LeagueClientUx.exe --app-port 51111 --remoting-auth-token secret"#,
194 )
195 .unwrap();
196
197 assert_eq!(credentials, Credentials::new(51111, "secret"));
198 }
199}