1use crate::auth::{ConfirmationError, ConfirmationMethod};
2use crate::connection::raw::RawConnection;
3use crate::connection::{ConnectionImpl, ConnectionTrait};
4use crate::eresult::EResult;
5use crate::net::{JobId, NetMessageHeader, NetworkError};
6use crate::proto::steammessages_base::CMsgIPAddress;
7use crate::proto::steammessages_clientserver_login::{
8 CMsgClientHello, CMsgClientLogon, CMsgClientLogonResponse,
9};
10use crate::NetMessage;
11use protobuf::MessageField;
12use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::Arc;
15use std::time::Duration;
16use steam_vent_crypto::CryptError;
17use steam_vent_proto::steammessages_base::cmsg_ipaddress;
18use steamid_ng::{
19 AccountType, Instance, InstanceFlags, InstanceType, SteamID, SteamIDParseError, Universe,
20};
21use thiserror::Error;
22use tracing::debug;
23
24type Result<T, E = ConnectionError> = std::result::Result<T, E>;
25
26#[derive(Debug, Error)]
27#[non_exhaustive]
28pub enum ConnectionError {
29 #[error("Network error: {0:#}")]
30 Network(#[from] NetworkError),
31 #[error("Login failed: {0:#}")]
32 LoginError(#[from] LoginError),
33 #[error("Aborted")]
34 Aborted,
35 #[error("Unsupported confirmation action")]
36 UnsupportedConfirmationAction(Vec<ConfirmationMethod>),
37}
38
39impl From<ConfirmationError> for ConnectionError {
40 fn from(value: ConfirmationError) -> Self {
41 match value {
42 ConfirmationError::Network(err) => err.into(),
43 ConfirmationError::Aborted => ConnectionError::Aborted,
44 }
45 }
46}
47
48#[derive(Debug, Error)]
49#[non_exhaustive]
50pub enum LoginError {
51 #[error("invalid credentials")]
52 InvalidCredentials,
53 #[error("unknown error {0:?}")]
54 Unknown(EResult),
55 #[error("steam guard required")]
56 SteamGuardRequired,
57 #[error("steam returned an invalid public key: {0:#}")]
58 InvalidPubKey(CryptError),
59 #[error("account not available")]
60 UnavailableAccount,
61 #[error("rate limited")]
62 RateLimited,
63 #[error("invalid steam id")]
64 InvalidSteamId,
65}
66
67impl From<EResult> for LoginError {
68 fn from(value: EResult) -> Self {
69 match value {
70 EResult::InvalidPassword => LoginError::InvalidCredentials,
71 EResult::AccountDisabled
72 | EResult::AccountLockedDown
73 | EResult::AccountHasBeenDeleted
74 | EResult::AccountNotFound => LoginError::InvalidCredentials,
75 EResult::RateLimitExceeded
76 | EResult::AccountActivityLimitExceeded
77 | EResult::LimitExceeded
78 | EResult::AccountLimitExceeded => LoginError::RateLimited,
79 EResult::AccountLoginDeniedNeedTwoFactor => LoginError::SteamGuardRequired,
80 EResult::InvalidSteamID => LoginError::InvalidSteamId,
81 value => LoginError::Unknown(value),
82 }
83 }
84}
85
86impl From<SteamIDParseError> for LoginError {
87 fn from(_: SteamIDParseError) -> Self {
88 LoginError::InvalidSteamId
89 }
90}
91
92#[derive(Debug, Clone)]
93pub struct JobIdCounter(Arc<AtomicU64>);
94
95impl JobIdCounter {
96 #[allow(clippy::should_implement_trait)]
97 pub fn next(&self) -> JobId {
98 JobId(self.0.fetch_add(1, Ordering::Relaxed))
99 }
100}
101
102impl Default for JobIdCounter {
103 fn default() -> Self {
104 Self(Arc::new(AtomicU64::new(1)))
105 }
106}
107
108#[derive(Debug, Clone)]
109pub struct Session {
110 pub session_id: i32,
111 pub cell_id: u32,
112 pub public_ip: Option<IpAddr>,
113 pub ip_country_code: Option<String>,
114 pub job_id: JobIdCounter,
115 pub steam_id: SteamID,
116 pub heartbeat_interval: Duration,
117 pub app_id: Option<u32>,
118}
119
120impl Default for Session {
121 fn default() -> Self {
122 Session {
123 session_id: 0,
124 cell_id: 0,
125 public_ip: None,
126 ip_country_code: None,
127 job_id: JobIdCounter::default(),
128 steam_id: SteamID::default(),
129 heartbeat_interval: Duration::from_secs(15),
130 app_id: None,
131 }
132 }
133}
134
135impl Session {
136 pub fn header(&self, job: bool) -> NetMessageHeader {
137 NetMessageHeader {
138 session_id: self.session_id,
139 source_job_id: if job { self.job_id.next() } else { JobId::NONE },
140 target_job_id: JobId::NONE,
141 steam_id: self.steam_id,
142 source_app_id: self.app_id,
143 ..NetMessageHeader::default()
144 }
145 }
146
147 pub fn is_server(&self) -> bool {
148 self.steam_id.account_type() == AccountType::AnonGameServer
149 || self.steam_id.account_type() == AccountType::GameServer
150 }
151
152 pub fn with_app_id(mut self, app_id: u32) -> Self {
153 self.app_id = Some(app_id);
154 self
155 }
156}
157
158pub async fn anonymous(connection: &RawConnection, account_type: AccountType) -> Result<Session> {
159 let mut ip = CMsgIPAddress::new();
160 ip.set_v4(0);
161
162 let logon = CMsgClientLogon {
163 protocol_version: Some(65580),
164 client_os_type: Some(203),
165 anon_user_target_account_name: Some(String::from("anonymous")),
166 account_name: Some(String::from("anonymous")),
167 supports_rate_limit_response: Some(false),
168 obfuscated_private_ip: MessageField::some(ip),
169 client_language: Some(String::new()),
170 chat_mode: Some(2),
171 client_package_version: Some(1771),
172 ..CMsgClientLogon::default()
173 };
174
175 send_logon(
176 connection,
177 logon,
178 SteamID::new(
179 0,
180 Instance::new(InstanceType::All, InstanceFlags::None),
181 account_type,
182 Universe::Public,
183 ),
184 )
185 .await
186}
187
188pub async fn login(
189 connection: &mut RawConnection,
190 account: &str,
191 steam_id: SteamID,
192 access_token: &str,
193) -> Result<Session> {
194 let mut ip = CMsgIPAddress::new();
195 ip.set_v4(0);
196
197 let logon = CMsgClientLogon {
198 protocol_version: Some(65580),
199 client_os_type: Some(203),
200 account_name: Some(String::from(account)),
201 supports_rate_limit_response: Some(false),
202 obfuscated_private_ip: MessageField::some(ip),
203 client_language: Some(String::new()),
204 machine_name: Some(String::new()),
205 steamguard_dont_remember_computer: Some(false),
206 chat_mode: Some(2),
207 access_token: Some(access_token.into()),
208 client_package_version: Some(1771),
209 ..CMsgClientLogon::default()
210 };
211
212 send_logon(connection, logon, steam_id).await
213}
214
215async fn send_logon(
216 connection: &RawConnection,
217 logon: CMsgClientLogon,
218 steam_id: SteamID,
219) -> Result<Session> {
220 let header = NetMessageHeader {
221 source_job_id: JobId::NONE,
222 target_job_id: JobId::NONE,
223 steam_id,
224 ..NetMessageHeader::default()
225 };
226
227 let filter = connection.filter();
228 let fut = filter.one_kind(CMsgClientLogonResponse::KIND);
229 connection.raw_send(header, logon).await?;
230
231 debug!("waiting for login response");
232 let raw_response = fut.await.map_err(|_| NetworkError::EOF)?;
233 let (header, response) = raw_response.into_header_and_message::<CMsgClientLogonResponse>()?;
234 EResult::from_result(response.eresult()).map_err(LoginError::from)?;
235 debug!(steam_id = u64::from(steam_id), "session started");
236 Ok(Session {
237 session_id: header.session_id,
238 cell_id: response.cell_id(),
239 public_ip: response.public_ip.ip.as_ref().and_then(|ip| match &ip {
240 cmsg_ipaddress::Ip::V4(bits) => Some(IpAddr::V4(Ipv4Addr::from(*bits))),
241 cmsg_ipaddress::Ip::V6(bytes) if bytes.len() == 16 => {
242 let mut bits = [0u8; 16];
243 bits.copy_from_slice(&bytes[..]);
244 Some(IpAddr::V6(Ipv6Addr::from(bits)))
245 }
246 _ => None,
247 }),
248 ip_country_code: response.ip_country_code.clone(),
249 steam_id: header.steam_id,
250 job_id: JobIdCounter::default(),
251 heartbeat_interval: Duration::from_secs(response.heartbeat_seconds() as u64),
252 app_id: None,
253 })
254}
255
256pub async fn hello<C: ConnectionImpl>(conn: &mut C) -> std::result::Result<(), NetworkError> {
257 const PROTOCOL_VERSION: u32 = 65580;
258 let req = CMsgClientHello {
259 protocol_version: Some(PROTOCOL_VERSION),
260 ..CMsgClientHello::default()
261 };
262
263 let header = NetMessageHeader {
264 session_id: 0,
265 source_job_id: JobId::NONE,
266 target_job_id: JobId::NONE,
267 steam_id: SteamID::default(),
268 ..NetMessageHeader::default()
269 };
270
271 conn.raw_send_with_kind(
272 header,
273 req,
274 CMsgClientHello::KIND,
275 CMsgClientHello::IS_PROTOBUF,
276 )
277 .await?;
278 Ok(())
279}