Przeglądaj źródła

Merge pull request #2158 from jjlin/icons

Add support for external icon services
Daniel García 3 lat temu
rodzic
commit
0a5df06e77
3 zmienionych plików z 91 dodań i 9 usunięć
  1. 17 3
      .env.template
  2. 47 3
      src/api/icons.rs
  3. 27 3
      src/config.rs

+ 17 - 3
.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

+ 47 - 3
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<Route> {
-    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<Client> = Lazy::new(|| {
@@ -50,8 +60,42 @@ static ICON_SIZE_REGEX: Lazy<Regex> = 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<RwLock<HashMap<String, Regex>>> = Lazy::new(|| RwLock::new(HashMap::new()));
 
+fn icon_redirect(domain: &str, template: &str) -> Option<Redirect> {
+    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("/<domain>/icon.png")]
+fn icon_custom(domain: String) -> Option<Redirect> {
+    icon_redirect(&domain, &CONFIG.icon_service())
+}
+
+#[get("/<domain>/icon.png")]
+fn icon_bitwarden(domain: String) -> Option<Redirect> {
+    icon_redirect(&domain, "https://icons.bitwarden.net/{}/icon.png")
+}
+
+#[get("/<domain>/icon.png")]
+fn icon_duckduckgo(domain: String) -> Option<Redirect> {
+    icon_redirect(&domain, "https://icons.duckduckgo.com/ip3/{}.ico")
+}
+
+#[get("/<domain>/icon.png")]
+fn icon_google(domain: String) -> Option<Redirect> {
+    icon_redirect(&domain, "https://www.google.com/s2/favicons?domain={}&sz=32")
+}
+
 #[get("/<domain>/icon.png")]
-fn icon(domain: String) -> Cached<Content<Vec<u8>>> {
+fn icon_internal(domain: String) -> Cached<Content<Vec<u8>>> {
     const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
 
     if !is_valid_domain(&domain) {

+ 27 - 3
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(())
 }