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