Skip to main content

steamkit/steam/
cm_client.rs

1//! The base client that owns the connection to a CM server.
2//!
3//! [`SteamClient`](crate::steam::steam_client::steam_client::SteamClient) owns a `CMClient`
4//! and plugs into it through the [`ClientMsgDispatcher`] trait.
5
6use std::fmt;
7use std::io::Read;
8use std::net::{IpAddr, SocketAddr};
9use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
10use std::sync::Arc;
11use std::time::Duration;
12
13use async_trait::async_trait;
14use bytes::Bytes;
15use parking_lot::RwLock;
16use prost::Message;
17use tokio::sync::mpsc;
18use tokio_util::sync::CancellationToken;
19
20use crate::base::client_msg::{ClientMsgProtobuf, IClientMsg, SteamIDRaw};
21use crate::base::generated::steam_language::{EMsg, EResult};
22use crate::base::packet_base::{IPacketMsg, PacketClientMsg, PacketClientMsgProtobuf, PacketMsg};
23use crate::error::SteamError;
24use crate::networking::steam3::envelope_encrypted_connection::EnvelopeEncryptedConnection;
25use crate::networking::steam3::i_connection::{Connection, ConnectionEvent};
26use crate::networking::steam3::protocol_type::ProtocolTypes;
27use crate::networking::steam3::tcp_connection::TcpConnection;
28use crate::networking::steam3::udp_connection::UdpConnection;
29use crate::networking::steam3::web_socket_connection::WebSocketConnection;
30use crate::steam::discovery::server_record::ServerRecord;
31use crate::steam::discovery::smart_cm_server_list::{ServerQuality, SmartCMServerList};
32use crate::steam::steam_client::configuration::steam_configuration::{
33    default_user_agent, SteamConfiguration,
34};
35use crate::types::steam_id::SteamID;
36use crate::util::debug_log::DebugLog;
37use crate::util::debug_network_listener::IDebugNetworkListener;
38use crate::util::net_helpers::NetHelpers;
39use steamkit_protos::steam::{
40    CMsgClientHello, CMsgClientHeartBeat, CMsgClientLogonResponse, CMsgClientLoggedOff,
41    CMsgClientSessionToken, CMsgMulti, CMsgProtoBufHeader,
42};
43
44/// The protocol version sent in `ClientHello` (same value as
45/// `MsgClientLogon::CURRENT_PROTOCOL`).
46pub const CURRENT_PROTOCOL: u32 = 65581;
47
48/// The bit that marks a message as protobuf-backed.
49const PROTO_MASK: u32 = 0x8000_0000;
50
51/// The size of the extended client message header.
52const EXTENDED_HEADER_SIZE: usize = 36;
53
54/// The offset of the SteamID inside the extended client message header.
55const EXTENDED_HEADER_STEAM_ID_OFFSET: usize = 24;
56
57/// The offset of the session id inside the extended client message header.
58const EXTENDED_HEADER_SESSION_ID_OFFSET: usize = 32;
59
60/// How many CM servers to try when the server asks us to move.
61const SWITCH_ATTEMPTS: usize = 3;
62
63/// How many times in a row the client moves to another CM server without a
64/// successful logon before giving up and reporting the disconnect. Without a
65/// limit, a server that keeps closing the connection would make the client
66/// reconnect (and repeat its logon) forever.
67pub const MAX_CONSECUTIVE_SERVER_SWITCHES: u32 = 5;
68
69/// Receives the messages and lifecycle events of a [`CMClient`].
70#[async_trait]
71pub trait ClientMsgDispatcher: Send + Sync {
72    /// Handles a received client message.
73    /// Returning `false` drops the connection.
74    async fn on_client_msg_received(&self, client: &CMClient, packet: &dyn IPacketMsg) -> bool;
75
76    /// Called once the client is securely connected.
77    async fn on_client_connected(&self, client: &CMClient);
78
79    /// Called when the connection is gone.
80    async fn on_client_disconnected(&self, client: &CMClient, user_initiated: bool);
81}
82
83/// Creates the connection for a given protocol selection. Set it with
84/// [`CMClient::set_connection_factory`].
85pub type ConnectionFactory = Arc<
86    dyn Fn(&CMClient, &ServerRecord, ProtocolTypes) -> Result<Arc<dyn Connection>, SteamError>
87        + Send
88        + Sync,
89>;
90
91/// The default connection factory: TCP first, then WebSocket, then UDP.
92///
93/// SteamKit2 prefers WebSocket, but Steam closes a WebSocket CM session after a
94/// minute or two while a TCP one stays up, so TCP goes first here. The
95/// WebSocket is opened with `tokio-tungstenite`, so the `CMWebSocket` HTTP
96/// client purpose from the configuration isn't used for it.
97fn default_connection_factory(
98    client: &CMClient,
99    record: &ServerRecord,
100    protocol: ProtocolTypes,
101) -> Result<Arc<dyn Connection>, SteamError> {
102    let universe = client.universe();
103    let debug_network_listener = client.debug_network_listener();
104
105    if protocol.has(ProtocolTypes::TCP) {
106        return Ok(Arc::new(EnvelopeEncryptedConnection::with_debug_network_listener(
107            Arc::new(TcpConnection::new()),
108            universe,
109            debug_network_listener,
110        )));
111    }
112
113    if protocol.has(ProtocolTypes::WEB_SOCKET) {
114        return Ok(Arc::new(
115            WebSocketConnection::new_tungstenite_for_host_with_user_agent(
116                record.get_host(),
117                default_user_agent(),
118            ),
119        ));
120    }
121
122    if protocol.has(ProtocolTypes::UDP) {
123        return Ok(Arc::new(EnvelopeEncryptedConnection::with_debug_network_listener(
124            Arc::new(UdpConnection::new()),
125            universe,
126            debug_network_listener,
127        )));
128    }
129
130    Err(SteamError::InvalidArgument(
131        "Protocol bitmask has no supported protocols set.".to_owned(),
132    ))
133}
134
135/// The background task that periodically sends the heartbeat message.
136struct HeartBeat {
137    cancel: CancellationToken,
138    task: tokio::task::JoinHandle<()>,
139}
140
141/// The base client that handles the connection to a CM server. Use it through
142/// [`SteamClient`](crate::steam::steam_client::steam_client::SteamClient).
143pub struct CMClient {
144    configuration: Arc<SteamConfiguration>,
145    id: String,
146    servers: Arc<SmartCMServerList>,
147    dispatcher: Arc<dyn ClientMsgDispatcher>,
148    connection_factory: RwLock<Option<ConnectionFactory>>,
149    connection: RwLock<Option<Arc<dyn Connection>>>,
150    connection_cancellation: RwLock<Option<CancellationToken>>,
151    outbound: RwLock<Option<mpsc::UnboundedSender<Bytes>>>,
152    expect_disconnection: AtomicBool,
153    reconnect_requested: AtomicBool,
154    /// The serialized `ClientLogon` of the current session, repeated when the
155    /// client moves to another CM server. `None` when there's no logged-on
156    /// session worth resuming.
157    pending_logon: RwLock<Option<Vec<u8>>>,
158    /// The number of server switches since the last successful logon.
159    consecutive_switches: AtomicU32,
160    /// Bumped by every `connect`. A dispatch loop serves one generation and
161    /// only touches the client's state while it's current.
162    generation: AtomicU64,
163    /// The latest generation whose disconnect has been announced, so each
164    /// session announces it exactly once.
165    announced_generation: AtomicU64,
166    is_connected: AtomicBool,
167    connected_handled: AtomicBool,
168    session_token: AtomicU64,
169    cell_id: RwLock<Option<u32>>,
170    session_id: RwLock<Option<i32>>,
171    steam_id: RwLock<Option<SteamID>>,
172    public_ip: RwLock<Option<IpAddr>>,
173    ip_country_code: RwLock<Option<String>>,
174    user_country_code: RwLock<Option<String>>,
175    debug_network_listener: RwLock<Option<Arc<dyn IDebugNetworkListener>>>,
176    heart_beat: RwLock<Option<HeartBeat>>,
177}
178
179/// A connection plus its event stream. The dispatch loop owns this so it can
180/// swap it out when the server asks us to move.
181struct ConnectionSession {
182    connection: Arc<dyn Connection>,
183    /// The connection's events, in order, none dropped.
184    events: mpsc::UnboundedReceiver<ConnectionEvent>,
185}
186
187impl fmt::Debug for CMClient {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        f.debug_struct("CMClient")
190            .field("id", &self.id)
191            .field("is_connected", &self.is_connected.load(Ordering::SeqCst))
192            .field("session_id", &*self.session_id.read())
193            .field("steam_id", &*self.steam_id.read())
194            .field("cell_id", &*self.cell_id.read())
195            .finish()
196    }
197}
198
199impl CMClient {
200    /// Creates a client with the given configuration. The identifier must not
201    /// be empty (the [`SteamClient`](crate::steam::steam_client::steam_client::SteamClient)
202    /// constructors check this).
203    pub fn new(
204        configuration: Arc<SteamConfiguration>,
205        identifier: &str,
206        dispatcher: Arc<dyn ClientMsgDispatcher>,
207    ) -> CMClient {
208        let servers = configuration.server_list().clone();
209
210        CMClient {
211            configuration,
212            id: identifier.to_owned(),
213            servers,
214            dispatcher,
215            connection_factory: RwLock::new(None),
216            connection: RwLock::new(None),
217            connection_cancellation: RwLock::new(None),
218            outbound: RwLock::new(None),
219            expect_disconnection: AtomicBool::new(false),
220            reconnect_requested: AtomicBool::new(false),
221            pending_logon: RwLock::new(None),
222            consecutive_switches: AtomicU32::new(0),
223            generation: AtomicU64::new(0),
224            announced_generation: AtomicU64::new(0),
225            is_connected: AtomicBool::new(false),
226            connected_handled: AtomicBool::new(false),
227            session_token: AtomicU64::new(0),
228            cell_id: RwLock::new(None),
229            session_id: RwLock::new(None),
230            steam_id: RwLock::new(None),
231            public_ip: RwLock::new(None),
232            ip_country_code: RwLock::new(None),
233            user_country_code: RwLock::new(None),
234            debug_network_listener: RwLock::new(None),
235            heart_beat: RwLock::new(None),
236        }
237    }
238
239    /// The configuration for this client.
240    pub fn configuration(&self) -> &Arc<SteamConfiguration> {
241        &self.configuration
242    }
243
244    /// A unique identifier for this client.
245    pub fn id(&self) -> &str {
246        &self.id
247    }
248
249    /// The list of CM servers.
250    pub fn servers(&self) -> &Arc<SmartCMServerList> {
251        &self.servers
252    }
253
254    /// The local IP of the connection.
255    pub fn local_ip(&self) -> Option<IpAddr> {
256        self.connection.read().as_ref().and_then(|connection| connection.local_ip())
257    }
258
259    /// The endpoint we're connected to.
260    pub fn current_end_point(&self) -> Option<SocketAddr> {
261        self.connection.read().as_ref().and_then(|connection| connection.current_end_point())
262    }
263
264    /// Our public IP address, known after a successful logon.
265    pub fn public_ip(&self) -> Option<IpAddr> {
266        *self.public_ip.read()
267    }
268
269    /// The country code of our public IP, according to Steam.
270    pub fn ip_country_code(&self) -> Option<String> {
271        self.ip_country_code.read().clone()
272    }
273
274    /// The country code of the account.
275    pub fn user_country_code(&self) -> Option<String> {
276        self.user_country_code.read().clone()
277    }
278
279    /// The universe of this client.
280    pub fn universe(&self) -> crate::base::generated::steam_language::EUniverse {
281        self.configuration.universe()
282    }
283
284    /// Whether we're connected to a CM server.
285    pub fn is_connected(&self) -> bool {
286        self.is_connected.load(Ordering::SeqCst)
287    }
288
289    /// The session token assigned by the server.
290    pub fn session_token(&self) -> u64 {
291        self.session_token.load(Ordering::SeqCst)
292    }
293
294    /// The cell id Steam recommends for this client.
295    pub fn cell_id(&self) -> Option<u32> {
296        *self.cell_id.read()
297    }
298
299    /// The session id.
300    pub fn session_id(&self) -> Option<i32> {
301        *self.session_id.read()
302    }
303
304    /// The SteamID.
305    pub fn steam_id(&self) -> Option<SteamID> {
306        *self.steam_id.read()
307    }
308
309    /// The timeout for connecting to a CM server.
310    pub fn connection_timeout(&self) -> Duration {
311        self.configuration.connection_timeout()
312    }
313
314    /// The debug network listener, if any.
315    pub fn debug_network_listener(&self) -> Option<Arc<dyn IDebugNetworkListener>> {
316        self.debug_network_listener.read().clone()
317    }
318
319    /// Sets the debug network listener.
320    pub fn set_debug_network_listener(&self, listener: Option<Arc<dyn IDebugNetworkListener>>) {
321        *self.debug_network_listener.write() = listener;
322    }
323
324    /// Whether the server is about to close the connection on purpose (e.g. after a log off).
325    pub fn expect_disconnection(&self) -> bool {
326        self.expect_disconnection.load(Ordering::SeqCst)
327    }
328
329    /// Sets whether the server is about to close the connection on purpose.
330    pub fn set_expect_disconnection(&self, expect: bool) {
331        self.expect_disconnection.store(expect, Ordering::SeqCst);
332    }
333
334    /// Sets the factory that creates this client's connections.
335    pub fn set_connection_factory(&self, factory: ConnectionFactory) {
336        *self.connection_factory.write() = Some(factory);
337    }
338
339    /// Writes a line to the debug log under `<client id>/<category>`, and emits
340    /// a `tracing` event too.
341    pub fn log_debug(&self, category: &str, message: &str) {
342        let full_category = format!("{}/{}", self.id, category);
343
344        tracing::debug!(client = %self.id, category = %full_category, "{}", message);
345
346        DebugLog::write_line(&full_category, message);
347    }
348
349    /// Connects to a CM server; if `cm_server` is `None`, the next one from the
350    /// server list is used.
351    ///
352    /// A logged-on session that the server closes is moved to another server
353    /// automatically. Any other disconnect is reported, and it's up to you to
354    /// call this again.
355    pub async fn connect(self: &Arc<Self>, cm_server: Option<ServerRecord>) -> Result<(), SteamError> {
356        self.disconnect_with(true).await;
357
358        // A new session; never repeat an earlier session's logon on it.
359        *self.pending_logon.write() = None;
360        self.consecutive_switches.store(0, Ordering::SeqCst);
361        self.reconnect_requested.store(false, Ordering::SeqCst);
362        let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
363
364        let cancellation = CancellationToken::new();
365        *self.connection_cancellation.write() = Some(cancellation.clone());
366        self.expect_disconnection.store(false, Ordering::SeqCst);
367        self.connected_handled.store(false, Ordering::SeqCst);
368
369        let record = match cm_server {
370            Some(record) => Some(record),
371            None => {
372                self.servers
373                    .get_next_server_candidate_async(self.configuration.protocol_types())
374                    .await
375            }
376        };
377
378        let Some(record) = record else {
379            self.log_debug("CMClient", "Server record task returned no result.");
380            self.announced_generation.fetch_max(generation, Ordering::SeqCst);
381            self.on_client_disconnected(false).await;
382            return Err(SteamError::Connection("no server candidate is available".to_owned()));
383        };
384
385        // Host names were already resolved when the server record was created.
386        let end_point = record.end_point();
387
388        let session = match self.open_session(record, generation).await {
389            Ok(session) => session,
390            Err(error) => {
391                // No session, so a later `disconnect` has nothing to announce.
392                self.announced_generation.fetch_max(generation, Ordering::SeqCst);
393                return Err(error);
394            }
395        };
396        let connect_target = session.connection.clone();
397
398        let this = Arc::clone(self);
399        let task_cancellation = cancellation.clone();
400        tokio::spawn(async move {
401            this.dispatch_loop(session, task_cancellation, generation).await;
402        });
403
404        if let Err(error) = connect_target.connect(end_point, self.connection_timeout()).await {
405            self.log_debug("CMClient", &format!("Unhandled exception when attempting to connect to Steam: {}", error));
406            // The dispatch loop has nothing to serve. Some transports also raise
407            // `Disconnected` for a failed attempt; the generation check in
408            // `end_session` makes sure the failure is announced once and doesn't
409            // start a move to another server.
410            cancellation.cancel();
411            self.end_session(generation, &connect_target, false).await;
412            return Err(error);
413        }
414
415        Ok(())
416    }
417
418    /// Disconnects. Counts as user-initiated.
419    pub async fn disconnect(&self) {
420        self.disconnect_with(true).await;
421    }
422
423    /// Disconnects, saying whether it was user-initiated.
424    pub async fn disconnect_with(&self, user_initiated: bool) {
425        let generation = self.generation.load(Ordering::SeqCst);
426
427        self.stop_heart_beat();
428
429        // Cancel the dispatch loop first so it never acts on the `Disconnected`
430        // event this raises.
431        if let Some(cancellation) = self.connection_cancellation.write().take() {
432            cancellation.cancel();
433        }
434
435        // Dropping the outbound queue stops the writer task.
436        *self.outbound.write() = None;
437
438        let connection = self.connection.write().take();
439        if let Some(connection) = &connection {
440            connection.disconnect(user_initiated).await;
441        }
442
443        // The connection can be missing while we're moving to another server;
444        // the session is still live, so announce its end here.
445        if self.claim_disconnect_announcement(generation) {
446            self.teardown(connection.as_ref(), user_initiated).await;
447        }
448    }
449
450    /// Creates a connection to the given server and starts its writer task.
451    ///
452    /// The dispatch loop isn't started here; it owns the session so it can
453    /// replace it when the server asks us to move. The connection is only
454    /// installed while `generation` is current, so a superseded session can't
455    /// install its connection over the new one.
456    async fn open_session(
457        self: &Arc<Self>,
458        record: ServerRecord,
459        generation: u64,
460    ) -> Result<ConnectionSession, SteamError> {
461        let protocol = ProtocolTypes(
462            record.protocol_types().raw() & self.configuration.protocol_types().raw(),
463        );
464
465        let factory = self.connection_factory.read().clone();
466        let connection = match factory {
467            Some(factory) => factory(self, &record, protocol)?,
468            None => default_connection_factory(self, &record, protocol)?,
469        };
470
471        // Subscribe to events before starting the connection so none are missed.
472        let events = connection.events();
473
474        // The outbound queue keeps messages in order even though `send` is sync.
475        let (outbound_tx, mut outbound_rx) = mpsc::unbounded_channel::<Bytes>();
476
477        {
478            let mut slot = self.connection.write();
479            if self.generation.load(Ordering::SeqCst) != generation {
480                return Err(SteamError::Cancelled);
481            }
482            *slot = Some(connection.clone());
483            *self.outbound.write() = Some(outbound_tx);
484        }
485
486        let writer_connection = connection.clone();
487        tokio::spawn(async move {
488            while let Some(bytes) = outbound_rx.recv().await {
489                if let Err(error) = writer_connection.send(bytes).await {
490                    tracing::debug!(%error, "failed to send a message to Steam");
491                }
492            }
493        });
494
495        Ok(ConnectionSession { connection, events })
496    }
497
498    /// Removes `connection` from the client, if it's still the client's
499    /// connection and `generation` is still current.
500    fn release_connection(&self, generation: u64, connection: &Arc<dyn Connection>) {
501        let mut slot = self.connection.write();
502        if self.generation.load(Ordering::SeqCst) != generation {
503            return;
504        }
505        if slot.as_ref().is_some_and(|current| same_connection(current, connection)) {
506            *slot = None;
507            *self.outbound.write() = None;
508        }
509    }
510
511    /// Claims the right to announce the disconnect of `generation`. Returns
512    /// `true` exactly once per generation, so the dispatch loop and a concurrent
513    /// `disconnect` don't both announce it.
514    fn claim_disconnect_announcement(&self, generation: u64) -> bool {
515        self.announced_generation.fetch_max(generation, Ordering::SeqCst) < generation
516    }
517
518    /// Whether a session the server closed is worth resuming on another CM
519    /// server: a logon was accepted (or is still pending), and the disconnect
520    /// wasn't expected (logging off sets that expectation).
521    fn can_resume_session(&self) -> bool {
522        !self.expect_disconnection.load(Ordering::SeqCst) && self.pending_logon.read().is_some()
523    }
524
525    /// Counts a move to another CM server. Returns `false` once we've moved
526    /// [`MAX_CONSECUTIVE_SERVER_SWITCHES`] times without a successful logon.
527    fn record_switch_attempt(&self) -> bool {
528        self.consecutive_switches.fetch_add(1, Ordering::SeqCst) < MAX_CONSECUTIVE_SERVER_SWITCHES
529    }
530
531    /// Ends the session of the dispatch loop serving `generation` and announces
532    /// the disconnect, unless that already happened.
533    ///
534    /// The client's connection is only removed while `generation` is current;
535    /// a dispatch loop superseded by a later `connect` must not tear down the
536    /// newer session.
537    async fn end_session(
538        &self,
539        generation: u64,
540        connection: &Arc<dyn Connection>,
541        user_initiated: bool,
542    ) {
543        let current = {
544            let mut slot = self.connection.write();
545            if self.generation.load(Ordering::SeqCst) != generation {
546                return;
547            }
548            let current = slot.take();
549            *self.outbound.write() = None;
550            current
551        };
552
553        if !self.claim_disconnect_announcement(generation) {
554            return;
555        }
556
557        let connection = current.unwrap_or_else(|| connection.clone());
558        self.teardown(Some(&connection), user_initiated).await;
559    }
560
561    /// Moves to a different CM server after the server asked us to.
562    ///
563    /// The session isn't really over (the connection is fine, the server just
564    /// wants us elsewhere), so no disconnect is announced. This can't call
565    /// [`CMClient::connect`], since that starts a dispatch loop and the dispatch
566    /// loop is what calls this.
567    ///
568    /// Returns [`SteamError::Cancelled`] if the client was disconnected or
569    /// reconnected in the meantime.
570    async fn switch_session(
571        self: &Arc<Self>,
572        session: &mut ConnectionSession,
573        cancellation: &CancellationToken,
574        generation: u64,
575    ) -> Result<(), SteamError> {
576        self.stop_heart_beat();
577
578        // Take the old connection out of the client but DON'T disconnect it
579        // yet: its disconnect event would reach the dispatch loop (still
580        // listening on the old stream) and end the loop doing this switch.
581        let old_connection = {
582            let mut slot = self.connection.write();
583            if self.generation.load(Ordering::SeqCst) != generation {
584                return Err(SteamError::Cancelled);
585            }
586            *self.outbound.write() = None;
587            slot.take()
588        };
589
590        let mut last_error = None;
591
592        for _ in 0..SWITCH_ATTEMPTS {
593            if cancellation.is_cancelled() {
594                last_error = Some(SteamError::Cancelled);
595                break;
596            }
597
598            let mut record = self
599                .servers
600                .get_next_server_candidate_async(self.configuration.protocol_types())
601                .await;
602
603            if record.is_none() {
604                // Every server in the list is marked bad; fetch a fresh list
605                // before giving up.
606                let _ = self.servers.force_refresh_server_list().await;
607
608                record = self
609                    .servers
610                    .get_next_server_candidate_async(self.configuration.protocol_types())
611                    .await;
612            }
613
614            let Some(record) = record else {
615                break;
616            };
617
618            let end_point = record.end_point();
619            self.log_debug(
620                "CMClient",
621                &format!("Switching to another CM server: {end_point}"),
622            );
623
624            let new_session = match self.open_session(record, generation).await {
625                Ok(new_session) => new_session,
626                Err(SteamError::Cancelled) => {
627                    last_error = Some(SteamError::Cancelled);
628                    break;
629                }
630                Err(error) => {
631                    last_error = Some(error);
632                    continue;
633                }
634            };
635            let new_connection = new_session.connection.clone();
636
637            match new_connection
638                .connect(end_point, self.connection_timeout())
639                .await
640            {
641                Ok(()) if cancellation.is_cancelled() => {
642                    // We were disconnected while this connection was being made;
643                    // it mustn't survive the switch.
644                    self.release_connection(generation, &new_connection);
645                    new_connection.disconnect(true).await;
646                    last_error = Some(SteamError::Cancelled);
647                    break;
648                }
649                Ok(()) => {
650                    // Replacing the session drops the old stream, so the old
651                    // connection's disconnect never reaches the dispatch loop.
652                    *session = new_session;
653
654                    // The new server sends its own hello, and the logon is
655                    // repeated after it.
656                    self.connected_handled.store(false, Ordering::SeqCst);
657                    self.is_connected.store(false, Ordering::SeqCst);
658                    *self.session_id.write() = None;
659                    *self.steam_id.write() = None;
660
661                    if let Some(old_connection) = old_connection {
662                        old_connection.disconnect(false).await;
663                    }
664
665                    return Ok(());
666                }
667                Err(error) => {
668                    // No answer from this server; try another one so a single
669                    // unreachable server doesn't end the session.
670                    self.log_debug(
671                        "CMClient",
672                        &format!("The CM server at {end_point} did not answer: {error}"),
673                    );
674                    last_error = Some(error);
675
676                    self.release_connection(generation, &new_connection);
677                }
678            }
679        }
680
681        if let Some(old_connection) = old_connection {
682            old_connection.disconnect(false).await;
683        }
684
685        Err(last_error.unwrap_or_else(|| {
686            SteamError::Connection("no server candidate is available".to_owned())
687        }))
688    }
689
690    /// Sends a client message, filling in our session id and SteamID.
691    ///
692    /// The message is taken by shared reference, so the session id and SteamID
693    /// are written into the serialized bytes rather than into `msg`.
694    pub fn send(&self, msg: &dyn IClientMsg) -> Result<(), SteamError> {
695        let serialized = self.serialize_message(msg);
696
697        self.log_debug("CMClient", &format!("CM: --> {:?}", msg.msg_type()));
698
699        if let Some(listener) = self.debug_network_listener.read().clone() {
700            listener.on_outgoing_network_message(msg.msg_type(), &serialized);
701        }
702
703        // Remember the logon so it can be repeated on another CM server.
704        if msg.msg_type() == EMsg::ClientLogon {
705            *self.pending_logon.write() = Some(serialized.clone());
706        }
707
708        let outbound = self.outbound.read().clone();
709        let Some(outbound) = outbound else {
710            return Err(SteamError::Connection(
711                "the client is not connected to Steam".to_owned(),
712            ));
713        };
714
715        outbound
716            .send(Bytes::from(serialized))
717            .map_err(|_| SteamError::Connection("the connection to Steam has been closed".to_owned()))
718    }
719
720    /// Serializes a message and fills in our session id and SteamID.
721    fn serialize_message(&self, msg: &dyn IClientMsg) -> Vec<u8> {
722        let session_id = *self.session_id.read();
723        let steam_id = *self.steam_id.read();
724
725        if session_id.is_none() && steam_id.is_none() {
726            return msg.serialize();
727        }
728
729        stamp_identity(msg, msg.serialize(), session_id, steam_id)
730    }
731
732    /// Called once the client is securely connected.
733    pub async fn on_client_connected(&self) {
734        let mut request = ClientMsgProtobuf::<CMsgClientHello>::new(EMsg::ClientHello);
735        request.body_mut().protocol_version = Some(CURRENT_PROTOCOL);
736
737        let _ = self.send(&request);
738
739        self.dispatcher.on_client_connected(self).await;
740    }
741
742    /// Called when the connection is gone.
743    pub async fn on_client_disconnected(&self, user_initiated: bool) {
744        self.dispatcher.on_client_disconnected(self, user_initiated).await;
745    }
746
747    /// Parses a raw payload into a packet. Returns `None` if it can't be parsed.
748    pub fn get_packet_msg(&self, data: Vec<u8>) -> Option<Box<dyn IPacketMsg>> {
749        if data.len() < 4 {
750            self.log_debug(
751                "CMClient",
752                &format!(
753                    "PacketMsg too small to contain a message, was only {} bytes. Message: 0x{}",
754                    data.len(),
755                    hex::encode(&data)
756                ),
757            );
758            return None;
759        }
760
761        let raw = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
762        let e_msg = EMsg::from_value((raw & !PROTO_MASK) as i32).unwrap_or(EMsg::Invalid);
763
764        match e_msg {
765            // These are always plain `MsgHdr` messages.
766            EMsg::ChannelEncryptRequest | EMsg::ChannelEncryptResponse | EMsg::ChannelEncryptResult => {
767                return match PacketMsg::new(e_msg, data) {
768                    Ok(packet) => Some(Box::new(packet) as Box<dyn IPacketMsg>),
769                    Err(error) => {
770                        self.log_debug("CMClient", &format!("Exception deserializing emsg {}: {}", e_msg.value(), error));
771                        None
772                    }
773                };
774            }
775            _ => {}
776        }
777
778        let result = if raw & PROTO_MASK != 0 {
779            PacketClientMsgProtobuf::new(e_msg, data).map(|packet| Box::new(packet) as Box<dyn IPacketMsg>)
780        } else {
781            PacketClientMsg::new(e_msg, data).map(|packet| Box::new(packet) as Box<dyn IPacketMsg>)
782        };
783
784        match result {
785            Ok(packet) => Some(packet),
786            Err(error) => {
787                self.log_debug(
788                    "CMClient",
789                    &format!("Exception deserializing emsg {} ({}): {}", e_msg.value(), raw & PROTO_MASK != 0, error),
790                );
791                None
792            }
793        }
794    }
795
796    /// Runs the inbound dispatch loop for a connection.
797    async fn dispatch_loop(
798        self: Arc<Self>,
799        mut session: ConnectionSession,
800        cancellation: CancellationToken,
801        generation: u64,
802    ) {
803        loop {
804            if self.reconnect_requested.swap(false, Ordering::SeqCst) {
805                if !self.record_switch_attempt() {
806                    self.log_debug(
807                        "CMClient",
808                        "Not moving to another CM server: too many moves without a logon",
809                    );
810                    self.end_session(generation, &session.connection, false).await;
811                    break;
812                }
813
814                if let Err(error) = self.switch_session(&mut session, &cancellation, generation).await {
815                    self.log_debug(
816                        "CMClient",
817                        &format!("Unable to switch to another CM server: {error}"),
818                    );
819                    let user_initiated = cancellation.is_cancelled();
820                    self.end_session(generation, &session.connection, user_initiated).await;
821                    break;
822                }
823                continue;
824            }
825
826            // `biased`: once we've been disconnected, nothing else still queued
827            // may be acted on.
828            let event = tokio::select! {
829                biased;
830                _ = cancellation.cancelled() => break,
831                event = session.events.recv() => event,
832            };
833
834            match event {
835                Some(ConnectionEvent::NetMsgReceived(args)) => {
836                    let packet = self.get_packet_msg(args.data);
837                    let Some(packet) = packet else {
838                        self.log_debug("CMClient", "Packet message failed to parse, shutting down connection");
839                        self.disconnect_with(false).await;
840                        break;
841                    };
842
843                    self.log_debug(
844                        "CMClient",
845                        &format!("CM: <-- {:?}", packet.msg_type()),
846                    );
847
848                    if !self.dispatch_client_msg(Some(packet.as_ref())).await {
849                        break;
850                    }
851                }
852                Some(ConnectionEvent::Connected) => {
853                    self.handle_connected(&session.connection).await;
854                }
855                Some(ConnectionEvent::Disconnected(args)) => {
856                    // A logged-on session the server closed is moved to another
857                    // CM server. Not one that was logged off, whose logon failed,
858                    // or that never logged on: repeating the logon there would log
859                    // the user back on against their will.
860                    let resume = !args.user_initiated && self.can_resume_session();
861                    if !resume {
862                        self.end_session(generation, &session.connection, args.user_initiated)
863                            .await;
864                        break;
865                    }
866
867                    if !self.record_switch_attempt() {
868                        self.log_debug(
869                            "CMClient",
870                            "Not reconnecting to Steam: too many reconnections without a logon",
871                        );
872                        self.end_session(generation, &session.connection, false).await;
873                        break;
874                    }
875
876                    if let Err(error) = self.switch_session(&mut session, &cancellation, generation).await {
877                        self.log_debug(
878                            "CMClient",
879                            &format!("Unable to reconnect to Steam: {error}"),
880                        );
881                        let user_initiated = cancellation.is_cancelled();
882                        self.end_session(generation, &session.connection, user_initiated).await;
883                        break;
884                    }
885                }
886                None => {
887                    // The connection went away without a disconnect event.
888                    self.end_session(generation, &session.connection, false).await;
889                    break;
890                }
891            }
892        }
893    }
894
895    /// Handles the connection's `Connected` event.
896    async fn handle_connected(&self, connection: &Arc<dyn Connection>) {
897        if self.connected_handled.swap(true, Ordering::SeqCst) {
898            return;
899        }
900
901        if let Some(end_point) = connection.current_end_point() {
902            self.servers.try_mark(end_point, connection.protocol_types(), ServerQuality::Good);
903        }
904
905        self.is_connected.store(true, Ordering::SeqCst);
906
907        self.on_client_connected().await;
908
909        // If the previous server sent us here ("try another CM"), repeat the
910        // logon after the new server's hello.
911        let pending = self.pending_logon.read().clone();
912        if let Some(bytes) = pending {
913            let outbound = self.outbound.read().clone();
914            if let Some(outbound) = outbound {
915                let _ = outbound.send(Bytes::from(bytes));
916            }
917        }
918    }
919
920    /// Runs the dispatch pipeline for a received message: the base handling
921    /// first, then the dispatcher.
922    async fn dispatch_client_msg(self: &Arc<Self>, packet: Option<&dyn IPacketMsg>) -> bool {
923        let Some(packet) = packet else {
924            self.log_debug("CMClient", "Packet message failed to parse, shutting down connection");
925            self.disconnect_with(false).await;
926            return false;
927        };
928
929        if !self.on_client_msg_received(packet).await {
930            return false;
931        }
932
933        self.dispatcher.on_client_msg_received(self, packet).await
934    }
935
936    /// The base handling of a received message.
937    async fn on_client_msg_received(self: &Arc<Self>, packet: &dyn IPacketMsg) -> bool {
938        // Multi messages are logged after they're unpacked.
939        if packet.msg_type() != EMsg::Multi {
940            if let Some(listener) = self.debug_network_listener.read().clone() {
941                listener.on_incoming_network_message(packet.msg_type(), packet.get_data());
942            }
943        }
944
945        match packet.msg_type() {
946            EMsg::Multi => return self.handle_multi(packet).await,
947            EMsg::ClientLogOnResponse => self.handle_logon_response(packet).await,
948            EMsg::ClientLoggedOff => self.handle_logged_off(packet).await,
949            EMsg::ClientServerUnavailable => self.handle_server_unavailable(packet).await,
950            EMsg::ClientSessionToken => self.handle_session_token(packet).await,
951            _ => {}
952        }
953
954        true
955    }
956
957    /// Unpacks a `Multi` message and dispatches the messages it carries.
958    async fn handle_multi(self: &Arc<Self>, packet: &dyn IPacketMsg) -> bool {
959        if !packet.is_proto() {
960            self.log_debug("CMClient", "HandleMulti got non-proto MsgMulti!!");
961            return true;
962        }
963
964        let Ok(msg_multi) = ClientMsgProtobuf::<CMsgMulti>::from_packet(packet) else {
965            return true;
966        };
967
968        let payload = msg_multi.body().message_body.clone().unwrap_or_default();
969        let unzipped = msg_multi.body().size_unzipped.unwrap_or(0);
970
971        let data = if unzipped > 0 {
972            match decompress_gzip(&payload) {
973                Ok(data) => data,
974                Err(error) => {
975                    self.log_debug("CMClient", &format!("Failed to decompress multi message: {}", error));
976                    return true;
977                }
978            }
979        } else {
980            payload
981        };
982
983        let mut offset = 0usize;
984        while offset + 4 <= data.len() {
985            let sub_size = i32::from_le_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]);
986            offset += 4;
987
988            if sub_size <= 0 {
989                break;
990            }
991
992            let sub_size = sub_size as usize;
993            if offset + sub_size > data.len() {
994                break;
995            }
996
997            let sub_data = data[offset..offset + sub_size].to_vec();
998            offset += sub_size;
999
1000            match self.get_packet_msg(sub_data) {
1001                Some(sub_packet) => {
1002                    // Boxed, or unpacking and dispatching would be an
1003                    // infinitely sized async recursion.
1004                    if !Box::pin(self.dispatch_client_msg(Some(sub_packet.as_ref()))).await {
1005                        return false;
1006                    }
1007                }
1008                // An unparseable message inside a Multi disconnects. Just
1009                // returning `false` would end the dispatch loop and leave the
1010                // connection open with nobody reading it.
1011                None => return Box::pin(self.dispatch_client_msg(None)).await,
1012            }
1013        }
1014
1015        true
1016    }
1017
1018    async fn handle_logon_response(self: &Arc<Self>, packet: &dyn IPacketMsg) {
1019        if !packet.is_proto() {
1020            // A non-protobuf logon response comes from connecting without ever
1021            // sending a ClientLogon. It always fails; nothing to do.
1022            self.log_debug(
1023                "CMClient",
1024                "Got non-proto logon response, this is indicative of no logon attempt after connecting.",
1025            );
1026            return;
1027        }
1028
1029        let Ok(response) = ClientMsgProtobuf::<CMsgClientLogonResponse>::from_packet(packet) else {
1030            return;
1031        };
1032
1033        let logon_result = response
1034            .body()
1035            .eresult
1036            .and_then(EResult::from_value)
1037            .unwrap_or(EResult::Invalid);
1038
1039        if logon_result == EResult::OK {
1040            let header = response.proto_header();
1041
1042            *self.session_id.write() = header.client_sessionid;
1043            *self.steam_id.write() = header.steamid.map(SteamID::from_id);
1044            *self.cell_id.write() = response.body().cell_id;
1045            *self.public_ip.write() = response
1046                .body()
1047                .public_ip
1048                .as_ref()
1049                .and_then(NetHelpers::get_ip_address_from_msg);
1050            *self.ip_country_code.write() = response.body().ip_country_code.clone();
1051            *self.user_country_code.write() = response.body().user_country.clone();
1052
1053            let heartbeat_seconds = response.body().legacy_out_of_game_heartbeat_seconds.unwrap_or(0);
1054
1055            self.restart_heart_beat(Duration::from_secs(heartbeat_seconds.max(0) as u64));
1056
1057            // We're logged on, so the moves that got us here are done.
1058            self.consecutive_switches.store(0, Ordering::SeqCst);
1059        } else if logon_result == EResult::TryAnotherCM || logon_result == EResult::ServiceUnavailable {
1060            let connection = self.connection.read().clone();
1061            if let Some(connection) = connection {
1062                if let Some(end_point) = connection.current_end_point() {
1063                    self.servers.try_mark(end_point, connection.protocol_types(), ServerQuality::Bad);
1064                }
1065            }
1066
1067            // Move to another server ourselves; the connection this arrived on
1068            // is still perfectly healthy.
1069            self.reconnect_requested.store(true, Ordering::SeqCst);
1070        } else {
1071            // The logon was refused (wrong password, expired token, Steam Guard
1072            // code needed). It would fail the same way on another server, and
1073            // retrying a wrong password is how accounts get rate limited.
1074            *self.pending_logon.write() = None;
1075        }
1076    }
1077
1078    async fn handle_logged_off(self: &Arc<Self>, packet: &dyn IPacketMsg) {
1079        *self.session_id.write() = None;
1080        *self.steam_id.write() = None;
1081        *self.cell_id.write() = None;
1082        *self.public_ip.write() = None;
1083        *self.ip_country_code.write() = None;
1084        *self.user_country_code.write() = None;
1085
1086        self.stop_heart_beat();
1087
1088        // The session is over; never log back on automatically. Especially not
1089        // after `LoggedInElsewhere`: logging back on would kick the other
1090        // session, which would then do the same to us.
1091        *self.pending_logon.write() = None;
1092
1093        if !packet.is_proto() {
1094            return;
1095        }
1096
1097        let Ok(logoff) = ClientMsgProtobuf::<CMsgClientLoggedOff>::from_packet(packet) else {
1098            return;
1099        };
1100
1101        let logoff_result = logoff
1102            .body()
1103            .eresult
1104            .and_then(EResult::from_value)
1105            .unwrap_or(EResult::Invalid);
1106
1107        if logoff_result == EResult::TryAnotherCM || logoff_result == EResult::ServiceUnavailable {
1108            let connection = self.connection.read().clone();
1109            if let Some(connection) = connection {
1110                if let Some(end_point) = connection.current_end_point() {
1111                    self.servers.try_mark(end_point, connection.protocol_types(), ServerQuality::Bad);
1112                }
1113            }
1114        }
1115    }
1116
1117    async fn handle_server_unavailable(self: &Arc<Self>, packet: &dyn IPacketMsg) {
1118        self.log_debug(
1119            "SteamClient",
1120            &format!("A server was not available for request: '{}'", packet.msg_type().value()),
1121        );
1122        self.disconnect_with(false).await;
1123    }
1124
1125    async fn handle_session_token(self: &Arc<Self>, packet: &dyn IPacketMsg) {
1126        let Ok(token) = ClientMsgProtobuf::<CMsgClientSessionToken>::from_packet(packet) else {
1127            return;
1128        };
1129
1130        self.session_token.store(token.body().token.unwrap_or(0), Ordering::SeqCst);
1131    }
1132
1133    /// Resets the session state and announces the disconnect. Callers make sure
1134    /// this runs once per session.
1135    async fn teardown(&self, connection: Option<&Arc<dyn Connection>>, user_initiated: bool) {
1136        self.is_connected.store(false, Ordering::SeqCst);
1137
1138        let expect_disconnection = self.expect_disconnection.load(Ordering::SeqCst);
1139
1140        if !user_initiated && !expect_disconnection {
1141            if let Some(connection) = connection {
1142                if let Some(end_point) = connection.current_end_point() {
1143                    self.servers.try_mark(end_point, connection.protocol_types(), ServerQuality::Bad);
1144                }
1145            }
1146        }
1147
1148        if user_initiated {
1149            // The user disconnected, so the session is over.
1150            *self.pending_logon.write() = None;
1151        }
1152
1153        *self.session_id.write() = None;
1154        *self.steam_id.write() = None;
1155
1156        self.stop_heart_beat();
1157
1158        self.on_client_disconnected(user_initiated || expect_disconnection).await;
1159    }
1160
1161    fn restart_heart_beat(self: &Arc<Self>, delay: Duration) {
1162        self.stop_heart_beat();
1163
1164        let delay = if delay.is_zero() { Duration::from_secs(1) } else { delay };
1165        let Ok(handle) = tokio::runtime::Handle::try_current() else {
1166            return;
1167        };
1168
1169        let cancel = CancellationToken::new();
1170        let token = cancel.clone();
1171        let this = Arc::clone(self);
1172
1173        let task = handle.spawn(async move {
1174            let mut interval = tokio::time::interval(delay);
1175            interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1176
1177            loop {
1178                tokio::select! {
1179                    _ = token.cancelled() => break,
1180                    _ = interval.tick() => {
1181                        let request = ClientMsgProtobuf::<CMsgClientHeartBeat>::new(EMsg::ClientHeartBeat);
1182                        let _ = this.send(&request);
1183                    }
1184                }
1185            }
1186        });
1187
1188        *self.heart_beat.write() = Some(HeartBeat { cancel, task });
1189    }
1190
1191    fn stop_heart_beat(&self) {
1192        if let Some(heart_beat) = self.heart_beat.write().take() {
1193            heart_beat.cancel.cancel();
1194            heart_beat.task.abort();
1195        }
1196    }
1197}
1198
1199/// Writes our session id and SteamID into a serialized message. Only the
1200/// protobuf header and the extended header have room for them; messages with
1201/// a plain header come back unchanged.
1202fn stamp_identity(
1203    msg: &dyn IClientMsg,
1204    mut bytes: Vec<u8>,
1205    session_id: Option<i32>,
1206    steam_id: Option<SteamID>,
1207) -> Vec<u8> {
1208    if msg.is_proto() {
1209        // Layout: message type (4 bytes), protobuf header length (4 bytes), the
1210        // protobuf header, then the body.
1211        if bytes.len() < 8 {
1212            return bytes;
1213        }
1214
1215        let header_length = i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
1216        if header_length < 0 {
1217            return bytes;
1218        }
1219
1220        let header_length = header_length as usize;
1221        if bytes.len() < 8 + header_length {
1222            return bytes;
1223        }
1224
1225        let Ok(mut header) = CMsgProtoBufHeader::decode(&bytes[8..8 + header_length]) else {
1226            return bytes;
1227        };
1228
1229        if let Some(session_id) = session_id {
1230            header.client_sessionid = Some(session_id);
1231        }
1232        if let Some(steam_id) = steam_id {
1233            header.steamid = Some(steam_id.raw_u64());
1234        }
1235
1236        let encoded = header.encode_to_vec();
1237
1238        let mut stamped = Vec::with_capacity(bytes.len() + encoded.len());
1239        stamped.extend_from_slice(&bytes[..4]);
1240        stamped.extend_from_slice(&(encoded.len() as i32).to_le_bytes());
1241        stamped.extend_from_slice(&encoded);
1242        stamped.extend_from_slice(&bytes[8 + header_length..]);
1243        return stamped;
1244    }
1245
1246    // The extended header: message type (4), header size (1), header version
1247    // (2), target job id (8), source job id (8), canary (1), SteamID (8) and
1248    // session id (4).
1249    if bytes.len() >= EXTENDED_HEADER_SIZE && bytes[4] as usize == EXTENDED_HEADER_SIZE {
1250        if let Some(steam_id) = steam_id {
1251            bytes[EXTENDED_HEADER_STEAM_ID_OFFSET..EXTENDED_HEADER_STEAM_ID_OFFSET + 8]
1252                .copy_from_slice(&steam_id.raw_u64().to_le_bytes());
1253        }
1254        if let Some(session_id) = session_id {
1255            bytes[EXTENDED_HEADER_SESSION_ID_OFFSET..EXTENDED_HEADER_SESSION_ID_OFFSET + 4]
1256                .copy_from_slice(&session_id.to_le_bytes());
1257        }
1258    }
1259
1260    bytes
1261}
1262
1263/// Whether two handles point at the same connection. Only the data pointers
1264/// are compared, since the vtable half of a `dyn` pointer isn't guaranteed to
1265/// be unique.
1266fn same_connection(a: &Arc<dyn Connection>, b: &Arc<dyn Connection>) -> bool {
1267    std::ptr::eq(Arc::as_ptr(a) as *const (), Arc::as_ptr(b) as *const ())
1268}
1269
1270fn decompress_gzip(data: &[u8]) -> Result<Vec<u8>, SteamError> {
1271    let mut decoder = flate2::read::GzDecoder::new(data);
1272    let mut decompressed = Vec::new();
1273    decoder
1274        .read_to_end(&mut decompressed)
1275        .map_err(|error| SteamError::Decompression(error.to_string()))?;
1276    Ok(decompressed)
1277}