Skip to main content

steamkit/steam/handlers/steam_user/
steam_user.rs

1//! Logging on and off as a user.
2
3use 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
26/// The account instance of desktop clients.
27const DESKTOP_INSTANCE: u32 = 1;
28
29/// The client package version reported at logon. Needed to get a proper sentry
30/// file for Steam Guard.
31const CLIENT_PACKAGE_VERSION: u32 = 1771;
32
33/// The mask applied to the private IP address in the logon.
34const OBFUSCATION_MASK: u32 = MsgClientLogon::OBFUSCATION_MASK;
35
36/// The chat mode to log on with.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38#[repr(u32)]
39pub enum ChatMode {
40    /// The default chat mode.
41    Default = 0,
42
43    /// The new Steam group chat.
44    NewSteamChat = 2,
45}
46
47/// What's needed to log on as a user.
48#[derive(Clone, Debug)]
49pub struct LogOnDetails {
50    /// The username.
51    pub username: Option<String>,
52
53    /// The password.
54    pub password: Option<String>,
55
56    /// The cell id.
57    pub cell_id: Option<u32>,
58
59    /// Identifies the logon session.
60    ///
61    /// Steam doesn't allow more than one session with the same login id on an
62    /// account from the same public IP at the same time, so give every
63    /// session from the same IP its own login id if you need several. If
64    /// `None`, it's derived from the machine's primary address (the same for
65    /// every session). If in doubt, leave it `None`.
66    pub login_id: Option<u32>,
67
68    /// The Steam Guard code from the email.
69    pub auth_code: Option<String>,
70
71    /// The 2-factor code from the authenticator app.
72    pub two_factor_code: Option<String>,
73
74    /// "Remember password". Used together with the access token for
75    /// password-less login.
76    pub should_remember_password: bool,
77
78    /// The access token from a successful login through the authentication
79    /// module (e.g. the QR flow).
80    pub access_token: Option<String>,
81
82    /// The account instance: 1 for PC, 2 for console (PS3).
83    pub account_instance: u32,
84
85    /// The account id for console instance logins.
86    pub account_id: u32,
87
88    /// Whether to request a Steam2 ticket (only needed for Steam2 content
89    /// downloads).
90    pub request_steam2_ticket: bool,
91
92    /// The client operating system type.
93    pub client_os_type: EOSType,
94
95    /// The client language.
96    pub client_language: String,
97
98    /// The machine name.
99    pub machine_name: Option<String>,
100
101    /// The chat mode.
102    pub chat_mode: ChatMode,
103
104    /// The UI mode.
105    pub ui_mode: EUIMode,
106
107    /// The gaming device type.
108    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            // The default PC instance.
123            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/// What's needed to log on anonymously.
137#[derive(Clone, Debug)]
138pub struct AnonymousLogOnDetails {
139    /// The cell id.
140    pub cell_id: Option<u32>,
141
142    /// The client operating system type.
143    pub client_os_type: EOSType,
144
145    /// The client language.
146    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
159/// The name of this machine.
160fn machine_name() -> String {
161    std::env::var("COMPUTERNAME")
162        .or_else(|_| std::env::var("HOSTNAME"))
163        .unwrap_or_else(|_| "unknown".to_string())
164}
165
166/// Packs SteamID components into the 64-bit form: account id in bits 0-31,
167/// instance in 32-51, account type in 52-55 and universe in 56-63.
168fn 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
180/// Logging on and off as a user.
181pub struct SteamUser {
182    client: Weak<SteamClient>,
183}
184
185impl SteamUser {
186    /// Creates the handler and stores a weak back-reference to the client.
187    pub fn new(client: &Arc<SteamClient>) -> Self {
188        Self {
189            client: Arc::downgrade(client),
190        }
191    }
192
193    /// Upgrades the weak client reference.
194    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    /// Our SteamID, set after a successful logon.
201    pub fn steam_id(&self) -> Option<SteamID> {
202        self.client().ok().and_then(|client| client.steam_id())
203    }
204
205    /// Whether the server is about to close the connection on purpose.
206    pub fn expect_disconnection(&self) -> bool {
207        self.client
208            .upgrade()
209            .map(|client| client.expect_disconnection())
210            .unwrap_or(false)
211    }
212
213    /// Logs on. The client has to be connected already. The result comes in a
214    /// [`LoggedOnCallback`].
215    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        // Legacy field, but the Steam client still sets it.
267        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        // Required to get a proper sentry file for Steam Guard.
290        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        // Steam Guard
310        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    /// Logs on anonymously. The client has to be connected already. The result
321    /// comes in a [`LoggedOnCallback`].
322    pub fn log_on_anonymous(&self) -> SteamResult<()> {
323        self.log_on_anonymous_with_details(&AnonymousLogOnDetails::default())
324    }
325
326    /// Logs on anonymously with the given details. The client has to be
327    /// connected already. The result comes in a [`LoggedOnCallback`].
328    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    /// Logs off. The server then disconnects us and a `DisconnectedCallback`
368    /// is posted.
369    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
381/// The local IP address in the obfuscated form Steam expects in the logon.
382fn obfuscate_private_ip(local_ip: std::net::IpAddr) -> CMsgIpAddress {
383    match local_ip {
384        std::net::IpAddr::V4(v4) => {
385            // Network byte order.
386            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            // Not a message we handle.
441            _ => None,
442        };
443
444        if let Some(callback) = callback {
445            client.post_callback(callback);
446        }
447
448        Ok(())
449    }
450}