1use std::io::Read;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::Duration;
6
7use reqwest::Url;
8
9use crate::error::SteamError;
10use crate::steam::steam_client::configuration::steam_configuration::HttpClientPurpose;
11use crate::steam::steam_client::steam_client::SteamClient;
12use crate::types::depot_manifest::{ChunkData, DepotManifest};
13
14use super::depot_chunk::DepotChunk;
15use super::server::{ConnectionProtocol, Server};
16
17static REQUEST_TIMEOUT: AtomicU64 = AtomicU64::new(10_000);
19
20static RESPONSE_BODY_TIMEOUT: AtomicU64 = AtomicU64::new(60_000);
22
23const MANIFEST_VERSION: u32 = 5;
25
26#[derive(Clone, Debug)]
28pub struct Client {
29 http_client: reqwest::Client,
30}
31
32impl Client {
33 pub fn new(http_client: reqwest::Client) -> Self {
35 Client { http_client }
36 }
37
38 pub fn from_steam_client(steam_client: &SteamClient) -> Self {
41 let factory = steam_client.configuration().http_client_factory().clone();
42
43 Client::new(factory(HttpClientPurpose::CDN))
44 }
45
46 pub fn with_default_client() -> Result<Self, SteamError> {
48 let http_client = reqwest::Client::builder()
49 .build()
50 .map_err(|error| SteamError::Connection(error.to_string()))?;
51
52 Ok(Client::new(http_client))
53 }
54
55 pub fn request_timeout() -> Duration {
57 Duration::from_millis(REQUEST_TIMEOUT.load(Ordering::Relaxed))
58 }
59
60 pub fn set_request_timeout(value: Duration) {
62 REQUEST_TIMEOUT.store(value.as_millis() as u64, Ordering::Relaxed);
63 }
64
65 pub fn response_body_timeout() -> Duration {
67 Duration::from_millis(RESPONSE_BODY_TIMEOUT.load(Ordering::Relaxed))
68 }
69
70 pub fn set_response_body_timeout(value: Duration) {
72 RESPONSE_BODY_TIMEOUT.store(value.as_millis() as u64, Ordering::Relaxed);
73 }
74
75 pub async fn download_manifest_async(
90 &self,
91 depot_id: u32,
92 manifest_id: u64,
93 manifest_request_code: u64,
94 server: &Server,
95 depot_key: Option<&[u8]>,
96 proxy_server: Option<&Server>,
97 cdn_auth_token: Option<&str>,
98 ) -> Result<DepotManifest, SteamError> {
99 let url = if manifest_request_code > 0 {
100 format!(
101 "depot/{depot_id}/manifest/{manifest_id}/{MANIFEST_VERSION}/{manifest_request_code}"
102 )
103 } else {
104 format!("depot/{depot_id}/manifest/{manifest_id}/{MANIFEST_VERSION}")
105 };
106
107 let built_url = Self::build_command(server, &url, cdn_auth_token, proxy_server)?;
108
109 let response = self
110 .send_request(self.http_client.get(built_url))
111 .await
112 .map_err(|error| {
113 tracing::debug!("Failed to download manifest: {error}");
114 error
115 })?;
116
117 let body = Self::read_body(response).await?;
118
119 let manifest_data = extract_first_zip_entry(&body)?;
121
122 let mut depot_manifest = DepotManifest::deserialize(&manifest_data)?;
123
124 if let Some(depot_key) = depot_key {
125 depot_manifest.decrypt_filenames(depot_key);
127 }
128
129 Ok(depot_manifest)
130 }
131
132 pub async fn download_depot_chunk_async(
154 &self,
155 depot_id: u32,
156 chunk: &ChunkData,
157 server: &Server,
158 destination: &mut [u8],
159 depot_key: Option<&[u8]>,
160 proxy_server: Option<&Server>,
161 cdn_auth_token: Option<&str>,
162 ) -> Result<usize, SteamError> {
163 let chunk_id = chunk.chunk_id.as_ref().ok_or_else(|| {
164 SteamError::InvalidArgument("Chunk must have a ChunkID.".to_string())
165 })?;
166
167 match depot_key {
168 None => {
169 if destination.len() < chunk.compressed_length as usize {
170 return Err(SteamError::InvalidArgument(format!(
171 "The destination buffer must be longer than the chunk CompressedLength ({}) (since no depot key was provided).",
172 chunk.compressed_length
173 )));
174 }
175 }
176 Some(_) => {
177 if destination.len() < chunk.uncompressed_length as usize {
178 return Err(SteamError::InvalidArgument(format!(
179 "The destination buffer must be longer than the chunk UncompressedLength ({}).",
180 chunk.uncompressed_length
181 )));
182 }
183 }
184 }
185
186 let chunk_id = hex::encode(chunk_id);
187 let url = format!("depot/{depot_id}/chunk/{chunk_id}");
188
189 let request = if Self::use_lancache_server() {
190 Self::build_lancache_request(&self.http_client, server, &url, cdn_auth_token)?
191 } else {
192 let built_url = Self::build_command(server, &url, cdn_auth_token, proxy_server)?;
193 self.http_client.get(built_url)
194 };
195
196 let response = self.send_request(request).await.map_err(|error| {
197 tracing::debug!("Failed to download a depot chunk: {error}");
198 error
199 })?;
200
201 let mut content_length = chunk.compressed_length as usize;
202
203 if let Some(length) = response.content_length() {
204 content_length = length as usize;
205
206 if chunk.compressed_length > 0 && content_length != chunk.compressed_length as usize {
208 return Err(SteamError::Malformed(format!(
209 "Content-Length mismatch for depot chunk! (was {content_length}, but should be {})",
210 chunk.compressed_length
211 )));
212 }
213 } else if content_length > 0 {
214 tracing::debug!(
215 "Response does not have Content-Length, falling back to chunk.CompressedLength."
216 );
217 } else {
218 return Err(SteamError::Other(
219 "Response does not have Content-Length and chunk.CompressedLength is not set."
220 .to_string(),
221 ));
222 }
223
224 let body = Self::read_body(response).await?;
225
226 if body.len() != content_length {
227 return Err(SteamError::Malformed(format!(
228 "Length mismatch after downloading depot chunk! (was {}, but should be {content_length})",
229 body.len()
230 )));
231 }
232
233 match depot_key {
234 None => {
236 destination[..content_length].copy_from_slice(&body[..content_length]);
237
238 Ok(content_length)
239 }
240 Some(depot_key) => DepotChunk::process(chunk, &body[..content_length], destination, depot_key),
242 }
243 }
244
245 pub fn build_command(
253 server: &Server,
254 command: &str,
255 query: Option<&str>,
256 proxy_server: Option<&Server>,
257 ) -> Result<Url, SteamError> {
258 let mut scheme = match server.protocol {
259 ConnectionProtocol::Http => "http",
260 ConnectionProtocol::Https => "https",
261 };
262 let mut host = server.v_host.as_deref().ok_or_else(|| {
263 SteamError::InvalidArgument("The server does not have a virtual host.".to_string())
264 })?;
265 let mut port = server.port;
266 let mut path = format!("/{}", command.trim_start_matches('/'));
267
268 if let Some(proxy_server) = proxy_server {
269 if proxy_server.use_as_proxy {
270 if let Some(path_template) = proxy_server.proxy_request_path_template.as_deref() {
271 path = path_template.replace("%host%", host).replace("%path%", &path);
272 scheme = match proxy_server.protocol {
273 ConnectionProtocol::Http => "http",
274 ConnectionProtocol::Https => "https",
275 };
276 host = proxy_server.v_host.as_deref().ok_or_else(|| {
277 SteamError::InvalidArgument(
278 "The proxy server does not have a virtual host.".to_string(),
279 )
280 })?;
281 port = proxy_server.port;
282 }
283 }
284 }
285
286 if !path.starts_with('/') {
287 path.insert(0, '/');
288 }
289
290 let mut url = Url::parse(&format!("{scheme}://{host}:{port}{path}"))
291 .map_err(|error| SteamError::InvalidArgument(format!("Invalid CDN url: {error}")))?;
292
293 if let Some(query) = query {
294 if !query.is_empty() {
295 url.set_query(Some(query));
296 }
297 }
298
299 Ok(url)
300 }
301
302 async fn send_request(
304 &self,
305 request: reqwest::RequestBuilder,
306 ) -> Result<reqwest::Response, SteamError> {
307 let timeout = Self::request_timeout();
308
309 match tokio::time::timeout(timeout, request.send()).await {
310 Ok(result) => {
311 let response = result.map_err(|error| map_reqwest_error(error, timeout))?;
312
313 if !response.status().is_success() {
314 return Err(SteamError::Other(format!(
315 "Response status code does not indicate success: {}",
316 response.status()
317 )));
318 }
319
320 Ok(response)
321 }
322 Err(_) => Err(SteamError::Timeout(timeout)),
323 }
324 }
325
326 async fn read_body(response: reqwest::Response) -> Result<bytes::Bytes, SteamError> {
328 let timeout = Self::response_body_timeout();
329
330 match tokio::time::timeout(timeout, response.bytes()).await {
331 Ok(result) => result.map_err(|error| map_reqwest_error(error, timeout)),
332 Err(_) => Err(SteamError::Timeout(timeout)),
333 }
334 }
335}
336
337fn map_reqwest_error(error: reqwest::Error, timeout: Duration) -> SteamError {
339 if error.is_timeout() {
340 SteamError::Timeout(timeout)
341 } else if error.is_connect() {
342 SteamError::Connection(error.to_string())
343 } else {
344 SteamError::Other(error.to_string())
345 }
346}
347
348fn extract_first_zip_entry(data: &[u8]) -> Result<Vec<u8>, SteamError> {
351 const CENTRAL_FILE_HEADER: u32 = 0x0201_4b50;
352 const LOCAL_FILE_HEADER: u32 = 0x0403_4b50;
353
354 let eocd = find_end_of_central_directory(data)?;
355
356 let entry_count = read_u16(data, eocd + 10)? as usize;
357 let central_offset = read_u32(data, eocd + 16)? as usize;
358
359 if entry_count == 0 {
360 return Err(SteamError::Malformed(
361 "Expected the zip to contain only one file".to_string(),
362 ));
363 }
364
365 if read_u32(data, central_offset)? != CENTRAL_FILE_HEADER {
366 return Err(SteamError::Malformed(
367 "Invalid zip central directory header".to_string(),
368 ));
369 }
370
371 let method = read_u16(data, central_offset + 10)?;
372 let compressed_size = read_u32(data, central_offset + 20)? as usize;
373 let uncompressed_size = read_u32(data, central_offset + 24)? as usize;
374 let local_offset = read_u32(data, central_offset + 42)? as usize;
375
376 if read_u32(data, local_offset)? != LOCAL_FILE_HEADER {
377 return Err(SteamError::Malformed(
378 "Invalid zip local file header".to_string(),
379 ));
380 }
381
382 let local_name_length = read_u16(data, local_offset + 26)? as usize;
383 let local_extra_length = read_u16(data, local_offset + 28)? as usize;
384
385 let data_start = local_offset + 30 + local_name_length + local_extra_length;
386 let data_end = if compressed_size == 0 {
387 data.len()
388 } else {
389 data_start + compressed_size
390 };
391
392 if data_end > data.len() {
393 return Err(SteamError::Malformed(
394 "Truncated zip archive".to_string(),
395 ));
396 }
397
398 let compressed = &data[data_start..data_end];
399
400 match method {
401 0 => Ok(compressed.to_vec()),
402 8 => {
403 let mut output = Vec::with_capacity(uncompressed_size);
404 let mut decoder = flate2::read::DeflateDecoder::new(compressed);
405
406 decoder
407 .read_to_end(&mut output)
408 .map_err(|error| SteamError::Decompression(format!("Failed to inflate zip entry: {error}")))?;
409
410 Ok(output)
411 }
412 other => Err(SteamError::Decompression(format!(
413 "Unsupported zip compression method {other}"
414 ))),
415 }
416}
417
418fn find_end_of_central_directory(data: &[u8]) -> Result<usize, SteamError> {
420 const END_OF_CENTRAL_DIRECTORY: u32 = 0x0605_4b50;
421
422 if data.len() < 22 {
423 return Err(SteamError::Malformed(
424 "The zip archive is too small".to_string(),
425 ));
426 }
427
428 let start = data.len().saturating_sub(22 + 0xFFFF);
429 let mut index = data.len() - 22;
430
431 loop {
432 if read_u32(data, index)? == END_OF_CENTRAL_DIRECTORY {
433 return Ok(index);
434 }
435
436 if index == start {
437 break;
438 }
439
440 index -= 1;
441 }
442
443 Err(SteamError::Malformed(
444 "Zip end of central directory record not found".to_string(),
445 ))
446}
447
448fn read_u16(data: &[u8], offset: usize) -> Result<u16, SteamError> {
450 let bytes = data
451 .get(offset..offset + 2)
452 .ok_or_else(|| SteamError::Malformed("Truncated zip archive".to_string()))?;
453
454 Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
455}
456
457fn read_u32(data: &[u8], offset: usize) -> Result<u32, SteamError> {
459 let bytes = data
460 .get(offset..offset + 4)
461 .ok_or_else(|| SteamError::Malformed("Truncated zip archive".to_string()))?;
462
463 Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
464}