Merge pull request #2158 from jjlin/icons
Add support for external icon services
This commit is contained in:
commit
0a5df06e77
|
@ -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 New Issue