use std::sync::{Arc, LazyLock};
use parking_lot::Mutex;
use reqwest::{cookie::Jar, Url};
use steamid::SteamID;
static 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")]);
pub struct Session {
pub(crate) jar: Arc<Jar>,
pub steam_id: Option<SteamID>,
pub session_id: Option<String>,
pub(crate) mobile_access_token: Option<String>,
pub(crate) access_token: Option<String>,
pub(crate) refresh_token: Option<String>,
pub(crate) shared_secret: Mutex<Option<String>>,
pub(crate) profile_url: Mutex<Option<String>>,
pub(crate) cookie_string: String,
}
impl std::fmt::Debug for Session {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Session")
.field("steam_id", &self.steam_id)
.field("session_id", &self.session_id)
.field("mobile_access_token", &self.mobile_access_token.as_ref().map(|_| "<redacted>"))
.field("access_token", &self.access_token.as_ref().map(|_| "<redacted>"))
.field("refresh_token", &self.refresh_token.as_ref().map(|_| "<redacted>"))
.field("shared_secret", &self.shared_secret.lock().as_ref().map(|_| "<redacted>"))
.field("profile_url", &"<Mutex>")
.finish()
}
}
impl Clone for Session {
fn clone(&self) -> Self {
Self {
jar: Arc::clone(&self.jar),
steam_id: self.steam_id,
session_id: self.session_id.clone(),
mobile_access_token: self.mobile_access_token.clone(),
access_token: self.access_token.clone(),
refresh_token: self.refresh_token.clone(),
shared_secret: Mutex::new(self.shared_secret.lock().clone()),
profile_url: Mutex::new(self.profile_url.lock().clone()),
cookie_string: self.cookie_string.clone(),
}
}
}
impl Default for Session {
fn default() -> Self {
Self::new()
}
}
impl Session {
pub fn new() -> Self {
Self {
jar: Arc::new(Jar::default()),
steam_id: None,
session_id: None,
mobile_access_token: None,
access_token: None,
refresh_token: None,
shared_secret: Mutex::new(None),
profile_url: Mutex::new(None),
cookie_string: String::new(),
}
}
pub fn set_cookies(&mut self, cookies: &[&str]) -> Result<(), crate::SteamUserError> {
self.cookie_string = cookies.join("; ");
for raw_cookie in cookies {
for cookie in raw_cookie.split(';') {
let cookie = cookie.trim();
if cookie.is_empty() {
continue;
}
let name = cookie.split('=').next().unwrap_or("");
if name == "steamLoginSecure" || name == "steamLogin" {
if let Some(value) = cookie.split_once('=').map(|x| x.1) {
let decoded = urlencoding::decode(value).unwrap_or_default();
if let Some(id_str) = decoded.split("||").next() {
if let Ok(id) = id_str.parse::<u64>() {
self.steam_id = Some(SteamID::from(id));
}
}
}
}
if name == "sessionid" {
if let Some(value) = cookie.split_once('=').map(|x| x.1) {
self.session_id = Some(urlencoding::decode(value).unwrap_or_default().to_string());
}
}
for url in STEAM_URLS.iter() {
self.jar.add_cookie_str(cookie, url);
}
}
}
Ok(())
}
pub fn ensure_session_id(&mut self) -> &str {
if self.session_id.is_none() {
use rand::Rng;
let bytes: [u8; 12] = rand::rng().random();
let new_id = hex::encode(bytes);
self.session_id = Some(new_id.clone());
let cookie_str = format!("sessionid={}", new_id);
for url in STEAM_URLS.iter() {
self.jar.add_cookie_str(&cookie_str, url);
}
if !self.cookie_string.is_empty() {
self.cookie_string.push_str("; ");
}
self.cookie_string.push_str(&cookie_str);
}
self.session_id.as_deref().expect("session_id was just initialized")
}
pub fn get_session_id(&mut self) -> &str {
self.ensure_session_id()
}
pub fn get_cookie_string(&self) -> &str {
&self.cookie_string
}
pub fn get_steam_id(&self) -> SteamID {
self.steam_id.unwrap_or_default()
}
pub fn is_logged_in(&self) -> bool {
self.steam_id.is_some()
}
pub fn mobile_access_token(&self) -> Option<&str> {
self.mobile_access_token.as_deref()
}
pub fn access_token(&self) -> Option<&str> {
self.access_token.as_deref()
}
pub fn refresh_token(&self) -> Option<&str> {
self.refresh_token.as_deref()
}
pub fn set_mobile_access_token(&mut self, token: String) {
self.mobile_access_token = Some(token);
}
pub fn set_refresh_token(&mut self, token: String) {
self.refresh_token = Some(token);
}
pub fn set_access_token(&mut self, token: String) {
self.access_token = Some(token);
}
pub fn clear_profile_url(&self) {
*self.profile_url.lock() = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_new() {
let session = Session::new();
assert!(session.steam_id.is_none());
assert!(session.session_id.is_none());
assert!(!session.is_logged_in());
}
#[test]
fn test_set_cookies() {
let mut session = Session::new();
session.set_cookies(&["steamLoginSecure=76561198012345678%7C%7Ctoken123", "sessionid=abc123def456"]).unwrap();
assert!(session.is_logged_in());
assert_eq!(session.steam_id.unwrap().steam_id64(), 76561198012345678);
assert_eq!(session.session_id.as_ref().unwrap(), "abc123def456");
}
#[test]
fn test_ensure_session_id() {
let mut session = Session::new();
let id = session.ensure_session_id().to_string();
assert_eq!(id.len(), 24);
let id2 = session.ensure_session_id();
assert_eq!(id, id2);
}
}