commit 2f9ac61a4e86c272b8029226ad4b7b3fa7171088
parent 840cf8740ad4b88f8d1c224553c9b912abbfd441
Author: Jeremy Lin <jeremy.lin@gmail.com>
Date: Mon, 20 Dec 2021 01:34:31 -0800
Add support for external icon services
If an external icon service is configured, icon requests return an HTTP
redirect to the corresponding icon at the external service.
An external service may be useful for various reasons, such as if:
* The Vaultwarden instance has no external network connectivity.
* The Vaultwarden instance has trouble handling large bursts of icon requests.
* There are concerns that an attacker may probe the instance to try to detect
whether icons for certain sites have been cached, which would suggest that
the instance contains entries for those sites.
* The external icon service does a better job of providing icons than the
built-in fetcher.
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(())
}