steam-vent 0.5.0

Interact with the Steam network via rust
Documentation
use crate::auth::{ConfirmationError, ConfirmationMethod};
use crate::connection::raw::RawConnection;
use crate::connection::unauthenticated::AccessTokenError;
use crate::connection::{ConnectionImpl, ConnectionTrait};
use crate::eresult::EResult;
use crate::net::{JobId, NetMessageHeader, NetworkError};
use steam_vent_proto_steam::steammessages_base::CMsgIPAddress;
use steam_vent_proto_steam::steammessages_clientserver_login::{
    CMsgClientHello, CMsgClientLogon, CMsgClientLogonResponse,
};
use crate::NetMessage;
use protobuf::MessageField;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use steam_vent_crypto::CryptError;
use steam_vent_proto_steam::steammessages_base::cmsg_ipaddress;
use steamid_ng::{
    AccountType, Instance, InstanceFlags, InstanceType, SteamID, SteamIDParseError, Universe,
};
use thiserror::Error;
use tracing::debug;

type Result<T, E = ConnectionError> = std::result::Result<T, E>;

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ConnectionError {
    #[error("Access token error: {0:#}")]
    AccessToken(#[from] AccessTokenError),
    #[error("Network error: {0:#}")]
    Network(#[from] NetworkError),
    #[error("Login failed: {0:#}")]
    LoginError(#[from] LoginError),
    #[error("Aborted")]
    Aborted,
    #[error("Unsupported confirmation action")]
    UnsupportedConfirmationAction(Vec<ConfirmationMethod>),
}

impl From<ConfirmationError> for ConnectionError {
    fn from(value: ConfirmationError) -> Self {
        match value {
            ConfirmationError::Network(err) => err.into(),
            ConfirmationError::Aborted => ConnectionError::Aborted,
        }
    }
}

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum LoginError {
    #[error("invalid credentials")]
    InvalidCredentials,
    #[error("unknown error {0:?}")]
    Unknown(EResult),
    #[error("steam guard required")]
    SteamGuardRequired,
    #[error("steam returned an invalid public key: {0:#}")]
    InvalidPubKey(CryptError),
    #[error("account not available")]
    UnavailableAccount,
    #[error("rate limited")]
    RateLimited,
    #[error("invalid steam id")]
    InvalidSteamId,
}

impl From<EResult> for LoginError {
    fn from(value: EResult) -> Self {
        match value {
            EResult::InvalidPassword => LoginError::InvalidCredentials,
            EResult::AccountDisabled
            | EResult::AccountLockedDown
            | EResult::AccountHasBeenDeleted
            | EResult::AccountNotFound => LoginError::InvalidCredentials,
            EResult::RateLimitExceeded
            | EResult::AccountActivityLimitExceeded
            | EResult::LimitExceeded
            | EResult::AccountLimitExceeded => LoginError::RateLimited,
            EResult::AccountLoginDeniedNeedTwoFactor => LoginError::SteamGuardRequired,
            EResult::InvalidSteamID => LoginError::InvalidSteamId,
            value => LoginError::Unknown(value),
        }
    }
}

impl From<SteamIDParseError> for LoginError {
    fn from(_: SteamIDParseError) -> Self {
        LoginError::InvalidSteamId
    }
}

#[derive(Debug, Clone)]
pub struct JobIdCounter(Arc<AtomicU64>);

impl JobIdCounter {
    #[allow(clippy::should_implement_trait)]
    pub fn next(&self) -> JobId {
        JobId(self.0.fetch_add(1, Ordering::Relaxed))
    }
}

impl Default for JobIdCounter {
    fn default() -> Self {
        Self(Arc::new(AtomicU64::new(1)))
    }
}

#[derive(Debug, Clone)]
pub struct Session {
    pub session_id: i32,
    pub cell_id: u32,
    pub public_ip: Option<IpAddr>,
    pub ip_country_code: Option<String>,
    pub job_id: JobIdCounter,
    pub steam_id: SteamID,
    pub heartbeat_interval: Duration,
    pub app_id: Option<u32>,
    pub access_token: Option<String>,
}

impl Default for Session {
    fn default() -> Self {
        Session {
            session_id: 0,
            cell_id: 0,
            public_ip: None,
            ip_country_code: None,
            job_id: JobIdCounter::default(),
            steam_id: crate::net::steam_id_nil(),
            heartbeat_interval: Duration::from_secs(15),
            app_id: None,
            access_token: None,
        }
    }
}

impl Session {
    pub fn header(&self, job: bool) -> NetMessageHeader {
        NetMessageHeader {
            session_id: self.session_id,
            source_job_id: if job { self.job_id.next() } else { JobId::NONE },
            target_job_id: JobId::NONE,
            steam_id: self.steam_id,
            source_app_id: self.app_id,
            ..NetMessageHeader::default()
        }
    }

    pub fn is_server(&self) -> bool {
        self.steam_id.account_type() == AccountType::AnonGameServer
            || self.steam_id.account_type() == AccountType::GameServer
    }

    pub fn with_app_id(mut self, app_id: u32) -> Self {
        self.app_id = Some(app_id);
        self
    }
}

pub async fn anonymous(connection: &RawConnection, account_type: AccountType) -> Result<Session> {
    let mut ip = CMsgIPAddress::new();
    ip.set_v4(0);

    let logon = CMsgClientLogon {
        protocol_version: Some(65580),
        client_os_type: Some(203),
        anon_user_target_account_name: Some(String::from("anonymous")),
        account_name: Some(String::from("anonymous")),
        supports_rate_limit_response: Some(false),
        obfuscated_private_ip: MessageField::some(ip),
        client_language: Some(String::new()),
        chat_mode: Some(2),
        client_package_version: Some(1771),
        ..CMsgClientLogon::default()
    };

    send_logon(
        connection,
        logon,
        SteamID::new(
            0,
            Instance::new(InstanceType::All, InstanceFlags::None),
            account_type,
            Universe::Public,
        ),
    )
    .await
}

pub async fn login(
    connection: &mut RawConnection,
    account: &str,
    steam_id: SteamID,
    access_token: &str,
) -> Result<Session> {
    let mut ip = CMsgIPAddress::new();
    ip.set_v4(0);

    let logon = CMsgClientLogon {
        protocol_version: Some(65580),
        client_os_type: Some(203),
        account_name: Some(String::from(account)),
        supports_rate_limit_response: Some(false),
        obfuscated_private_ip: MessageField::some(ip),
        client_language: Some(String::new()),
        machine_name: Some(String::new()),
        steamguard_dont_remember_computer: Some(false),
        chat_mode: Some(2),
        access_token: Some(access_token.into()),
        client_package_version: Some(1771),
        ..CMsgClientLogon::default()
    };

    send_logon(connection, logon, steam_id).await
}

async fn send_logon(
    connection: &RawConnection,
    logon: CMsgClientLogon,
    steam_id: SteamID,
) -> Result<Session> {
    let access_token = logon.access_token.clone();

    let header = NetMessageHeader {
        source_job_id: JobId::NONE,
        target_job_id: JobId::NONE,
        steam_id,
        ..NetMessageHeader::default()
    };

    let filter = connection.filter();
    let fut = filter.one_kind(CMsgClientLogonResponse::KIND);
    connection.raw_send(header, logon).await?;

    debug!("waiting for login response");
    let raw_response = fut.await.map_err(|_| NetworkError::EOF)?;
    let (header, response) = raw_response.into_header_and_message::<CMsgClientLogonResponse>()?;
    EResult::from_result(response.eresult()).map_err(LoginError::from)?;

    let assigned_steam_id = if response.has_client_supplied_steamid() {
        let raw = response.client_supplied_steamid();
        SteamID::try_from(raw).unwrap_or(steam_id)
    } else if header.steam_id != crate::net::steam_id_nil() {
        header.steam_id
    } else {
        steam_id
    };

    debug!(steam_id = %u64::from(assigned_steam_id), "session started");
    Ok(Session {
        session_id: header.session_id,
        cell_id: response.cell_id(),
        public_ip: response.public_ip.ip.as_ref().and_then(|ip| match &ip {
            cmsg_ipaddress::Ip::V4(bits) => Some(IpAddr::V4(Ipv4Addr::from(*bits))),
            cmsg_ipaddress::Ip::V6(bytes) if bytes.len() == 16 => {
                let mut bits = [0u8; 16];
                bits.copy_from_slice(&bytes[..]);
                Some(IpAddr::V6(Ipv6Addr::from(bits)))
            }
            _ => None,
        }),
        ip_country_code: response.ip_country_code.clone(),
        steam_id: assigned_steam_id,
        job_id: JobIdCounter::default(),
        heartbeat_interval: Duration::from_secs(response.heartbeat_seconds() as u64),
        app_id: None,
        access_token,
    })
}

pub async fn hello<C: ConnectionImpl>(conn: &mut C) -> std::result::Result<(), NetworkError> {
    const PROTOCOL_VERSION: u32 = 65580;
    let req = CMsgClientHello {
        protocol_version: Some(PROTOCOL_VERSION),
        ..CMsgClientHello::default()
    };

    let header = NetMessageHeader {
        session_id: 0,
        source_job_id: JobId::NONE,
        target_job_id: JobId::NONE,
        steam_id: crate::net::steam_id_nil(),
        ..NetMessageHeader::default()
    };

    conn.raw_send_with_kind(
        header,
        req,
        CMsgClientHello::KIND,
        CMsgClientHello::IS_PROTOBUF,
    )
    .await?;
    Ok(())
}