1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
//! App authentication for Steam client.
//!
//! This module provides functionality for creating app tickets and
//! auth session tickets used for game authentication/DRM.
use std::io::Write;
use byteorder::{LittleEndian, WriteBytesExt};
use steamid::SteamID;
use crate::{error::SteamError, SteamClient};
/// An auth session ticket for game authentication.
#[derive(Debug, Clone)]
pub struct AuthSessionTicket {
/// The raw ticket data.
pub ticket: Vec<u8>,
/// Handle for this ticket (used to cancel).
pub handle: u32,
/// App ID this ticket is for.
pub appid: u32,
/// SteamID of the ticket owner (0 for self).
pub steam_id: u64,
/// CRC32 of the ticket.
pub ticket_crc: u32,
/// State of the ticket.
pub estate: u32,
}
/// Result of activating an auth session ticket.
#[derive(Debug, Clone)]
pub struct AuthSessionResult {
/// The SteamID of the ticket owner.
pub steamid: SteamID,
/// Auth session response code.
pub auth_session_response: u32,
}
impl SteamClient {
/// Request an encrypted app ticket for a particular app.
///
/// The app must be set up on the Steam backend for encrypted app tickets.
///
/// # Arguments
/// * `appid` - The Steam AppID of the app you want a ticket for
/// * `user_data` - Optional user data if the app expects it
///
/// # Returns
/// The encrypted app ticket as raw bytes.
pub async fn create_encrypted_app_ticket(&mut self, appid: u32, user_data: Option<&[u8]>) -> Result<Vec<u8>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientRequestEncryptedAppTicket { app_id: Some(appid), userdata: user_data.map(|d| d.to_vec()) };
// Send request and wait for response
let response: steam_protos::CMsgClientEncryptedAppTicketResponse = self.send_request_and_wait(steam_enums::EMsg::ClientRequestEncryptedAppTicket, &msg).await?;
if response.eresult.unwrap_or(1) != 1 {
return Err(SteamError::SteamResult(steam_enums::EResult::from_i32(response.eresult.unwrap_or(2)).unwrap_or(steam_enums::EResult::Fail)));
}
Ok(response.encrypted_ticket.and_then(|t| t.encrypted_ticket).unwrap_or_default())
}
/// Request an app ownership ticket for a particular app.
///
/// # Arguments
/// * `appid` - The Steam AppID of the app you want a ticket for
///
/// # Returns
/// The ownership ticket as raw bytes.
pub async fn get_app_ownership_ticket(&mut self, appid: u32) -> Result<Vec<u8>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientGetAppOwnershipTicket { app_id: Some(appid) };
// Send request and wait for response
let response: steam_protos::CMsgClientGetAppOwnershipTicketResponse = self.send_request_and_wait(steam_enums::EMsg::ClientGetAppOwnershipTicket, &msg).await?;
if response.eresult.unwrap_or(1) != 1 {
return Err(SteamError::SteamResult(steam_enums::EResult::from_i32(response.eresult.unwrap_or(2) as i32).unwrap_or(steam_enums::EResult::Fail)));
}
Ok(response.ticket.unwrap_or_default())
}
/// Create an auth session ticket for game server authentication.
///
/// This ticket can be sent to a game server which will validate it
/// with Steam to verify your identity and ownership.
///
/// # Arguments
/// * `appid` - The Steam AppID of the game
///
/// # Returns
/// An AuthSessionTicket that can be used for authentication.
pub async fn create_auth_session_ticket(&mut self, appid: u32) -> Result<AuthSessionTicket, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
if self.gc_tokens.is_empty() {
return Err(SteamError::Other("No GC tokens available. Wait for connection to establish fully.".to_string()));
}
// Get an ownership ticket first
// Note: In a real implementation this would wait for the ticket response
// For now we'll assume we can get one or fail
// Since we can't properly wait for the ticket in this structure without a large
// refactor, we'll proceed but acknowledge this is incomplete.
// In node-steam-user, this gets a ticket from cache or requests one.
let ownership_ticket = self.get_app_ownership_ticket(appid).await?;
// Consume a GC token
let gc_token = self.gc_tokens.remove(0);
// Construct the session ticket buffer
let mut buffer = Vec::new();
// 1. Length-prefixed GC Token
buffer.write_u32::<LittleEndian>(gc_token.len() as u32)?;
buffer.write_all(&gc_token)?;
// 2. Length-prefixed Session Header (24 bytes)
buffer.write_u32::<LittleEndian>(24)?;
buffer.write_u32::<LittleEndian>(1)?; // unknown 1
buffer.write_u32::<LittleEndian>(2)?; // unknown 2
// Convert IP string to int
let ip_int = if let Some(ip_str) = &self.public_ip {
match ip_str.parse::<std::net::Ipv4Addr>() {
Ok(ip) => u32::from(ip).swap_bytes(), // Network byte order
Err(_) => 0,
}
} else {
0
};
buffer.write_u32::<LittleEndian>(ip_int)?;
buffer.write_u32::<LittleEndian>(0)?; // filler
let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis() as u64;
let connect_time_ms = self.connect_time; // Assuming this is ms
let session_time = (timestamp.saturating_sub(connect_time_ms)) as u32;
buffer.write_u32::<LittleEndian>(session_time)?;
self.connection_count += 1;
buffer.write_u32::<LittleEndian>(self.connection_count)?;
// 3. Length-prefixed Ownership Ticket
buffer.write_u32::<LittleEndian>(ownership_ticket.len() as u32)?;
buffer.write_all(&ownership_ticket)?;
// Calculate CRC32
let mut crc = flate2::Crc::new();
crc.update(&buffer);
let crc32 = crc.sum();
// Create the ticket object
let ticket = AuthSessionTicket {
ticket: buffer,
handle: 0,
appid,
steam_id: 0, // 0 indicates "self" / our own ticket
ticket_crc: crc32,
estate: 0,
};
// Activate the ticket
self.activate_auth_session_tickets(appid, vec![ticket.clone()]).await?;
Ok(ticket)
}
/// Cancel an auth session ticket.
///
/// Call this when you're done using a ticket to free resources.
///
/// # Arguments
/// * `ticket` - The ticket to cancel
pub async fn cancel_auth_session_ticket(&mut self, ticket: AuthSessionTicket) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
// Remove from active tickets
if let Some(pos) = self.active_tickets.iter().position(|t| t.steam_id == ticket.steam_id && t.appid == ticket.appid && t.ticket_crc == ticket.ticket_crc) {
self.active_tickets.remove(pos);
}
// Update the auth list to remove this ticket
self.send_auth_list(Some(ticket.appid)).await
}
/// Activate auth session tickets to validate players.
///
/// This is typically used by game servers to validate connecting players.
///
/// # Arguments
/// * `appid` - The app ID
/// * `tickets` - Tickets to activate
pub async fn activate_auth_session_tickets(&mut self, appid: u32, tickets: Vec<AuthSessionTicket>) -> Result<Vec<AuthSessionResult>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
for mut ticket in tickets {
// Check if already active
if self.active_tickets.iter().any(|t| t.steam_id == ticket.steam_id && t.appid == ticket.appid && t.ticket_crc == ticket.ticket_crc) {
continue;
}
// If we have an active ticket for this user/app, remove it (unless it's our
// own)
if ticket.steam_id != 0 {
if let Some(pos) = self.active_tickets.iter().position(|t| t.steam_id == ticket.steam_id && t.appid == ticket.appid) {
self.active_tickets.remove(pos);
}
}
// Add to active list
ticket.estate = if ticket.steam_id == 0 { 0 } else { 1 };
self.active_tickets.push(ticket);
}
self.send_auth_list(Some(appid)).await?;
// Return empty for now - proper implementation would wait for response
Ok(Vec::new())
}
/// End auth sessions for specific users.
///
/// # Arguments
/// * `appid` - The app ID
/// * `steamids` - SteamIDs of users to end sessions for
pub async fn end_auth_sessions(&mut self, appid: u32, steamids: Vec<SteamID>) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
// Remove from active tickets
let steamids_u64: Vec<u64> = steamids.iter().map(|s| s.steam_id64()).collect();
self.active_tickets.retain(|t| !(t.appid == appid && steamids_u64.contains(&t.steam_id)));
self.send_auth_list(Some(appid)).await
}
/// Send the ClientAuthList message with current active tickets.
async fn send_auth_list(&mut self, force_appid: Option<u32>) -> Result<(), SteamError> {
let mut app_ids: Vec<u32> = self.active_tickets.iter().map(|t| t.appid).collect();
app_ids.sort();
app_ids.dedup();
if let Some(aid) = force_appid {
if !app_ids.contains(&aid) {
app_ids.push(aid);
}
}
let mut msg = steam_protos::CMsgClientAuthList {
tokens_left: Some(self.gc_tokens.len() as u32),
last_request_seq: Some(self.auth_seq_me),
last_request_seq_from_server: Some(self.auth_seq_them),
app_ids: app_ids.clone(),
message_sequence: Some(self.auth_seq_me + 1),
..Default::default()
};
for ticket in &self.active_tickets {
let ticket_msg = steam_protos::CMsgAuthTicket {
gameid: Some(ticket.appid as u64),
ticket: Some(ticket.ticket.clone()),
h_steam_pipe: Some(self.h_steam_pipe),
ticket_crc: Some(ticket.ticket_crc),
steamid: Some(ticket.steam_id),
..Default::default()
};
msg.tickets.push(ticket_msg);
}
self.auth_seq_me += 1;
self.send_message(steam_enums::EMsg::ClientAuthList, &msg).await
}
}