diff --git a/.env.template b/.env.template index 6af6b53b..ca6962b4 100644 --- a/.env.template +++ b/.env.template @@ -129,10 +129,24 @@ ## Number of times to retry the database connection during startup, with 1 second delay between each retry, set to 0 to retry indefinitely # DB_CONNECTION_RETRIES=15 +## Icon service +## The predefined icon services are: internal, bitwarden, duckduckgo, google. +## To specify a custom icon service, set a URL template with exactly one instance of `{}`, +## which is replaced with the domain. For example: `https://icon.example.com/domain/{}`. +## +## `internal` refers to Vaultwarden's built-in icon fetching implementation. +## If an external service is set, an icon request to Vaultwarden will return an HTTP 307 +## redirect to the corresponding icon at the external service. An external service may +## be useful if your Vaultwarden instance has no external network connectivity, or if +## you are concerned that someone may probe your instance to try to detect whether icons +## for certain sites have been cached. +# ICON_SERVICE=internal + ## Disable icon downloading -## Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER, -## but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0, -## otherwise it will delete them and they won't be downloaded again. +## Set to true to disable icon downloading in the internal icon service. +## This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external +## network requests. $ICON_CACHE_TTL must also be set to 0; otherwise, the existing icons +## will be deleted eventually, but won't be downloaded again. # DISABLE_ICON_DOWNLOAD=false ## Icon download timeout diff --git a/src/api/icons.rs b/src/api/icons.rs index 675ba43d..ff71cd57 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -10,7 +10,11 @@ use std::{ use once_cell::sync::Lazy; use regex::Regex; use reqwest::{blocking::Client, blocking::Response, header}; -use rocket::{http::ContentType, response::Content, Route}; +use rocket::{ + http::ContentType, + response::{Content, Redirect}, + Route, +}; use crate::{ error::Error, @@ -19,7 +23,13 @@ use crate::{ }; pub fn routes() -> Vec { - routes![icon] + match CONFIG.icon_service().as_str() { + "internal" => routes![icon_internal], + "bitwarden" => routes![icon_bitwarden], + "duckduckgo" => routes![icon_duckduckgo], + "google" => routes![icon_google], + _ => routes![icon_custom], + } } static CLIENT: Lazy = Lazy::new(|| { @@ -50,8 +60,42 @@ static ICON_SIZE_REGEX: Lazy = Lazy::new(|| Regex::new(r"(?x)(\d+)\D*(\d+ // Special HashMap which holds the user defined Regex to speedup matching the regex. static ICON_BLACKLIST_REGEX: Lazy>> = Lazy::new(|| RwLock::new(HashMap::new())); +fn icon_redirect(domain: &str, template: &str) -> Option { + if !is_valid_domain(domain) { + warn!("Invalid domain: {}", domain); + return None; + } + + if is_domain_blacklisted(domain) { + return None; + } + + let url = template.replace("{}", domain); + Some(Redirect::temporary(url)) +} + +#[get("//icon.png")] +fn icon_custom(domain: String) -> Option { + icon_redirect(&domain, &CONFIG.icon_service()) +} + +#[get("//icon.png")] +fn icon_bitwarden(domain: String) -> Option { + icon_redirect(&domain, "https://icons.bitwarden.net/{}/icon.png") +} + +#[get("//icon.png")] +fn icon_duckduckgo(domain: String) -> Option { + icon_redirect(&domain, "https://icons.duckduckgo.com/ip3/{}.ico") +} + +#[get("//icon.png")] +fn icon_google(domain: String) -> Option { + icon_redirect(&domain, "https://www.google.com/s2/favicons?domain={}&sz=32") +} + #[get("//icon.png")] -fn icon(domain: String) -> Cached>> { +fn icon_internal(domain: String) -> Cached>> { const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png"); if !is_valid_domain(&domain) { diff --git a/src/config.rs b/src/config.rs index 17b39e04..9639b3c4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -406,9 +406,10 @@ make_config! { /// This setting applies globally to all users. incomplete_2fa_time_limit: i64, true, def, 3; - /// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from - /// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0, - /// otherwise it will delete them and they won't be downloaded again. + /// Disable icon downloads |> Set to true to disable icon downloading in the internal icon service. + /// This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external + /// network requests. $ICON_CACHE_TTL must also be set to 0; otherwise, the existing icons + /// will be deleted eventually, but won't be downloaded again. disable_icon_download: bool, true, def, false; /// Allow new signups |> Controls whether new users can register. Users can be invited by the vaultwarden admin even if this is disabled signups_allowed: bool, true, def, true; @@ -449,6 +450,13 @@ make_config! { ip_header: String, true, def, "X-Real-IP".to_string(); /// Internal IP header property, used to avoid recomputing each time _ip_header_enabled: bool, false, gen, |c| &c.ip_header.trim().to_lowercase() != "none"; + /// Icon service |> The predefined icon services are: internal, bitwarden, duckduckgo, google. + /// To specify a custom icon service, set a URL template with exactly one instance of `{}`, + /// which is replaced with the domain. For example: `https://icon.example.com/domain/{}`. + /// `internal` refers to Vaultwarden's built-in icon fetching implementation. If an external + /// service is set, an icon request to Vaultwarden will return an HTTP 307 redirect to the + /// corresponding icon at the external service. + icon_service: String, false, def, "internal".to_string(); /// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded 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. @@ -659,6 +667,22 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } } + // Check if the icon service is valid + let icon_service = cfg.icon_service.as_str(); + match icon_service { + "internal" | "bitwarden" | "duckduckgo" | "google" => (), + _ => { + if !icon_service.starts_with("http") { + err!(format!("Icon service URL `{}` must start with \"http\"", icon_service)) + } + match icon_service.matches("{}").count() { + 1 => (), // nominal + 0 => err!(format!("Icon service URL `{}` has no placeholder \"{{}}\"", icon_service)), + _ => err!(format!("Icon service URL `{}` has more than one placeholder \"{{}}\"", icon_service)), + } + } + } + Ok(()) }