Compare commits

...

2 commits

10 changed files with 419 additions and 11 deletions

58
Cargo.lock generated
View file

@ -896,6 +896,27 @@ dependencies = [
"libm",
]
[[package]]
name = "num_enum"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "object"
version = "0.32.2"
@ -1042,6 +1063,15 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
@ -1700,6 +1730,23 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "toml_edit"
version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower"
version = "0.4.13"
@ -1886,6 +1933,7 @@ version = "0.1.0"
dependencies = [
"async-tungstenite",
"base64",
"bitflags",
"color-eyre",
"dialoguer",
"env_logger",
@ -1894,6 +1942,7 @@ dependencies = [
"hex",
"keyvalues-serde",
"log",
"num_enum",
"protobuf",
"qrcode",
"rand",
@ -2144,6 +2193,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
dependencies = [
"memchr",
]
[[package]]
name = "zeroize"
version = "1.8.1"

View file

@ -5,11 +5,13 @@ version.workspace = true
[dependencies]
async-tungstenite = { version = "0.27.0", features = ["tokio-rustls-native-certs"] }
bitflags = "2.6.0"
color-eyre.workspace = true
flate2 = "1.0.33"
futures = "0.3.30"
keyvalues-serde.workspace = true
log.workspace = true
num_enum = "0.7.3"
protobuf.workspace = true
rand = "0.8.5"
reqwest = { version = "0.12", features = ["rustls-tls-native-roots"], default-features = false}

View file

@ -22,7 +22,9 @@ pub async fn main() -> eyre::Result<()> {
let servers = vapore::selection::bootstrap_find_servers().await?;
log::debug!("Found servers: {:?}", servers);
let session = vapore::connection::CMSession::connect(&servers[0]).await?;
let (session, context) = vapore::connection::CMSession::connect(&servers[0]).await?;
tokio::spawn(context);
session.send_notification(
EMsg::k_EMsgClientHello,

View file

@ -30,7 +30,9 @@ pub async fn main() -> eyre::Result<()> {
let servers = vapore::selection::bootstrap_find_servers().await?;
log::debug!("Found servers: {:?}", servers);
let session = CMSession::connect(&servers[0]).await?;
let (session, context) = CMSession::connect(&servers[0]).await?;
tokio::spawn(context);
session.send_notification(
EMsg::k_EMsgClientHello,

50
lib/src/client.rs Normal file
View file

@ -0,0 +1,50 @@
use std::sync::Arc;
use tokio::sync::watch;
use crate::{
connection::{CMContext, CMSession},
state::apps::License,
ClientError,
};
struct SteamClientInner {
/// The currently active socket to a Connection Manager
/// TODO: Support recreation when one fails
pub session: CMSession,
/// Licenses owned by current user, as told periodically by Valve.
/// Use an Arc in the receiver so that the sender doesn't block if
/// someone keeps a long-lived reference to the licenses
pub licenses: watch::Receiver<Arc<Vec<License>>>,
}
/// Higher-level Steam client. Unlike [crate::connection::CMSession],
/// SteamClient records state sent from the server, like owned games
/// and email address.
#[derive(Clone)]
pub struct SteamClient {
/// Inner content.
inner: Arc<SteamClientInner>,
}
impl SteamClient {
pub async fn connect(servers: &[&str]) -> Result<(Self, CMContext), ClientError> {
let (session, context) = CMSession::connect(servers[0]).await?;
let inner = Arc::new(SteamClientInner {
session,
licenses: todo!(),
});
Ok((Self { inner }, context))
}
pub fn licenses(&self) -> Arc<Vec<License>> {
self.inner.licenses.borrow().clone()
}
pub fn session(&self) -> CMSession {
self.inner.session.clone()
}
}

View file

@ -71,7 +71,12 @@ impl CMSessionInner {
}
}
struct CMContext {
/// Task to manage a single Connection Manager socket.
/// Functions on the matching [CMSession] objects will use this Context
/// to send and recieve messages.
/// Users _must_ poll this to make any progress, e.g. with `tokio::spawn(context)`
#[must_use = "Messages will not be sent or received unless context is polled"]
pub struct CMContext {
socket: WebSocketStream<ConnectStream>,
session: Arc<Mutex<CMSessionInner>>,
}
@ -90,7 +95,7 @@ impl CMContext {
let raw_messages = CMRawProtoBufMessage::try_parse_multi(&message_data)?;
let mut session = self.session.lock().expect("Lock was poisoned");
let mut session = self.session.lock()?;
for message in raw_messages.into_iter() {
log::trace!("Got message: {:?}", message);
@ -159,7 +164,8 @@ impl CMContext {
cx: &mut std::task::Context<'_>,
) -> Result<(), ClientError> {
{
let mut session = self.session.lock().expect("Lock was poisoned");
// If the lock was poisoned we're never going to make any forward progress anyway
let mut session = self.session.lock()?;
session.send_waker = Some(cx.waker().clone());
}
@ -204,7 +210,8 @@ pub struct CMSession {
}
impl CMSession {
pub async fn connect(server: &str) -> Result<Self, ClientError> {
/// Connect to a given Steam Connection Manager server
pub async fn connect(server: &str) -> Result<(Self, CMContext), ClientError> {
let (socket, _) = connect_async(server)
.await
.with_context(|_| WebSocketConnectSnafu {
@ -229,13 +236,11 @@ impl CMSession {
session: inner_wrapped.clone(),
};
tokio::spawn(context);
let session = Self {
inner: inner_wrapped,
};
Ok(session)
Ok((session, context))
}
pub fn begin_heartbeat(&self, interval: u32) {
@ -285,7 +290,7 @@ impl CMSession {
action: EMsg,
body: T,
) -> Result<(), ClientError> {
let mut inner = self.inner.lock().expect("Lock was poisoned");
let mut inner = self.inner.lock()?;
log::trace!("Sending notification of type {:?}", action);
@ -359,7 +364,7 @@ impl<T: protobuf::Message, U: protobuf::Message> Future for CallServiceMethod<'_
fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
let session_arc = self.session.inner.clone();
let mut session = session_arc.lock().expect("Lock was poisoned");
let mut session = session_arc.lock()?;
// We only have to send the message once, use jobid for that flag
if self.jobid.is_none() {

View file

@ -1,5 +1,8 @@
use num_enum::TryFromPrimitiveError;
use snafu::prelude::*;
use crate::state::apps::{ELicenseType, EPaymentMethod};
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum ClientError {
@ -62,6 +65,16 @@ pub enum ClientError {
BadResponseAction {
actual: vapore_proto::enums_clientserver::EMsg,
},
#[snafu(display("Unknown value while parsing enum EPaymentMethod"))]
TryFromEPaymentMethod {
source: TryFromPrimitiveError<EPaymentMethod>,
},
#[snafu(display("Unknown value while parsing enum ELicenseType"))]
TryFromELicenseType {
source: TryFromPrimitiveError<ELicenseType>,
},
}
impl<T> From<std::sync::PoisonError<T>> for ClientError {

View file

@ -1,6 +1,8 @@
pub mod client;
pub mod connection;
pub mod error;
pub mod message;
pub mod selection;
pub mod state;
pub use error::ClientError;

273
lib/src/state/apps.rs Normal file
View file

@ -0,0 +1,273 @@
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use num_enum::TryFromPrimitive;
use snafu::prelude::*;
use vapore_proto::steammessages_clientserver::cmsg_client_license_list;
use crate::{
error::{TryFromELicenseTypeSnafu, TryFromEPaymentMethodSnafu},
ClientError,
};
#[repr(u32)]
#[derive(Debug, Copy, Clone, TryFromPrimitive)]
pub enum EPaymentMethod {
None = 0,
ActivationCode = 1,
CreditCard = 2,
Giropay = 3,
PayPal = 4,
Ideal = 5,
PaySafeCard = 6,
Sofort = 7,
GuestPass = 8,
WebMoney = 9,
MoneyBookers = 10,
AliPay = 11,
Yandex = 12,
Kiosk = 13,
Qiwi = 14,
GameStop = 15,
HardwarePromo = 16,
MoPay = 17,
BoletoBancario = 18,
BoaCompraGold = 19,
BancoDoBrasilOnline = 20,
ItauOnline = 21,
BradescoOnline = 22,
Pagseguro = 23,
VisaBrazil = 24,
AmexBrazil = 25,
Aura = 26,
Hipercard = 27,
MastercardBrazil = 28,
DinersCardBrazil = 29,
AuthorizedDevice = 30,
MOLPoints = 31,
ClickAndBuy = 32,
Beeline = 33,
Konbini = 34,
EClubPoints = 35,
CreditCardJapan = 36,
BankTransferJapan = 37,
PayEasy = 38,
Zong = 39,
CultureVoucher = 40,
BookVoucher = 41,
HappymoneyVoucher = 42,
ConvenientStoreVoucher = 43,
GameVoucher = 44,
Multibanco = 45,
Payshop = 46,
MaestroBoaCompra = 47,
OXXO = 48,
ToditoCash = 49,
Carnet = 50,
SPEI = 51,
ThreePay = 52,
IsBank = 53,
Garanti = 54,
Akbank = 55,
YapiKredi = 56,
Halkbank = 57,
BankAsya = 58,
Finansbank = 59,
DenizBank = 60,
PTT = 61,
CashU = 62,
SantanderRio = 63,
AutoGrant = 64,
WebMoneyJapan = 65,
OneCard = 66,
PSE = 67,
Exito = 68,
Efecty = 69,
Paloto = 70,
PinValidda = 71,
MangirKart = 72,
BancoCreditoDePeru = 73,
BBVAContinental = 74,
SafetyPay = 75,
PagoEfectivo = 76,
Trustly = 77,
UnionPay = 78,
BitCoin = 79,
LicensedSite = 80,
BitCash = 81,
NetCash = 82,
Nanaco = 83,
Tenpay = 84,
WeChat = 85,
CashonDelivery = 86,
CreditCardNodwin = 87,
DebitCardNodwin = 88,
NetBankingNodwin = 89,
CashCardNodwin = 90,
WalletNodwin = 91,
MobileDegica = 92,
Naranja = 93,
Cencosud = 94,
Cabal = 95,
PagoFacil = 96,
Rapipago = 97,
BancoNacionaldeCostaRica = 98,
BancoPoplar = 99,
RedPagos = 100,
SPE = 101,
Multicaja = 102,
RedCompra = 103,
ZiraatBank = 104,
VakiflarBank = 105,
KuveytTurkBank = 106,
EkonomiBank = 107,
Pichincha = 108,
PichinchaCash = 109,
Przelewy24 = 110,
Trustpay = 111,
POLi = 112,
MercadoPago = 113,
PayU = 114,
VTCPayWallet = 115,
MrCash = 116,
EPS = 117,
Interac = 118,
VTCPayCards = 119,
VTCPayOnlineBanking = 120,
VisaElectronBoaCompra = 121,
CafeFunded = 122,
OCA = 123,
Lider = 124,
WebMoneySteamCardJapan = 125,
WebMoneySteamCardTopUpJapan = 126,
Toss = 127,
Wallet = 128,
Valve = 129,
MasterComp = 130,
Promotional = 131,
MasterSubscription = 134,
Payco = 135,
MobileWalletJapan = 136,
BoletoFlash = 137,
PIX = 138,
GCash = 139,
KakaoPay = 140,
Dana = 141,
TrueMoney = 142,
TouchnGo = 143,
LinePay = 144,
MerPay = 145,
PayPay = 146,
AlfaClick = 147,
Sberbank = 148,
YooMoney = 149,
Tinkoff = 150,
CashInCIS = 151,
AuPAY = 152,
AliPayHK = 153,
NaverPay = 154,
Linkaja = 155,
ShopeePay = 156,
GrabPay = 157,
PayNow = 158,
OnlineBankingThailand = 159,
CashOptionsThailand = 160,
OEMTicket = 256,
Split = 512,
Complimentary = 1024,
FamilyGroup = 1025,
}
#[derive(Debug, Copy, Clone, TryFromPrimitive)]
#[repr(u32)]
pub enum ELicenseType {
NoLicense = 0,
SinglePurchase = 1,
SinglePurchaseLimitedUse = 2,
RecurringCharge = 3,
RecurringChargeLimitedUse = 4,
RecurringChargeLimitedUseWithOverages = 5,
RecurringOption = 6,
LimitedUseDelayedActivation = 7,
}
bitflags::bitflags! {
pub struct ELicenseFlags: u32 {
const None = 0;
const Renew = 0x01;
const RenewalFailed = 0x02;
const Pending = 0x04;
const Expired = 0x08;
const CancelledByUser = 0x10;
const CancelledByAdmin = 0x20;
const LowViolenceContent = 0x40;
const ImportedFromSteam2 = 0x80;
const ForceRunRestriction = 0x100;
const RegionRestrictionExpired = 0x200;
const CancelledByFriendlyFraudLock = 0x400;
const NotActivated = 0x800;
const PendingRefund = 0x2000;
const Borrowed = 0x4000;
const ReleaseStateOverride = 0x8000;
const CancelledByPartner = 0x40000;
const NonPermanent = 0x80000;
const PreferredOwner = 0x100000;
}
}
pub struct License {
pub package_id: u32,
pub time_created: SystemTime,
pub time_next_process: Option<SystemTime>,
pub minute_limit: i32,
pub minutes_used: i32,
pub payment_method: EPaymentMethod,
pub flags: ELicenseFlags,
pub purchase_country_code: String,
pub license_type: ELicenseType,
pub territory_code: i32,
pub change_number: i32,
pub owner_id: u32,
pub initial_period: u32,
pub initial_time_unit: u32,
pub renewal_period: u32,
pub renewal_time_unit: u32,
pub access_token: u64,
pub master_package_id: Option<u32>,
}
impl TryFrom<cmsg_client_license_list::License> for License {
type Error = ClientError;
fn try_from(value: cmsg_client_license_list::License) -> Result<Self, Self::Error> {
Ok(Self {
package_id: value.package_id(),
time_created: UNIX_EPOCH + Duration::from_secs(value.time_created().into()),
time_next_process: if value.time_next_process() == 0 {
None
} else {
Some(UNIX_EPOCH + Duration::from_secs(value.time_next_process().into()))
},
minute_limit: value.minute_limit(),
minutes_used: value.minutes_used(),
payment_method: value
.payment_method()
.try_into()
.context(TryFromEPaymentMethodSnafu {})?,
flags: ELicenseFlags::from_bits_truncate(value.flags()),
purchase_country_code: value.purchase_country_code().to_string(),
license_type: value
.license_type()
.try_into()
.context(TryFromELicenseTypeSnafu {})?,
territory_code: value.territory_code(),
change_number: value.change_number(),
owner_id: value.owner_id(),
initial_period: value.initial_period(),
initial_time_unit: value.initial_time_unit(),
renewal_period: value.renewal_period(),
renewal_time_unit: value.renewal_time_unit(),
access_token: value.access_token(),
master_package_id: value.master_package_id,
})
}
}

1
lib/src/state/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod apps;