#![allow(dead_code)]
use prost::Message;
use steam_enums::EMsg;
use steam_protos::{CMsgClientLogon, CMsgIPAddress, PRIVATE_IP_OBFUSCATION_MASK, PROTOCOL_VERSION};
use super::{ProtobufMessageHeader, SteamMessage};
use crate::types::LogOnDetails;
#[derive(Debug, Clone, Default)]
pub struct LogonConfig {
pub protocol_version: u32,
pub cell_id: Option<u32>,
pub logon_id: Option<u32>,
pub machine_name: Option<String>,
pub identifier: Option<String>,
}
impl LogonConfig {
pub fn new() -> Self {
Self { protocol_version: PROTOCOL_VERSION, ..Default::default() }
}
pub fn with_cell_id(mut self, cell_id: Option<u32>) -> Self {
self.cell_id = cell_id;
self
}
pub fn with_logon_id(mut self, logon_id: Option<u32>) -> Self {
self.logon_id = logon_id;
self
}
pub fn with_machine_name(mut self, name: Option<String>) -> Self {
self.machine_name = name;
self
}
pub fn with_identifier(mut self, identifier: Option<String>) -> Self {
self.identifier = identifier;
self
}
}
pub fn build_client_logon(config: &LogonConfig, details: &LogOnDetails, is_anonymous: bool) -> CMsgClientLogon {
let mut logon = CMsgClientLogon::default();
let is_web_logon = details.web_logon_token.is_some() && details.steam_id.is_some();
logon.protocol_version = Some(config.protocol_version);
logon.chat_mode = Some(2);
if is_web_logon {
logon.client_os_type = Some(4294966596); logon.ui_mode = Some(4);
logon.web_logon_nonce = details.web_logon_token.clone();
logon.account_name = details.account_name.clone();
} else {
logon.client_os_type = Some(16); logon.cell_id = config.cell_id;
if let Some(logon_id) = details.logon_id.or(config.logon_id) {
logon.obfuscated_private_ip = Some(CMsgIPAddress { ip: Some(steam_protos::cmsg_ip_address::Ip::V4(logon_id ^ PRIVATE_IP_OBFUSCATION_MASK)) });
} else {
logon.obfuscated_private_ip = Some(CMsgIPAddress { ip: Some(steam_protos::cmsg_ip_address::Ip::V4(0)) });
}
if is_anonymous {
logon.anon_user_target_account_name = Some("anonymous".to_string());
logon.client_language = Some(String::new());
logon.ping_ms_from_cell_search = Some(0);
logon.machine_name = Some(String::new());
} else {
logon.client_language = Some("english".to_string());
logon.ping_ms_from_cell_search = Some(4 + 16);
if let Some(ref token) = details.refresh_token {
logon.access_token = Some(token.clone());
logon.should_remember_password = Some(true);
} else {
logon.account_name = details.account_name.clone();
logon.password = details.password.clone();
logon.auth_code = details.auth_code.clone();
logon.two_factor_code = details.two_factor_code.clone();
}
if let Some(ref name) = details.machine_name.as_ref().or(config.machine_name.as_ref()) {
logon.machine_name = Some(name.to_string());
}
if let Some(ref mid) = details.machine_id {
logon.machine_id = Some(mid.clone());
} else if let Some(ref identifier) = config.identifier {
logon.machine_id = Some(steam_auth::helpers::create_machine_id(identifier));
}
}
logon.supports_rate_limit_response = Some(!is_anonymous);
}
logon
}
pub fn build_proto_header(session_id: i32, steam_id: u64, job_id_source: u64, job_id_target: u64) -> ProtobufMessageHeader {
ProtobufMessageHeader {
header_length: 0, session_id,
steam_id,
job_id_source,
job_id_target,
target_job_name: None,
routing_appid: None,
}
}
pub fn build_proto_header_with_job_name(session_id: i32, steam_id: u64, job_id_source: u64, target_job_name: String) -> ProtobufMessageHeader {
ProtobufMessageHeader {
header_length: 0,
session_id,
steam_id,
job_id_source,
job_id_target: u64::MAX,
target_job_name: Some(target_job_name),
routing_appid: None,
}
}
pub fn create_steam_message<T: Message>(msg: EMsg, header: ProtobufMessageHeader, body: &T) -> SteamMessage {
SteamMessage::new_proto(msg, header, body)
}
#[derive(Debug, Clone, Default)]
pub struct ParsedLogonResponse {
pub eresult: i32,
pub steam_id: Option<u64>,
pub public_ip: Option<String>,
pub cell_id: Option<u32>,
pub vanity_url: Option<String>,
pub email_domain: Option<String>,
}
pub fn parse_logon_response(response: &steam_protos::CMsgClientLogonResponse) -> ParsedLogonResponse {
let public_ip = response.public_ip.as_ref().and_then(|ip| if let Some(steam_protos::cmsg_ip_address::Ip::V4(v4)) = &ip.ip { Some(format!("{}.{}.{}.{}", (v4 >> 24) & 0xFF, (v4 >> 16) & 0xFF, (v4 >> 8) & 0xFF, v4 & 0xFF)) } else { None });
ParsedLogonResponse {
eresult: response.eresult.unwrap_or(2),
steam_id: response.client_supplied_steamid,
public_ip,
cell_id: response.cell_id,
vanity_url: response.vanity_url.clone(),
email_domain: response.email_domain.clone(),
}
}
#[derive(Debug, Clone, Default)]
pub struct ParsedFriendEntry {
pub steam_id: u64,
pub relationship: i32,
}
pub fn parse_friends_list(response: &steam_protos::CMsgClientFriendsList) -> Vec<ParsedFriendEntry> {
response.friends.iter().map(|f| ParsedFriendEntry { steam_id: f.ulfriendid.unwrap_or(0), relationship: f.efriendrelationship.unwrap_or(0) as i32 }).collect()
}
#[derive(Debug, Clone, Default)]
pub struct ParsedPersonaState {
pub steam_id: u64,
pub player_name: Option<String>,
pub persona_state: i32,
pub avatar_hash: Option<Vec<u8>>,
pub game_name: Option<String>,
pub game_id: Option<u64>,
pub last_logoff: Option<u32>,
pub last_logon: Option<u32>,
}
pub fn parse_persona_state(response: &steam_protos::CMsgClientPersonaState) -> Vec<ParsedPersonaState> {
response
.friends
.iter()
.map(|f| ParsedPersonaState {
steam_id: f.friendid.unwrap_or(0),
player_name: f.player_name.clone(),
persona_state: f.persona_state.unwrap_or(0) as i32,
avatar_hash: f.avatar_hash.clone(),
game_name: f.game_name.clone(),
game_id: f.gameid,
last_logoff: f.last_logoff,
last_logon: f.last_logon,
})
.collect()
}
#[derive(Debug, Clone, Default)]
pub struct ParsedAccountInfo {
pub persona_name: Option<String>,
pub country: Option<String>,
pub is_phone_verified: bool,
pub two_factor_state: u32,
pub account_flags: u32,
}
pub fn parse_account_info(response: &steam_protos::CMsgClientAccountInfo) -> ParsedAccountInfo {
ParsedAccountInfo {
persona_name: response.persona_name.clone(),
country: response.ip_country.clone(),
is_phone_verified: response.is_phone_verified.unwrap_or(false),
two_factor_state: response.two_factor_state.unwrap_or(0),
account_flags: response.account_flags.unwrap_or(0),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_client_logon_anonymous() {
let config = LogonConfig::new();
let details = LogOnDetails { anonymous: true, ..Default::default() };
let logon = build_client_logon(&config, &details, true);
assert_eq!(logon.anon_user_target_account_name, Some("anonymous".to_string()));
assert_eq!(logon.protocol_version, Some(PROTOCOL_VERSION));
assert_eq!(logon.client_os_type, Some(16));
assert_eq!(logon.chat_mode, Some(2));
assert_eq!(logon.supports_rate_limit_response, Some(false));
assert!(logon.access_token.is_none());
assert!(logon.account_name.is_none());
}
#[test]
fn test_build_client_logon_with_token() {
let config = LogonConfig::new();
let details = LogOnDetails { refresh_token: Some("test_token_12345".to_string()), ..Default::default() };
let logon = build_client_logon(&config, &details, false);
assert_eq!(logon.access_token, Some("test_token_12345".to_string()));
assert_eq!(logon.should_remember_password, Some(true));
assert_eq!(logon.supports_rate_limit_response, Some(true));
assert!(logon.anon_user_target_account_name.is_none());
}
#[test]
fn test_build_client_logon_with_password() {
let config = LogonConfig::new();
let details = LogOnDetails {
account_name: Some("testuser".to_string()),
password: Some("testpass".to_string()),
auth_code: Some("ABC123".to_string()),
..Default::default()
};
let logon = build_client_logon(&config, &details, false);
assert_eq!(logon.account_name, Some("testuser".to_string()));
assert_eq!(logon.password, Some("testpass".to_string()));
assert_eq!(logon.auth_code, Some("ABC123".to_string()));
assert!(logon.access_token.is_none());
}
#[test]
fn test_build_client_logon_with_cell_id() {
let config = LogonConfig::new().with_cell_id(Some(42));
let details = LogOnDetails::default();
let logon = build_client_logon(&config, &details, true);
assert_eq!(logon.cell_id, Some(42));
}
#[test]
fn test_build_client_logon_with_logon_id() {
let config = LogonConfig::new().with_logon_id(Some(0x12345678));
let details = LogOnDetails::default();
let logon = build_client_logon(&config, &details, true);
assert!(logon.obfuscated_private_ip.is_some());
if let Some(ip) = &logon.obfuscated_private_ip {
if let Some(steam_protos::cmsg_ip_address::Ip::V4(v4)) = &ip.ip {
assert_eq!(*v4, 0x12345678 ^ PRIVATE_IP_OBFUSCATION_MASK);
} else {
panic!("Expected V4 IP");
}
}
}
#[test]
fn test_build_proto_header() {
let header = build_proto_header(12345, 76561198012345678, u64::MAX, u64::MAX);
assert_eq!(header.session_id, 12345);
assert_eq!(header.steam_id, 76561198012345678);
assert_eq!(header.job_id_source, u64::MAX);
assert_eq!(header.job_id_target, u64::MAX);
assert!(header.target_job_name.is_none());
}
#[test]
fn test_build_proto_header_with_job_id() {
let header = build_proto_header(999, 76561198000000000, 42, u64::MAX);
assert_eq!(header.session_id, 999);
assert_eq!(header.job_id_source, 42);
assert_eq!(header.job_id_target, u64::MAX);
}
#[test]
fn test_build_proto_header_with_job_name() {
let header = build_proto_header_with_job_name(12345, 76561198012345678, 42, "FriendsList.GetFriendsList#1".to_string());
assert_eq!(header.session_id, 12345);
assert_eq!(header.job_id_source, 42);
assert_eq!(header.target_job_name, Some("FriendsList.GetFriendsList#1".to_string()));
}
#[test]
fn test_create_steam_message() {
use steam_protos::CMsgClientHello;
let header = build_proto_header(12345, 0, u64::MAX, u64::MAX);
let body = CMsgClientHello { protocol_version: Some(PROTOCOL_VERSION) };
let msg = create_steam_message(EMsg::ClientHello, header, &body);
assert_eq!(msg.msg, EMsg::ClientHello);
assert!(msg.is_proto);
assert!(!msg.body.is_empty());
}
#[test]
fn test_create_steam_message_roundtrip() {
use steam_protos::CMsgClientHello;
let header = build_proto_header(12345, 76561198012345678, 42, 99);
let body = CMsgClientHello { protocol_version: Some(65580) };
let msg = create_steam_message(EMsg::ClientHello, header, &body);
let encoded = msg.encode();
let decoded = SteamMessage::decode_from_bytes(&encoded).expect("Failed to decode");
assert_eq!(decoded.msg, EMsg::ClientHello);
assert!(decoded.is_proto);
let decoded_body: CMsgClientHello = decoded.decode_body().expect("Failed to decode body");
assert_eq!(decoded_body.protocol_version, Some(65580));
}
#[test]
fn test_parse_logon_response() {
let response = steam_protos::CMsgClientLogonResponse {
eresult: Some(1),
client_supplied_steamid: Some(76561198012345678),
cell_id: Some(42),
vanity_url: Some("testuser".to_string()),
email_domain: Some("example.com".to_string()),
..Default::default()
};
let parsed = parse_logon_response(&response);
assert_eq!(parsed.eresult, 1);
assert_eq!(parsed.steam_id, Some(76561198012345678));
assert_eq!(parsed.cell_id, Some(42));
assert_eq!(parsed.vanity_url, Some("testuser".to_string()));
assert_eq!(parsed.email_domain, Some("example.com".to_string()));
}
#[test]
fn test_parse_logon_response_with_public_ip() {
let response = steam_protos::CMsgClientLogonResponse {
eresult: Some(1),
public_ip: Some(CMsgIPAddress {
ip: Some(steam_protos::cmsg_ip_address::Ip::V4(0xC0A80001)), }),
..Default::default()
};
let parsed = parse_logon_response(&response);
assert_eq!(parsed.public_ip, Some("192.168.0.1".to_string()));
}
#[test]
fn test_parse_logon_response_defaults() {
let response = steam_protos::CMsgClientLogonResponse::default();
let parsed = parse_logon_response(&response);
assert_eq!(parsed.eresult, 2); assert!(parsed.steam_id.is_none());
assert!(parsed.public_ip.is_none());
}
#[test]
fn test_logon_config_builder() {
let config = LogonConfig::new().with_cell_id(Some(42)).with_logon_id(Some(12345)).with_machine_name(Some("TEST-PC".to_string()));
assert_eq!(config.protocol_version, PROTOCOL_VERSION);
assert_eq!(config.cell_id, Some(42));
assert_eq!(config.logon_id, Some(12345));
assert_eq!(config.machine_name, Some("TEST-PC".to_string()));
}
#[test]
fn test_parse_friends_list() {
use steam_protos::cmsg_client_friends_list::Friend;
let response = steam_protos::CMsgClientFriendsList {
friends: vec![
Friend {
ulfriendid: Some(76561198012345678),
efriendrelationship: Some(3), },
Friend {
ulfriendid: Some(76561198087654321),
efriendrelationship: Some(4), },
],
..Default::default()
};
let friends = parse_friends_list(&response);
assert_eq!(friends.len(), 2);
assert_eq!(friends[0].steam_id, 76561198012345678);
assert_eq!(friends[0].relationship, 3);
assert_eq!(friends[1].steam_id, 76561198087654321);
assert_eq!(friends[1].relationship, 4);
}
#[test]
fn test_parse_friends_list_empty() {
let response = steam_protos::CMsgClientFriendsList::default();
let friends = parse_friends_list(&response);
assert!(friends.is_empty());
}
#[test]
fn test_parse_persona_state() {
use steam_protos::cmsg_client_persona_state::Friend;
let response = steam_protos::CMsgClientPersonaState {
friends: vec![Friend {
friendid: Some(76561198012345678),
player_name: Some("TestPlayer".to_string()),
persona_state: Some(1), game_name: Some("Dota 2".to_string()),
gameid: Some(570),
last_logoff: Some(1700000000),
..Default::default()
}],
..Default::default()
};
let personas = parse_persona_state(&response);
assert_eq!(personas.len(), 1);
assert_eq!(personas[0].steam_id, 76561198012345678);
assert_eq!(personas[0].player_name, Some("TestPlayer".to_string()));
assert_eq!(personas[0].persona_state, 1);
assert_eq!(personas[0].game_name, Some("Dota 2".to_string()));
assert_eq!(personas[0].game_id, Some(570));
assert_eq!(personas[0].last_logoff, Some(1700000000));
}
#[test]
fn test_parse_persona_state_empty() {
let response = steam_protos::CMsgClientPersonaState::default();
let personas = parse_persona_state(&response);
assert!(personas.is_empty());
}
#[test]
fn test_parse_account_info() {
let response = steam_protos::CMsgClientAccountInfo {
persona_name: Some("MyName".to_string()),
ip_country: Some("US".to_string()),
is_phone_verified: Some(true),
two_factor_state: Some(2),
account_flags: Some(0x0001),
..Default::default()
};
let account = parse_account_info(&response);
assert_eq!(account.persona_name, Some("MyName".to_string()));
assert_eq!(account.country, Some("US".to_string()));
assert!(account.is_phone_verified);
assert_eq!(account.two_factor_state, 2);
assert_eq!(account.account_flags, 0x0001);
}
#[test]
fn test_parse_account_info_defaults() {
let response = steam_protos::CMsgClientAccountInfo::default();
let account = parse_account_info(&response);
assert!(account.persona_name.is_none());
assert!(account.country.is_none());
assert!(!account.is_phone_verified);
assert_eq!(account.two_factor_state, 0);
assert_eq!(account.account_flags, 0);
}
}