Compare commits
No commits in common. "rust" and "canon" have entirely different histories.
2
.clang-format
Normal file
2
.clang-format
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
BasedOnStyle: LLVM
|
||||||
|
IndentWidth: 4
|
|
@ -3,7 +3,5 @@ root = true
|
||||||
[*]
|
[*]
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.toml]
|
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,3 +1,3 @@
|
||||||
|
.cache
|
||||||
.direnv
|
.direnv
|
||||||
result
|
build
|
||||||
target
|
|
||||||
|
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -1,6 +0,0 @@
|
||||||
[submodule "proto/proto"]
|
|
||||||
path = proto/proto
|
|
||||||
url = https://github.com/SteamDatabase/Protobufs
|
|
||||||
[submodule "abigen/proton"]
|
|
||||||
path = abigen/proton
|
|
||||||
url = https://github.com/ValveSoftware/Proton
|
|
2322
Cargo.lock
generated
2322
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
21
Cargo.toml
21
Cargo.toml
|
@ -1,21 +0,0 @@
|
||||||
[workspace]
|
|
||||||
resolver = "2"
|
|
||||||
members = [
|
|
||||||
"abigen",
|
|
||||||
"client",
|
|
||||||
"lib",
|
|
||||||
"proto",
|
|
||||||
"struct",
|
|
||||||
]
|
|
||||||
|
|
||||||
[workspace.package]
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
bitflags = "2.6.0"
|
|
||||||
color-eyre = "0.6"
|
|
||||||
env_logger = "0.11"
|
|
||||||
keyvalues-serde = "0.2"
|
|
||||||
log = "0.4"
|
|
||||||
num_enum = "0.7.3"
|
|
||||||
protobuf = "3.5.1"
|
|
|
@ -1,13 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "vapore-abigen"
|
|
||||||
edition = "2021"
|
|
||||||
version.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bindgen = "0.70.1"
|
|
||||||
color-eyre.workspace = true
|
|
||||||
env_logger.workspace = true
|
|
||||||
log.workspace = true
|
|
||||||
regex = "1.10.6"
|
|
||||||
serde = { version = "1.0.209", features = ["derive"] }
|
|
||||||
serde_json = "1.0.128"
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 962bbc4e74dde0643a6edab7c845bc628601f23f
|
|
|
@ -1,79 +0,0 @@
|
||||||
use color_eyre::eyre;
|
|
||||||
use std::{fs, path::Path};
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
pub struct ApiDescription {
|
|
||||||
pub callback_structs: Vec<CallbackStruct>,
|
|
||||||
pub consts: Vec<Const>,
|
|
||||||
pub enums: Vec<Enum>,
|
|
||||||
pub interfaces: Vec<Interface>,
|
|
||||||
pub structs: Vec<Struct>,
|
|
||||||
pub typedefs: Vec<Typedef>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
pub struct CallbackStruct {
|
|
||||||
pub callback_id: u64,
|
|
||||||
#[serde(rename = "struct")]
|
|
||||||
pub _struct: String,
|
|
||||||
pub fields: Vec<StructField>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
pub struct StructField {
|
|
||||||
#[serde(rename = "fieldname")]
|
|
||||||
pub field_name: String,
|
|
||||||
#[serde(rename = "fieldtype")]
|
|
||||||
pub field_type: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
pub struct Const {
|
|
||||||
#[serde(rename = "constname")]
|
|
||||||
pub const_name: String,
|
|
||||||
#[serde(rename = "consttype")]
|
|
||||||
pub const_type: String,
|
|
||||||
#[serde(rename = "constval")]
|
|
||||||
pub const_val: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
pub struct Enum {
|
|
||||||
#[serde(rename = "enumname")]
|
|
||||||
pub enum_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
pub struct Interface {
|
|
||||||
#[serde(rename = "classname")]
|
|
||||||
pub class_name: String,
|
|
||||||
pub version_string: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
pub struct Struct {
|
|
||||||
#[serde(rename = "struct")]
|
|
||||||
pub _struct: String,
|
|
||||||
}
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
pub struct Typedef {
|
|
||||||
pub typedef: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiDescription {
|
|
||||||
pub fn read(sdk_path: &Path) -> eyre::Result<ApiDescription> {
|
|
||||||
let content = fs::read(sdk_path.join("steam_api.json"))?;
|
|
||||||
Ok(serde_json::from_slice(&content)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn non_interface_names(&self) -> Vec<&str> {
|
|
||||||
self.callback_structs
|
|
||||||
.iter()
|
|
||||||
.map(|item| item._struct.as_str())
|
|
||||||
.chain(self.consts.iter().map(|item| item.const_name.as_str()))
|
|
||||||
.chain(self.enums.iter().map(|item| item.enum_name.as_str()))
|
|
||||||
.chain(self.structs.iter().map(|item| item._struct.as_str()))
|
|
||||||
.chain(self.typedefs.iter().map(|item| item.typedef.as_str()))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
use std::{
|
|
||||||
collections::BTreeMap,
|
|
||||||
env,
|
|
||||||
fs::{self, File},
|
|
||||||
io::Read,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use color_eyre::eyre::{self, OptionExt};
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
mod apidesc;
|
|
||||||
mod rename;
|
|
||||||
|
|
||||||
fn main() -> eyre::Result<()> {
|
|
||||||
env_logger::init();
|
|
||||||
color_eyre::install()?;
|
|
||||||
|
|
||||||
let lsteamclient_path =
|
|
||||||
PathBuf::from(env::var_os("PROTON_SOURCE").unwrap_or_else(|| "proton".into()))
|
|
||||||
.join("lsteamclient");
|
|
||||||
|
|
||||||
let mut interface_versions = BTreeMap::new();
|
|
||||||
|
|
||||||
for maybe_sdk in fs::read_dir(&lsteamclient_path)? {
|
|
||||||
let sdk = maybe_sdk?;
|
|
||||||
if !sdk.file_type()?.is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let this_versions = parse_interface_versions(&sdk.path())?;
|
|
||||||
|
|
||||||
interface_versions.insert(sdk.file_name(), this_versions);
|
|
||||||
}
|
|
||||||
|
|
||||||
let greatest_sdk_version = interface_versions
|
|
||||||
.keys()
|
|
||||||
.next_back()
|
|
||||||
.ok_or_eyre("No SDKs found")?
|
|
||||||
.clone();
|
|
||||||
log::info!("Last SDK found is {:?}", greatest_sdk_version);
|
|
||||||
|
|
||||||
let mut sdk_by_iface = BTreeMap::new();
|
|
||||||
|
|
||||||
for (sdk, sdk_versions) in interface_versions.iter() {
|
|
||||||
for iface_version in sdk_versions.values() {
|
|
||||||
sdk_by_iface.insert(iface_version.to_string(), sdk.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{:#?}", sdk_by_iface);
|
|
||||||
|
|
||||||
let api_desc = apidesc::ApiDescription::read(&lsteamclient_path.join(&greatest_sdk_version))?;
|
|
||||||
|
|
||||||
let mut latest_builder = bindgen::builder()
|
|
||||||
.header(
|
|
||||||
lsteamclient_path
|
|
||||||
.join(&greatest_sdk_version)
|
|
||||||
.join("steam_api.h")
|
|
||||||
.to_str()
|
|
||||||
.ok_or_eyre("Invalid path")?,
|
|
||||||
)
|
|
||||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
|
||||||
.parse_callbacks(Box::new(rename::RenameEnumCallbacks))
|
|
||||||
.ignore_methods()
|
|
||||||
.ignore_functions()
|
|
||||||
.clang_args(["-x", "c++"])
|
|
||||||
.default_enum_style(bindgen::EnumVariation::NewType {
|
|
||||||
is_bitfield: false,
|
|
||||||
is_global: false,
|
|
||||||
})
|
|
||||||
.bitfield_enum(".*Flags")
|
|
||||||
.vtable_generation(true);
|
|
||||||
|
|
||||||
// We only need the public API, steam_api.h implementation details aren't super useful
|
|
||||||
for item in api_desc.non_interface_names() {
|
|
||||||
latest_builder = latest_builder.allowlist_item(item);
|
|
||||||
log::trace!("Allowing item `{}`", item);
|
|
||||||
}
|
|
||||||
|
|
||||||
let latest_bindings = latest_builder.generate()?;
|
|
||||||
latest_bindings.write_to_file("target/src/deps.rs")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_interface_versions(path: &Path) -> eyre::Result<BTreeMap<String, String>> {
|
|
||||||
let re = Regex::new(r#"#define\s*(STEAM[A-Z]*)_INTERFACE_VERSION\s*"([a-zA-Z0-9_]*)""#)?;
|
|
||||||
|
|
||||||
let mut versions = BTreeMap::new();
|
|
||||||
|
|
||||||
for maybe_file in fs::read_dir(path)? {
|
|
||||||
let file = maybe_file?;
|
|
||||||
if !file.file_name().to_string_lossy().ends_with(".h") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut content_bytes = Vec::new();
|
|
||||||
if let Err(e) = File::open(file.path())?.read_to_end(&mut content_bytes) {
|
|
||||||
log::warn!("Got error in {:?}: {}", file.path(), e)
|
|
||||||
}
|
|
||||||
|
|
||||||
let content: String = content_bytes.into_iter().map(|c| c as char).collect();
|
|
||||||
|
|
||||||
for capture in re.captures_iter(&content) {
|
|
||||||
versions.insert(
|
|
||||||
capture.get(1).unwrap().as_str().to_string(),
|
|
||||||
capture.get(2).unwrap().as_str().to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(versions)
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use bindgen::callbacks::ParseCallbacks;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RenameEnumCallbacks;
|
|
||||||
|
|
||||||
impl ParseCallbacks for RenameEnumCallbacks {
|
|
||||||
fn enum_variant_name(
|
|
||||||
&self,
|
|
||||||
enum_name: Option<&str>,
|
|
||||||
original_variant_name: &str,
|
|
||||||
_variant_value: bindgen::callbacks::EnumVariantValue,
|
|
||||||
) -> Option<String> {
|
|
||||||
let base_name = enum_name?;
|
|
||||||
let name = if base_name.ends_with("Flags") {
|
|
||||||
base_name.split_at(base_name.len() - 1).0
|
|
||||||
} else {
|
|
||||||
base_name
|
|
||||||
};
|
|
||||||
let re = Regex::new(&format!("^k_{}_?(.*)$", regex::escape(name))).unwrap();
|
|
||||||
let c = re.captures(original_variant_name)?;
|
|
||||||
let new_name = c.get(1)?.as_str();
|
|
||||||
if new_name.chars().next()?.is_ascii_digit() {
|
|
||||||
Some(format!("_{}", new_name))
|
|
||||||
} else {
|
|
||||||
Some(new_name.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RenameInterfaceCallbacks {
|
|
||||||
interface_versions: BTreeMap<String, String>,
|
|
||||||
re: Regex,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenameInterfaceCallbacks {
|
|
||||||
pub fn new(interface_versions: BTreeMap<String, String>) -> Self {
|
|
||||||
Self {
|
|
||||||
interface_versions,
|
|
||||||
re: Regex::new("^I(Steam[a-zA-Z]+)$").unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParseCallbacks for RenameInterfaceCallbacks {
|
|
||||||
fn item_name(&self, original_item_name: &str) -> Option<String> {
|
|
||||||
let c = self.re.captures(original_item_name)?;
|
|
||||||
let full_version = self.interface_versions.get(c.get(1)?.as_str())?;
|
|
||||||
let (_, short_version) = full_version.split_at(full_version.len() - 3);
|
|
||||||
|
|
||||||
Some(format!("{}{}", original_item_name, short_version))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "vapore-client"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
ctor = "0.2"
|
|
||||||
env_logger.workspace = true
|
|
||||||
log.workspace = true
|
|
|
@ -1,23 +0,0 @@
|
||||||
pub mod steam_client;
|
|
||||||
pub use steam_client::*;
|
|
||||||
pub mod steam_utils;
|
|
||||||
pub use steam_utils::*;
|
|
||||||
|
|
||||||
pub type SteamPipe = i32;
|
|
||||||
pub type SteamUser = i32;
|
|
||||||
|
|
||||||
#[repr(u32)]
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum AccountType {
|
|
||||||
Invalid = 0,
|
|
||||||
Individual = 1,
|
|
||||||
Multiseat = 2,
|
|
||||||
GameServer = 3,
|
|
||||||
AnonGameServer = 4,
|
|
||||||
Pending = 5,
|
|
||||||
ContentServer = 6,
|
|
||||||
Clan = 7,
|
|
||||||
Chat = 8,
|
|
||||||
ConsoleUser = 9,
|
|
||||||
AnonUser = 10,
|
|
||||||
}
|
|
|
@ -1,276 +0,0 @@
|
||||||
#![allow(non_snake_case)]
|
|
||||||
use std::ffi::{c_char, c_void, CStr};
|
|
||||||
use std::ptr;
|
|
||||||
|
|
||||||
use log::warn;
|
|
||||||
|
|
||||||
use super::{AccountType, ISteamUtils, SteamPipe, SteamUser};
|
|
||||||
|
|
||||||
/// The initial steam interface from CreateInterface
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct ISteamClient {
|
|
||||||
pub vtbl: ISteamClientVtbl,
|
|
||||||
pub version: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ISteamClient {
|
|
||||||
pub fn new(version: &CStr) -> Option<Box<Self>> {
|
|
||||||
let (vtbl, version) = match version.to_bytes() {
|
|
||||||
b"SteamClient017" => (
|
|
||||||
ISteamClientVtbl {
|
|
||||||
ver017: &STEAM_CLIENT_017_VTBL_DEFAULT as *const ISteamClient017Vtbl,
|
|
||||||
},
|
|
||||||
17,
|
|
||||||
),
|
|
||||||
b"SteamClient020" => (
|
|
||||||
ISteamClientVtbl {
|
|
||||||
ver020: &STEAM_CLIENT_020_VTBL_DEFAULT as *const ISteamClient020Vtbl,
|
|
||||||
},
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(Box::new(Self { vtbl, version }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Vtable for any version of steam client
|
|
||||||
#[repr(C)]
|
|
||||||
pub union ISteamClientVtbl {
|
|
||||||
pub ver017: *const ISteamClient017Vtbl,
|
|
||||||
pub ver020: *const ISteamClient020Vtbl,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Vtable for SteamClient017
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct ISteamClient017Vtbl {
|
|
||||||
pub CreateSteamPipe: extern "C" fn(self_ptr: *mut ISteamClient) -> SteamPipe,
|
|
||||||
pub BReleaseSteamPipe: extern "C" fn(self_ptr: *mut ISteamClient, pipe: SteamPipe) -> bool,
|
|
||||||
pub ConnectToGlobalUser:
|
|
||||||
extern "C" fn(self_ptr: *mut ISteamClient, pipe: SteamPipe) -> SteamUser,
|
|
||||||
pub CreateLocalUser: extern "C" fn(
|
|
||||||
self_ptr: *mut ISteamClient,
|
|
||||||
pipe: *const SteamPipe,
|
|
||||||
account_type: AccountType,
|
|
||||||
) -> SteamUser,
|
|
||||||
pub ReleaseUser: *const c_void,
|
|
||||||
pub GetISteamUser: *const c_void,
|
|
||||||
pub GetISteamGameServer: *const c_void,
|
|
||||||
pub SetLocalIPBinding: *const c_void,
|
|
||||||
pub GetISteamFriends: *const c_void,
|
|
||||||
pub GetISteamUtils: extern "C" fn(
|
|
||||||
self_ptr: *mut ISteamClient,
|
|
||||||
pipe: SteamPipe,
|
|
||||||
version: *const c_char,
|
|
||||||
) -> *const ISteamUtils,
|
|
||||||
pub GetISteamMatchmaking: *const c_void,
|
|
||||||
pub GetISteamMatchmakingServers: *const c_void,
|
|
||||||
pub GetISteamGenericInterface: *const c_void,
|
|
||||||
pub GetISteamUserStats: *const c_void,
|
|
||||||
pub GetISteamGameServerStats: *const c_void,
|
|
||||||
pub GetISteamApps: *const c_void,
|
|
||||||
pub GetISteamNetworking: *const c_void,
|
|
||||||
pub GetISteamRemoteStorage: *const c_void,
|
|
||||||
pub GetISteamScreenshots: *const c_void,
|
|
||||||
pub RunFrame: *const c_void,
|
|
||||||
pub GetIPCCallCount: *const c_void,
|
|
||||||
pub SetWarningMessageHook: *const c_void,
|
|
||||||
pub BShutdownIfAllPipesClosed: *const c_void,
|
|
||||||
pub GetISteamHTTP: *const c_void,
|
|
||||||
pub DEPRECATED_GetISteamUnifiedMessages: *const c_void,
|
|
||||||
pub GetISteamController: *const c_void,
|
|
||||||
pub GetISteamUGC: *const c_void,
|
|
||||||
pub GetISteamAppList: *const c_void,
|
|
||||||
pub GetISteamMusic: *const c_void,
|
|
||||||
pub GetISteamMusicRemote: *const c_void,
|
|
||||||
pub GetISteamHTMLSurface: *const c_void,
|
|
||||||
pub DEPRECATED_Set_SteamAPI_CPostAPIResultInProcess: *const c_void,
|
|
||||||
pub DEPRECATED_Remove_SteamAPI_CPostAPIResultInProcess: *const c_void,
|
|
||||||
pub Set_SteamAPI_CCheckCallbackRegisteredInProcess: *const c_void,
|
|
||||||
pub GetISteamInventory: *const c_void,
|
|
||||||
pub GetISteamVideo: *const c_void,
|
|
||||||
pub GetISteamParentalSettings: *const c_void,
|
|
||||||
}
|
|
||||||
|
|
||||||
const STEAM_CLIENT_017_VTBL_DEFAULT: ISteamClient017Vtbl = ISteamClient017Vtbl {
|
|
||||||
CreateSteamPipe,
|
|
||||||
BReleaseSteamPipe,
|
|
||||||
ConnectToGlobalUser,
|
|
||||||
CreateLocalUser,
|
|
||||||
ReleaseUser: ptr::null(),
|
|
||||||
GetISteamUser: ptr::null(),
|
|
||||||
GetISteamGameServer: ptr::null(),
|
|
||||||
SetLocalIPBinding: ptr::null(),
|
|
||||||
GetISteamFriends: ptr::null(),
|
|
||||||
GetISteamUtils,
|
|
||||||
GetISteamMatchmaking: ptr::null(),
|
|
||||||
GetISteamMatchmakingServers: ptr::null(),
|
|
||||||
GetISteamGenericInterface: ptr::null(),
|
|
||||||
GetISteamUserStats: ptr::null(),
|
|
||||||
GetISteamGameServerStats: ptr::null(),
|
|
||||||
GetISteamApps: ptr::null(),
|
|
||||||
GetISteamNetworking: ptr::null(),
|
|
||||||
GetISteamRemoteStorage: ptr::null(),
|
|
||||||
GetISteamScreenshots: ptr::null(),
|
|
||||||
RunFrame: ptr::null(),
|
|
||||||
GetIPCCallCount: ptr::null(),
|
|
||||||
SetWarningMessageHook: ptr::null(),
|
|
||||||
BShutdownIfAllPipesClosed: ptr::null(),
|
|
||||||
GetISteamHTTP: ptr::null(),
|
|
||||||
DEPRECATED_GetISteamUnifiedMessages: ptr::null(),
|
|
||||||
GetISteamController: ptr::null(),
|
|
||||||
GetISteamUGC: ptr::null(),
|
|
||||||
GetISteamAppList: ptr::null(),
|
|
||||||
GetISteamMusic: ptr::null(),
|
|
||||||
GetISteamMusicRemote: ptr::null(),
|
|
||||||
GetISteamHTMLSurface: ptr::null(),
|
|
||||||
DEPRECATED_Set_SteamAPI_CPostAPIResultInProcess: ptr::null(),
|
|
||||||
DEPRECATED_Remove_SteamAPI_CPostAPIResultInProcess: ptr::null(),
|
|
||||||
Set_SteamAPI_CCheckCallbackRegisteredInProcess: ptr::null(),
|
|
||||||
GetISteamInventory: ptr::null(),
|
|
||||||
GetISteamVideo: ptr::null(),
|
|
||||||
GetISteamParentalSettings: ptr::null(),
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Vtable for SteamClient020
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct ISteamClient020Vtbl {
|
|
||||||
pub CreateSteamPipe: extern "C" fn(self_ptr: *mut ISteamClient) -> SteamPipe,
|
|
||||||
pub BReleaseSteamPipe: extern "C" fn(self_ptr: *mut ISteamClient, pipe: SteamPipe) -> bool,
|
|
||||||
pub ConnectToGlobalUser:
|
|
||||||
extern "C" fn(self_ptr: *mut ISteamClient, pipe: SteamPipe) -> SteamUser,
|
|
||||||
pub CreateLocalUser: extern "C" fn(
|
|
||||||
self_ptr: *mut ISteamClient,
|
|
||||||
pipe: *const SteamPipe,
|
|
||||||
account_type: AccountType,
|
|
||||||
) -> SteamUser,
|
|
||||||
pub ReleaseUser: *const c_void,
|
|
||||||
pub GetISteamUser: *const c_void,
|
|
||||||
pub GetISteamGameServer: *const c_void,
|
|
||||||
pub SetLocalIPBinding: *const c_void,
|
|
||||||
pub GetISteamFriends: *const c_void,
|
|
||||||
pub GetISteamUtils: extern "C" fn(
|
|
||||||
self_ptr: *mut ISteamClient,
|
|
||||||
pipe: SteamPipe,
|
|
||||||
version: *const c_char,
|
|
||||||
) -> *const ISteamUtils,
|
|
||||||
pub GetISteamMatchmaking: *const c_void,
|
|
||||||
pub GetISteamMatchmakingServers: *const c_void,
|
|
||||||
pub GetISteamGenericInterface: *const c_void,
|
|
||||||
pub GetISteamUserStats: *const c_void,
|
|
||||||
pub GetISteamGameServerStats: *const c_void,
|
|
||||||
pub GetISteamApps: *const c_void,
|
|
||||||
pub GetISteamNetworking: *const c_void,
|
|
||||||
pub GetISteamRemoteStorage: *const c_void,
|
|
||||||
pub GetISteamScreenshots: *const c_void,
|
|
||||||
pub GetISteamGameSearch: *const c_void,
|
|
||||||
pub RunFrame: *const c_void,
|
|
||||||
pub GetIPCCallCount: *const c_void,
|
|
||||||
pub SetWarningMessageHook: *const c_void,
|
|
||||||
pub BShutdownIfAllPipesClosed: *const c_void,
|
|
||||||
pub GetISteamHTTP: *const c_void,
|
|
||||||
pub DEPRECATED_GetISteamUnifiedMessages: *const c_void,
|
|
||||||
pub GetISteamController: *const c_void,
|
|
||||||
pub GetISteamUGC: *const c_void,
|
|
||||||
pub GetISteamAppList: *const c_void,
|
|
||||||
pub GetISteamMusic: *const c_void,
|
|
||||||
pub GetISteamMusicRemote: *const c_void,
|
|
||||||
pub GetISteamHTMLSurface: *const c_void,
|
|
||||||
pub DEPRECATED_Set_SteamAPI_CPostAPIResultInProcess: *const c_void,
|
|
||||||
pub DEPRECATED_Remove_SteamAPI_CPostAPIResultInProcess: *const c_void,
|
|
||||||
pub Set_SteamAPI_CCheckCallbackRegisteredInProcess: *const c_void,
|
|
||||||
pub GetISteamInventory: *const c_void,
|
|
||||||
pub GetISteamVideo: *const c_void,
|
|
||||||
pub GetISteamParentalSettings: *const c_void,
|
|
||||||
pub GetISteamInput: *const c_void,
|
|
||||||
pub GetISteamParties: *const c_void,
|
|
||||||
pub GetISteamRemotePlay: *const c_void,
|
|
||||||
pub DestroyAllInterfaces: *const c_void,
|
|
||||||
}
|
|
||||||
|
|
||||||
const STEAM_CLIENT_020_VTBL_DEFAULT: ISteamClient020Vtbl = ISteamClient020Vtbl {
|
|
||||||
CreateSteamPipe,
|
|
||||||
BReleaseSteamPipe,
|
|
||||||
ConnectToGlobalUser,
|
|
||||||
CreateLocalUser,
|
|
||||||
ReleaseUser: ptr::null(),
|
|
||||||
GetISteamUser: ptr::null(),
|
|
||||||
GetISteamGameServer: ptr::null(),
|
|
||||||
SetLocalIPBinding: ptr::null(),
|
|
||||||
GetISteamFriends: ptr::null(),
|
|
||||||
GetISteamUtils,
|
|
||||||
GetISteamMatchmaking: ptr::null(),
|
|
||||||
GetISteamMatchmakingServers: ptr::null(),
|
|
||||||
GetISteamGenericInterface: ptr::null(),
|
|
||||||
GetISteamUserStats: ptr::null(),
|
|
||||||
GetISteamGameServerStats: ptr::null(),
|
|
||||||
GetISteamApps: ptr::null(),
|
|
||||||
GetISteamNetworking: ptr::null(),
|
|
||||||
GetISteamRemoteStorage: ptr::null(),
|
|
||||||
GetISteamScreenshots: ptr::null(),
|
|
||||||
GetISteamGameSearch: ptr::null(),
|
|
||||||
RunFrame: ptr::null(),
|
|
||||||
GetIPCCallCount: ptr::null(),
|
|
||||||
SetWarningMessageHook: ptr::null(),
|
|
||||||
BShutdownIfAllPipesClosed: ptr::null(),
|
|
||||||
GetISteamHTTP: ptr::null(),
|
|
||||||
DEPRECATED_GetISteamUnifiedMessages: ptr::null(),
|
|
||||||
GetISteamController: ptr::null(),
|
|
||||||
GetISteamUGC: ptr::null(),
|
|
||||||
GetISteamAppList: ptr::null(),
|
|
||||||
GetISteamMusic: ptr::null(),
|
|
||||||
GetISteamMusicRemote: ptr::null(),
|
|
||||||
GetISteamHTMLSurface: ptr::null(),
|
|
||||||
DEPRECATED_Set_SteamAPI_CPostAPIResultInProcess: ptr::null(),
|
|
||||||
DEPRECATED_Remove_SteamAPI_CPostAPIResultInProcess: ptr::null(),
|
|
||||||
Set_SteamAPI_CCheckCallbackRegisteredInProcess: ptr::null(),
|
|
||||||
GetISteamInventory: ptr::null(),
|
|
||||||
GetISteamVideo: ptr::null(),
|
|
||||||
GetISteamParentalSettings: ptr::null(),
|
|
||||||
GetISteamInput: ptr::null(),
|
|
||||||
GetISteamParties: ptr::null(),
|
|
||||||
GetISteamRemotePlay: ptr::null(),
|
|
||||||
DestroyAllInterfaces: ptr::null(),
|
|
||||||
};
|
|
||||||
|
|
||||||
extern "C" fn CreateSteamPipe(self_ptr: *mut ISteamClient) -> SteamPipe {
|
|
||||||
warn!("STUB: ISteamClient::CreateSteamPipe({:?})", self_ptr);
|
|
||||||
0
|
|
||||||
}
|
|
||||||
extern "C" fn BReleaseSteamPipe(self_ptr: *mut ISteamClient, pipe: SteamPipe) -> bool {
|
|
||||||
warn!(
|
|
||||||
"STUB: ISteamClient::BReleaseSteamPipe({:?}, {})",
|
|
||||||
self_ptr, pipe
|
|
||||||
);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
extern "C" fn ConnectToGlobalUser(self_ptr: *mut ISteamClient, pipe: SteamPipe) -> SteamUser {
|
|
||||||
warn!(
|
|
||||||
"STUB: ISteamClient::ConnectToGlobalUser({:?}, {})",
|
|
||||||
self_ptr, pipe
|
|
||||||
);
|
|
||||||
0
|
|
||||||
}
|
|
||||||
extern "C" fn CreateLocalUser(
|
|
||||||
self_ptr: *mut ISteamClient,
|
|
||||||
pipe: *const SteamPipe,
|
|
||||||
account_type: AccountType,
|
|
||||||
) -> SteamUser {
|
|
||||||
warn!(
|
|
||||||
"STUB: ISteamClient::CreateLocalUser({:?}, {:?}, {:?})",
|
|
||||||
self_ptr, pipe, account_type
|
|
||||||
);
|
|
||||||
0
|
|
||||||
}
|
|
||||||
extern "C" fn GetISteamUtils(
|
|
||||||
self_ptr: *mut ISteamClient,
|
|
||||||
pipe: SteamPipe,
|
|
||||||
version: *const c_char,
|
|
||||||
) -> *const ISteamUtils {
|
|
||||||
warn!(
|
|
||||||
"STUB: ISteamClient::GetISteamUtils({:?}, {}, {:?})",
|
|
||||||
self_ptr, pipe, version
|
|
||||||
);
|
|
||||||
ptr::null()
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
#![allow(non_snake_case)]
|
|
||||||
use std::ffi::c_void;
|
|
||||||
|
|
||||||
/// Various weird steam utility functions
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct ISteamUtils {
|
|
||||||
pub vtbl: ISteamUtilsVtbl,
|
|
||||||
pub version: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Vtable for any version of SteamUtils
|
|
||||||
#[repr(C)]
|
|
||||||
pub union ISteamUtilsVtbl {
|
|
||||||
pub ver017: *const ISteamUtils010Vtbl,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Vtable for ISteamUtils010
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct ISteamUtils010Vtbl {
|
|
||||||
pub GetSecondsSinceAppActive: *const c_void,
|
|
||||||
pub GetSecondsSinceComputerActive: *const c_void,
|
|
||||||
pub GetConnectedUniverse: *const c_void,
|
|
||||||
pub GetServerRealTime: *const c_void,
|
|
||||||
pub GetIPCountry: *const c_void,
|
|
||||||
pub GetImageSize: *const c_void,
|
|
||||||
pub GetImageRGBA: *const c_void,
|
|
||||||
pub GetCSERIPPort: *const c_void,
|
|
||||||
pub GetCurrentBatteryPower: *const c_void,
|
|
||||||
pub GetAppID: *const c_void,
|
|
||||||
pub SetOverlayNotificationPosition: *const c_void,
|
|
||||||
pub IsAPICallCompleted: *const c_void,
|
|
||||||
pub GetAPICallFailureReason: *const c_void,
|
|
||||||
pub GetAPICallResult: *const c_void,
|
|
||||||
pub RunFrame: *const c_void,
|
|
||||||
pub GetIPCCallCount: *const c_void,
|
|
||||||
pub SetWarningMessageHook: *const c_void,
|
|
||||||
pub IsOverlayEnabled: *const c_void,
|
|
||||||
pub BOverlayNeedsPresent: *const c_void,
|
|
||||||
pub CheckFileSignature: *const c_void,
|
|
||||||
pub ShowGamepadTextInput: *const c_void,
|
|
||||||
pub GetEnteredGamepadTextLength: *const c_void,
|
|
||||||
pub GetEnteredGamepadTextInput: *const c_void,
|
|
||||||
pub GetSteamUILanguage: *const c_void,
|
|
||||||
pub IsSteamRunningInVR: *const c_void,
|
|
||||||
pub SetOverlayNotificationInset: *const c_void,
|
|
||||||
pub IsSteamInBigPictureMode: *const c_void,
|
|
||||||
pub StartVRDashboard: *const c_void,
|
|
||||||
pub IsVRHeadsetStreamingEnabled: *const c_void,
|
|
||||||
pub SetVRHeadsetStreamingEnabled: *const c_void,
|
|
||||||
pub IsSteamChinaLauncher: *const c_void,
|
|
||||||
pub InitFilterText: *const c_void,
|
|
||||||
pub FilterText: *const c_void,
|
|
||||||
pub GetIPv6ConnectivityState: *const c_void,
|
|
||||||
pub IsSteamRunningOnSteamDeck: *const c_void,
|
|
||||||
pub ShowFloatingGamepadTextInput: *const c_void,
|
|
||||||
pub SetGameLauncherMode: *const c_void,
|
|
||||||
pub DismissFloatingGamepadTextInput: *const c_void,
|
|
||||||
pub DismissGamepadTextInput: *const c_void,
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
pub mod abi;
|
|
||||||
pub mod loader;
|
|
||||||
|
|
||||||
#[ctor::ctor]
|
|
||||||
fn init() {
|
|
||||||
env_logger::init();
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
use std::{
|
|
||||||
ffi::{c_char, CStr},
|
|
||||||
ptr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use log::trace;
|
|
||||||
|
|
||||||
use crate::abi::ISteamClient;
|
|
||||||
|
|
||||||
/// Lookup a new interface, should be the first function called
|
|
||||||
/// # Safety
|
|
||||||
/// version_ptr must be null or a null-terminated string
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn CreateInterface(version_ptr: *const c_char) -> *const ISteamClient {
|
|
||||||
if version_ptr.is_null() {
|
|
||||||
trace!("CreateInterface(nullptr)");
|
|
||||||
return ptr::null();
|
|
||||||
}
|
|
||||||
// SAFETY: We already know it's not null, there's not much else we can do
|
|
||||||
let version = unsafe { CStr::from_ptr(version_ptr) };
|
|
||||||
trace!("CreateInterface({:?})", version);
|
|
||||||
|
|
||||||
match ISteamClient::new(version) {
|
|
||||||
// TODO: figure out how the hell to deallocate this
|
|
||||||
Some(bx) => Box::leak(bx),
|
|
||||||
None => ptr::null(),
|
|
||||||
}
|
|
||||||
}
|
|
36
flake.lock
36
flake.lock
|
@ -18,41 +18,7 @@
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs"
|
||||||
"utils": "utils"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1710146030,
|
|
||||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
43
flake.nix
43
flake.nix
|
@ -1,40 +1,29 @@
|
||||||
{
|
{
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||||
utils.url = "github:numtide/flake-utils";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{
|
{ self, nixpkgs }:
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
utils,
|
|
||||||
}:
|
|
||||||
utils.lib.eachDefaultSystem (
|
|
||||||
system:
|
|
||||||
let
|
let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
inherit (nixpkgs) lib;
|
||||||
|
makePkgs = system: import nixpkgs { inherit system; };
|
||||||
|
forAllSystems = f: lib.genAttrs lib.systems.flakeExposed (system: f (makePkgs system));
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
devShells.default =
|
formatter = forAllSystems (pkgs: pkgs.nixfmt-rfc-style);
|
||||||
with pkgs;
|
|
||||||
(mkShell.override { stdenv = llvmPackages.stdenv; }) {
|
|
||||||
packages = [
|
|
||||||
rustPackages.cargo
|
|
||||||
rustPackages.rustc
|
|
||||||
rustPackages.rustfmt
|
|
||||||
rustPackages.clippy
|
|
||||||
|
|
||||||
protobuf
|
devShells = forAllSystems (pkgs: {
|
||||||
|
default = (pkgs.mkShell.override { stdenv = pkgs.llvmPackages.stdenv; }) {
|
||||||
|
packages = with pkgs; [
|
||||||
|
meson
|
||||||
|
ninja
|
||||||
|
pkg-config
|
||||||
|
|
||||||
|
boost
|
||||||
|
curl
|
||||||
];
|
];
|
||||||
LIBCLANG_PATH = "${llvmPackages.libclang.lib}/lib";
|
|
||||||
|
|
||||||
RUST_SRC_PATH = "${rustPackages.rustPlatform.rustLibSrc}";
|
|
||||||
RUST_LOG = "debug,vapore=trace,vapore-client=trace,bindgen=error";
|
|
||||||
RUST_BACKTRACE = "1";
|
|
||||||
};
|
};
|
||||||
|
});
|
||||||
formatter = pkgs.nixfmt-rfc-style;
|
};
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "vapore"
|
|
||||||
edition = "2021"
|
|
||||||
version.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
async-tungstenite = { version = "0.27.0", features = ["tokio-rustls-native-certs"] }
|
|
||||||
base64 = "0.22.1"
|
|
||||||
bitflags.workspace = true
|
|
||||||
color-eyre.workspace = true
|
|
||||||
flate2 = "1.0.33"
|
|
||||||
futures = "0.3.30"
|
|
||||||
hex = "0.4.3"
|
|
||||||
keyvalues-serde.workspace = true
|
|
||||||
log.workspace = true
|
|
||||||
num_enum.workspace = true
|
|
||||||
protobuf.workspace = true
|
|
||||||
rand = "0.8.5"
|
|
||||||
reqwest = { version = "0.12", features = ["rustls-tls-native-roots"], default-features = false}
|
|
||||||
rsa = "0.9.6"
|
|
||||||
serde = { version = "1.0.209", features = ["derive"] }
|
|
||||||
snafu = "0.8.4"
|
|
||||||
tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros", "time"]}
|
|
||||||
vapore-proto.path = "../proto"
|
|
||||||
vapore-struct.path = "../struct"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
dialoguer = "0.11.0"
|
|
||||||
env_logger.workspace = true
|
|
||||||
qrcode = "0.14.1"
|
|
|
@ -1,191 +0,0 @@
|
||||||
use color_eyre::eyre;
|
|
||||||
use rand::RngCore;
|
|
||||||
use vapore::message::CMProtoBufMessage;
|
|
||||||
use vapore_proto::{
|
|
||||||
enums_clientserver::EMsg,
|
|
||||||
steammessages_auth_steamclient::{
|
|
||||||
CAuthentication_BeginAuthSessionViaQR_Request,
|
|
||||||
CAuthentication_BeginAuthSessionViaQR_Response, CAuthentication_DeviceDetails,
|
|
||||||
CAuthentication_PollAuthSessionStatus_Request,
|
|
||||||
CAuthentication_PollAuthSessionStatus_Response, EAuthTokenPlatformType,
|
|
||||||
},
|
|
||||||
steammessages_base::{cmsg_ipaddress, CMsgIPAddress},
|
|
||||||
steammessages_clientserver::CMsgClientLicenseList,
|
|
||||||
steammessages_clientserver_login::{CMsgClientHello, CMsgClientLogon, CMsgClientLogonResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
pub async fn main() -> eyre::Result<()> {
|
|
||||||
env_logger::init();
|
|
||||||
color_eyre::install()?;
|
|
||||||
|
|
||||||
let servers = vapore::selection::bootstrap_find_servers().await?;
|
|
||||||
log::debug!("Found servers: {:?}", servers);
|
|
||||||
|
|
||||||
let (session, context) = vapore::connection::CMSession::connect(&servers[0]).await?;
|
|
||||||
|
|
||||||
tokio::spawn(context);
|
|
||||||
|
|
||||||
session.send_notification(
|
|
||||||
EMsg::k_EMsgClientHello,
|
|
||||||
CMsgClientHello {
|
|
||||||
protocol_version: Some(0x1002c),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
log::debug!("Sent hello");
|
|
||||||
|
|
||||||
// machine_id is supposed to be a binary key/value with the SHA1 of the machine's
|
|
||||||
// BB3: Machine GUID
|
|
||||||
// FF2: MAC address
|
|
||||||
// 3B3: Disk ID
|
|
||||||
// We should probably make these consistent so Valve doesn't get suspicious,
|
|
||||||
// but for now let's make them random
|
|
||||||
// TODO: Find a more generic way to make this
|
|
||||||
let mut machine_id = Vec::with_capacity(155);
|
|
||||||
machine_id.extend_from_slice(b"\x00MessageObject\x00");
|
|
||||||
for key in [b"BB3", b"FF2", b"3B3"] {
|
|
||||||
let mut data = [0u8; 20];
|
|
||||||
rand::thread_rng().fill_bytes(&mut data);
|
|
||||||
let hex_bytes = hex::encode(data).into_bytes();
|
|
||||||
|
|
||||||
// Type is string
|
|
||||||
machine_id.push(b'\x01');
|
|
||||||
machine_id.extend_from_slice(key);
|
|
||||||
machine_id.push(b'\x00');
|
|
||||||
machine_id.extend_from_slice(&hex_bytes);
|
|
||||||
machine_id.push(b'\x00');
|
|
||||||
}
|
|
||||||
// suitable end bytes
|
|
||||||
machine_id.extend_from_slice(b"\x08\x08");
|
|
||||||
|
|
||||||
let response = session
|
|
||||||
.call_service_method::<_, CAuthentication_BeginAuthSessionViaQR_Response>(
|
|
||||||
"Authentication.BeginAuthSessionViaQR#1".to_string(),
|
|
||||||
CAuthentication_BeginAuthSessionViaQR_Request {
|
|
||||||
device_details: protobuf::MessageField::some(CAuthentication_DeviceDetails {
|
|
||||||
device_friendly_name: Some("vapore".to_string()),
|
|
||||||
platform_type: Some(protobuf::EnumOrUnknown::new(
|
|
||||||
EAuthTokenPlatformType::k_EAuthTokenPlatformType_SteamClient,
|
|
||||||
)),
|
|
||||||
// Windows 11
|
|
||||||
os_type: Some(20),
|
|
||||||
machine_id: Some(machine_id.clone()),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
log::debug!("Got response {:#x?}", response);
|
|
||||||
|
|
||||||
let code = qrcode::QrCode::new(response.body.challenge_url()).unwrap();
|
|
||||||
log::info!(
|
|
||||||
"Got new QR code:\n{}",
|
|
||||||
code.render::<qrcode::render::unicode::Dense1x2>().build()
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs_f32(
|
|
||||||
response.body.interval(),
|
|
||||||
));
|
|
||||||
|
|
||||||
let (refresh_token, account_name) = loop {
|
|
||||||
interval.tick().await;
|
|
||||||
let request = CAuthentication_PollAuthSessionStatus_Request {
|
|
||||||
client_id: response.body.client_id,
|
|
||||||
request_id: response.body.request_id.clone(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let response: CMProtoBufMessage<CAuthentication_PollAuthSessionStatus_Response> = session
|
|
||||||
.call_service_method(
|
|
||||||
"Authentication.PollAuthSessionStatus#1".to_string(),
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
log::debug!("Got auth poll status {:#?}", response.body);
|
|
||||||
|
|
||||||
if let Some(access_token) = response.body.refresh_token {
|
|
||||||
let account_name = response.body.account_name.unwrap_or_default();
|
|
||||||
break (access_token, account_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(new_url) = response.body.new_challenge_url {
|
|
||||||
let code = qrcode::QrCode::new(new_url)?;
|
|
||||||
log::info!(
|
|
||||||
"Got new QR code:\n{}",
|
|
||||||
code.render::<qrcode::render::unicode::Dense1x2>().build()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
log::debug!(
|
|
||||||
"Got account name {}, access token {}",
|
|
||||||
account_name,
|
|
||||||
refresh_token
|
|
||||||
);
|
|
||||||
|
|
||||||
// normal user, desktop instance, public universe
|
|
||||||
session.set_steam_id(0x0110_0001_0000_0000);
|
|
||||||
|
|
||||||
session.send_notification(
|
|
||||||
EMsg::k_EMsgClientLogon,
|
|
||||||
CMsgClientLogon {
|
|
||||||
account_name: Some(account_name),
|
|
||||||
access_token: Some(refresh_token),
|
|
||||||
machine_name: Some("vapore".to_string()),
|
|
||||||
machine_id: Some(machine_id),
|
|
||||||
client_language: Some("english".to_string()),
|
|
||||||
protocol_version: Some(0x1002c),
|
|
||||||
client_os_type: Some(20),
|
|
||||||
client_package_version: Some(1771),
|
|
||||||
supports_rate_limit_response: Some(true),
|
|
||||||
should_remember_password: Some(true),
|
|
||||||
obfuscated_private_ip: protobuf::MessageField::some(CMsgIPAddress {
|
|
||||||
ip: Some(cmsg_ipaddress::Ip::V4(0xc0a8_0102 ^ 0xbaad_f00d)),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
deprecated_obfustucated_private_ip: Some(0xc0a8_0102 ^ 0xbaad_f00d),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let session_cloned = session.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let license_list_raw = session_cloned
|
|
||||||
.subscribe_message_type(EMsg::k_EMsgClientLicenseList)
|
|
||||||
.recv()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let license_list: CMProtoBufMessage<CMsgClientLicenseList> =
|
|
||||||
CMProtoBufMessage::deserialize(license_list_raw).unwrap();
|
|
||||||
for license in &license_list.body.licenses {
|
|
||||||
log::info!("Own package ID: {}", license.package_id());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut finish_receiver = session.subscribe_message_type(EMsg::k_EMsgClientLogOnResponse);
|
|
||||||
|
|
||||||
let raw_response = finish_receiver.recv().await?;
|
|
||||||
let response: CMProtoBufMessage<CMsgClientLogonResponse> =
|
|
||||||
CMProtoBufMessage::deserialize(raw_response)?;
|
|
||||||
|
|
||||||
log::debug!("Got logon response: {:#x?}", response);
|
|
||||||
|
|
||||||
if response.body.eresult != Some(0x01) {
|
|
||||||
eyre::bail!("Login failed");
|
|
||||||
};
|
|
||||||
|
|
||||||
session.set_steam_id(response.header.steamid());
|
|
||||||
session.set_client_session_id(response.header.client_sessionid());
|
|
||||||
|
|
||||||
if let Some(heartbeat_seconds) = response.body.heartbeat_seconds {
|
|
||||||
if heartbeat_seconds >= 5 {
|
|
||||||
session.begin_heartbeat(heartbeat_seconds as u32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,294 +0,0 @@
|
||||||
use base64::Engine;
|
|
||||||
use color_eyre::eyre::{self, Context, OptionExt};
|
|
||||||
use rand::RngCore;
|
|
||||||
use vapore::connection::CMSession;
|
|
||||||
use vapore::message::CMProtoBufMessage;
|
|
||||||
use vapore_proto::{
|
|
||||||
enums::ESessionPersistence,
|
|
||||||
enums_clientserver::EMsg,
|
|
||||||
steammessages_auth_steamclient::{
|
|
||||||
CAuthentication_BeginAuthSessionViaCredentials_Request,
|
|
||||||
CAuthentication_BeginAuthSessionViaCredentials_Response, CAuthentication_DeviceDetails,
|
|
||||||
CAuthentication_GetPasswordRSAPublicKey_Request,
|
|
||||||
CAuthentication_GetPasswordRSAPublicKey_Response,
|
|
||||||
CAuthentication_PollAuthSessionStatus_Request,
|
|
||||||
CAuthentication_PollAuthSessionStatus_Response,
|
|
||||||
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request,
|
|
||||||
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response, EAuthSessionGuardType,
|
|
||||||
EAuthTokenPlatformType,
|
|
||||||
},
|
|
||||||
steammessages_base::{cmsg_ipaddress, CMsgIPAddress},
|
|
||||||
steammessages_clientserver::CMsgClientLicenseList,
|
|
||||||
steammessages_clientserver_login::{CMsgClientHello, CMsgClientLogon, CMsgClientLogonResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
pub async fn main() -> eyre::Result<()> {
|
|
||||||
env_logger::init();
|
|
||||||
color_eyre::install()?;
|
|
||||||
|
|
||||||
let servers = vapore::selection::bootstrap_find_servers().await?;
|
|
||||||
log::debug!("Found servers: {:?}", servers);
|
|
||||||
|
|
||||||
let (session, context) = CMSession::connect(&servers[0]).await?;
|
|
||||||
|
|
||||||
tokio::spawn(context);
|
|
||||||
|
|
||||||
session.send_notification(
|
|
||||||
EMsg::k_EMsgClientHello,
|
|
||||||
CMsgClientHello {
|
|
||||||
protocol_version: Some(0x1002c),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
log::debug!("Sent hello");
|
|
||||||
|
|
||||||
// machine_id is supposed to be a binary key/value with the SHA1 of the machine's
|
|
||||||
// BB3: Machine GUID
|
|
||||||
// FF2: MAC address
|
|
||||||
// 3B3: Disk ID
|
|
||||||
// We should probably make these consistent so Valve doesn't get suspicious,
|
|
||||||
// but for now let's make them random
|
|
||||||
// TODO: Find a more generic way to make this
|
|
||||||
let mut machine_id = Vec::with_capacity(155);
|
|
||||||
machine_id.extend_from_slice(b"\x00MessageObject\x00");
|
|
||||||
for key in [b"BB3", b"FF2", b"3B3"] {
|
|
||||||
let mut data = [0u8; 20];
|
|
||||||
rand::thread_rng().fill_bytes(&mut data);
|
|
||||||
let hex_bytes = hex::encode(data).into_bytes();
|
|
||||||
|
|
||||||
// Type is string
|
|
||||||
machine_id.push(b'\x01');
|
|
||||||
machine_id.extend_from_slice(key);
|
|
||||||
machine_id.push(b'\x00');
|
|
||||||
machine_id.extend_from_slice(&hex_bytes);
|
|
||||||
machine_id.push(b'\x00');
|
|
||||||
}
|
|
||||||
// suitable end bytes
|
|
||||||
machine_id.extend_from_slice(b"\x08\x08");
|
|
||||||
|
|
||||||
let username = dialoguer::Input::<String>::new()
|
|
||||||
.with_prompt("Username")
|
|
||||||
.interact_text()?;
|
|
||||||
let password = dialoguer::Password::new()
|
|
||||||
.with_prompt("Password")
|
|
||||||
.interact()?;
|
|
||||||
|
|
||||||
let password_key_response: CMProtoBufMessage<CAuthentication_GetPasswordRSAPublicKey_Response> =
|
|
||||||
session
|
|
||||||
.call_service_method(
|
|
||||||
"Authentication.GetPasswordRSAPublicKey#1".to_string(),
|
|
||||||
CAuthentication_GetPasswordRSAPublicKey_Request {
|
|
||||||
account_name: Some(username.clone()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let password_key = rsa::RsaPublicKey::new(
|
|
||||||
rsa::BigUint::parse_bytes(password_key_response.body.publickey_mod().as_bytes(), 16)
|
|
||||||
.ok_or_eyre("Invalid RSA public modulus")?,
|
|
||||||
rsa::BigUint::parse_bytes(password_key_response.body.publickey_exp().as_bytes(), 16)
|
|
||||||
.ok_or_eyre("Invalid RSA public exponent")?,
|
|
||||||
)
|
|
||||||
.wrap_err("Invalid RSA public key")?;
|
|
||||||
|
|
||||||
let encrypted_password = password_key
|
|
||||||
.encrypt(
|
|
||||||
&mut rand::thread_rng(),
|
|
||||||
rsa::Pkcs1v15Encrypt,
|
|
||||||
password.as_bytes(),
|
|
||||||
)
|
|
||||||
.wrap_err("Unable to encrypt password")?;
|
|
||||||
|
|
||||||
let auth_session_response = session
|
|
||||||
.call_service_method::<_, CAuthentication_BeginAuthSessionViaCredentials_Response>(
|
|
||||||
"Authentication.BeginAuthSessionViaCredentials#1".to_string(),
|
|
||||||
CAuthentication_BeginAuthSessionViaCredentials_Request {
|
|
||||||
account_name: Some(username.clone()),
|
|
||||||
encrypted_password: Some(
|
|
||||||
base64::engine::general_purpose::STANDARD.encode(&encrypted_password),
|
|
||||||
),
|
|
||||||
encryption_timestamp: password_key_response.body.timestamp,
|
|
||||||
persistence: Some(protobuf::EnumOrUnknown::new(
|
|
||||||
ESessionPersistence::k_ESessionPersistence_Ephemeral,
|
|
||||||
)),
|
|
||||||
device_details: protobuf::MessageField::some(CAuthentication_DeviceDetails {
|
|
||||||
device_friendly_name: Some("vapore".to_string()),
|
|
||||||
platform_type: Some(protobuf::EnumOrUnknown::new(
|
|
||||||
EAuthTokenPlatformType::k_EAuthTokenPlatformType_SteamClient,
|
|
||||||
)),
|
|
||||||
// Windows 11
|
|
||||||
os_type: Some(20),
|
|
||||||
machine_id: Some(machine_id.clone()),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
log::debug!("Got response {:#x?}", auth_session_response);
|
|
||||||
|
|
||||||
if auth_session_response.header.eresult != Some(1) {
|
|
||||||
eyre::bail!("Got bad resonse from BeginAuthSessionViaCredentials");
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: check allowed_confirmations
|
|
||||||
|
|
||||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs_f32(
|
|
||||||
auth_session_response.body.interval(),
|
|
||||||
));
|
|
||||||
|
|
||||||
let session_cloned = session.clone();
|
|
||||||
let poll_handle = tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
let request = CAuthentication_PollAuthSessionStatus_Request {
|
|
||||||
client_id: auth_session_response.body.client_id,
|
|
||||||
request_id: auth_session_response.body.request_id.clone(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let response: CMProtoBufMessage<CAuthentication_PollAuthSessionStatus_Response> =
|
|
||||||
session_cloned
|
|
||||||
.call_service_method(
|
|
||||||
"Authentication.PollAuthSessionStatus#1".to_string(),
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to call PollAuthSessionStatus");
|
|
||||||
|
|
||||||
log::debug!("Got auth poll status {:#?}", response.body);
|
|
||||||
|
|
||||||
if let Some(refresh_token) = response.body.refresh_token {
|
|
||||||
let account_name = response.body.account_name.unwrap_or_default();
|
|
||||||
break (refresh_token, account_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(new_url) = response.body.new_challenge_url {
|
|
||||||
let code = qrcode::QrCode::new(new_url).expect("Failed to create QR Code");
|
|
||||||
log::info!(
|
|
||||||
"Got new QR code:\n{}",
|
|
||||||
code.render::<qrcode::render::unicode::Dense1x2>().build()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let guard_code = dialoguer::Input::<String>::new()
|
|
||||||
.with_prompt("Steam Guard Code")
|
|
||||||
.interact_text()?;
|
|
||||||
|
|
||||||
let update_response: CMProtoBufMessage<
|
|
||||||
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response,
|
|
||||||
> = session
|
|
||||||
.call_service_method(
|
|
||||||
"Authentication.UpdateAuthSessionWithSteamGuardCode#1".to_string(),
|
|
||||||
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request {
|
|
||||||
client_id: auth_session_response.body.client_id,
|
|
||||||
steamid: auth_session_response.body.steamid,
|
|
||||||
code: Some(guard_code),
|
|
||||||
code_type: Some(protobuf::EnumOrUnknown::new(
|
|
||||||
EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceCode,
|
|
||||||
)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if update_response.header.eresult == Some(1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (refresh_token, account_name) = poll_handle.await?;
|
|
||||||
|
|
||||||
log::debug!(
|
|
||||||
"Got account name {}, access token {}",
|
|
||||||
account_name,
|
|
||||||
refresh_token
|
|
||||||
);
|
|
||||||
|
|
||||||
// normal user, desktop instance, public universe
|
|
||||||
session.set_steam_id(0x0110_0001_0000_0000);
|
|
||||||
|
|
||||||
session.send_notification(
|
|
||||||
EMsg::k_EMsgClientLogon,
|
|
||||||
CMsgClientLogon {
|
|
||||||
account_name: Some(account_name),
|
|
||||||
access_token: Some(refresh_token),
|
|
||||||
machine_name: Some("vapore".to_string()),
|
|
||||||
machine_id: Some(machine_id),
|
|
||||||
client_language: Some("english".to_string()),
|
|
||||||
protocol_version: Some(0x1002c),
|
|
||||||
client_os_type: Some(20),
|
|
||||||
client_package_version: Some(1771),
|
|
||||||
supports_rate_limit_response: Some(true),
|
|
||||||
should_remember_password: Some(true),
|
|
||||||
obfuscated_private_ip: protobuf::MessageField::some(CMsgIPAddress {
|
|
||||||
ip: Some(cmsg_ipaddress::Ip::V4(0xc0a8_0102 ^ 0xbaad_f00d)),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
deprecated_obfustucated_private_ip: Some(0xc0a8_0102 ^ 0xbaad_f00d),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let session_cloned = session.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let license_list_raw = session_cloned
|
|
||||||
.subscribe_message_type(EMsg::k_EMsgClientLicenseList)
|
|
||||||
.recv()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let license_list: CMProtoBufMessage<CMsgClientLicenseList> =
|
|
||||||
CMProtoBufMessage::deserialize(license_list_raw).unwrap();
|
|
||||||
for license in &license_list.body.licenses {
|
|
||||||
log::info!("Own package ID: {}", license.package_id());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut finish_receiver = session.subscribe_message_type(EMsg::k_EMsgClientLogOnResponse);
|
|
||||||
|
|
||||||
let raw_response = finish_receiver.recv().await?;
|
|
||||||
let response: CMProtoBufMessage<CMsgClientLogonResponse> =
|
|
||||||
CMProtoBufMessage::deserialize(raw_response)?;
|
|
||||||
|
|
||||||
log::debug!("Got logon response: {:#x?}", response);
|
|
||||||
|
|
||||||
if response.body.eresult != Some(0x01) {
|
|
||||||
eyre::bail!("Login failed");
|
|
||||||
};
|
|
||||||
|
|
||||||
session.set_steam_id(response.header.steamid());
|
|
||||||
session.set_client_session_id(response.header.client_sessionid());
|
|
||||||
|
|
||||||
if let Some(heartbeat_seconds) = response.body.heartbeat_seconds {
|
|
||||||
if heartbeat_seconds >= 5 {
|
|
||||||
session.begin_heartbeat(heartbeat_seconds as u32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn login_anonymous(session: &mut CMSession, machine_id: Vec<u8>) -> eyre::Result<()> {
|
|
||||||
// Anonymous user
|
|
||||||
session.set_steam_id(0x01a0_0000_0000_0000);
|
|
||||||
|
|
||||||
session.send_notification(
|
|
||||||
EMsg::k_EMsgClientLogon,
|
|
||||||
CMsgClientLogon {
|
|
||||||
machine_name: Some("vapore".to_string()),
|
|
||||||
machine_id: Some(machine_id),
|
|
||||||
client_language: Some("english".to_string()),
|
|
||||||
protocol_version: Some(0x1002c),
|
|
||||||
client_os_type: Some(20),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
use color_eyre::eyre::{self};
|
|
||||||
use vapore::client::SteamClient;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
pub async fn main() -> eyre::Result<()> {
|
|
||||||
env_logger::init();
|
|
||||||
color_eyre::install()?;
|
|
||||||
|
|
||||||
let servers = vapore::selection::bootstrap_find_servers().await?;
|
|
||||||
log::debug!("Found servers: {:?}", servers);
|
|
||||||
|
|
||||||
let client = SteamClient::connect(&servers).await?;
|
|
||||||
|
|
||||||
let username = dialoguer::Input::<String>::new()
|
|
||||||
.with_prompt("Username")
|
|
||||||
.interact_text()?;
|
|
||||||
let password = dialoguer::Password::new()
|
|
||||||
.with_prompt("Password")
|
|
||||||
.interact()?;
|
|
||||||
let guard_code = dialoguer::Input::<String>::new()
|
|
||||||
.with_prompt("Steam Guard Code")
|
|
||||||
.interact_text()?;
|
|
||||||
|
|
||||||
client
|
|
||||||
.auth_password(username, password, Some(guard_code))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,264 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use base64::{prelude::BASE64_STANDARD, Engine as _};
|
|
||||||
use snafu::prelude::*;
|
|
||||||
use vapore_proto::{
|
|
||||||
enums::ESessionPersistence,
|
|
||||||
enums_clientserver::EMsg,
|
|
||||||
steammessages_auth_steamclient::{
|
|
||||||
CAuthentication_BeginAuthSessionViaCredentials_Request,
|
|
||||||
CAuthentication_BeginAuthSessionViaCredentials_Response, CAuthentication_DeviceDetails,
|
|
||||||
CAuthentication_GetPasswordRSAPublicKey_Request,
|
|
||||||
CAuthentication_GetPasswordRSAPublicKey_Response,
|
|
||||||
CAuthentication_PollAuthSessionStatus_Request,
|
|
||||||
CAuthentication_PollAuthSessionStatus_Response,
|
|
||||||
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request,
|
|
||||||
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response, EAuthSessionGuardType,
|
|
||||||
EAuthTokenPlatformType,
|
|
||||||
},
|
|
||||||
steammessages_base::{cmsg_ipaddress, CMsgIPAddress},
|
|
||||||
steammessages_clientserver_login::{CMsgClientHello, CMsgClientLogon, CMsgClientLogonResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
connection::CMSession,
|
|
||||||
error::{
|
|
||||||
ListenRecvSnafu, MissingFieldSnafu, RSAEncryptSnafu, RSAParameterParseSnafu, RSAParseSnafu,
|
|
||||||
},
|
|
||||||
message::CMProtoBufMessage,
|
|
||||||
platform::generate_machine_id,
|
|
||||||
state::apps::AppsHandler,
|
|
||||||
ClientError,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SteamClientInner {
|
|
||||||
/// The currently active socket to a Connection Manager
|
|
||||||
/// TODO: Support recreation when one fails
|
|
||||||
pub session: CMSession,
|
|
||||||
|
|
||||||
apps: AppsHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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: &[String]) -> Result<Self, ClientError> {
|
|
||||||
let (session, context) = CMSession::connect(&servers[0]).await?;
|
|
||||||
|
|
||||||
let inner = Arc::new(SteamClientInner {
|
|
||||||
apps: AppsHandler::listen(&session),
|
|
||||||
session,
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::spawn(context);
|
|
||||||
|
|
||||||
inner.session.send_notification(
|
|
||||||
EMsg::k_EMsgClientHello,
|
|
||||||
CMsgClientHello {
|
|
||||||
protocol_version: Some(0x1002c),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Self { inner })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn auth_password(
|
|
||||||
&self,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
code: Option<String>,
|
|
||||||
) -> Result<(), ClientError> {
|
|
||||||
let machine_id = generate_machine_id();
|
|
||||||
|
|
||||||
let password_key_response: CMProtoBufMessage<
|
|
||||||
CAuthentication_GetPasswordRSAPublicKey_Response,
|
|
||||||
> = self
|
|
||||||
.inner
|
|
||||||
.session
|
|
||||||
.call_service_method(
|
|
||||||
"Authentication.GetPasswordRSAPublicKey#1".to_string(),
|
|
||||||
CAuthentication_GetPasswordRSAPublicKey_Request {
|
|
||||||
account_name: Some(username.clone()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
password_key_response.ok()?;
|
|
||||||
|
|
||||||
let password_key = rsa::RsaPublicKey::new(
|
|
||||||
rsa::BigUint::parse_bytes(password_key_response.body.publickey_mod().as_bytes(), 16)
|
|
||||||
.context(RSAParameterParseSnafu {})?,
|
|
||||||
rsa::BigUint::parse_bytes(password_key_response.body.publickey_exp().as_bytes(), 16)
|
|
||||||
.context(RSAParameterParseSnafu {})?,
|
|
||||||
)
|
|
||||||
.context(RSAParseSnafu {})?;
|
|
||||||
|
|
||||||
let encrypted_password = password_key
|
|
||||||
.encrypt(
|
|
||||||
&mut rand::thread_rng(),
|
|
||||||
rsa::Pkcs1v15Encrypt,
|
|
||||||
password.as_bytes(),
|
|
||||||
)
|
|
||||||
.context(RSAEncryptSnafu {})?;
|
|
||||||
|
|
||||||
let auth_session_response = self
|
|
||||||
.inner
|
|
||||||
.session
|
|
||||||
.call_service_method::<_, CAuthentication_BeginAuthSessionViaCredentials_Response>(
|
|
||||||
"Authentication.BeginAuthSessionViaCredentials#1".to_string(),
|
|
||||||
CAuthentication_BeginAuthSessionViaCredentials_Request {
|
|
||||||
account_name: Some(username.clone()),
|
|
||||||
encrypted_password: Some(BASE64_STANDARD.encode(&encrypted_password)),
|
|
||||||
encryption_timestamp: password_key_response.body.timestamp,
|
|
||||||
persistence: Some(ESessionPersistence::k_ESessionPersistence_Ephemeral.into()),
|
|
||||||
device_details: protobuf::MessageField::some(CAuthentication_DeviceDetails {
|
|
||||||
device_friendly_name: Some("vapore".to_string()),
|
|
||||||
platform_type: Some(protobuf::EnumOrUnknown::new(
|
|
||||||
EAuthTokenPlatformType::k_EAuthTokenPlatformType_SteamClient,
|
|
||||||
)),
|
|
||||||
// Windows 11
|
|
||||||
os_type: Some(20),
|
|
||||||
machine_id: Some(machine_id.clone()),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
log::debug!("Got auth response {:#x?}", auth_session_response);
|
|
||||||
auth_session_response.ok()?;
|
|
||||||
|
|
||||||
let confirmation_type = auth_session_response
|
|
||||||
.body
|
|
||||||
.allowed_confirmations
|
|
||||||
.first()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.confirmation_type();
|
|
||||||
|
|
||||||
if confirmation_type == EAuthSessionGuardType::k_EAuthSessionGuardType_Unknown {
|
|
||||||
return Err(ClientError::NoAllowedConfirmations);
|
|
||||||
} else if confirmation_type == EAuthSessionGuardType::k_EAuthSessionGuardType_None {
|
|
||||||
// No required confirmation, we can go directly to login
|
|
||||||
} else if confirmation_type == EAuthSessionGuardType::k_EAuthSessionGuardType_EmailCode
|
|
||||||
|| confirmation_type == EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceCode
|
|
||||||
{
|
|
||||||
let update_response: CMProtoBufMessage<
|
|
||||||
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response,
|
|
||||||
> = self
|
|
||||||
.inner
|
|
||||||
.session
|
|
||||||
.call_service_method(
|
|
||||||
"Authentication.UpdateAuthSessionWithSteamGuardCode#1".to_string(),
|
|
||||||
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request {
|
|
||||||
client_id: auth_session_response.body.client_id,
|
|
||||||
steamid: auth_session_response.body.steamid,
|
|
||||||
code,
|
|
||||||
code_type: Some(confirmation_type.into()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
update_response.ok()?;
|
|
||||||
} else {
|
|
||||||
return Err(ClientError::UnsupportedConfirmation {
|
|
||||||
typ: confirmation_type,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let poll_response: CMProtoBufMessage<CAuthentication_PollAuthSessionStatus_Response> = self
|
|
||||||
.inner
|
|
||||||
.session
|
|
||||||
.call_service_method(
|
|
||||||
"Authentication.PollAuthSessionStatus#1".to_string(),
|
|
||||||
CAuthentication_PollAuthSessionStatus_Request {
|
|
||||||
client_id: auth_session_response.body.client_id,
|
|
||||||
request_id: auth_session_response.body.request_id.clone(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
poll_response.ok()?;
|
|
||||||
log::debug!("Got poll response: {:#?}", poll_response);
|
|
||||||
|
|
||||||
let mut logon_receiver = self
|
|
||||||
.inner
|
|
||||||
.session
|
|
||||||
.subscribe_message_type(EMsg::k_EMsgClientLogOnResponse);
|
|
||||||
|
|
||||||
let account_name = poll_response.body.account_name.context(MissingFieldSnafu {
|
|
||||||
field: "account_name",
|
|
||||||
})?;
|
|
||||||
let refresh_token = poll_response
|
|
||||||
.body
|
|
||||||
.refresh_token
|
|
||||||
.context(MissingFieldSnafu {
|
|
||||||
field: "refresh_token",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
log::debug!(
|
|
||||||
"Got account name {}, access token {}",
|
|
||||||
account_name,
|
|
||||||
refresh_token
|
|
||||||
);
|
|
||||||
|
|
||||||
// normal user, desktop instance, public universe
|
|
||||||
self.inner.session.set_steam_id(0x0110_0001_0000_0000);
|
|
||||||
|
|
||||||
self.inner.session.send_notification(
|
|
||||||
EMsg::k_EMsgClientLogon,
|
|
||||||
CMsgClientLogon {
|
|
||||||
account_name: Some(account_name),
|
|
||||||
access_token: Some(refresh_token),
|
|
||||||
machine_name: Some("vapore".to_string()),
|
|
||||||
machine_id: Some(machine_id),
|
|
||||||
client_language: Some("english".to_string()),
|
|
||||||
protocol_version: Some(0x1002c),
|
|
||||||
client_os_type: Some(20),
|
|
||||||
client_package_version: Some(1771),
|
|
||||||
supports_rate_limit_response: Some(true),
|
|
||||||
should_remember_password: Some(true),
|
|
||||||
obfuscated_private_ip: protobuf::MessageField::some(CMsgIPAddress {
|
|
||||||
ip: Some(cmsg_ipaddress::Ip::V4(0xc0a8_0102 ^ 0xbaad_f00d)),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
deprecated_obfustucated_private_ip: Some(0xc0a8_0102 ^ 0xbaad_f00d),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let raw_logon = logon_receiver.recv().await.context(ListenRecvSnafu {})?;
|
|
||||||
let logon: CMProtoBufMessage<CMsgClientLogonResponse> =
|
|
||||||
CMProtoBufMessage::deserialize(raw_logon)?;
|
|
||||||
|
|
||||||
self.inner.session.set_steam_id(logon.header.steamid());
|
|
||||||
self.inner
|
|
||||||
.session
|
|
||||||
.set_client_session_id(logon.header.client_sessionid());
|
|
||||||
|
|
||||||
if let Some(heartbeat_seconds) = logon.body.heartbeat_seconds {
|
|
||||||
if heartbeat_seconds >= 5 {
|
|
||||||
self.inner.session.begin_heartbeat(heartbeat_seconds as u32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apps(&self) -> &AppsHandler {
|
|
||||||
&self.inner.apps
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn session(&self) -> CMSession {
|
|
||||||
self.inner.session.clone()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,414 +0,0 @@
|
||||||
use core::time;
|
|
||||||
use std::{
|
|
||||||
collections::{HashMap, VecDeque},
|
|
||||||
future::Future,
|
|
||||||
marker::PhantomData,
|
|
||||||
pin::Pin,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
task::{Poll, Waker},
|
|
||||||
};
|
|
||||||
|
|
||||||
use async_tungstenite::{
|
|
||||||
tokio::{connect_async, ConnectStream},
|
|
||||||
tungstenite, WebSocketStream,
|
|
||||||
};
|
|
||||||
use futures::{SinkExt as _, StreamExt};
|
|
||||||
use snafu::prelude::*;
|
|
||||||
use tokio::sync::broadcast;
|
|
||||||
use vapore_proto::{
|
|
||||||
enums_clientserver::EMsg, steammessages_base::CMsgProtoBufHeader,
|
|
||||||
steammessages_clientserver_login::CMsgClientHeartBeat,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
error::{BadResponseActionSnafu, WebSocketConnectSnafu, WebSocketSnafu},
|
|
||||||
message::{CMProtoBufMessage, CMRawProtoBufMessage},
|
|
||||||
ClientError,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Maximum number of messages in the buffer for by-message-type subscriptions
|
|
||||||
const CHANNEL_CAPACITY: usize = 16;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct CMSessionInner {
|
|
||||||
/// Steam ID of current user. When set to None we are not logged in
|
|
||||||
steam_id: Option<u64>,
|
|
||||||
/// Realm we're connecting to. AIUI this corresponds to account universe.
|
|
||||||
/// Should normally be 1 for Public
|
|
||||||
realm: u32,
|
|
||||||
/// Session ID for our socket, assigned by the server after login.
|
|
||||||
/// Should be 0 before we login
|
|
||||||
client_session_id: i32,
|
|
||||||
|
|
||||||
/// Messages ready to send
|
|
||||||
send_queue: VecDeque<tungstenite::Message>,
|
|
||||||
/// Waker for the sending thread
|
|
||||||
send_waker: Option<Waker>,
|
|
||||||
|
|
||||||
/// Recievers waiting for responses by jobid
|
|
||||||
receive_wakers: HashMap<u64, Waker>,
|
|
||||||
/// Messages ready for receivers by jobid
|
|
||||||
/// TODO: Support multiple messages for same job ID
|
|
||||||
receive_messages: HashMap<u64, CMRawProtoBufMessage>,
|
|
||||||
|
|
||||||
/// Senders for per-type subscriptions
|
|
||||||
subscribe_senders: HashMap<EMsg, broadcast::Sender<CMRawProtoBufMessage>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CMSessionInner {
|
|
||||||
/// Create a new job ID.
|
|
||||||
/// Officially there's some complicated format for this,
|
|
||||||
/// but we're using random for now
|
|
||||||
fn alloc_jobid(&mut self) -> u64 {
|
|
||||||
rand::random()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wake the send thread, use after adding messages to the send queue
|
|
||||||
fn wake_sender(&mut self) {
|
|
||||||
if let Some(waker) = self.send_waker.take() {
|
|
||||||
waker.wake()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CMContext {
|
|
||||||
fn handle_receive(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
message: tungstenite::Result<tungstenite::Message>,
|
|
||||||
) -> Result<(), ClientError> {
|
|
||||||
// Technically everything should be Binary but I think I saw some Text before
|
|
||||||
let message_data = match message.context(WebSocketSnafu {})? {
|
|
||||||
tungstenite::Message::Text(t) => t.into_bytes(),
|
|
||||||
tungstenite::Message::Binary(b) => b,
|
|
||||||
_ => return Err(ClientError::BadWSMessageType),
|
|
||||||
};
|
|
||||||
|
|
||||||
let raw_messages = CMRawProtoBufMessage::try_parse_multi(&message_data)?;
|
|
||||||
|
|
||||||
let mut session = self.session.lock()?;
|
|
||||||
|
|
||||||
for message in raw_messages.into_iter() {
|
|
||||||
log::trace!("Got message: {:?}", message);
|
|
||||||
|
|
||||||
// Send based on message type to subscribers
|
|
||||||
let action = message.action;
|
|
||||||
if session.subscribe_senders.contains_key(&action) {
|
|
||||||
let sender_unused = session
|
|
||||||
.subscribe_senders
|
|
||||||
.get(&action)
|
|
||||||
.unwrap()
|
|
||||||
.send(message.clone())
|
|
||||||
.is_err();
|
|
||||||
|
|
||||||
if sender_unused {
|
|
||||||
log::debug!("No more subscribers for type {:?}", action);
|
|
||||||
session.subscribe_senders.remove(&action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send based on jobid to call_service_method etc.
|
|
||||||
if let Some(jobid) = message.header.jobid_target {
|
|
||||||
if let Some(waker) = session.receive_wakers.remove(&jobid) {
|
|
||||||
if session.receive_messages.insert(jobid, message).is_some() {
|
|
||||||
log::info!("Received duplicate message for jobid {}", jobid);
|
|
||||||
};
|
|
||||||
waker.wake();
|
|
||||||
} else {
|
|
||||||
log::info!("Received message for unknown jobid {}", jobid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_send(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context,
|
|
||||||
) -> Result<(), ClientError> {
|
|
||||||
// TODO: figure out how to not cloe the Arc
|
|
||||||
let session_arc = self.session.clone();
|
|
||||||
let mut session = session_arc.lock()?;
|
|
||||||
|
|
||||||
while !session.send_queue.is_empty() {
|
|
||||||
match self.socket.poll_ready_unpin(cx) {
|
|
||||||
Poll::Ready(ret) => ret.context(WebSocketSnafu {})?,
|
|
||||||
Poll::Pending => return Ok(()),
|
|
||||||
}
|
|
||||||
let message = session.send_queue.pop_front().unwrap();
|
|
||||||
self.socket
|
|
||||||
.start_send_unpin(message)
|
|
||||||
.context(WebSocketSnafu {})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.socket.poll_flush_unpin(cx) {
|
|
||||||
Poll::Ready(ret) => ret.context(WebSocketSnafu {})?,
|
|
||||||
Poll::Pending => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_inner(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
) -> Result<(), ClientError> {
|
|
||||||
{
|
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Poll::Ready(maybe_message) = self.as_mut().socket.poll_next_unpin(cx) {
|
|
||||||
let message = maybe_message.ok_or(ClientError::ClosedSocket)?;
|
|
||||||
if let Err(err) = self.as_mut().handle_receive(message) {
|
|
||||||
log::warn!("Got error while processing message: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All errors in send are critical, since everything's encoded by the time we get there
|
|
||||||
self.handle_send(cx)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Future for CMContext {
|
|
||||||
type Output = Result<(), ClientError>;
|
|
||||||
|
|
||||||
fn poll(
|
|
||||||
self: std::pin::Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
) -> std::task::Poll<Self::Output> {
|
|
||||||
if let Err(error) = self.poll_inner(cx) {
|
|
||||||
log::error!("Failed while talking to socket: {}", error);
|
|
||||||
todo!("Reopen new socket");
|
|
||||||
};
|
|
||||||
|
|
||||||
// We should always be pending unless we're going to return an error
|
|
||||||
Poll::Pending
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A low-level connection to a Valve Connection Manager server.
|
|
||||||
/// You can use it to send requests, call RPC functions,
|
|
||||||
/// and subscribe to message types, but it will not keep a record
|
|
||||||
/// of data recieved.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct CMSession {
|
|
||||||
inner: Arc<Mutex<CMSessionInner>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CMSession {
|
|
||||||
/// 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 {
|
|
||||||
url: server.to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let inner = CMSessionInner {
|
|
||||||
steam_id: None,
|
|
||||||
realm: 1,
|
|
||||||
client_session_id: 0,
|
|
||||||
send_queue: VecDeque::new(),
|
|
||||||
send_waker: None,
|
|
||||||
receive_wakers: HashMap::new(),
|
|
||||||
receive_messages: HashMap::new(),
|
|
||||||
subscribe_senders: HashMap::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let inner_wrapped = Arc::new(Mutex::new(inner));
|
|
||||||
|
|
||||||
let context = CMContext {
|
|
||||||
socket,
|
|
||||||
session: inner_wrapped.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let session = Self {
|
|
||||||
inner: inner_wrapped,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((session, context))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn begin_heartbeat(&self, interval: u32) {
|
|
||||||
log::debug!("Starting heartbeats every {} seconds", interval);
|
|
||||||
let cloned = self.clone();
|
|
||||||
tokio::spawn(async move { cloned.send_heartbeat_task(interval).await });
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_heartbeat_task(self, interval_secs: u32) -> Result<(), ClientError> {
|
|
||||||
let mut interval = tokio::time::interval(time::Duration::from_secs(interval_secs as u64));
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
self.send_notification(EMsg::k_EMsgClientHeartBeat, CMsgClientHeartBeat::default())?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_steam_id(&self, new_steam_id: u64) {
|
|
||||||
let mut inner = self.inner.lock().expect("Lock was poisoned");
|
|
||||||
inner.steam_id = Some(new_steam_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_client_session_id(&self, new_client_session_id: i32) {
|
|
||||||
let mut inner = self.inner.lock().expect("Lock was poisoned");
|
|
||||||
inner.client_session_id = new_client_session_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn call_service_method<T: protobuf::Message, U: protobuf::Message>(
|
|
||||||
&self,
|
|
||||||
method: String,
|
|
||||||
body: T,
|
|
||||||
) -> CallServiceMethod<T, U> {
|
|
||||||
log::trace!("Calling service method {}", method);
|
|
||||||
|
|
||||||
CallServiceMethod::<T, U> {
|
|
||||||
session: self,
|
|
||||||
body,
|
|
||||||
method,
|
|
||||||
jobid: None,
|
|
||||||
_phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a message without a jobid.
|
|
||||||
/// Returns as soon as the message is in the send buffer
|
|
||||||
pub fn send_notification<T: protobuf::Message>(
|
|
||||||
&self,
|
|
||||||
action: EMsg,
|
|
||||||
body: T,
|
|
||||||
) -> Result<(), ClientError> {
|
|
||||||
let mut inner = self.inner.lock()?;
|
|
||||||
|
|
||||||
log::trace!("Sending notification of type {:?}", action);
|
|
||||||
|
|
||||||
let header = CMsgProtoBufHeader {
|
|
||||||
steamid: inner.steam_id,
|
|
||||||
realm: Some(inner.realm),
|
|
||||||
client_sessionid: Some(inner.client_session_id),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let message = CMProtoBufMessage {
|
|
||||||
action,
|
|
||||||
header,
|
|
||||||
body,
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = message.serialize()?;
|
|
||||||
|
|
||||||
inner
|
|
||||||
.send_queue
|
|
||||||
.push_back(tungstenite::protocol::Message::Binary(serialized));
|
|
||||||
inner.wake_sender();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subscribe to receive a notification every time a message of a
|
|
||||||
/// given type is received
|
|
||||||
pub fn subscribe_message_type(
|
|
||||||
&self,
|
|
||||||
action: EMsg,
|
|
||||||
) -> broadcast::Receiver<CMRawProtoBufMessage> {
|
|
||||||
let mut inner = self.inner.lock().expect("Lock was poisoned");
|
|
||||||
if let Some(sender) = inner.subscribe_senders.get(&action) {
|
|
||||||
sender.subscribe()
|
|
||||||
} else {
|
|
||||||
let (sender, receiver) = broadcast::channel(CHANNEL_CAPACITY);
|
|
||||||
inner.subscribe_senders.insert(action, sender);
|
|
||||||
receiver
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use = "futures do nothing unless polled"]
|
|
||||||
pub struct CallServiceMethod<'a, T: protobuf::Message, U: protobuf::Message> {
|
|
||||||
session: &'a CMSession,
|
|
||||||
body: T,
|
|
||||||
method: String,
|
|
||||||
jobid: Option<u64>,
|
|
||||||
_phantom: PhantomData<U>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T: protobuf::Message, U: protobuf::Message> Unpin for CallServiceMethod<'a, T, U> {}
|
|
||||||
|
|
||||||
impl<'a, T: protobuf::Message, U: protobuf::Message> CallServiceMethod<'a, T, U> {
|
|
||||||
fn finalize_response(
|
|
||||||
&self,
|
|
||||||
response: CMRawProtoBufMessage,
|
|
||||||
) -> Result<CMProtoBufMessage<U>, ClientError> {
|
|
||||||
ensure!(
|
|
||||||
response.action == EMsg::k_EMsgServiceMethodResponse,
|
|
||||||
BadResponseActionSnafu {
|
|
||||||
actual: response.action
|
|
||||||
}
|
|
||||||
);
|
|
||||||
CMProtoBufMessage::<U>::deserialize(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: protobuf::Message, U: protobuf::Message> Future for CallServiceMethod<'_, T, U> {
|
|
||||||
type Output = Result<CMProtoBufMessage<U>, ClientError>;
|
|
||||||
|
|
||||||
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()?;
|
|
||||||
|
|
||||||
// We only have to send the message once, use jobid for that flag
|
|
||||||
if self.jobid.is_none() {
|
|
||||||
let jobid = session.alloc_jobid();
|
|
||||||
self.jobid = Some(jobid);
|
|
||||||
|
|
||||||
let action = if session.steam_id.is_some() {
|
|
||||||
EMsg::k_EMsgServiceMethodCallFromClient
|
|
||||||
} else {
|
|
||||||
EMsg::k_EMsgServiceMethodCallFromClientNonAuthed
|
|
||||||
};
|
|
||||||
|
|
||||||
let header = CMsgProtoBufHeader {
|
|
||||||
steamid: session.steam_id,
|
|
||||||
target_job_name: Some(self.method.clone()),
|
|
||||||
realm: Some(session.realm),
|
|
||||||
client_sessionid: Some(session.client_session_id),
|
|
||||||
jobid_source: self.jobid,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = CMProtoBufMessage {
|
|
||||||
action,
|
|
||||||
header,
|
|
||||||
body: self.body.clone(),
|
|
||||||
};
|
|
||||||
let serialized = message.serialize()?;
|
|
||||||
session
|
|
||||||
.send_queue
|
|
||||||
.push_back(tungstenite::protocol::Message::Binary(serialized));
|
|
||||||
|
|
||||||
session.receive_wakers.insert(jobid, cx.waker().clone());
|
|
||||||
|
|
||||||
session.wake_sender();
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
|
|
||||||
let jobid = self.jobid.unwrap();
|
|
||||||
|
|
||||||
let Some(response) = session.receive_messages.remove(&jobid) else {
|
|
||||||
session.receive_wakers.insert(jobid, cx.waker().clone());
|
|
||||||
return Poll::Pending;
|
|
||||||
};
|
|
||||||
|
|
||||||
Poll::Ready(self.finalize_response(response))
|
|
||||||
}
|
|
||||||
}
|
|
114
lib/src/error.rs
114
lib/src/error.rs
|
@ -1,114 +0,0 @@
|
||||||
use snafu::prelude::*;
|
|
||||||
use vapore_proto::steammessages_auth_steamclient::EAuthSessionGuardType;
|
|
||||||
use vapore_struct::eresult::EResult;
|
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
#[snafu(visibility(pub(crate)))]
|
|
||||||
pub enum ClientError {
|
|
||||||
#[snafu(display("Valve returned bad result {eresult:?} with message `{message}`"))]
|
|
||||||
BadResult { eresult: EResult, message: String },
|
|
||||||
|
|
||||||
#[snafu(display("Request failure"))]
|
|
||||||
Reqwest { source: reqwest::Error },
|
|
||||||
|
|
||||||
#[snafu(display("VDF Deserialization failure"))]
|
|
||||||
Vdf {
|
|
||||||
#[snafu(source(from(keyvalues_serde::Error, Box::new)))]
|
|
||||||
source: Box<keyvalues_serde::Error>,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[snafu(display("Unable to connect to WebSocket at `{url}`"))]
|
|
||||||
WebSocketConnect {
|
|
||||||
#[snafu(source(from(async_tungstenite::tungstenite::Error, Box::new)))]
|
|
||||||
source: Box<async_tungstenite::tungstenite::Error>,
|
|
||||||
url: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[snafu(display("WebSocket connection error"))]
|
|
||||||
WebSocket {
|
|
||||||
#[snafu(source(from(async_tungstenite::tungstenite::Error, Box::new)))]
|
|
||||||
source: Box<async_tungstenite::tungstenite::Error>,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[snafu(display("WebSocket was closed while trying to recieve"))]
|
|
||||||
ClosedSocket,
|
|
||||||
|
|
||||||
#[snafu(display("Protobuf Deserialization error"))]
|
|
||||||
ProtobufDe { source: protobuf::Error },
|
|
||||||
|
|
||||||
#[snafu(display("Protobuf Serialization error"))]
|
|
||||||
ProtobufSer { source: protobuf::Error },
|
|
||||||
|
|
||||||
#[snafu(display("Struct Missing Field `{field}`"))]
|
|
||||||
MissingField { field: &'static str },
|
|
||||||
|
|
||||||
#[snafu(display("Invalid WebSocket message type from server"))]
|
|
||||||
BadWSMessageType,
|
|
||||||
|
|
||||||
#[snafu(display("Decompression Error"))]
|
|
||||||
Decompression { source: std::io::Error },
|
|
||||||
|
|
||||||
#[snafu(display("Invalid decompressed output (expected {expected} bytes, got {actual})"))]
|
|
||||||
DecompressionInvalid { expected: u32, actual: usize },
|
|
||||||
|
|
||||||
#[snafu(display("Invalid message action {action}"))]
|
|
||||||
InvalidAction { action: u32 },
|
|
||||||
|
|
||||||
#[snafu(display("Invalid message length"))]
|
|
||||||
InvalidMessageLength,
|
|
||||||
|
|
||||||
#[snafu(display("Message too short (need {expected} bytes, got {actual})"))]
|
|
||||||
MessageTooShort { expected: usize, actual: usize },
|
|
||||||
|
|
||||||
#[snafu(display("Lock was poisoned"))]
|
|
||||||
LockPoisoned,
|
|
||||||
|
|
||||||
#[snafu(display("Expected action ServiceMethodResponse, got {actual:?}"))]
|
|
||||||
BadResponseAction {
|
|
||||||
actual: vapore_proto::enums_clientserver::EMsg,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[snafu(display("Unknown value while parsing enum"))]
|
|
||||||
TryFromPrimitive {
|
|
||||||
source: Box<dyn std::error::Error + Send + Sync>,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[snafu(display("Unable to parse RSA key parameters"))]
|
|
||||||
RSAParameterParse,
|
|
||||||
|
|
||||||
#[snafu(display("Unable to parse RSA key"))]
|
|
||||||
RSAParse { source: rsa::Error },
|
|
||||||
|
|
||||||
#[snafu(display("Unable to encrupt with RSA"))]
|
|
||||||
RSAEncrypt { source: rsa::Error },
|
|
||||||
|
|
||||||
#[snafu(display("Unsupported Confirmation type {typ:?}"))]
|
|
||||||
UnsupportedConfirmation { typ: EAuthSessionGuardType },
|
|
||||||
|
|
||||||
#[snafu(display("No allowed confirmations"))]
|
|
||||||
NoAllowedConfirmations,
|
|
||||||
|
|
||||||
#[snafu(display("Unable to fetch messages from listen thread"))]
|
|
||||||
ListenRecv {
|
|
||||||
source: tokio::sync::broadcast::error::RecvError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> From<std::sync::PoisonError<T>> for ClientError {
|
|
||||||
fn from(_value: std::sync::PoisonError<T>) -> Self {
|
|
||||||
// The guard won't be Send so we can't return it from async functions
|
|
||||||
ClientError::LockPoisoned
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Enum: num_enum::TryFromPrimitive + 'static> From<num_enum::TryFromPrimitiveError<Enum>>
|
|
||||||
for ClientError
|
|
||||||
where
|
|
||||||
<Enum as num_enum::TryFromPrimitive>::Primitive: Send + Sync,
|
|
||||||
{
|
|
||||||
fn from(value: num_enum::TryFromPrimitiveError<Enum>) -> Self {
|
|
||||||
Self::TryFromPrimitive {
|
|
||||||
source: Box::new(value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
pub mod client;
|
|
||||||
pub mod connection;
|
|
||||||
pub mod error;
|
|
||||||
pub mod message;
|
|
||||||
pub mod platform;
|
|
||||||
pub mod selection;
|
|
||||||
pub mod state;
|
|
||||||
|
|
||||||
pub use error::ClientError;
|
|
|
@ -1,187 +0,0 @@
|
||||||
use std::io::Read;
|
|
||||||
|
|
||||||
use flate2::read::GzDecoder;
|
|
||||||
use protobuf::{Enum as _, Message as _};
|
|
||||||
use snafu::prelude::*;
|
|
||||||
use vapore_proto::{
|
|
||||||
enums_clientserver::EMsg,
|
|
||||||
steammessages_base::{CMsgMulti, CMsgProtoBufHeader},
|
|
||||||
};
|
|
||||||
use vapore_struct::eresult::EResult;
|
|
||||||
|
|
||||||
use crate::error::{
|
|
||||||
ClientError, DecompressionInvalidSnafu, DecompressionSnafu, InvalidActionSnafu,
|
|
||||||
MessageTooShortSnafu, ProtobufDeSnafu, ProtobufSerSnafu,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A message sent over the socket. Can be either sent or recieved
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CMProtoBufMessage<T: protobuf::Message> {
|
|
||||||
pub action: EMsg,
|
|
||||||
pub header: CMsgProtoBufHeader,
|
|
||||||
pub body: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: protobuf::Message> CMProtoBufMessage<T> {
|
|
||||||
pub fn serialize(&self) -> Result<Vec<u8>, ClientError> {
|
|
||||||
// 4 bytes for type, 4 bytes for header length, then header and body
|
|
||||||
// No alignment requirements
|
|
||||||
let length = 4 + 4 + self.header.compute_size() + self.body.compute_size();
|
|
||||||
let mut out = Vec::with_capacity(
|
|
||||||
length
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| ClientError::InvalidMessageLength)?,
|
|
||||||
);
|
|
||||||
|
|
||||||
out.extend_from_slice(&(self.action.value() as u32 | 0x80000000).to_le_bytes());
|
|
||||||
out.extend_from_slice(&self.header.cached_size().to_le_bytes());
|
|
||||||
self.header
|
|
||||||
.write_to_vec(&mut out)
|
|
||||||
.context(ProtobufSerSnafu {})?;
|
|
||||||
self.body
|
|
||||||
.write_to_vec(&mut out)
|
|
||||||
.context(ProtobufSerSnafu {})?;
|
|
||||||
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize(raw: CMRawProtoBufMessage) -> Result<Self, ClientError> {
|
|
||||||
let body = T::parse_from_bytes(&raw.body).context(ProtobufDeSnafu {})?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
action: raw.action,
|
|
||||||
header: raw.header,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eresult(&self) -> Result<EResult, ClientError> {
|
|
||||||
Ok((self.header.eresult() as u32).try_into()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ok(&self) -> Result<(), ClientError> {
|
|
||||||
let eresult = self.eresult()?;
|
|
||||||
if eresult == EResult::OK {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ClientError::BadResult {
|
|
||||||
eresult,
|
|
||||||
message: self.header.error_message().to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A message sent over the socket, but the body is still serialized
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CMRawProtoBufMessage {
|
|
||||||
pub action: EMsg,
|
|
||||||
pub header: CMsgProtoBufHeader,
|
|
||||||
pub body: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CMRawProtoBufMessage {
|
|
||||||
pub fn try_parse(binary: &[u8]) -> Result<Self, ClientError> {
|
|
||||||
ensure!(
|
|
||||||
binary.len() >= 8,
|
|
||||||
MessageTooShortSnafu {
|
|
||||||
expected: 8usize,
|
|
||||||
actual: binary.len()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let raw_action = u32::from_le_bytes(binary[0..4].try_into().unwrap()) & !0x8000_0000;
|
|
||||||
let action = EMsg::from_i32(raw_action as i32)
|
|
||||||
.with_context(|| InvalidActionSnafu { action: raw_action })?;
|
|
||||||
|
|
||||||
let header_length = u32::from_le_bytes(binary[4..8].try_into().unwrap());
|
|
||||||
let header_end = 8 + header_length as usize;
|
|
||||||
ensure!(
|
|
||||||
binary.len() >= header_end,
|
|
||||||
MessageTooShortSnafu {
|
|
||||||
expected: header_end,
|
|
||||||
actual: binary.len()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let header = CMsgProtoBufHeader::parse_from_bytes(&binary[8..header_end])
|
|
||||||
.context(ProtobufDeSnafu {})?;
|
|
||||||
let body = binary[header_end..].to_vec();
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
action,
|
|
||||||
header,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_parse_multi(binary: &[u8]) -> Result<Vec<Self>, ClientError> {
|
|
||||||
let root_raw = Self::try_parse(binary)?;
|
|
||||||
if root_raw.action != EMsg::k_EMsgMulti {
|
|
||||||
return Ok(vec![root_raw]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi messages are a bunch of [full_length][action][header_length][header][body] inside
|
|
||||||
// possibly gzipped bytes inside a protobuf.
|
|
||||||
// why, valve
|
|
||||||
let root = CMProtoBufMessage::<CMsgMulti>::deserialize(root_raw)?;
|
|
||||||
let mut gzip_decompressed = Vec::new();
|
|
||||||
|
|
||||||
let mut body = if let Some(size_unzipped) = root.body.size_unzipped {
|
|
||||||
gzip_decompressed.reserve(size_unzipped as usize);
|
|
||||||
|
|
||||||
let mut gz = GzDecoder::new(root.body.message_body());
|
|
||||||
gz.read_to_end(&mut gzip_decompressed)
|
|
||||||
.context(DecompressionSnafu {})?;
|
|
||||||
|
|
||||||
ensure!(
|
|
||||||
gzip_decompressed.len() == size_unzipped as usize,
|
|
||||||
DecompressionInvalidSnafu {
|
|
||||||
expected: size_unzipped,
|
|
||||||
actual: gzip_decompressed.len(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
&gzip_decompressed
|
|
||||||
} else {
|
|
||||||
root.body.message_body()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut items = Vec::new();
|
|
||||||
while body.len() >= 4 {
|
|
||||||
let full_length = u32::from_le_bytes(body[0..4].try_into().unwrap());
|
|
||||||
let message_end = 4 + full_length as usize;
|
|
||||||
ensure!(
|
|
||||||
body.len() >= message_end,
|
|
||||||
MessageTooShortSnafu {
|
|
||||||
expected: message_end,
|
|
||||||
actual: body.len()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
match Self::try_parse(&body[4..message_end]) {
|
|
||||||
Ok(msg) => items.push(msg),
|
|
||||||
Err(err) => log::warn!("Failed to parse sub-message: {:?}", err),
|
|
||||||
}
|
|
||||||
|
|
||||||
body = &body[message_end..];
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eresult(&self) -> Result<EResult, ClientError> {
|
|
||||||
Ok((self.header.eresult() as u32).try_into()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ok(&self) -> Result<(), ClientError> {
|
|
||||||
let eresult = self.eresult()?;
|
|
||||||
if eresult == EResult::OK {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ClientError::BadResult {
|
|
||||||
eresult,
|
|
||||||
message: self.header.error_message().to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
use rand::RngCore;
|
|
||||||
|
|
||||||
pub fn generate_machine_id() -> Vec<u8> {
|
|
||||||
// machine_id is supposed to be a binary key/value with the SHA1 of the machine's
|
|
||||||
// BB3: Machine GUID
|
|
||||||
// FF2: MAC address
|
|
||||||
// 3B3: Disk ID
|
|
||||||
// We should probably make these consistent so Valve doesn't get suspicious,
|
|
||||||
// but for now let's make them random
|
|
||||||
// TODO: Find a more generic way to make this
|
|
||||||
let mut machine_id = Vec::with_capacity(155);
|
|
||||||
machine_id.extend_from_slice(b"\x00MessageObject\x00");
|
|
||||||
for key in [b"BB3", b"FF2", b"3B3"] {
|
|
||||||
let mut data = [0u8; 20];
|
|
||||||
rand::thread_rng().fill_bytes(&mut data);
|
|
||||||
let hex_bytes = hex::encode(data).into_bytes();
|
|
||||||
|
|
||||||
// Type is string
|
|
||||||
machine_id.push(b'\x01');
|
|
||||||
machine_id.extend_from_slice(key);
|
|
||||||
machine_id.push(b'\x00');
|
|
||||||
machine_id.extend_from_slice(&hex_bytes);
|
|
||||||
machine_id.push(b'\x00');
|
|
||||||
}
|
|
||||||
// suitable end bytes
|
|
||||||
machine_id.extend_from_slice(b"\x08\x08");
|
|
||||||
machine_id
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use num_enum::TryFromPrimitive;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use snafu::prelude::*;
|
|
||||||
use vapore_struct::eresult::EResult;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
error::{BadResultSnafu, ReqwestSnafu, VdfSnafu},
|
|
||||||
ClientError,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct CMServerEntry<'a> {
|
|
||||||
pub endpoint: &'a str,
|
|
||||||
pub legacy_endpoint: &'a str,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub server_type: &'a str,
|
|
||||||
pub dc: &'a str,
|
|
||||||
pub realm: &'a str,
|
|
||||||
pub load: u32,
|
|
||||||
pub wtd_load: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct GetCMListForConnectResponse<'a> {
|
|
||||||
pub serverlist: BTreeMap<u32, CMServerEntry<'a>>,
|
|
||||||
pub success: u32,
|
|
||||||
pub message: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn bootstrap_find_servers() -> Result<Vec<String>, ClientError> {
|
|
||||||
let response = reqwest::get(
|
|
||||||
"https://api.steampowered.com/ISteamDirectory/GetCMListForConnect/v1/?cellid=0&format=vdf",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.context(ReqwestSnafu {})?
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.context(ReqwestSnafu {})?;
|
|
||||||
let result: GetCMListForConnectResponse =
|
|
||||||
keyvalues_serde::from_str(&response).context(VdfSnafu {})?;
|
|
||||||
|
|
||||||
let eresult = EResult::try_from_primitive(result.success)?;
|
|
||||||
ensure!(
|
|
||||||
eresult == EResult::OK,
|
|
||||||
BadResultSnafu {
|
|
||||||
eresult,
|
|
||||||
message: result.message
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(result
|
|
||||||
.serverlist
|
|
||||||
.values()
|
|
||||||
.filter(|entry| entry.server_type == "websockets")
|
|
||||||
.map(|entry| format!("wss://{}/cmsocket/", entry.endpoint))
|
|
||||||
.collect())
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
use std::{
|
|
||||||
sync::Arc,
|
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
|
||||||
|
|
||||||
use tokio::sync::watch;
|
|
||||||
use vapore_proto::{
|
|
||||||
enums_clientserver::EMsg,
|
|
||||||
steammessages_clientserver::{cmsg_client_license_list, CMsgClientLicenseList},
|
|
||||||
};
|
|
||||||
use vapore_struct::enums::{ELicenseFlags, ELicenseType, EPaymentMethod};
|
|
||||||
|
|
||||||
use crate::{connection::CMSession, message::CMProtoBufMessage, ClientError};
|
|
||||||
|
|
||||||
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()?,
|
|
||||||
flags: ELicenseFlags::from_bits_truncate(value.flags()),
|
|
||||||
purchase_country_code: value.purchase_country_code().to_string(),
|
|
||||||
license_type: value.license_type().try_into()?,
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AppsHandler {
|
|
||||||
/// 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>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppsHandler {
|
|
||||||
pub(crate) fn listen(session: &CMSession) -> Self {
|
|
||||||
let (licenses_sender, licenses) = watch::channel(Arc::new(Vec::new()));
|
|
||||||
|
|
||||||
let session_cloned = session.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut listener = session_cloned.subscribe_message_type(EMsg::k_EMsgClientLicenseList);
|
|
||||||
while let Ok(raw_message) = listener.recv().await {
|
|
||||||
match CMProtoBufMessage::<CMsgClientLicenseList>::deserialize(raw_message) {
|
|
||||||
Ok(message) => {
|
|
||||||
let licenses = message
|
|
||||||
.body
|
|
||||||
.licenses
|
|
||||||
.into_iter()
|
|
||||||
.map(License::try_from)
|
|
||||||
.filter_map(|item| match item {
|
|
||||||
Ok(license) => Some(license),
|
|
||||||
Err(err) => {
|
|
||||||
log::debug!("Invalid License object received: {}", err);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if licenses_sender.send(Arc::new(licenses)).is_err() {
|
|
||||||
// If there's no receiver then there's no more SteamClient, just die
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => log::debug!("Invalid CMsgClientList: {}", err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self { licenses }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod apps;
|
|
11
meson.build
Normal file
11
meson.build
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
project('vapore', 'cpp',
|
||||||
|
default_options : [
|
||||||
|
'cpp_std=c++20',
|
||||||
|
'optimization=2',
|
||||||
|
'debug=true',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
curl = dependency('libcurl', 'curl', required : true)
|
||||||
|
|
||||||
|
tool = executable('vaporetool', 'src/tool/main.cpp', dependencies: [ curl ])
|
|
@ -1,10 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "vapore-proto"
|
|
||||||
edition = "2021"
|
|
||||||
version.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
protobuf.workspace = true
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
protobuf-codegen = "3.5.1"
|
|
|
@ -1,19 +0,0 @@
|
||||||
use std::{fs, io};
|
|
||||||
|
|
||||||
pub fn main() -> io::Result<()> {
|
|
||||||
let mut files = Vec::new();
|
|
||||||
|
|
||||||
for file in fs::read_dir("proto/steam")? {
|
|
||||||
let path = file?.path();
|
|
||||||
println!("cargo:rerun-if-changed={}", path.to_str().unwrap());
|
|
||||||
files.push(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
protobuf_codegen::Codegen::new()
|
|
||||||
.includes(["proto/steam", "proto"])
|
|
||||||
.inputs(files)
|
|
||||||
.cargo_out_dir("gen")
|
|
||||||
.run_from_script();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 3873936b20de1221ed7f8de3d2457612b7795804
|
|
|
@ -1,5 +0,0 @@
|
||||||
//! Generated data types used by the Steam API.
|
|
||||||
//! This only includes protobufs, not `*.steamd` files representing raw structs
|
|
||||||
//! and some enums.
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/gen/mod.rs"));
|
|
38
src/tool/main.cpp
Normal file
38
src/tool/main.cpp
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
|
|
||||||
|
namespace vapore {
|
||||||
|
void initCurl() {
|
||||||
|
static std::once_flag globalInit;
|
||||||
|
std::call_once(globalInit, curl_global_init, CURL_GLOBAL_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
int wrappedMain(int argc, char **argv) {
|
||||||
|
initCurl();
|
||||||
|
|
||||||
|
auto curl = std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>(
|
||||||
|
curl_easy_init(), curl_easy_cleanup);
|
||||||
|
|
||||||
|
if (curl.get() == nullptr) {
|
||||||
|
throw std::runtime_error("Could not create curl_easy");
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_URL, "https://example.com");
|
||||||
|
curl_easy_perform(curl.get());
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} // namespace vapore
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
try {
|
||||||
|
return vapore::wrappedMain(argc, argv);
|
||||||
|
} catch (std::exception &ex) {
|
||||||
|
std::cerr << std::string("Got exception: ") << ex.what() << std::endl;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "vapore-struct"
|
|
||||||
edition = "2021"
|
|
||||||
version.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bitflags.workspace = true
|
|
||||||
num_enum.workspace = true
|
|
||||||
|
|
|
@ -1,349 +0,0 @@
|
||||||
//! Structures from SteamKit2 `*.steamd` files.
|
|
||||||
//! Currently these are manually copied, but may be autogenerated in the future
|
|
||||||
|
|
||||||
pub mod eresult {
|
|
||||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
|
||||||
#[repr(u32)]
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
|
||||||
pub enum EResult {
|
|
||||||
Invalid = 0,
|
|
||||||
|
|
||||||
OK = 1,
|
|
||||||
Fail = 2,
|
|
||||||
NoConnection = 3,
|
|
||||||
InvalidPassword = 5,
|
|
||||||
LoggedInElsewhere = 6,
|
|
||||||
InvalidProtocolVer = 7,
|
|
||||||
InvalidParam = 8,
|
|
||||||
FileNotFound = 9,
|
|
||||||
Busy = 10,
|
|
||||||
InvalidState = 11,
|
|
||||||
InvalidName = 12,
|
|
||||||
InvalidEmail = 13,
|
|
||||||
DuplicateName = 14,
|
|
||||||
AccessDenied = 15,
|
|
||||||
Timeout = 16,
|
|
||||||
Banned = 17,
|
|
||||||
AccountNotFound = 18,
|
|
||||||
InvalidSteamID = 19,
|
|
||||||
ServiceUnavailable = 20,
|
|
||||||
NotLoggedOn = 21,
|
|
||||||
Pending = 22,
|
|
||||||
EncryptionFailure = 23,
|
|
||||||
InsufficientPrivilege = 24,
|
|
||||||
LimitExceeded = 25,
|
|
||||||
Revoked = 26,
|
|
||||||
Expired = 27,
|
|
||||||
AlreadyRedeemed = 28,
|
|
||||||
DuplicateRequest = 29,
|
|
||||||
AlreadyOwned = 30,
|
|
||||||
IPNotFound = 31,
|
|
||||||
PersistFailed = 32,
|
|
||||||
LockingFailed = 33,
|
|
||||||
LogonSessionReplaced = 34,
|
|
||||||
ConnectFailed = 35,
|
|
||||||
HandshakeFailed = 36,
|
|
||||||
IOFailure = 37,
|
|
||||||
RemoteDisconnect = 38,
|
|
||||||
ShoppingCartNotFound = 39,
|
|
||||||
Blocked = 40,
|
|
||||||
Ignored = 41,
|
|
||||||
NoMatch = 42,
|
|
||||||
AccountDisabled = 43,
|
|
||||||
ServiceReadOnly = 44,
|
|
||||||
AccountNotFeatured = 45,
|
|
||||||
AdministratorOK = 46,
|
|
||||||
ContentVersion = 47,
|
|
||||||
TryAnotherCM = 48,
|
|
||||||
PasswordRequiredToKickSession = 49,
|
|
||||||
AlreadyLoggedInElsewhere = 50,
|
|
||||||
Suspended = 51,
|
|
||||||
Cancelled = 52,
|
|
||||||
DataCorruption = 53,
|
|
||||||
DiskFull = 54,
|
|
||||||
RemoteCallFailed = 55,
|
|
||||||
PasswordUnset = 56,
|
|
||||||
ExternalAccountUnlinked = 57,
|
|
||||||
PSNTicketInvalid = 58,
|
|
||||||
ExternalAccountAlreadyLinked = 59,
|
|
||||||
RemoteFileConflict = 60,
|
|
||||||
IllegalPassword = 61,
|
|
||||||
SameAsPreviousValue = 62,
|
|
||||||
AccountLogonDenied = 63,
|
|
||||||
CannotUseOldPassword = 64,
|
|
||||||
InvalidLoginAuthCode = 65,
|
|
||||||
AccountLogonDeniedNoMail = 66,
|
|
||||||
HardwareNotCapableOfIPT = 67,
|
|
||||||
IPTInitError = 68,
|
|
||||||
ParentalControlRestricted = 69,
|
|
||||||
FacebookQueryError = 70,
|
|
||||||
ExpiredLoginAuthCode = 71,
|
|
||||||
IPLoginRestrictionFailed = 72,
|
|
||||||
AccountLockedDown = 73,
|
|
||||||
AccountLogonDeniedVerifiedEmailRequired = 74,
|
|
||||||
NoMatchingURL = 75,
|
|
||||||
BadResponse = 76,
|
|
||||||
RequirePasswordReEntry = 77,
|
|
||||||
ValueOutOfRange = 78,
|
|
||||||
UnexpectedError = 79,
|
|
||||||
Disabled = 80,
|
|
||||||
InvalidCEGSubmission = 81,
|
|
||||||
RestrictedDevice = 82,
|
|
||||||
RegionLocked = 83,
|
|
||||||
RateLimitExceeded = 84,
|
|
||||||
AccountLoginDeniedNeedTwoFactor = 85,
|
|
||||||
ItemDeleted = 86,
|
|
||||||
AccountLoginDeniedThrottle = 87,
|
|
||||||
TwoFactorCodeMismatch = 88,
|
|
||||||
TwoFactorActivationCodeMismatch = 89,
|
|
||||||
AccountAssociatedToMultiplePartners = 90,
|
|
||||||
NotModified = 91,
|
|
||||||
NoMobileDevice = 92,
|
|
||||||
TimeNotSynced = 93,
|
|
||||||
SMSCodeFailed = 94,
|
|
||||||
AccountLimitExceeded = 95,
|
|
||||||
AccountActivityLimitExceeded = 96,
|
|
||||||
PhoneActivityLimitExceeded = 97,
|
|
||||||
RefundToWallet = 98,
|
|
||||||
EmailSendFailure = 99,
|
|
||||||
NotSettled = 100,
|
|
||||||
NeedCaptcha = 101,
|
|
||||||
GSLTDenied = 102,
|
|
||||||
GSOwnerDenied = 103,
|
|
||||||
InvalidItemType = 104,
|
|
||||||
IPBanned = 105,
|
|
||||||
GSLTExpired = 106,
|
|
||||||
InsufficientFunds = 107,
|
|
||||||
TooManyPending = 108,
|
|
||||||
NoSiteLicensesFound = 109,
|
|
||||||
WGNetworkSendExceeded = 110,
|
|
||||||
AccountNotFriends = 111,
|
|
||||||
LimitedUserAccount = 112,
|
|
||||||
CantRemoveItem = 113,
|
|
||||||
AccountDeleted = 114,
|
|
||||||
ExistingUserCancelledLicense = 115,
|
|
||||||
CommunityCooldown = 116,
|
|
||||||
NoLauncherSpecified = 117,
|
|
||||||
MustAgreeToSSA = 118,
|
|
||||||
LauncherMigrated = 119,
|
|
||||||
SteamRealmMismatch = 120,
|
|
||||||
InvalidSignature = 121,
|
|
||||||
ParseFailure = 122,
|
|
||||||
NoVerifiedPhone = 123,
|
|
||||||
InsufficientBattery = 124,
|
|
||||||
ChargerRequired = 125,
|
|
||||||
CachedCredentialInvalid = 126,
|
|
||||||
PhoneNumberIsVOIP = 127,
|
|
||||||
NotSupported = 128,
|
|
||||||
FamilySizeLimitExceeded = 129,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod enums {
|
|
||||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
|
||||||
bitflags::bitflags! {
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
|
||||||
#[repr(u32)]
|
|
||||||
pub enum ELicenseType {
|
|
||||||
NoLicense = 0,
|
|
||||||
SinglePurchase = 1,
|
|
||||||
SinglePurchaseLimitedUse = 2,
|
|
||||||
RecurringCharge = 3,
|
|
||||||
RecurringChargeLimitedUse = 4,
|
|
||||||
RecurringChargeLimitedUseWithOverages = 5,
|
|
||||||
RecurringOption = 6,
|
|
||||||
LimitedUseDelayedActivation = 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u32)]
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue