Skip to main content

rusty_lcu/
credentials.rs

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}