Skip to main content

steamkit/networking/steam3/
web_socket_connection.rs

1//! Steam's WebSocket transport.
2//!
3//! The socket itself is behind the [`WebSocketFactory`] and [`WebSocketSocket`]
4//! traits (see `web_socket_context`), so tests can swap it out.
5
6use std::net::{IpAddr, SocketAddr};
7use std::sync::{Arc, Mutex, MutexGuard};
8use std::time::Duration;
9
10use async_trait::async_trait;
11use bytes::Bytes;
12use tokio::sync::{broadcast, mpsc};
13
14use crate::error::SteamError;
15
16use super::disconnected_event_args::DisconnectedEventArgs;
17use super::i_connection::{ConnectionEvent, ConnectionEventSenders, ConnectionEvents, IConnection};
18use super::net_msg_event_args::NetMsgEventArgs;
19use super::protocol_type::ProtocolTypes;
20use super::web_socket_context::{
21    construct_uri, construct_uri_for_host, TungsteniteWebSocketFactory, WebSocketContext,
22    WebSocketFactory, WebSocketSocket,
23};
24
25#[derive(Clone)]
26struct CurrentConnection {
27    context: Arc<WebSocketContext>,
28    socket: Arc<dyn WebSocketSocket>,
29}
30
31/// A Steam WebSocket connection.
32pub struct WebSocketConnection {
33    events: ConnectionEventSenders,
34    factory: Arc<dyn WebSocketFactory>,
35    host: Option<String>,
36    current: Arc<Mutex<Option<CurrentConnection>>>,
37    current_end_point: Arc<Mutex<Option<SocketAddr>>>,
38}
39
40impl std::fmt::Debug for WebSocketConnection {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.debug_struct("WebSocketConnection").finish_non_exhaustive()
43    }
44}
45
46impl WebSocketConnection {
47    /// Creates a websocket connection that uses the given factory to open
48    /// sockets.
49    pub fn new(factory: Arc<dyn WebSocketFactory>) -> Self {
50        Self {
51            events: ConnectionEventSenders::with_default_capacity(),
52            factory,
53            host: None,
54            current: Arc::new(Mutex::new(None)),
55            current_end_point: Arc::new(Mutex::new(None)),
56        }
57    }
58
59    /// Creates a websocket connection that opens `tokio-tungstenite` sockets.
60    pub fn new_tungstenite() -> Self {
61            Self::new(Arc::new(TungsteniteWebSocketFactory::new()))
62    }
63
64    /// Opens `tokio-tungstenite` sockets to the given host name. The host name
65    /// is kept because it's the TLS server name.
66    pub fn new_tungstenite_for_host(host: impl Into<String>) -> Self {
67        let mut connection = Self::new_tungstenite();
68        connection.host = Some(host.into());
69        connection
70    }
71
72    /// Like [`new_tungstenite_for_host`](Self::new_tungstenite_for_host), and
73    /// also sends the given `User-Agent` with the upgrade request.
74    pub fn new_tungstenite_for_host_with_user_agent(
75        host: impl Into<String>,
76        user_agent: impl Into<String>,
77    ) -> Self {
78        let mut connection = Self::new(Arc::new(
79            TungsteniteWebSocketFactory::with_user_agent(user_agent),
80        ));
81        connection.host = Some(host.into());
82        connection
83    }
84
85    /// Subscribes to the `NetMsgReceived` event.
86    pub fn subscribe_net_msg_received(&self) -> broadcast::Receiver<NetMsgEventArgs> {
87        self.events.subscribe_net_msg_received()
88    }
89
90    /// Subscribes to the `Connected` event.
91    pub fn subscribe_connected(&self) -> broadcast::Receiver<()> {
92        self.events.subscribe_connected()
93    }
94
95    /// Subscribes to the `Disconnected` event.
96    pub fn subscribe_disconnected(&self) -> broadcast::Receiver<DisconnectedEventArgs> {
97        self.events.subscribe_disconnected()
98    }
99}
100
101#[async_trait]
102impl IConnection for WebSocketConnection {
103    fn current_end_point(&self) -> Option<SocketAddr> {
104        lock(&self.current_end_point).clone()
105    }
106
107    fn protocol_types(&self) -> ProtocolTypes {
108        ProtocolTypes::WEB_SOCKET
109    }
110
111    async fn connect(&self, end_point: SocketAddr, timeout: Duration) -> Result<(), SteamError> {
112        let uri = match &self.host {
113            Some(host) => construct_uri_for_host(host, end_point.port()),
114            None => construct_uri(end_point),
115        };
116
117        let socket = match self.factory.connect(&uri, timeout).await {
118            Ok(socket) => socket,
119            Err(err) => {
120                tracing::debug!("WebSocketConnection: Exception connecting websocket: {err}");
121                self.events
122                    .emit_disconnected(DisconnectedEventArgs::new(false));
123                return Err(err);
124            }
125        };
126
127        let context = Arc::new(WebSocketContext::new_with_uri(
128            end_point,
129            uri,
130            self.events.clone(),
131        ));
132
133        let old = {
134            let mut current = lock(&self.current);
135            current.replace(CurrentConnection {
136                context: context.clone(),
137                socket: socket.clone(),
138            })
139        };
140
141        if let Some(old) = old {
142            tracing::debug!(
143                "WebSocketConnection: Attempted to connect while already connected. Closing old connection..."
144            );
145            old.context.cancel();
146            let _ = old.socket.close().await;
147            self.events
148                .emit_disconnected(DisconnectedEventArgs::new(false));
149        }
150
151        *lock(&self.current_end_point) = Some(end_point);
152
153        context.start(socket);
154
155        Ok(())
156    }
157
158    async fn disconnect(&self, user_initiated: bool) {
159        let current = {
160            let mut guard = lock(&self.current);
161            guard.take()
162        };
163
164        if let Some(current) = current {
165            current.context.cancel();
166            current.context.notify_disconnected(user_initiated);
167            let _ = current.socket.close().await;
168        }
169
170        *lock(&self.current_end_point) = None;
171    }
172
173    async fn send(&self, data: Bytes) -> Result<(), SteamError> {
174        let current = lock(&self.current).clone();
175
176        let Some(current) = current else {
177            return Ok(());
178        };
179
180        match current.socket.send_binary(data).await {
181            Ok(()) => Ok(()),
182            Err(err) => {
183                tracing::debug!("WebSocketConnection: Exception while sending data: {err}");
184                self.disconnect(false).await;
185                Err(err)
186            }
187        }
188    }
189
190    fn local_ip(&self) -> Option<IpAddr> {
191        // Not `None`: SteamKit2 reports `IPAddress.None` (255.255.255.255) here
192        // and the logon carries an obfuscated form of it. With `None` the logon
193        // isn't sent at all, and with 0.0.0.0 the CM server doesn't answer it.
194        Some(IpAddr::V4(std::net::Ipv4Addr::new(255, 255, 255, 255)))
195    }
196}
197
198impl ConnectionEvents for WebSocketConnection {
199    fn events(&self) -> mpsc::UnboundedReceiver<ConnectionEvent> {
200        self.events.subscribe_events()
201    }
202
203    fn net_msg_received(&self) -> broadcast::Receiver<NetMsgEventArgs> {
204        self.events.subscribe_net_msg_received()
205    }
206
207    fn connected(&self) -> broadcast::Receiver<()> {
208        self.events.subscribe_connected()
209    }
210
211    fn disconnected(&self) -> broadcast::Receiver<DisconnectedEventArgs> {
212        self.events.subscribe_disconnected()
213    }
214}
215
216fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
217    match mutex.lock() {
218        Ok(guard) => guard,
219        Err(poisoned) => poisoned.into_inner(),
220    }
221}