vapore/lib/examples/login_qr.rs

192 lines
6.8 KiB
Rust

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(())
}