Skip to main content

steamkit/steam/handlers/steam_game_server/
steam_game_server.rs

1//! Logging on as a game server and reporting its status.
2
3use std::fmt;
4use std::net::IpAddr;
5use std::sync::{Arc, Weak};
6
7use async_trait::async_trait;
8use steamkit_protos::steam::{CMsgClientLogOff, CMsgClientLogon, CMsgGsServerType};
9
10use crate::base::client_msg::{ClientMsgProtobuf, IClientMsg};
11use crate::base::generated::steam_language::internal::MsgClientLogon;
12use crate::base::generated::steam_language::{EAccountType, EMsg, EResult, EServerFlags};
13use crate::base::packet_base::IPacketMsg;
14use crate::error::{SteamError, SteamResult};
15use crate::steam::handlers::client_msg_handler::ClientMsgHandler;
16use crate::steam::handlers::steam_user::callbacks::LoggedOnCallback;
17use crate::steam::steam_client::callback_mgr::callback_msg::CallbackMsg;
18use crate::steam::steam_client::steam_client::SteamClient;
19use crate::types::steam_id::SteamID;
20use crate::util::net_helpers::NetHelpers;
21
22use super::callbacks::{StatusReplyCallback, TicketAuthCallback};
23
24/// What's needed to log on as a game server.
25#[derive(Clone, Debug, Default)]
26pub struct LogOnDetails {
27    /// The game server's login token.
28    pub token: Option<String>,
29    /// The app id this game server will serve.
30    pub app_id: u32,
31}
32
33/// The current status of the game server.
34#[derive(Clone, Debug)]
35pub struct StatusDetails {
36    /// The app id this game server is serving.
37    pub app_id: u32,
38    /// The server's basic state, as flags.
39    pub server_flags: EServerFlags,
40    /// The directory the game data is in.
41    pub game_directory: Option<String>,
42    /// The IP address the game server listens on.
43    pub address: Option<IpAddr>,
44    /// The port the game server listens on.
45    pub port: u32,
46    /// The port the game server responds to queries on.
47    pub query_port: u32,
48    /// The current version of the game server.
49    pub version: Option<String>,
50}
51
52impl Default for StatusDetails {
53    fn default() -> Self {
54        Self {
55            app_id: 0,
56            server_flags: EServerFlags::None,
57            game_directory: None,
58            address: None,
59            port: 0,
60            query_port: 0,
61            version: None,
62        }
63    }
64}
65
66/// Interacts with the Steam network as a game server.
67pub struct SteamGameServer {
68    client: Weak<SteamClient>,
69}
70
71impl fmt::Debug for SteamGameServer {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.debug_struct("SteamGameServer").finish_non_exhaustive()
74    }
75}
76
77impl SteamGameServer {
78    /// Creates the handler and stores a weak back-reference to the client.
79    pub fn new(client: &Arc<SteamClient>) -> Self {
80        Self {
81            client: Arc::downgrade(client),
82        }
83    }
84
85    /// Upgrades the weak client reference.
86    fn client(&self) -> SteamResult<Arc<SteamClient>> {
87        self.client
88            .upgrade()
89            .ok_or_else(|| SteamError::Connection("SteamClient has been dropped".to_string()))
90    }
91
92    /// Whether the server is about to close the connection on purpose.
93    pub fn expect_disconnection(&self) -> bool {
94        self.client().map(|client| client.expect_disconnection()).unwrap_or(false)
95    }
96
97    /// Logs on as a persistent game server. The client has to be connected
98    /// already. The result comes in a [`LoggedOnCallback`].
99    pub fn log_on(&self, details: &LogOnDetails) -> SteamResult<()> {
100        let client = self.client()?;
101
102        let token = details.token.as_deref().unwrap_or_default();
103        if token.is_empty() {
104            return Err(SteamError::InvalidArgument(
105                "LogOn requires a game server token to be set in 'details'.".to_string(),
106            ));
107        }
108
109        if !client.is_connected() {
110            client.post_callback(Box::new(LoggedOnCallback::from_result(
111                EResult::NoConnection,
112            )));
113            return Ok(());
114        }
115
116        let universe = client.configuration().universe();
117        let gs_id = SteamID::from_parts_instanced(EAccountType::GameServer, universe, 0, 0);
118
119        let mut logon = ClientMsgProtobuf::<CMsgClientLogon>::new(EMsg::ClientLogonGameServer);
120
121        logon.set_session_id(0);
122        // Write the raw id: `set_steam_id` rebuilds the id from its parts and
123        // would use the desktop account instance, but a game server id has
124        // instance 0.
125        logon.proto_header_mut().steamid = Some(gs_id.convert_to_u64());
126
127        let local_ip = client.local_ip().ok_or_else(|| {
128            SteamError::Connection("the local IP address is not known yet".to_string())
129        })?;
130        let mut obfuscated_ip = NetHelpers::get_msg_ip_address(&local_ip);
131        NetHelpers::obfuscate_private_ip(&mut obfuscated_ip);
132        logon.body.obfuscated_private_ip = Some(obfuscated_ip);
133
134        logon.body.protocol_version = Some(MsgClientLogon::CURRENT_PROTOCOL);
135        logon.body.client_os_type = Some(crate::util::utils::get_os_type() as u32);
136        logon.body.game_server_app_id = Some(details.app_id as i32);
137        logon.body.machine_id = crate::util::hardware_utils::get_machine_id(
138            client.configuration().machine_info_provider(),
139        );
140        logon.body.game_server_token = Some(token.to_string());
141
142        client.send(&logon)?;
143
144        Ok(())
145    }
146
147    /// Logs on as an anonymous game server. The client has to be connected
148    /// already. The result comes in a [`LoggedOnCallback`].
149    ///
150    /// `app_id` is the app this server serves, or 0 for the default.
151    pub fn log_on_anonymous(&self, app_id: u32) -> SteamResult<()> {
152        let client = self.client()?;
153
154        if !client.is_connected() {
155            client.post_callback(Box::new(LoggedOnCallback::from_result(
156                EResult::NoConnection,
157            )));
158            return Ok(());
159        }
160
161        let universe = client.configuration().universe();
162        let gs_id = SteamID::from_parts_instanced(EAccountType::AnonGameServer, universe, 0, 0);
163
164        let mut logon = ClientMsgProtobuf::<CMsgClientLogon>::new(EMsg::ClientLogon);
165
166        logon.set_session_id(0);
167        // Write the raw id: `set_steam_id` rebuilds the id from its parts and
168        // would use the desktop account instance, but an anonymous game server
169        // id has instance 0.
170        logon.proto_header_mut().steamid = Some(gs_id.convert_to_u64());
171
172        let local_ip = client.local_ip().ok_or_else(|| {
173            SteamError::Connection("the local IP address is not known yet".to_string())
174        })?;
175        let mut obfuscated_ip = NetHelpers::get_msg_ip_address(&local_ip);
176        NetHelpers::obfuscate_private_ip(&mut obfuscated_ip);
177        logon.body.obfuscated_private_ip = Some(obfuscated_ip);
178
179        logon.body.protocol_version = Some(MsgClientLogon::CURRENT_PROTOCOL);
180        logon.body.client_os_type = Some(crate::util::utils::get_os_type() as u32);
181        logon.body.game_server_app_id = Some(app_id as i32);
182        logon.body.machine_id = crate::util::hardware_utils::get_machine_id(
183            client.configuration().machine_info_provider(),
184        );
185
186        client.send(&logon)?;
187
188        Ok(())
189    }
190
191    /// Logs off. The server then disconnects us and a `DisconnectedCallback`
192    /// is posted.
193    pub fn log_off(&self) -> SteamResult<()> {
194        let client = self.client()?;
195
196        client.set_expect_disconnection(true);
197
198        let log_off = ClientMsgProtobuf::<CMsgClientLogOff>::new(EMsg::ClientLogOff);
199        client.send(&log_off)?;
200
201        Ok(())
202    }
203
204    /// Sends the server's status to Steam. The reply comes in a
205    /// [`StatusReplyCallback`].
206    pub fn send_status(&self, details: &StatusDetails) -> SteamResult<()> {
207        let client = self.client()?;
208
209        let game_ip_address = match details.address {
210            Some(IpAddr::V4(v4)) => Some(u32::from_be_bytes(v4.octets())),
211            Some(IpAddr::V6(_)) => {
212                return Err(SteamError::InvalidArgument(
213                    "Only IPv4 addresses are supported.".to_string(),
214                ))
215            }
216            None => None,
217        };
218
219        let mut status = ClientMsgProtobuf::<CMsgGsServerType>::new(EMsg::GSServerType);
220
221        status.body.app_id_served = Some(details.app_id);
222        status.body.flags = Some(details.server_flags.value() as u32);
223        status.body.game_dir = details.game_directory.clone();
224        status.body.game_port = Some(details.port);
225        status.body.game_query_port = Some(details.query_port);
226        status.body.game_version = details.version.clone();
227        status.body.deprecated_game_ip_address = game_ip_address;
228
229        client.send(&status)?;
230
231
232        Ok(())
233    }
234}
235
236#[async_trait]
237impl ClientMsgHandler for SteamGameServer {
238    async fn handle_msg(
239        &self,
240        client: &SteamClient,
241        packet_msg: &dyn IPacketMsg,
242    ) -> SteamResult<()> {
243        let callback: Option<Box<dyn CallbackMsg>> = match packet_msg.msg_type() {
244            EMsg::GSStatusReply => Some(Box::new(StatusReplyCallback::from_packet(packet_msg)?)),
245            EMsg::ClientTicketAuthComplete => {
246                Some(Box::new(TicketAuthCallback::from_packet(packet_msg)?))
247            }
248            // Not a message we handle.
249            _ => None,
250        };
251
252        if let Some(callback) = callback {
253            client.post_callback(callback);
254        }
255
256        Ok(())
257    }
258}