diff --git a/src/cli.rs b/src/cli.rs index 18b4e14..2cbb561 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -112,7 +112,7 @@ impl FilesystemArgs { for_variable = Some(value); } } else { buffer.push('=') }, - ',' => if !in_string && !buffer.is_empty() { + ',' => if !in_string { let value: String = buffer.drain(..).collect(); if let Some(var) = for_variable.take() { let var_trimmed = var.trim(); @@ -142,6 +142,14 @@ pub struct ProxyArgs { /// Proxy offerings from another store #[arg(name = "store", long, default_value_t = {"https://plugins.deckbrew.xyz".into()})] pub proxy_store: String, + /// Proxy artifact (plugin download zips) urls + #[arg(name = "artifacts", short, long)] + pub intercept_artifacts: bool, + /// Proxy preview image urls + #[arg(name = "images", short, long)] + pub intercept_images: bool, + #[arg(name = "domain", default_value_t = {"http://localhost:22252".into()})] + pub domain_root: String, } impl ProxyArgs { @@ -154,22 +162,61 @@ impl ProxyArgs { return Err(format!("Proxy descriptor too short")); } let mut buffer = Vec::new(); + let mut for_variable: Option = None; + let mut in_string = false; let mut escaped = false; + let mut proxy_store = None; + let mut intercept_artifacts = false; + let mut intercept_images = false; + let mut domain = None; for c in chars { match c { - '}' => if escaped { - buffer.push('}') - } else { + '}' if !escaped && !in_string => { return Ok(Self { - proxy_store: if buffer.is_empty() { "https://plugins.deckbrew.xyz".into() } else { buffer.iter().collect() } + proxy_store: if let Some(url) = proxy_store { + url + } else if buffer.is_empty() { + "https://plugins.deckbrew.xyz".into() + } else { + buffer.iter().collect() + }, + intercept_artifacts, + intercept_images, + domain_root: domain.unwrap_or_else(|| "http://localhost:22252".into()), }) } - '\\' => escaped = true, + '"' if !escaped => in_string = !in_string, + '=' if !escaped && !in_string => { + let value: String = buffer.drain(..).collect(); + if for_variable.is_some() { + return Err("Unexpected = in filesystem descriptor".to_owned()); + } else { + for_variable = Some(value); + } + } + ',' if !escaped && !in_string => { + let value: String = buffer.drain(..).collect(); + if let Some(var) = for_variable.take() { + let var_trimmed = var.trim(); + match &var_trimmed as &str { + "intercept" => { + intercept_images = true; + intercept_artifacts = true; + }, + "img" | "images" | "intercept_images" => intercept_images = true, + "dl" | "downloads" | "artifacts" | "intercept_artifacts" => intercept_artifacts = true, + "url" | "store" | "proxy_store" => proxy_store = Some(value), + "d" | "domain" => domain = Some(value), + v => return Err(format!("Unexpected variable name {} in filesystem descriptor", v)), + } + } + }, + '\\' if !escaped => escaped = true, c => { if escaped { escaped = false; - buffer.push('\\'); + //buffer.push('\\'); } buffer.push(c) }, @@ -179,7 +226,13 @@ impl ProxyArgs { } fn to_descriptor(self) -> String { - format!("{{{}}}", self.proxy_store) + match (self.intercept_artifacts, self.intercept_images) { + (true, true) => format!("{{url=\"{}\",intercept,}}", self.proxy_store), + (true, false) => format!("{{url=\"{}\",intercept_artifacts,}}", self.proxy_store), + (false, true) => format!("{{url=\"{}\",intercept_images,}}", self.proxy_store), + (false, false) => format!("{{{}}}", self.proxy_store), + } + } } @@ -256,7 +309,7 @@ mod tests { #[test] fn storage_descriptor() { - let descriptor = "f{root=\"\",domain=\"\",stats=0}"; + let descriptor = "f{root=\"\",domain=\"\",stats=0,}"; let parsed = StorageArgs::from_descriptor(&mut descriptor.chars()); parsed.expect("StorageArgs parse error"); let descriptor = "p{}"; @@ -272,7 +325,7 @@ mod tests { #[test] fn filesys_descriptor() { - let descriptor = "{root='',domain='',stats:0}"; + let descriptor = "{root=\"\",domain=\"\",stats=0,}"; let parsed = FilesystemArgs::from_descriptor(&mut descriptor.chars()); parsed.expect("FilesystemArgs parse error"); } @@ -284,6 +337,13 @@ mod tests { parsed.expect("ProxyArgs parse error"); } + #[test] + fn proxy_descriptor_complex() { + let descriptor = "{url=\"https://plugins.example.com\", intercept, intercept_image,}"; + let parsed = ProxyArgs::from_descriptor(&mut descriptor.chars()); + parsed.expect("ProxyArgs parse error"); + } + #[test] fn merge_descriptor() { let descriptor = "[(f{}),(p{}),( )]"; diff --git a/src/main.rs b/src/main.rs index 245a851..b45aaaa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,9 @@ fn build_storage_box(storage: &cli::StorageArgs) -> Box { )), cli::StorageArgs::Proxy(px) => Box::new(storage::ProxiedStorage::new( px.proxy_store.clone(), + px.intercept_artifacts, + px.intercept_images, + px.domain_root.clone(), )), cli::StorageArgs::Empty => Box::new(storage::EmptyStorage), cli::StorageArgs::Merge(ls) => Box::new(storage::MergedStorage::new( diff --git a/src/storage/proxy.rs b/src/storage/proxy.rs index 026bd90..7291be9 100644 --- a/src/storage/proxy.rs +++ b/src/storage/proxy.rs @@ -2,15 +2,23 @@ use decky_api::{StorePluginList, StorePluginVersion}; use super::IStorage; +const MAX_PROXY_RESPONSE_SIZE: u64 = 10_000_000; + pub struct ProxiedStorage { store_url: String, + intercept_artifacts: bool, + intercept_images: bool, + domain_root: String, agent: ureq::Agent, } impl ProxiedStorage { - pub fn new(target_store: String) -> Self { + pub fn new(target_store: String, intercept_artifacts: bool, intercept_images: bool, domain_root: String) -> Self { Self { store_url: target_store, + intercept_artifacts: intercept_artifacts, + intercept_images: intercept_images, + domain_root: domain_root, agent: ureq::AgentBuilder::new() .tls_connector(std::sync::Arc::new(native_tls::TlsConnector::new().expect("Native TLS init failed"))) .build(), @@ -22,7 +30,31 @@ impl ProxiedStorage { } fn default_artifact_url(ver: &StorePluginVersion) -> String { - format!("https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/{}.zip", ver.hash) + Self::default_artifact_url_by_hash(&ver.hash) + } + + fn default_artifact_url_by_hash(hash: &str) -> String { + format!("https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/{}.zip", hash) + } + + fn proxied_artifact_url(ver: &StorePluginVersion, plugin_name: &str, domain_root: &str) -> String { + // /plugins/{name}/{version}/{hash}.zip + format!("{}/plugins/{}/{}/{}.zip", domain_root, plugin_name, ver.name, ver.hash) + } + + fn image_url_to_name(image_url: &str) -> &str { + image_url + .trim_start_matches("https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/") + .trim_end_matches(".png") + } + + fn default_image_url_by_name(image_name: &str) -> String { + format!("https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/{}.png", image_name) + } + + fn proxied_image_url(image_name: &str, domain_root: &str) -> String { + // /plugins/{name}.png + format!("{}/plugins/{}.png", domain_root, image_name) } fn proxy_plugins(&self, query: &str) -> StorePluginList { @@ -49,23 +81,76 @@ impl IStorage for ProxiedStorage { fn plugins(&self, query: &str) -> StorePluginList { let mut proxy = self.proxy_plugins(query); for plugin in &mut proxy { + if self.intercept_images { + plugin.image_url = Self::proxied_image_url(Self::image_url_to_name(&plugin.image_url), &self.domain_root); + } for version in &mut plugin.versions { - if version.artifact.is_none() { - version.artifact = Some(Self::default_artifact_url(version)); + if self.intercept_artifacts { + version.artifact = Some(Self::proxied_artifact_url(&version, &plugin.name, &self.domain_root)); + } else { + if version.artifact.is_none() { + version.artifact = Some(Self::default_artifact_url(version)); + } } } } proxy } - /*fn get_artifact(&self, name: &str, version: &str, hash: &str) -> Result { - self.fallback.get_artifact(name, version, hash) + fn get_artifact(&self, _name: &str, _version: &str, hash: &str) -> Result { + let url = Self::default_artifact_url_by_hash(hash); + match self.agent.get(&url).call() { + Err(e) => { + log::error!("Plugins proxy error for {}: {}", url, e); + Err(std::io::Error::new(std::io::ErrorKind::ConnectionAborted, e)) + }, + Ok(resp) => { + let len: usize = resp.header("Content-Length") + .unwrap() + .parse() + .unwrap_or(MAX_PROXY_RESPONSE_SIZE as usize / 4); + let mut buffer = Vec::with_capacity(len); + use std::io::Read; + match resp.into_reader() + .take(MAX_PROXY_RESPONSE_SIZE) + .read_to_end(&mut buffer) { + Err(e) => { + log::error!("Plugins json error for {}: {}", url, e); + Err(e) + } + Ok(_) => Ok(buffer.into()), + } + } + } } fn get_image(&self, name: &str) -> Result { - self.fallback.get_image(name) + let url = Self::default_image_url_by_name(name); + match self.agent.get(&url).call() { + Err(e) => { + log::error!("Plugins image proxy error for {}: {}", url, e); + Err(std::io::Error::new(std::io::ErrorKind::ConnectionAborted, e)) + }, + Ok(resp) => { + let len: usize = resp.header("Content-Length") + .unwrap() + .parse() + .unwrap_or(MAX_PROXY_RESPONSE_SIZE as usize / 4); + let mut buffer = Vec::with_capacity(len); + use std::io::Read; + match resp.into_reader() + .take(MAX_PROXY_RESPONSE_SIZE) + .read_to_end(&mut buffer) { + Err(e) => { + log::error!("Plugins json error for {}: {}", url, e); + Err(e) + } + Ok(_) => Ok(buffer.into()), + } + } + } } - + /* fn get_statistics(&self) -> std::collections::HashMap { self.fallback.get_statistics() }*/