steam_vent/
session.rs

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}