commit 0a5df06e77fb2359dbe0496e1e669e6d5b42cde5
parent 840cf8740ad4b88f8d1c224553c9b912abbfd441
Author: Daniel GarcĂa <dani-garcia@users.noreply.github.com>
Date: Wed, 22 Dec 2021 15:46:30 +0100
Merge pull request #2158 from jjlin/icons
Add support for external icon services
Diffstat:
3 files changed, 91 insertions(+), 9 deletions(-)
diff --git 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
@@ -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) {
diff --git 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(())
}