steam_vent/
session.rs

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}