1use sha1::{Digest, Sha1};
7
8use crate::{error::SteamError, SteamClient};
9
10#[derive(Debug, Clone)]
12pub struct ContentServer {
13 pub server_type: String,
15 pub source_id: u32,
17 pub cell_id: u32,
19 pub load: f32,
21 pub weighted_load: f32,
23 pub host: String,
25 pub vhost: String,
27 pub https_support: bool,
29 pub allowed_app_ids: Option<Vec<u32>>,
31}
32
33#[derive(Debug, Clone)]
35pub struct CdnAuthToken {
36 pub token: String,
38 pub expires: u64,
40 pub hostname: String,
42}
43
44#[derive(Debug, Clone)]
46pub struct DepotManifest {
47 pub depot_id: u32,
49 pub manifest_id: u64,
51 pub creation_time: u32,
53 pub total_uncompressed_size: u64,
55 pub total_compressed_size: u64,
57 pub unique_chunks: u32,
59 pub file_count: u32,
61 pub files: Vec<ManifestFile>,
63}
64
65#[derive(Debug, Clone)]
67pub struct ManifestFile {
68 pub filename: String,
70 pub size: u64,
72 pub flags: u32,
74 pub sha_content: Vec<u8>,
76 pub sha_filename: Vec<u8>,
78 pub chunks: Vec<FileChunk>,
80}
81
82#[derive(Debug, Clone)]
84pub struct FileChunk {
85 pub sha: Vec<u8>,
87 pub crc: u32,
89 pub offset: u64,
91 pub compressed_size: u32,
93 pub uncompressed_size: u32,
95}
96
97#[allow(dead_code)]
99pub mod file_flags {
100 pub const DIRECTORY: u32 = 0x40;
102 pub const EXECUTABLE: u32 = 0x80;
104 pub const HIDDEN: u32 = 0x100;
106}
107
108impl SteamClient {
109 pub async fn get_content_servers(&mut self, appid: Option<u32>) -> Result<Vec<ContentServer>, SteamError> {
114 if !self.is_logged_in() {
115 return Err(SteamError::NotLoggedOn);
116 }
117
118 let cell_id = self.account.read().cell_id.unwrap_or(0);
120 let cell_id_str = cell_id.to_string();
121
122 let resp = self.http_client.get_with_query("https://api.steampowered.com/IContentServerDirectoryService/GetServersForSteamPipe/v1/", &[("cell_id", cell_id_str.as_str())]).await?;
123
124 let json: serde_json::Value = resp.json()?;
125
126 let mut servers = Vec::new();
127
128 if let Some(server_list) = json["response"]["servers"].as_array() {
129 for server in server_list {
130 if let Some(app) = appid {
132 if let Some(allowed) = server["allowed_app_ids"].as_array() {
133 let allowed_ids: Vec<u32> = allowed.iter().filter_map(|v| v.as_u64().map(|n| n as u32)).collect();
134 if !allowed_ids.is_empty() && !allowed_ids.contains(&app) {
135 continue;
136 }
137 }
138 }
139
140 let server_type = server["type"].as_str().unwrap_or("").to_string();
141 if server_type != "CDN" && server_type != "SteamCache" {
142 continue;
143 }
144
145 servers.push(ContentServer {
146 server_type,
147 source_id: server["source_id"].as_u64().unwrap_or(0) as u32,
148 cell_id: server["cell_id"].as_u64().unwrap_or(0) as u32,
149 load: server["load"].as_f64().unwrap_or(1.0) as f32,
150 weighted_load: server["weighted_load"].as_f64().unwrap_or(1.0) as f32,
151 host: server["host"].as_str().unwrap_or("").to_string(),
152 vhost: server["vhost"].as_str().unwrap_or("").to_string(),
153 https_support: server["https_support"].as_str() == Some("mandatory"),
154 allowed_app_ids: server["allowed_app_ids"].as_array().map(|arr| arr.iter().filter_map(|v| v.as_u64().map(|n| n as u32)).collect()),
155 });
156 }
157 }
158
159 Ok(servers)
160 }
161
162 pub async fn get_depot_decryption_key(&mut self, appid: u32, depotid: u32) -> Result<Vec<u8>, SteamError> {
171 if !self.is_logged_in() {
172 return Err(SteamError::NotLoggedOn);
173 }
174
175 let msg = steam_protos::CMsgClientGetDepotDecryptionKey { depot_id: Some(depotid), app_id: Some(appid) };
176
177 self.send_message(steam_enums::EMsg::ClientGetDepotDecryptionKey, &msg).await?;
178
179 Ok(Vec::new())
181 }
182
183 pub async fn get_app_beta_decryption_keys(&mut self, appid: u32, password: &str) -> Result<std::collections::HashMap<String, Vec<u8>>, SteamError> {
190 if !self.is_logged_in() {
191 return Err(SteamError::NotLoggedOn);
192 }
193
194 let msg = steam_protos::CMsgClientCheckAppBetaPassword { app_id: Some(appid), betapassword: Some(password.to_string()) };
195
196 let resp: steam_protos::CMsgClientCheckAppBetaPasswordResponse = self.send_request_and_wait(steam_enums::EMsg::ClientCheckAppBetaPassword, &msg).await?;
197
198 if resp.eresult != Some(1) {
199 return Err(SteamError::SteamResult(steam_enums::EResult::from_i32(resp.eresult.unwrap_or(2)).unwrap_or(steam_enums::EResult::Fail)));
200 }
201
202 let mut branches = std::collections::HashMap::new();
203 for beta in resp.betapasswords {
204 if let (Some(name), Some(key)) = (beta.betaname, beta.betapassword) {
205 branches.insert(name, key);
206 }
207 }
208
209 Ok(branches)
210 }
211
212 pub async fn get_manifest_request_code(&mut self, appid: u32, depotid: u32, manifest_id: u64, branch_name: Option<String>, branch_password: Option<String>) -> Result<u64, SteamError> {
221 if !self.is_logged_in() {
222 return Err(SteamError::NotLoggedOn);
223 }
224
225 let branch = branch_name.unwrap_or_else(|| "public".to_string());
226 let password_hash = branch_password.map(|p| {
227 let mut hasher = Sha1::new();
228 hasher.update(p.as_bytes());
229 hex::encode(hasher.finalize())
230 });
231
232 let req = steam_protos::CContentServerDirectoryGetManifestRequestCodeRequest {
233 app_id: Some(appid),
234 depot_id: Some(depotid),
235 manifest_id: Some(manifest_id),
236 app_branch: Some(branch),
237 branch_password_hash: password_hash,
238 };
239
240 let resp: steam_protos::CContentServerDirectoryGetManifestRequestCodeResponse = self.send_unified_request_and_wait("ContentServerDirectory.GetManifestRequestCode#1", &req).await?;
241
242 Ok(resp.manifest_request_code.unwrap_or(0))
243 }
244
245 pub async fn get_cdn_auth_token(&mut self, appid: u32, depotid: u32, hostname: &str) -> Result<CdnAuthToken, SteamError> {
252 if !self.is_logged_in() {
253 return Err(SteamError::NotLoggedOn);
254 }
255
256 let msg = steam_protos::CMsgClientGetCdnAuthToken { depot_id: Some(depotid), host_name: Some(hostname.to_string()), app_id: Some(appid) };
257
258 self.send_message(steam_enums::EMsg::ClientGetCDNAuthToken, &msg).await?;
259
260 Ok(CdnAuthToken { token: String::new(), expires: 0, hostname: hostname.to_string() })
262 }
263
264 #[allow(clippy::too_many_arguments)]
275 pub async fn get_manifest(&mut self, appid: u32, depotid: u32, manifest_id: u64, branch_name: Option<String>, branch_password: Option<String>, server: &ContentServer, _depot_key: &[u8]) -> Result<DepotManifest, SteamError> {
276 let scheme = if server.https_support { "https" } else { "http" };
278 let host = if !server.vhost.is_empty() { &server.vhost } else { &server.host };
279
280 let request_code = self.get_manifest_request_code(appid, depotid, manifest_id, branch_name, branch_password).await?;
282
283 let url = format!("{}://{}/depot/{}/manifest/{}/5/{}", scheme, host, depotid, manifest_id, request_code);
284
285 let auth_token = self.get_cdn_auth_token(appid, depotid, host).await?;
287
288 let resp = self.http_client.get_with_query(&url, &[("t", auth_token.token.as_str())]).await?;
290
291 if !resp.is_success() {
292 return Err(SteamError::Other(format!("Failed to download manifest: {}", resp.status)));
293 }
294
295 Ok(DepotManifest {
298 depot_id: depotid,
299 manifest_id,
300 creation_time: 0,
301 total_uncompressed_size: 0,
302 total_compressed_size: 0,
303 unique_chunks: 0,
304 file_count: 0,
305 files: Vec::new(),
306 })
307 }
308
309 pub async fn download_chunk_raw(&mut self, appid: u32, depotid: u32, chunk: &FileChunk, server: &ContentServer, _depot_key: &[u8]) -> Result<Vec<u8>, SteamError> {
326 let scheme = if server.https_support { "https" } else { "http" };
327 let host = if !server.vhost.is_empty() { &server.vhost } else { &server.host };
328 let chunk_id = hex::encode(&chunk.sha);
329 let url = format!("{}://{}/depot/{}/chunk/{}", scheme, host, depotid, chunk_id);
330
331 let auth_token = self.get_cdn_auth_token(appid, depotid, host).await?;
333
334 let resp = self.http_client.get_with_query(&url, &[("t", auth_token.token.as_str())]).await?;
335
336 if !resp.is_success() {
337 return Err(SteamError::Other(format!("Failed to download chunk: {}", resp.status)));
338 }
339
340 Ok(resp.body)
341 }
342}