diff --git a/src/api/icons.rs b/src/api/icons.rs index 5c9ed113..a9fc2f5e 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -496,7 +496,13 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> { use data_url::DataUrl; - let mut icons = icon_result.iconlist.iter().take(5).peekable(); + let fallback_icon = if CONFIG.icon_service_fallback().is_empty() { + None + } else { + Some(Icon::new(0, CONFIG._icon_service_fallback_url().replace("{}", domain))) + }; + let mut icons = icon_result.iconlist.into_iter().take(5).chain(fallback_icon).peekable(); + while let Some(icon) = icons.next() { if icon.href.starts_with("data:image") { let Ok(datauri) = DataUrl::process(&icon.href) else { diff --git a/src/config.rs b/src/config.rs index ae995f69..c1cf849e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -686,6 +686,12 @@ make_config! { /// has been decided on, consider using permanent redirects for cacheability. The legacy codes /// are currently better supported by the Bitwarden clients. icon_redirect_code: u32, true, def, 302; + /// Icon service to use as URL fallback for internal Icon service. Works only when icon_service is set to "internal" + /// Same predefined/custom values as icon_service (except "internal"). Unlike Icon service, the Fallback Icon service + /// does not send any redirect to the client and instead use the internal Icon service to download the icon located at the external service + icon_service_fallback: String, false, def, String::new(); + /// _icon_service_fallback + _icon_service_fallback_url: String, false, generated, |c| generate_icon_service_url(&c.icon_service_fallback); /// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be refreshed icon_cache_ttl: u64, true, def, 2_592_000; /// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again. @@ -1188,6 +1194,31 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { } } + // Check if the Fallback Icon service is valid + let icon_service_fallback = cfg.icon_service_fallback.as_str(); + if !icon_service_fallback.is_empty() { + if icon_service != "internal" { + err!(format!("Fallback Icon service can only be used for \"internal\" Icon service, you are currently using \"{icon_service}\"")) + } + match icon_service_fallback { + "bitwarden" | "duckduckgo" | "google" => (), + _ => { + if !icon_service_fallback.starts_with("http") { + err!(format!("Fallback Icon service URL `{icon_service_fallback}` must start with \"http\"")) + } + match icon_service_fallback.matches("{}").count() { + 1 => (), // nominal + 0 => { + err!(format!("Fallback Icon service URL `{icon_service_fallback}` has no placeholder \"{{}}\"")) + } + _ => { + err!(format!("Fallback Icon service URL `{icon_service_fallback}` has more than one placeholder \"{{}}\"")) + } + } + } + } + } + // Check if the icon redirect code is valid match cfg.icon_redirect_code { 301 | 302 | 307 | 308 => (),