use sha1::{Digest, Sha1};
use crate::{error::SteamError, SteamClient};
#[derive(Debug, Clone)]
pub struct ContentServer {
pub server_type: String,
pub source_id: u32,
pub cell_id: u32,
pub load: f32,
pub weighted_load: f32,
pub host: String,
pub vhost: String,
pub https_support: bool,
pub allowed_app_ids: Option<Vec<u32>>,
}
#[derive(Debug, Clone)]
pub struct CdnAuthToken {
pub token: String,
pub expires: u64,
pub hostname: String,
}
#[derive(Debug, Clone)]
pub struct DepotManifest {
pub depot_id: u32,
pub manifest_id: u64,
pub creation_time: u32,
pub total_uncompressed_size: u64,
pub total_compressed_size: u64,
pub unique_chunks: u32,
pub file_count: u32,
pub files: Vec<ManifestFile>,
}
#[derive(Debug, Clone)]
pub struct ManifestFile {
pub filename: String,
pub size: u64,
pub flags: u32,
pub sha_content: Vec<u8>,
pub sha_filename: Vec<u8>,
pub chunks: Vec<FileChunk>,
}
#[derive(Debug, Clone)]
pub struct FileChunk {
pub sha: Vec<u8>,
pub crc: u32,
pub offset: u64,
pub compressed_size: u32,
pub uncompressed_size: u32,
}
#[allow(dead_code)]
pub mod file_flags {
pub const DIRECTORY: u32 = 0x40;
pub const EXECUTABLE: u32 = 0x80;
pub const HIDDEN: u32 = 0x100;
}
impl SteamClient {
pub async fn get_content_servers(&mut self, appid: Option<u32>) -> Result<Vec<ContentServer>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let cell_id = self.account.read().cell_id.unwrap_or(0);
let cell_id_str = cell_id.to_string();
let resp = self.http_client.get_with_query("https://api.steampowered.com/IContentServerDirectoryService/GetServersForSteamPipe/v1/", &[("cell_id", cell_id_str.as_str())]).await?;
let json: serde_json::Value = resp.json()?;
let mut servers = Vec::new();
if let Some(server_list) = json["response"]["servers"].as_array() {
for server in server_list {
if let Some(app) = appid {
if let Some(allowed) = server["allowed_app_ids"].as_array() {
let allowed_ids: Vec<u32> = allowed.iter().filter_map(|v| v.as_u64().map(|n| n as u32)).collect();
if !allowed_ids.is_empty() && !allowed_ids.contains(&app) {
continue;
}
}
}
let server_type = server["type"].as_str().unwrap_or("").to_string();
if server_type != "CDN" && server_type != "SteamCache" {
continue;
}
servers.push(ContentServer {
server_type,
source_id: server["source_id"].as_u64().unwrap_or(0) as u32,
cell_id: server["cell_id"].as_u64().unwrap_or(0) as u32,
load: server["load"].as_f64().unwrap_or(1.0) as f32,
weighted_load: server["weighted_load"].as_f64().unwrap_or(1.0) as f32,
host: server["host"].as_str().unwrap_or("").to_string(),
vhost: server["vhost"].as_str().unwrap_or("").to_string(),
https_support: server["https_support"].as_str() == Some("mandatory"),
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()),
});
}
}
Ok(servers)
}
pub async fn get_depot_decryption_key(&mut self, appid: u32, depotid: u32) -> Result<Vec<u8>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientGetDepotDecryptionKey { depot_id: Some(depotid), app_id: Some(appid) };
self.send_message(steam_enums::EMsg::ClientGetDepotDecryptionKey, &msg).await?;
Ok(Vec::new())
}
pub async fn get_app_beta_decryption_keys(&mut self, appid: u32, password: &str) -> Result<std::collections::HashMap<String, Vec<u8>>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientCheckAppBetaPassword { app_id: Some(appid), betapassword: Some(password.to_string()) };
let resp: steam_protos::CMsgClientCheckAppBetaPasswordResponse = self.send_request_and_wait(steam_enums::EMsg::ClientCheckAppBetaPassword, &msg).await?;
if resp.eresult != Some(1) {
return Err(SteamError::SteamResult(steam_enums::EResult::from_i32(resp.eresult.unwrap_or(2)).unwrap_or(steam_enums::EResult::Fail)));
}
let mut branches = std::collections::HashMap::new();
for beta in resp.betapasswords {
if let (Some(name), Some(key)) = (beta.betaname, beta.betapassword) {
branches.insert(name, key);
}
}
Ok(branches)
}
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> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let branch = branch_name.unwrap_or_else(|| "public".to_string());
let password_hash = branch_password.map(|p| {
let mut hasher = Sha1::new();
hasher.update(p.as_bytes());
hex::encode(hasher.finalize())
});
let req = steam_protos::CContentServerDirectoryGetManifestRequestCodeRequest {
app_id: Some(appid),
depot_id: Some(depotid),
manifest_id: Some(manifest_id),
app_branch: Some(branch),
branch_password_hash: password_hash,
};
let resp: steam_protos::CContentServerDirectoryGetManifestRequestCodeResponse = self.send_unified_request_and_wait("ContentServerDirectory.GetManifestRequestCode#1", &req).await?;
Ok(resp.manifest_request_code.unwrap_or(0))
}
pub async fn get_cdn_auth_token(&mut self, appid: u32, depotid: u32, hostname: &str) -> Result<CdnAuthToken, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientGetCdnAuthToken { depot_id: Some(depotid), host_name: Some(hostname.to_string()), app_id: Some(appid) };
self.send_message(steam_enums::EMsg::ClientGetCDNAuthToken, &msg).await?;
Ok(CdnAuthToken { token: String::new(), expires: 0, hostname: hostname.to_string() })
}
#[allow(clippy::too_many_arguments)]
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> {
let scheme = if server.https_support { "https" } else { "http" };
let host = if !server.vhost.is_empty() { &server.vhost } else { &server.host };
let request_code = self.get_manifest_request_code(appid, depotid, manifest_id, branch_name, branch_password).await?;
let url = format!("{}://{}/depot/{}/manifest/{}/5/{}", scheme, host, depotid, manifest_id, request_code);
let auth_token = self.get_cdn_auth_token(appid, depotid, host).await?;
let resp = self.http_client.get_with_query(&url, &[("t", auth_token.token.as_str())]).await?;
if !resp.is_success() {
return Err(SteamError::Other(format!("Failed to download manifest: {}", resp.status)));
}
Ok(DepotManifest {
depot_id: depotid,
manifest_id,
creation_time: 0,
total_uncompressed_size: 0,
total_compressed_size: 0,
unique_chunks: 0,
file_count: 0,
files: Vec::new(),
})
}
pub async fn download_chunk_raw(&mut self, appid: u32, depotid: u32, chunk: &FileChunk, server: &ContentServer, _depot_key: &[u8]) -> Result<Vec<u8>, SteamError> {
let scheme = if server.https_support { "https" } else { "http" };
let host = if !server.vhost.is_empty() { &server.vhost } else { &server.host };
let chunk_id = hex::encode(&chunk.sha);
let url = format!("{}://{}/depot/{}/chunk/{}", scheme, host, depotid, chunk_id);
let auth_token = self.get_cdn_auth_token(appid, depotid, host).await?;
let resp = self.http_client.get_with_query(&url, &[("t", auth_token.token.as_str())]).await?;
if !resp.is_success() {
return Err(SteamError::Other(format!("Failed to download chunk: {}", resp.status)));
}
Ok(resp.body)
}
}