1use std::sync::{Arc, LazyLock};
4
5use parking_lot::Mutex;
6use reqwest::{cookie::Jar, Url};
7use steamid::SteamID;
8
9static STEAM_URLS: LazyLock<[Url; 4]> = LazyLock::new(|| ["https://steamcommunity.com".parse().expect("valid Steam URL"), "https://store.steampowered.com".parse().expect("valid Steam URL"), "https://help.steampowered.com".parse().expect("valid Steam URL"), "https://api.steampowered.com".parse().expect("valid Steam URL")]);
11
12pub struct Session {
16 pub(crate) jar: Arc<Jar>,
18 pub steam_id: Option<SteamID>,
20 pub session_id: Option<String>,
22 pub(crate) mobile_access_token: Option<String>,
24 pub(crate) access_token: Option<String>,
26 pub(crate) refresh_token: Option<String>,
28 pub(crate) shared_secret: Mutex<Option<String>>,
31 pub(crate) profile_url: Mutex<Option<String>>,
33 pub(crate) cookie_string: String,
35}
36
37impl std::fmt::Debug for Session {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("Session")
40 .field("steam_id", &self.steam_id)
41 .field("session_id", &self.session_id)
42 .field("mobile_access_token", &self.mobile_access_token.as_ref().map(|_| "<redacted>"))
43 .field("access_token", &self.access_token.as_ref().map(|_| "<redacted>"))
44 .field("refresh_token", &self.refresh_token.as_ref().map(|_| "<redacted>"))
45 .field("shared_secret", &self.shared_secret.lock().as_ref().map(|_| "<redacted>"))
46 .field("profile_url", &"<Mutex>")
47 .finish()
48 }
49}
50
51impl Clone for Session {
52 fn clone(&self) -> Self {
53 Self {
54 jar: Arc::clone(&self.jar),
55 steam_id: self.steam_id,
56 session_id: self.session_id.clone(),
57 mobile_access_token: self.mobile_access_token.clone(),
58 access_token: self.access_token.clone(),
59 refresh_token: self.refresh_token.clone(),
60 shared_secret: Mutex::new(self.shared_secret.lock().clone()),
61 profile_url: Mutex::new(self.profile_url.lock().clone()),
62 cookie_string: self.cookie_string.clone(),
63 }
64 }
65}
66
67impl Default for Session {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl Session {
74 pub fn new() -> Self {
76 Self {
77 jar: Arc::new(Jar::default()),
78 steam_id: None,
79 session_id: None,
80 mobile_access_token: None,
81 access_token: None,
82 refresh_token: None,
83 shared_secret: Mutex::new(None),
84 profile_url: Mutex::new(None),
85 cookie_string: String::new(),
86 }
87 }
88
89 pub fn set_cookies(&mut self, cookies: &[&str]) -> Result<(), crate::SteamUserError> {
93 self.cookie_string = cookies.join("; ");
95
96 for raw_cookie in cookies {
97 for cookie in raw_cookie.split(';') {
98 let cookie = cookie.trim();
99 if cookie.is_empty() {
100 continue;
101 }
102
103 let name = cookie.split('=').next().unwrap_or("");
105
106 if name == "steamLoginSecure" || name == "steamLogin" {
108 if let Some(value) = cookie.split_once('=').map(|x| x.1) {
109 let decoded = urlencoding::decode(value).unwrap_or_default();
111 if let Some(id_str) = decoded.split("||").next() {
112 if let Ok(id) = id_str.parse::<u64>() {
113 self.steam_id = Some(SteamID::from(id));
114 }
115 }
116 }
117 }
118
119 if name == "sessionid" {
121 if let Some(value) = cookie.split_once('=').map(|x| x.1) {
122 self.session_id = Some(urlencoding::decode(value).unwrap_or_default().to_string());
123 }
124 }
125
126 for url in STEAM_URLS.iter() {
128 self.jar.add_cookie_str(cookie, url);
129 }
130 }
131 }
132
133 Ok(())
134 }
135
136 pub fn ensure_session_id(&mut self) -> &str {
137 if self.session_id.is_none() {
138 use rand::Rng;
139 let bytes: [u8; 12] = rand::rng().random();
140 let new_id = hex::encode(bytes);
141 self.session_id = Some(new_id.clone());
142
143 let cookie_str = format!("sessionid={}", new_id);
145 for url in STEAM_URLS.iter() {
146 self.jar.add_cookie_str(&cookie_str, url);
147 }
148
149 if !self.cookie_string.is_empty() {
151 self.cookie_string.push_str("; ");
152 }
153 self.cookie_string.push_str(&cookie_str);
154 }
155 self.session_id.as_deref().expect("session_id was just initialized")
156 }
157
158 pub fn get_session_id(&mut self) -> &str {
160 self.ensure_session_id()
161 }
162
163 pub fn get_cookie_string(&self) -> &str {
165 &self.cookie_string
166 }
167
168 pub fn get_steam_id(&self) -> SteamID {
170 self.steam_id.unwrap_or_default()
171 }
172
173 pub fn is_logged_in(&self) -> bool {
175 self.steam_id.is_some()
176 }
177
178 pub fn mobile_access_token(&self) -> Option<&str> {
180 self.mobile_access_token.as_deref()
181 }
182
183 pub fn access_token(&self) -> Option<&str> {
185 self.access_token.as_deref()
186 }
187
188 pub fn refresh_token(&self) -> Option<&str> {
190 self.refresh_token.as_deref()
191 }
192
193 pub fn set_mobile_access_token(&mut self, token: String) {
195 self.mobile_access_token = Some(token);
196 }
197
198 pub fn set_refresh_token(&mut self, token: String) {
200 self.refresh_token = Some(token);
201 }
202
203 pub fn set_access_token(&mut self, token: String) {
205 self.access_token = Some(token);
206 }
207
208 pub fn clear_profile_url(&self) {
210 *self.profile_url.lock() = None;
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_session_new() {
220 let session = Session::new();
221 assert!(session.steam_id.is_none());
222 assert!(session.session_id.is_none());
223 assert!(!session.is_logged_in());
224 }
225
226 #[test]
227 fn test_set_cookies() {
228 let mut session = Session::new();
229 session.set_cookies(&["steamLoginSecure=76561198012345678%7C%7Ctoken123", "sessionid=abc123def456"]).unwrap();
230
231 assert!(session.is_logged_in());
232 assert_eq!(session.steam_id.unwrap().steam_id64(), 76561198012345678);
233 assert_eq!(session.session_id.as_ref().unwrap(), "abc123def456");
234 }
235
236 #[test]
237 fn test_ensure_session_id() {
238 let mut session = Session::new();
239 let id = session.ensure_session_id().to_string();
240 assert_eq!(id.len(), 24); let id2 = session.ensure_session_id();
244 assert_eq!(id, id2);
245 }
246}