mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2024-11-24 05:45:38 +03:00
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.
This commit is contained in:
parent
840cf8740a
commit
2f9ac61a4e
3 changed files with 91 additions and 9 deletions
|
@ -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
|
## 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
|
# 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
|
## Disable icon downloading
|
||||||
## Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
|
## Set to true to disable icon downloading in the internal icon service.
|
||||||
## but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
## This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external
|
||||||
## otherwise it will delete them and they won't be downloaded again.
|
## 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
|
# DISABLE_ICON_DOWNLOAD=false
|
||||||
|
|
||||||
## Icon download timeout
|
## Icon download timeout
|
||||||
|
|
|
@ -10,7 +10,11 @@ use std::{
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::{blocking::Client, blocking::Response, header};
|
use reqwest::{blocking::Client, blocking::Response, header};
|
||||||
use rocket::{http::ContentType, response::Content, Route};
|
use rocket::{
|
||||||
|
http::ContentType,
|
||||||
|
response::{Content, Redirect},
|
||||||
|
Route,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
|
@ -19,7 +23,13 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
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(|| {
|
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.
|
// 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()));
|
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")]
|
#[get("/<domain>/icon.png")]
|
||||||
fn icon(domain: String) -> Cached<Content<Vec<u8>>> {
|
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_internal(domain: String) -> Cached<Content<Vec<u8>>> {
|
||||||
const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
|
const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
|
||||||
|
|
||||||
if !is_valid_domain(&domain) {
|
if !is_valid_domain(&domain) {
|
||||||
|
|
|
@ -406,9 +406,10 @@ make_config! {
|
||||||
/// This setting applies globally to all users.
|
/// This setting applies globally to all users.
|
||||||
incomplete_2fa_time_limit: i64, true, def, 3;
|
incomplete_2fa_time_limit: i64, true, def, 3;
|
||||||
|
|
||||||
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
|
/// Disable icon downloads |> Set to true to disable icon downloading in the internal icon service.
|
||||||
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
/// This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external
|
||||||
/// otherwise it will delete them and they won't be downloaded again.
|
/// 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;
|
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
|
/// 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;
|
signups_allowed: bool, true, def, true;
|
||||||
|
@ -449,6 +450,13 @@ make_config! {
|
||||||
ip_header: String, true, def, "X-Real-IP".to_string();
|
ip_header: String, true, def, "X-Real-IP".to_string();
|
||||||
/// Internal IP header property, used to avoid recomputing each time
|
/// Internal IP header property, used to avoid recomputing each time
|
||||||
_ip_header_enabled: bool, false, gen, |c| &c.ip_header.trim().to_lowercase() != "none";
|
_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
|
/// 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;
|
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.
|
/// 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue