steamkit/steam/handlers/steam_user/
steam_user.rs1use std::sync::{Arc, Weak};
4
5use async_trait::async_trait;
6use steamkit_protos::steam::{c_msg_ip_address, CMsgClientLogOff, CMsgClientLogon, CMsgIpAddress};
7
8use crate::base::client_msg::{ClientMsgProtobuf, IClientMsg};
9use crate::base::generated::steam_language::internal::MsgClientLogon;
10use crate::base::generated::steam_language::{
11 EAccountType, EGamingDeviceType, EMsg, EOSType, EResult, EUIMode, EUniverse,
12};
13use crate::base::packet_base::IPacketMsg;
14use crate::error::{SteamError, SteamResult};
15use crate::steam::handlers::client_msg_handler::ClientMsgHandler;
16use crate::steam::steam_client::callback_mgr::callback_msg::CallbackMsg;
17use crate::steam::steam_client::steam_client::SteamClient;
18use crate::types::steam_id::SteamID;
19
20use super::callbacks::{
21 AccountInfoCallback, EmailAddrInfoCallback, LoggedOffCallback, LoggedOnCallback,
22 MarketingMessageCallback, PlayingSessionStateCallback, SessionTokenCallback,
23 VanityURLChangedCallback, WalletInfoCallback, WebAPIUserNonceCallback,
24};
25
26const DESKTOP_INSTANCE: u32 = 1;
28
29const CLIENT_PACKAGE_VERSION: u32 = 1771;
32
33const OBFUSCATION_MASK: u32 = MsgClientLogon::OBFUSCATION_MASK;
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38#[repr(u32)]
39pub enum ChatMode {
40 Default = 0,
42
43 NewSteamChat = 2,
45}
46
47#[derive(Clone, Debug)]
49pub struct LogOnDetails {
50 pub username: Option<String>,
52
53 pub password: Option<String>,
55
56 pub cell_id: Option<u32>,
58
59 pub login_id: Option<u32>,
67
68 pub auth_code: Option<String>,
70
71 pub two_factor_code: Option<String>,
73
74 pub should_remember_password: bool,
77
78 pub access_token: Option<String>,
81
82 pub account_instance: u32,
84
85 pub account_id: u32,
87
88 pub request_steam2_ticket: bool,
91
92 pub client_os_type: EOSType,
94
95 pub client_language: String,
97
98 pub machine_name: Option<String>,
100
101 pub chat_mode: ChatMode,
103
104 pub ui_mode: EUIMode,
106
107 pub gaming_device_type: EGamingDeviceType,
109}
110
111impl Default for LogOnDetails {
112 fn default() -> Self {
113 Self {
114 username: None,
115 password: None,
116 cell_id: None,
117 login_id: None,
118 auth_code: None,
119 two_factor_code: None,
120 should_remember_password: false,
121 access_token: None,
122 account_instance: DESKTOP_INSTANCE,
124 account_id: 0,
125 request_steam2_ticket: false,
126 client_os_type: crate::util::utils::get_os_type(),
127 client_language: "english".to_string(),
128 machine_name: Some(format!("{} (SteamKit2)", machine_name())),
129 chat_mode: ChatMode::Default,
130 ui_mode: EUIMode::Unknown,
131 gaming_device_type: EGamingDeviceType::Unknown,
132 }
133 }
134}
135
136#[derive(Clone, Debug)]
138pub struct AnonymousLogOnDetails {
139 pub cell_id: Option<u32>,
141
142 pub client_os_type: EOSType,
144
145 pub client_language: String,
147}
148
149impl Default for AnonymousLogOnDetails {
150 fn default() -> Self {
151 Self {
152 cell_id: None,
153 client_os_type: crate::util::utils::get_os_type(),
154 client_language: "english".to_string(),
155 }
156 }
157}
158
159fn machine_name() -> String {
161 std::env::var("COMPUTERNAME")
162 .or_else(|_| std::env::var("HOSTNAME"))
163 .unwrap_or_else(|_| "unknown".to_string())
164}
165
166fn pack_steam_id(
169 account_id: u32,
170 account_instance: u32,
171 universe: EUniverse,
172 account_type: EAccountType,
173) -> u64 {
174 ((universe.value() as u64) & 0xFF) << 56
175 | ((account_type.value() as u64) & 0x0F) << 52
176 | ((account_instance as u64) & 0x000F_FFFF) << 32
177 | (account_id as u64)
178}
179
180pub struct SteamUser {
182 client: Weak<SteamClient>,
183}
184
185impl SteamUser {
186 pub fn new(client: &Arc<SteamClient>) -> Self {
188 Self {
189 client: Arc::downgrade(client),
190 }
191 }
192
193 fn client(&self) -> SteamResult<Arc<SteamClient>> {
195 self.client
196 .upgrade()
197 .ok_or_else(|| SteamError::Connection("SteamClient has been dropped".to_string()))
198 }
199
200 pub fn steam_id(&self) -> Option<SteamID> {
202 self.client().ok().and_then(|client| client.steam_id())
203 }
204
205 pub fn expect_disconnection(&self) -> bool {
207 self.client
208 .upgrade()
209 .map(|client| client.expect_disconnection())
210 .unwrap_or(false)
211 }
212
213 pub fn log_on(&self, details: &LogOnDetails) -> SteamResult<()> {
216 let username = details.username.as_deref().unwrap_or_default();
217 let has_password = details
218 .password
219 .as_deref()
220 .is_some_and(|password| !password.is_empty());
221 let has_access_token = details
222 .access_token
223 .as_deref()
224 .is_some_and(|token| !token.is_empty());
225
226 if username.is_empty() || (!has_password && !has_access_token) {
227 return Err(SteamError::InvalidArgument(
228 "LogOn requires a username and password or access token to be set in 'details'."
229 .to_string(),
230 ));
231 }
232
233 let client = self.client()?;
234
235 if !client.is_connected() {
236 client.post_callback(Box::new(LoggedOnCallback::from_result(
237 EResult::NoConnection,
238 )));
239 return Ok(());
240 }
241
242 let universe = client.configuration().universe();
243 let steam_id = SteamID::from_id(pack_steam_id(
244 details.account_id,
245 details.account_instance,
246 universe,
247 EAccountType::Individual,
248 ));
249
250 let mut logon = ClientMsgProtobuf::<CMsgClientLogon>::new(EMsg::ClientLogon);
251
252 let obfuscated_private_ip = match details.login_id {
253 Some(login_id) => CMsgIpAddress {
254 ip: Some(c_msg_ip_address::Ip::V4(login_id)),
255 },
256 None => {
257 let local_ip = client.local_ip().ok_or_else(|| {
258 SteamError::Connection("the local IP address is not known yet".to_string())
259 })?;
260 obfuscate_private_ip(local_ip)
261 }
262 };
263
264 logon.body.obfuscated_private_ip = Some(obfuscated_private_ip.clone());
265
266 if let Some(c_msg_ip_address::Ip::V4(v4)) = obfuscated_private_ip.ip {
268 logon.body.deprecated_obfustucated_private_ip = Some(v4);
269 }
270
271 logon.set_session_id(0);
272 logon.set_steam_id(steam_id);
273
274 logon.body.account_name = details.username.clone();
275 logon.body.password = details.password.clone();
276 logon.body.should_remember_password = Some(details.should_remember_password);
277
278 logon.body.protocol_version = Some(MsgClientLogon::CURRENT_PROTOCOL);
279 logon.body.client_os_type = Some(details.client_os_type as u32);
280 logon.body.client_language = Some(details.client_language.clone());
281 logon.body.cell_id = Some(
282 details
283 .cell_id
284 .unwrap_or_else(|| client.configuration().cell_id()),
285 );
286
287 logon.body.steam2_ticket_request = Some(details.request_steam2_ticket);
288
289 logon.body.client_package_version = Some(CLIENT_PACKAGE_VERSION);
291 logon.body.supports_rate_limit_response = Some(true);
292 logon.body.machine_name = details.machine_name.clone();
293 logon.body.machine_id = crate::util::hardware_utils::get_machine_id(
294 client.configuration().machine_info_provider(),
295 );
296
297 if details.chat_mode != ChatMode::Default {
298 logon.body.chat_mode = Some(details.chat_mode as u32);
299 }
300
301 if details.ui_mode != EUIMode::Unknown {
302 logon.body.ui_mode = Some(details.ui_mode as u32);
303 }
304
305 if details.gaming_device_type != EGamingDeviceType::Unknown {
306 logon.body.gaming_device_type = Some(details.gaming_device_type as u32);
307 }
308
309 logon.body.auth_code = details.auth_code.clone();
311 logon.body.two_factor_code = details.two_factor_code.clone();
312
313 logon.body.access_token = details.access_token.clone();
314
315 client.send(&logon)?;
316
317 Ok(())
318 }
319
320 pub fn log_on_anonymous(&self) -> SteamResult<()> {
323 self.log_on_anonymous_with_details(&AnonymousLogOnDetails::default())
324 }
325
326 pub fn log_on_anonymous_with_details(
329 &self,
330 details: &AnonymousLogOnDetails,
331 ) -> SteamResult<()> {
332 let client = self.client()?;
333
334 if !client.is_connected() {
335 client.post_callback(Box::new(LoggedOnCallback::from_result(
336 EResult::NoConnection,
337 )));
338 return Ok(());
339 }
340
341 let universe = client.configuration().universe();
342 let au_id = SteamID::from_id(pack_steam_id(0, 0, universe, EAccountType::AnonUser));
343
344 let mut logon = ClientMsgProtobuf::<CMsgClientLogon>::new(EMsg::ClientLogon);
345
346 logon.set_session_id(0);
347 logon.set_steam_id(au_id);
348
349 logon.body.protocol_version = Some(MsgClientLogon::CURRENT_PROTOCOL);
350 logon.body.client_os_type = Some(details.client_os_type as u32);
351 logon.body.client_language = Some(details.client_language.clone());
352 logon.body.cell_id = Some(
353 details
354 .cell_id
355 .unwrap_or_else(|| client.configuration().cell_id()),
356 );
357
358 logon.body.machine_id = crate::util::hardware_utils::get_machine_id(
359 client.configuration().machine_info_provider(),
360 );
361
362 client.send(&logon)?;
363
364 Ok(())
365 }
366
367 pub fn log_off(&self) -> SteamResult<()> {
370 let client = self.client()?;
371
372 client.set_expect_disconnection(true);
373
374 let log_off = ClientMsgProtobuf::<CMsgClientLogOff>::new(EMsg::ClientLogOff);
375 client.send(&log_off)?;
376
377 Ok(())
378 }
379}
380
381fn obfuscate_private_ip(local_ip: std::net::IpAddr) -> CMsgIpAddress {
383 match local_ip {
384 std::net::IpAddr::V4(v4) => {
385 let raw = u32::from_be_bytes(v4.octets());
387 CMsgIpAddress {
388 ip: Some(c_msg_ip_address::Ip::V4(raw ^ OBFUSCATION_MASK)),
389 }
390 }
391 std::net::IpAddr::V6(v6) => {
392 let mut octets = v6.octets();
393 for chunk in octets.chunks_exact_mut(4) {
394 chunk[0] ^= 0x0D;
395 chunk[1] ^= 0xF0;
396 chunk[2] ^= 0xAD;
397 chunk[3] ^= 0xBA;
398 }
399 CMsgIpAddress {
400 ip: Some(c_msg_ip_address::Ip::V6(octets.to_vec())),
401 }
402 }
403 }
404}
405
406#[async_trait]
407impl ClientMsgHandler for SteamUser {
408 async fn handle_msg(
409 &self,
410 client: &SteamClient,
411 packet_msg: &dyn IPacketMsg,
412 ) -> SteamResult<()> {
413 let callback: Option<Box<dyn CallbackMsg>> = match packet_msg.msg_type() {
414 EMsg::ClientLogOnResponse => Some(Box::new(LoggedOnCallback::from_packet(packet_msg)?)),
415 EMsg::ClientLoggedOff => Some(Box::new(LoggedOffCallback::from_packet(packet_msg)?)),
416 EMsg::ClientSessionToken => {
417 Some(Box::new(SessionTokenCallback::from_packet(packet_msg)?))
418 }
419 EMsg::ClientAccountInfo => {
420 Some(Box::new(AccountInfoCallback::from_packet(packet_msg)?))
421 }
422 EMsg::ClientEmailAddrInfo => {
423 Some(Box::new(EmailAddrInfoCallback::from_packet(packet_msg)?))
424 }
425 EMsg::ClientWalletInfoUpdate => {
426 Some(Box::new(WalletInfoCallback::from_packet(packet_msg)?))
427 }
428 EMsg::ClientRequestWebAPIAuthenticateUserNonceResponse => {
429 Some(Box::new(WebAPIUserNonceCallback::from_packet(packet_msg)?))
430 }
431 EMsg::ClientVanityURLChangedNotification => {
432 Some(Box::new(VanityURLChangedCallback::from_packet(packet_msg)?))
433 }
434 EMsg::ClientMarketingMessageUpdate2 => {
435 Some(Box::new(MarketingMessageCallback::from_packet(packet_msg)?))
436 }
437 EMsg::ClientPlayingSessionState => Some(Box::new(
438 PlayingSessionStateCallback::from_packet(packet_msg)?,
439 )),
440 _ => None,
442 };
443
444 if let Some(callback) = callback {
445 client.post_callback(callback);
446 }
447
448 Ok(())
449 }
450}