From 80d3c61cc2b7098a3dd7750a4ab68590d7510dcf Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 08:50:45 +0200 Subject: [PATCH 01/40] add configuration support for multiple domains --- src/config.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 11 deletions(-) diff --git a/src/config.rs b/src/config.rs index 489a229d6a..e7cca8acfd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ -use std::env::consts::EXE_SUFFIX; +use std::{env::consts::EXE_SUFFIX, collections::HashMap}; use std::process::exit; use std::sync::RwLock; +use std::sync::OnceLock; use job_scheduler_ng::Schedule; use once_cell::sync::Lazy; @@ -47,6 +48,10 @@ macro_rules! make_config { _usr: ConfigBuilder, _overrides: Vec, + + domain_hostmap: OnceLock, + domain_origins: OnceLock, + domain_paths: OnceLock, } #[derive(Clone, Default, Deserialize, Serialize)] @@ -135,13 +140,20 @@ macro_rules! make_config { fn build(&self) -> ConfigItems { let mut config = ConfigItems::default(); - let _domain_set = self.domain.is_some(); + let _domain_set = self.domain_change_back.is_some(); $($( config.$name = make_config!{ @build self.$name.clone(), &config, $none_action, $($default)? }; )+)+ config.domain_set = _domain_set; - config.domain = config.domain.trim_end_matches('/').to_string(); + config.domain_change_back = config.domain_change_back.split(',').map(|d| d.trim_end_matches('/')).fold(String::new(), |acc, d| { + acc.push_str(d); + acc.push(','); + acc + }); + + // Remove trailing comma + config.domain_change_back.pop(); config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase(); config.org_creation_users = config.org_creation_users.trim().to_lowercase(); @@ -335,6 +347,8 @@ macro_rules! make_config { } +type HostHashMap = HashMap; + //STRUCTURE: // /// Short description (without this they won't appear on the list) // group { @@ -414,15 +428,15 @@ make_config! { /// General settings settings { - /// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://' - /// and port, if it's different than the default. Some server functions don't work correctly without this value - domain: String, true, def, "http://localhost".to_string(); + /// Comma seperated list of Domain URLs |> This needs to be set to the URL used to access the server, including + /// 'http[s]://' and port, if it's different than the default. Some server functions don't work correctly without this value + // TODO: Change back, this is only done to break existing references + domain_change_back: String, true, def, "http://localhost".to_string(); /// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used. domain_set: bool, false, def, false; - /// Domain origin |> Domain URL origin (in https://example.com:8443/path, https://example.com:8443 is the origin) - domain_origin: String, false, auto, |c| extract_url_origin(&c.domain); /// Domain path |> Domain URL path (in https://example.com:8443/path, /path is the path) - domain_path: String, false, auto, |c| extract_url_path(&c.domain); + /// MUST be the same for all domains. + domain_path: String, false, auto, |c| extract_url_path(&c.domain_change_back.split(',').nth(0).expect("Missing domain")); /// Enable web vault web_vault_enabled: bool, false, def, true; @@ -667,7 +681,7 @@ make_config! { /// Embed images as email attachments. smtp_embed_images: bool, true, def, true; /// _smtp_img_src - _smtp_img_src: String, false, gen, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain); + _smtp_img_src: String, false, gen, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain_change_back); /// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting! smtp_debug: bool, false, def, false; /// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks! @@ -1010,10 +1024,33 @@ fn extract_url_path(url: &str) -> String { } } -fn generate_smtp_img_src(embed_images: bool, domain: &str) -> String { +/// Extracts host part from a URL. +pub fn extract_url_host(url: &str) -> String { + match Url::parse(url) { + Ok(u) => { + let Some(mut host) = u.host_str().map(|s| s.to_string()) else { + println!("Domain does not contain host!"); + return String::new(); + }; + + if let Some(port) = u.port().map(|p| p.to_string()) { + host.push_str(&port); + } + + host + } + Err(_) => { + // we already print it in the method above, no need to do it again + String::new() + } + } +} + +fn generate_smtp_img_src(embed_images: bool, domains: &str) -> String { if embed_images { "cid:".to_string() } else { + let domain = domains.split(',').nth(0).expect("Domain missing"); format!("{domain}/vw_static/") } } @@ -1082,6 +1119,9 @@ impl Config { _env, _usr, _overrides, + domain_origins: OnceLock::new(), + domain_paths: OnceLock::new(), + domain_hostmap: OnceLock::new(), }), }) } @@ -1249,6 +1289,36 @@ impl Config { } } } + + pub fn domain_origin(&self, host: &str) -> Option { + // This is done to prevent deadlock, when read-locking an rwlock twice + let domains = self.domain_change_back(); + + self.inner.read().unwrap().domain_origins.get_or_init(|| { + domains.split(',') + .map(|d| { + (extract_url_host(d), extract_url_origin(d)) + }) + .collect() + }).get(host).map(|h| h.clone()) + } + + pub fn host_to_domain(&self, host: &str) -> Option { + // This is done to prevent deadlock, when read-locking an rwlock twice + let domains = self.domain_change_back(); + + self.inner.read().unwrap().domain_hostmap.get_or_init(|| { + domains.split(',') + .map(|d| { + (extract_url_host(d), extract_url_path(d)) + }) + .collect() + }).get(host).map(|h| h.clone()) + } + + pub fn main_domain(&self) -> String { + self.domain_change_back().split(',').nth(0).expect("Missing domain").to_string() + } } use handlebars::{ From 40edfa59900a828de65ec1702dbbc10bedfcc086 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 09:23:48 +0200 Subject: [PATCH 02/40] implement mutli domain support for auth headers --- src/auth.rs | 68 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 7eabbc1eb6..637dec8a52 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -360,20 +360,38 @@ use crate::db::{ DbConn, }; -pub struct Host { - pub host: String, +pub struct Domain { + pub domain: String, } #[rocket::async_trait] -impl<'r> FromRequest<'r> for Host { +impl<'r> FromRequest<'r> for Domain { type Error = &'static str; async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = request.headers(); // Get host - let host = if CONFIG.domain_set() { - CONFIG.domain() + // TODO: UPDATE THIS SECTION + let domain = if CONFIG.domain_set() { + let host = if let Some(host) = headers.get_one("X-Forwarded-Host") { + host + } else if let Some(host) = headers.get_one("Host") { + host + } else { + // TODO fix error handling + // This is probably a 400 bad request, + // because http requests require the host header + todo!() + }; + + let Some(domain) = CONFIG.host_to_domain(host) else { + // TODO fix error handling + // This is probably a 421 misdirected request. + todo!() + }; + + domain } else if let Some(referer) = headers.get_one("Referer") { referer.to_string() } else { @@ -399,14 +417,14 @@ impl<'r> FromRequest<'r> for Host { format!("{protocol}://{host}") }; - Outcome::Success(Host { - host, + Outcome::Success(Domain { + domain, }) } } pub struct ClientHeaders { - pub host: String, + pub domain: String, pub device_type: i32, pub ip: ClientIp, } @@ -416,7 +434,7 @@ impl<'r> FromRequest<'r> for ClientHeaders { type Error = &'static str; async fn from_request(request: &'r Request<'_>) -> Outcome { - let host = try_outcome!(Host::from_request(request).await).host; + let domain = try_outcome!(Domain::from_request(request).await).domain; let ip = match ClientIp::from_request(request).await { Outcome::Success(ip) => ip, _ => err_handler!("Error getting Client IP"), @@ -426,7 +444,7 @@ impl<'r> FromRequest<'r> for ClientHeaders { request.headers().get_one("device-type").map(|d| d.parse().unwrap_or(14)).unwrap_or_else(|| 14); Outcome::Success(ClientHeaders { - host, + domain, device_type, ip, }) @@ -434,7 +452,7 @@ impl<'r> FromRequest<'r> for ClientHeaders { } pub struct Headers { - pub host: String, + pub domain: String, pub device: Device, pub user: User, pub ip: ClientIp, @@ -447,7 +465,7 @@ impl<'r> FromRequest<'r> for Headers { async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = request.headers(); - let host = try_outcome!(Host::from_request(request).await).host; + let domain = try_outcome!(Domain::from_request(request).await).domain; let ip = match ClientIp::from_request(request).await { Outcome::Success(ip) => ip, _ => err_handler!("Error getting Client IP"), @@ -518,7 +536,7 @@ impl<'r> FromRequest<'r> for Headers { } Outcome::Success(Headers { - host, + domain, device, user, ip, @@ -527,7 +545,7 @@ impl<'r> FromRequest<'r> for Headers { } pub struct OrgHeaders { - pub host: String, + pub domain: String, pub device: Device, pub user: User, pub org_user_type: UserOrgType, @@ -583,7 +601,7 @@ impl<'r> FromRequest<'r> for OrgHeaders { }; Outcome::Success(Self { - host: headers.host, + domain: headers.domain, device: headers.device, user, org_user_type: { @@ -605,7 +623,7 @@ impl<'r> FromRequest<'r> for OrgHeaders { } pub struct AdminHeaders { - pub host: String, + pub domain: String, pub device: Device, pub user: User, pub org_user_type: UserOrgType, @@ -622,7 +640,7 @@ impl<'r> FromRequest<'r> for AdminHeaders { let client_version = request.headers().get_one("Bitwarden-Client-Version").map(String::from); if headers.org_user_type >= UserOrgType::Admin { Outcome::Success(Self { - host: headers.host, + domain: headers.domain, device: headers.device, user: headers.user, org_user_type: headers.org_user_type, @@ -638,7 +656,7 @@ impl<'r> FromRequest<'r> for AdminHeaders { impl From for Headers { fn from(h: AdminHeaders) -> Headers { Headers { - host: h.host, + domain: h.domain, device: h.device, user: h.user, ip: h.ip, @@ -669,7 +687,7 @@ fn get_col_id(request: &Request<'_>) -> Option { /// and have access to the specific collection provided via the /collections/collectionId. /// This does strict checking on the collection_id, ManagerHeadersLoose does not. pub struct ManagerHeaders { - pub host: String, + pub domain: String, pub device: Device, pub user: User, pub org_user_type: UserOrgType, @@ -698,7 +716,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders { } Outcome::Success(Self { - host: headers.host, + domain: headers.domain, device: headers.device, user: headers.user, org_user_type: headers.org_user_type, @@ -713,7 +731,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders { impl From for Headers { fn from(h: ManagerHeaders) -> Headers { Headers { - host: h.host, + domain: h.domain, device: h.device, user: h.user, ip: h.ip, @@ -724,7 +742,7 @@ impl From for Headers { /// The ManagerHeadersLoose is used when you at least need to be a Manager, /// but there is no collection_id sent with the request (either in the path or as form data). pub struct ManagerHeadersLoose { - pub host: String, + pub domain: String, pub device: Device, pub user: User, pub org_user: UserOrganization, @@ -740,7 +758,7 @@ impl<'r> FromRequest<'r> for ManagerHeadersLoose { let headers = try_outcome!(OrgHeaders::from_request(request).await); if headers.org_user_type >= UserOrgType::Manager { Outcome::Success(Self { - host: headers.host, + domain: headers.domain, device: headers.device, user: headers.user, org_user: headers.org_user, @@ -756,7 +774,7 @@ impl<'r> FromRequest<'r> for ManagerHeadersLoose { impl From for Headers { fn from(h: ManagerHeadersLoose) -> Headers { Headers { - host: h.host, + domain: h.domain, device: h.device, user: h.user, ip: h.ip, @@ -784,7 +802,7 @@ impl ManagerHeaders { } Ok(ManagerHeaders { - host: h.host, + domain: h.domain, device: h.device, user: h.user, org_user_type: h.org_user_type, From 303eb30ae4fc64afbb404b05db71705701a26709 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 09:43:19 +0200 Subject: [PATCH 03/40] remove domain_paths hashmap, since it's no longer used --- src/config.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index e7cca8acfd..0b737a5b4b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -51,7 +51,6 @@ macro_rules! make_config { domain_hostmap: OnceLock, domain_origins: OnceLock, - domain_paths: OnceLock, } #[derive(Clone, Default, Deserialize, Serialize)] @@ -1120,7 +1119,6 @@ impl Config { _usr, _overrides, domain_origins: OnceLock::new(), - domain_paths: OnceLock::new(), domain_hostmap: OnceLock::new(), }), }) From 17923c3fd0ca24a1d537c3e1e0d446f847ed0609 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 10:30:04 +0200 Subject: [PATCH 04/40] replace domain with base_url --- src/auth.rs | 56 ++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 637dec8a52..d4762d57cb 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -360,12 +360,12 @@ use crate::db::{ DbConn, }; -pub struct Domain { - pub domain: String, +pub struct BaseURL { + pub base_url: String, } #[rocket::async_trait] -impl<'r> FromRequest<'r> for Domain { +impl<'r> FromRequest<'r> for BaseURL { type Error = &'static str; async fn from_request(request: &'r Request<'_>) -> Outcome { @@ -373,7 +373,7 @@ impl<'r> FromRequest<'r> for Domain { // Get host // TODO: UPDATE THIS SECTION - let domain = if CONFIG.domain_set() { + let base_url = if CONFIG.domain_set() { let host = if let Some(host) = headers.get_one("X-Forwarded-Host") { host } else if let Some(host) = headers.get_one("Host") { @@ -385,13 +385,13 @@ impl<'r> FromRequest<'r> for Domain { todo!() }; - let Some(domain) = CONFIG.host_to_domain(host) else { + let Some(base_url) = CONFIG.host_to_base_url(host) else { // TODO fix error handling // This is probably a 421 misdirected request. todo!() }; - domain + base_url } else if let Some(referer) = headers.get_one("Referer") { referer.to_string() } else { @@ -417,14 +417,14 @@ impl<'r> FromRequest<'r> for Domain { format!("{protocol}://{host}") }; - Outcome::Success(Domain { - domain, + Outcome::Success(BaseURL { + base_url, }) } } pub struct ClientHeaders { - pub domain: String, + pub base_url: String, pub device_type: i32, pub ip: ClientIp, } @@ -434,7 +434,7 @@ impl<'r> FromRequest<'r> for ClientHeaders { type Error = &'static str; async fn from_request(request: &'r Request<'_>) -> Outcome { - let domain = try_outcome!(Domain::from_request(request).await).domain; + let base_url = try_outcome!(Domain::from_request(request).await).base_url; let ip = match ClientIp::from_request(request).await { Outcome::Success(ip) => ip, _ => err_handler!("Error getting Client IP"), @@ -444,7 +444,7 @@ impl<'r> FromRequest<'r> for ClientHeaders { request.headers().get_one("device-type").map(|d| d.parse().unwrap_or(14)).unwrap_or_else(|| 14); Outcome::Success(ClientHeaders { - domain, + base_url, device_type, ip, }) @@ -452,7 +452,7 @@ impl<'r> FromRequest<'r> for ClientHeaders { } pub struct Headers { - pub domain: String, + pub base_url: String, pub device: Device, pub user: User, pub ip: ClientIp, @@ -465,7 +465,7 @@ impl<'r> FromRequest<'r> for Headers { async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = request.headers(); - let domain = try_outcome!(Domain::from_request(request).await).domain; + let base_url = try_outcome!(BaseURL::from_request(request).await).base_url; let ip = match ClientIp::from_request(request).await { Outcome::Success(ip) => ip, _ => err_handler!("Error getting Client IP"), @@ -536,7 +536,7 @@ impl<'r> FromRequest<'r> for Headers { } Outcome::Success(Headers { - domain, + base_url, device, user, ip, @@ -545,7 +545,7 @@ impl<'r> FromRequest<'r> for Headers { } pub struct OrgHeaders { - pub domain: String, + pub base_url: String, pub device: Device, pub user: User, pub org_user_type: UserOrgType, @@ -601,7 +601,7 @@ impl<'r> FromRequest<'r> for OrgHeaders { }; Outcome::Success(Self { - domain: headers.domain, + base_url: headers.base_url, device: headers.device, user, org_user_type: { @@ -623,7 +623,7 @@ impl<'r> FromRequest<'r> for OrgHeaders { } pub struct AdminHeaders { - pub domain: String, + pub base_url: String, pub device: Device, pub user: User, pub org_user_type: UserOrgType, @@ -640,7 +640,7 @@ impl<'r> FromRequest<'r> for AdminHeaders { let client_version = request.headers().get_one("Bitwarden-Client-Version").map(String::from); if headers.org_user_type >= UserOrgType::Admin { Outcome::Success(Self { - domain: headers.domain, + base_url: headers.base_url, device: headers.device, user: headers.user, org_user_type: headers.org_user_type, @@ -656,7 +656,7 @@ impl<'r> FromRequest<'r> for AdminHeaders { impl From for Headers { fn from(h: AdminHeaders) -> Headers { Headers { - domain: h.domain, + base_url: h.base_url, device: h.device, user: h.user, ip: h.ip, @@ -687,7 +687,7 @@ fn get_col_id(request: &Request<'_>) -> Option { /// and have access to the specific collection provided via the /collections/collectionId. /// This does strict checking on the collection_id, ManagerHeadersLoose does not. pub struct ManagerHeaders { - pub domain: String, + pub base_url: String, pub device: Device, pub user: User, pub org_user_type: UserOrgType, @@ -716,7 +716,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders { } Outcome::Success(Self { - domain: headers.domain, + base_url: headers.base_url, device: headers.device, user: headers.user, org_user_type: headers.org_user_type, @@ -731,7 +731,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders { impl From for Headers { fn from(h: ManagerHeaders) -> Headers { Headers { - domain: h.domain, + base_url: h.base_url, device: h.device, user: h.user, ip: h.ip, @@ -742,7 +742,7 @@ impl From for Headers { /// The ManagerHeadersLoose is used when you at least need to be a Manager, /// but there is no collection_id sent with the request (either in the path or as form data). pub struct ManagerHeadersLoose { - pub domain: String, + pub base_url: String, pub device: Device, pub user: User, pub org_user: UserOrganization, @@ -758,7 +758,7 @@ impl<'r> FromRequest<'r> for ManagerHeadersLoose { let headers = try_outcome!(OrgHeaders::from_request(request).await); if headers.org_user_type >= UserOrgType::Manager { Outcome::Success(Self { - domain: headers.domain, + base_url: headers.base_url, device: headers.device, user: headers.user, org_user: headers.org_user, @@ -774,7 +774,7 @@ impl<'r> FromRequest<'r> for ManagerHeadersLoose { impl From for Headers { fn from(h: ManagerHeadersLoose) -> Headers { Headers { - domain: h.domain, + base_url: h.base_url, device: h.device, user: h.user, ip: h.ip, @@ -802,7 +802,7 @@ impl ManagerHeaders { } Ok(ManagerHeaders { - domain: h.domain, + base_url: h.base_url, device: h.device, user: h.user, org_user_type: h.org_user_type, @@ -812,7 +812,7 @@ impl ManagerHeaders { } pub struct OwnerHeaders { - pub host: String, + pub base_url: String, pub device: Device, pub user: User, pub ip: ClientIp, @@ -826,7 +826,7 @@ impl<'r> FromRequest<'r> for OwnerHeaders { let headers = try_outcome!(OrgHeaders::from_request(request).await); if headers.org_user_type == UserOrgType::Owner { Outcome::Success(Self { - host: headers.host, + base_url: headers.base_url, device: headers.device, user: headers.user, ip: headers.ip, From 0ebd877fb86741ac16019ccdddcebdd6bdda3bfa Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 10:57:48 +0200 Subject: [PATCH 05/40] make admin work with multi-domains --- src/api/admin.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 4c8d560460..2608def60e 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -12,6 +12,7 @@ use rocket::{ Catcher, Route, }; +use crate::auth::BaseURL; use crate::{ api::{ core::{log_event, two_factor}, @@ -98,7 +99,7 @@ const BASE_TEMPLATE: &str = "admin/base"; const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000"; fn admin_path() -> String { - format!("{}{}", CONFIG.domain_path(), ADMIN_PATH) + format!("{}", ADMIN_PATH) } #[derive(Debug)] @@ -123,8 +124,8 @@ impl<'r> FromRequest<'r> for IpHeader { } } -fn admin_url() -> String { - format!("{}{}", CONFIG.domain_origin(), admin_path()) +fn admin_url(base_url: &str) -> String { + format!("{}{}", base_url, admin_path()) } #[derive(Responder)] @@ -668,7 +669,7 @@ async fn get_ntp_time(has_http_access: bool) -> String { } #[get("/diagnostics")] -async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) -> ApiResult> { +async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn, base_url: BaseURL) -> ApiResult> { use chrono::prelude::*; use std::net::ToSocketAddrs; @@ -724,7 +725,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) "uses_proxy": uses_proxy, "db_type": *DB_TYPE, "db_version": get_sql_server_version(&mut conn).await, - "admin_url": format!("{}/diagnostics", admin_url()), + "admin_url": format!("{}/diagnostics", admin_url(&base_url.base_url)), "overrides": &CONFIG.get_overrides().join(", "), "host_arch": std::env::consts::ARCH, "host_os": std::env::consts::OS, From 2c7b739d497d44590525a9611935ee913b705a55 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 11:07:38 +0200 Subject: [PATCH 06/40] make fido app-id.json work with multi-domains --- src/api/web.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/api/web.rs b/src/api/web.rs index 67248c835e..d81bf9dc07 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -5,7 +5,8 @@ use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, - auth::decode_file_download, + auth::{decode_file_download, BaseURL}, + config::extract_url_host, error::Error, util::{Cached, SafeString}, CONFIG, @@ -62,9 +63,15 @@ fn web_index_head() -> EmptyResult { } #[get("/app-id.json")] -fn app_id() -> Cached<(ContentType, Json)> { +fn app_id(base_url: BaseURL) -> Cached<(ContentType, Json)> { let content_type = ContentType::new("application", "fido.trusted-apps+json"); + // TODO_MAYBE: add an extractor for getting the origin, so we only have to do 1 lookup. + let origin = CONFIG.domain_origin(&extract_url_host(&base_url.base_url)) + // This should never fail, because every host with a domain entry + // should have a origin entry. + .expect("Configured domain has no origin entry"); + Cached::long( ( content_type, @@ -83,7 +90,7 @@ fn app_id() -> Cached<(ContentType, Json)> { // This leaves it unclear as to whether the path must be empty, // or whether it can be non-empty and will be ignored. To be on // the safe side, use a proper web origin (with empty path). - &CONFIG.domain_origin(), + &origin, "ios:bundle-id:com.8bit.bitwarden", "android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ] }] From e313745f7c8d90b4dc6d3c350faef8e08af2ecd3 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 11:10:04 +0200 Subject: [PATCH 07/40] make domain protocol validation work with multi-domains --- src/config.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/config.rs b/src/config.rs index 0b737a5b4b..70878f06ab 100644 --- a/src/config.rs +++ b/src/config.rs @@ -733,11 +733,13 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } } - let dom = cfg.domain.to_lowercase(); - if !dom.starts_with("http://") && !dom.starts_with("https://") { - err!( - "DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'" - ); + let domains = cfg.domain_change_back.split(',').map(|d| d.to_string().to_lowercase()); + for dom in domains { + if !dom.starts_with("http://") && !dom.starts_with("https://") { + err!( + "DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'" + ); + } } let whitelist = &cfg.signups_domains_whitelist; @@ -1314,6 +1316,8 @@ impl Config { }).get(host).map(|h| h.clone()) } + // Yes this is a base_url + // But the configuration precedent says, that we call this a domain. pub fn main_domain(&self) -> String { self.domain_change_back().split(',').nth(0).expect("Missing domain").to_string() } From 0d7e678c2ee415cfc47e6455cfdbb1aed8d0177d Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 11:10:13 +0200 Subject: [PATCH 08/40] make mail work with multi-domains the domain chosen is always the first domain --- src/mail.rs | 52 ++++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/mail.rs b/src/mail.rs index 151554a1fd..6ac1b0c7fc 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -118,6 +118,10 @@ fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String Ok((subject, body)) } +fn mail_domain() -> String { + CONFIG.main_domain() +} + pub async fn send_password_hint(address: &str, hint: Option) -> EmptyResult { let template_name = if hint.is_some() { "email/pw_hint_some" @@ -128,7 +132,7 @@ pub async fn send_password_hint(address: &str, hint: Option) -> EmptyRes let (subject, body_html, body_text) = get_text( template_name, json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "hint": hint, }), @@ -144,7 +148,7 @@ pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/delete_account", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "user_id": uuid, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), @@ -162,7 +166,7 @@ pub async fn send_verify_email(address: &str, uuid: &str) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/verify_email", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "user_id": uuid, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), @@ -177,7 +181,7 @@ pub async fn send_welcome(address: &str) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/welcome", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), }), )?; @@ -192,7 +196,7 @@ pub async fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult let (subject, body_html, body_text) = get_text( "email/welcome_must_verify", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "user_id": uuid, "token": verify_email_token, @@ -206,7 +210,7 @@ pub async fn send_2fa_removed_from_org(address: &str, org_name: &str) -> EmptyRe let (subject, body_html, body_text) = get_text( "email/send_2fa_removed_from_org", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "org_name": org_name, }), @@ -219,7 +223,7 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) -> let (subject, body_html, body_text) = get_text( "email/send_single_org_removed_from_org", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "org_name": org_name, }), @@ -248,7 +252,7 @@ pub async fn send_invite( let (subject, body_html, body_text) = get_text( "email/send_org_invite", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "org_id": org_id.as_deref().unwrap_or("_"), "org_user_id": org_user_id.as_deref().unwrap_or("_"), @@ -282,7 +286,7 @@ pub async fn send_emergency_access_invite( let (subject, body_html, body_text) = get_text( "email/send_emergency_access_invite", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "emer_id": emer_id, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), @@ -298,7 +302,7 @@ pub async fn send_emergency_access_invite_accepted(address: &str, grantee_email: let (subject, body_html, body_text) = get_text( "email/emergency_access_invite_accepted", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "grantee_email": grantee_email, }), @@ -311,7 +315,7 @@ pub async fn send_emergency_access_invite_confirmed(address: &str, grantor_name: let (subject, body_html, body_text) = get_text( "email/emergency_access_invite_confirmed", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "grantor_name": grantor_name, }), @@ -324,7 +328,7 @@ pub async fn send_emergency_access_recovery_approved(address: &str, grantor_name let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_approved", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "grantor_name": grantor_name, }), @@ -342,7 +346,7 @@ pub async fn send_emergency_access_recovery_initiated( let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_initiated", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "grantee_name": grantee_name, "atype": atype, @@ -362,7 +366,7 @@ pub async fn send_emergency_access_recovery_reminder( let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_reminder", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "grantee_name": grantee_name, "atype": atype, @@ -377,7 +381,7 @@ pub async fn send_emergency_access_recovery_rejected(address: &str, grantor_name let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_rejected", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "grantor_name": grantor_name, }), @@ -390,7 +394,7 @@ pub async fn send_emergency_access_recovery_timed_out(address: &str, grantee_nam let (subject, body_html, body_text) = get_text( "email/emergency_access_recovery_timed_out", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "grantee_name": grantee_name, "atype": atype, @@ -404,7 +408,7 @@ pub async fn send_invite_accepted(new_user_email: &str, address: &str, org_name: let (subject, body_html, body_text) = get_text( "email/invite_accepted", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "email": new_user_email, "org_name": org_name, @@ -418,7 +422,7 @@ pub async fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult let (subject, body_html, body_text) = get_text( "email/invite_confirmed", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "org_name": org_name, }), @@ -435,7 +439,7 @@ pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTi let (subject, body_html, body_text) = get_text( "email/new_device_logged_in", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "ip": ip, "device": device, @@ -454,7 +458,7 @@ pub async fn send_incomplete_2fa_login(address: &str, ip: &str, dt: &NaiveDateTi let (subject, body_html, body_text) = get_text( "email/incomplete_2fa_login", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "ip": ip, "device": device, @@ -470,7 +474,7 @@ pub async fn send_token(address: &str, token: &str) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/twofactor_email", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "token": token, }), @@ -483,7 +487,7 @@ pub async fn send_change_email(address: &str, token: &str) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/change_email", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "token": token, }), @@ -496,7 +500,7 @@ pub async fn send_test(address: &str) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/smtp_test", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), }), )?; @@ -508,7 +512,7 @@ pub async fn send_admin_reset_password(address: &str, user_name: &str, org_name: let (subject, body_html, body_text) = get_text( "email/admin_reset_password", json!({ - "url": CONFIG.domain(), + "url": mail_domain(), "img_src": CONFIG._smtp_img_src(), "user_name": user_name, "org_name": org_name, From 5462b97c26cd7b8426919aee5bb6d5761299d15f Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 11:16:24 +0200 Subject: [PATCH 09/40] make cors work with multi-domains --- src/util.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/util.rs b/src/util.rs index 2f04fe3491..b00fb205c7 100644 --- a/src/util.rs +++ b/src/util.rs @@ -17,7 +17,7 @@ use tokio::{ time::{sleep, Duration}, }; -use crate::CONFIG; +use crate::{CONFIG, config::extract_url_host}; pub struct AppHeaders(); @@ -129,9 +129,19 @@ impl Cors { // If a match exists, return it. Otherwise, return None. fn get_allowed_origin(headers: &HeaderMap<'_>) -> Option { let origin = Cors::get_header(headers, "Origin"); - let domain_origin = CONFIG.domain_origin(); + + let domain_origin_opt = CONFIG.domain_origin(&extract_url_host(&origin)); let safari_extension_origin = "file://"; - if origin == domain_origin || origin == safari_extension_origin { + + let found_origin = { + if let Some(domain_origin) = domain_origin_opt { + origin == domain_origin + } else { + false + } + }; + + if found_origin || origin == safari_extension_origin { Some(origin) } else { None From f82a142ceed15f425e92c877abc11b4f5b3a0294 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 13:50:56 +0200 Subject: [PATCH 10/40] get domain and origin with single extractor --- src/auth.rs | 38 ++++++++++++++++++++++---------------- src/config.rs | 2 +- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index d4762d57cb..214bf8556c 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -10,6 +10,7 @@ use serde::de::DeserializeOwned; use serde::ser::Serialize; use crate::{error::Error, CONFIG}; +use crate::config::extract_url_origin; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -360,12 +361,13 @@ use crate::db::{ DbConn, }; -pub struct BaseURL { +pub struct HostInfo { pub base_url: String, + pub origin: String, } #[rocket::async_trait] -impl<'r> FromRequest<'r> for BaseURL { +impl<'r> FromRequest<'r> for HostInfo { type Error = &'static str; async fn from_request(request: &'r Request<'_>) -> Outcome { @@ -373,7 +375,7 @@ impl<'r> FromRequest<'r> for BaseURL { // Get host // TODO: UPDATE THIS SECTION - let base_url = if CONFIG.domain_set() { + if CONFIG.domain_set() { let host = if let Some(host) = headers.get_one("X-Forwarded-Host") { host } else if let Some(host) = headers.get_one("Host") { @@ -385,15 +387,18 @@ impl<'r> FromRequest<'r> for BaseURL { todo!() }; - let Some(base_url) = CONFIG.host_to_base_url(host) else { - // TODO fix error handling - // This is probably a 421 misdirected request. - todo!() - }; - - base_url + // TODO fix error handling + // This is probably a 421 misdirected request + let (base_url, origin) = CONFIG.host_to_domain(host).and_then(|base_url| { + Some((base_url, CONFIG.domain_origin(host)?)) + }).expect("This should not be merged like this!!!"); + + return Outcome::Success(HostInfo { base_url, origin }); } else if let Some(referer) = headers.get_one("Referer") { - referer.to_string() + return Outcome::Success(HostInfo { + base_url: referer.to_string(), + origin: extract_url_origin(referer), + }); } else { // Try to guess from the headers use std::env; @@ -414,12 +419,13 @@ impl<'r> FromRequest<'r> for BaseURL { "" }; - format!("{protocol}://{host}") - }; + let base_url_origin = format!("{protocol}://{host}"); - Outcome::Success(BaseURL { - base_url, - }) + return Outcome::Success(HostInfo { + base_url: base_url_origin, + origin: base_url_origin, + }); + } } } diff --git a/src/config.rs b/src/config.rs index 70878f06ab..1f62c22f51 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1003,7 +1003,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } /// Extracts an RFC 6454 web origin from a URL. -fn extract_url_origin(url: &str) -> String { +pub fn extract_url_origin(url: &str) -> String { match Url::parse(url) { Ok(u) => u.origin().ascii_serialization(), Err(e) => { From b5dea32ea5fe90454862f2e7085c48ba49b250fb Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 13:53:19 +0200 Subject: [PATCH 11/40] make attachments / ciphers support multi-domains --- src/db/models/attachment.rs | 10 ++++++---- src/db/models/cipher.rs | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index f8eca72f68..a1f1604d1e 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -35,15 +35,17 @@ impl Attachment { format!("{}/{}/{}", CONFIG.attachments_folder(), self.cipher_uuid, self.id) } - pub fn get_url(&self, host: &str) -> String { + // TODO: Change back + pub fn get_url(&self, base_url: &str, _parameter_to_break_existing_uses: ()) -> String { let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone())); - format!("{}/attachments/{}/{}?token={}", host, self.cipher_uuid, self.id, token) + format!("{}/attachments/{}/{}?token={}", base_url, self.cipher_uuid, self.id, token) } - pub fn to_json(&self, host: &str) -> Value { + // TODO: Change back + pub fn to_json(&self, base_url: &str, _parameter_to_break_existing_uses: ()) -> Value { json!({ "Id": self.id, - "Url": self.get_url(host), + "Url": self.get_url(base_url, ()), "FileName": self.file_name, "Size": self.file_size.to_string(), "SizeName": crate::util::get_display_size(self.file_size), diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 61683d85fa..f9192f6c24 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -113,25 +113,27 @@ use crate::error::MapResult; /// Database methods impl Cipher { + // TODO: Change back pub async fn to_json( &self, - host: &str, + base_url: &str, user_uuid: &str, cipher_sync_data: Option<&CipherSyncData>, sync_type: CipherSyncType, conn: &mut DbConn, + _parameter_to_break_existing_uses: (), ) -> Value { use crate::util::format_date; let mut attachments_json: Value = Value::Null; if let Some(cipher_sync_data) = cipher_sync_data { if let Some(attachments) = cipher_sync_data.cipher_attachments.get(&self.uuid) { - attachments_json = attachments.iter().map(|c| c.to_json(host)).collect(); + attachments_json = attachments.iter().map(|c| c.to_json(base_url, ())).collect(); } } else { let attachments = Attachment::find_by_cipher(&self.uuid, conn).await; if !attachments.is_empty() { - attachments_json = attachments.iter().map(|c| c.to_json(host)).collect() + attachments_json = attachments.iter().map(|c| c.to_json(base_url, ())).collect() } } From 968ed8a4530b7dec8c471c30de5fa6c762722148 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:00:17 +0200 Subject: [PATCH 12/40] make sends support multi-domain --- src/api/core/sends.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 1bc6d00f0a..a34d8b9728 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -9,8 +9,10 @@ use rocket::serde::json::Json; use serde_json::Value; use crate::{ - api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, - auth::{ClientIp, Headers, Host}, + api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, UpdateType}, + auth::{ClientIp, Headers, HostInfo}, + api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, UpdateType}, + auth::{ClientIp, Headers, HostInfo}, db::{models::*, DbConn, DbPool}, util::{NumberOrString, SafeString}, CONFIG, @@ -462,7 +464,7 @@ async fn post_access_file( send_id: &str, file_id: &str, data: JsonUpcase, - host: Host, + host_info: HostInfo, mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { @@ -517,7 +519,7 @@ async fn post_access_file( Ok(Json(json!({ "Object": "send-fileDownload", "Id": file_id, - "Url": format!("{}/api/sends/{}/{}?t={}", &host.host, send_id, file_id, token) + "Url": format!("{}/api/sends/{}/{}?t={}", &host_info.base_url, send_id, file_id, token) }))) } From 42e1018ad7dd5350ebde81bfcd3b48aaf28487fd Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:01:37 +0200 Subject: [PATCH 13/40] make admin support hostinfo --- src/api/admin.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 2608def60e..803fa39a61 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -12,7 +12,7 @@ use rocket::{ Catcher, Route, }; -use crate::auth::BaseURL; +use crate::auth::HostInfo; use crate::{ api::{ core::{log_event, two_factor}, @@ -669,7 +669,7 @@ async fn get_ntp_time(has_http_access: bool) -> String { } #[get("/diagnostics")] -async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn, base_url: BaseURL) -> ApiResult> { +async fn diagnostics(_token: AdminToken, ip_header: IpHeader, host_info: HostInfo, mut conn: DbConn) -> ApiResult> { use chrono::prelude::*; use std::net::ToSocketAddrs; @@ -725,7 +725,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn, "uses_proxy": uses_proxy, "db_type": *DB_TYPE, "db_version": get_sql_server_version(&mut conn).await, - "admin_url": format!("{}/diagnostics", admin_url(&base_url.base_url)), + "admin_url": format!("{}/diagnostics", admin_url(&host_info.base_url)), "overrides": &CONFIG.get_overrides().join(", "), "host_arch": std::env::consts::ARCH, "host_os": std::env::consts::OS, From 12c0005e7f7e1d4a0fff35a8b5ce84cd21ea8e84 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:06:42 +0200 Subject: [PATCH 14/40] make webauthn support multi-domain --- src/api/core/two_factor/webauthn.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 14ba851413..bf1460b803 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -9,7 +9,7 @@ use crate::{ core::{log_user_event, two_factor::_generate_recover_code}, EmptyResult, JsonResult, JsonUpcase, PasswordOrOtpData, }, - auth::Headers, + auth::{Headers, HostInfo}, db::{ models::{EventType, TwoFactor, TwoFactorType}, DbConn, @@ -52,12 +52,10 @@ struct WebauthnConfig { } impl WebauthnConfig { - fn load() -> Webauthn { - let domain = CONFIG.domain(); - let domain_origin = CONFIG.domain_origin(); + fn load(domain: &str, domain_origin: &str) -> Webauthn { Webauthn::new(Self { rpid: Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(), - url: domain, + url: domain.to_string(), origin: Url::parse(&domain_origin).unwrap(), }) } @@ -128,6 +126,7 @@ async fn get_webauthn(data: JsonUpcase, headers: Headers, mut async fn generate_webauthn_challenge( data: JsonUpcase, headers: Headers, + host_info: HostInfo, mut conn: DbConn, ) -> JsonResult { let data: PasswordOrOtpData = data.into_inner().data; @@ -142,7 +141,7 @@ async fn generate_webauthn_challenge( .map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering .collect(); - let (challenge, state) = WebauthnConfig::load().generate_challenge_register_options( + let (challenge, state) = WebauthnConfig::load(&host_info.base_url, &host_info.origin).generate_challenge_register_options( user.uuid.as_bytes().to_vec(), user.email, user.name, @@ -250,7 +249,7 @@ impl From for PublicKeyCredential { } #[post("/two-factor/webauthn", data = "")] -async fn activate_webauthn(data: JsonUpcase, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn activate_webauthn(data: JsonUpcase, headers: Headers, host_info: HostInfo, mut conn: DbConn) -> JsonResult { let data: EnableWebauthnData = data.into_inner().data; let mut user = headers.user; @@ -274,7 +273,7 @@ async fn activate_webauthn(data: JsonUpcase, headers: Header // Verify the credentials with the saved state let (credential, _data) = - WebauthnConfig::load().register_credential(&data.DeviceResponse.into(), &state, |_| Ok(false))?; + WebauthnConfig::load(&host_info.base_url, &host_info.origin).register_credential(&data.DeviceResponse.into(), &state, |_| Ok(false))?; let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1; // TODO: Check for repeated ID's @@ -303,8 +302,8 @@ async fn activate_webauthn(data: JsonUpcase, headers: Header } #[put("/two-factor/webauthn", data = "")] -async fn activate_webauthn_put(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { - activate_webauthn(data, headers, conn).await +async fn activate_webauthn_put(data: JsonUpcase, headers: Headers, host_info: HostInfo, conn: DbConn) -> JsonResult { + activate_webauthn(data, headers, host_info, conn).await } #[derive(Deserialize, Debug)] @@ -375,7 +374,7 @@ pub async fn get_webauthn_registrations( } } -pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> JsonResult { +pub async fn generate_webauthn_login(user_uuid: &str, base_url: &str, origin: &str, conn: &mut DbConn) -> JsonResult { // Load saved credentials let creds: Vec = get_webauthn_registrations(user_uuid, conn).await?.1.into_iter().map(|r| r.credential).collect(); @@ -385,8 +384,8 @@ pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> Json } // Generate a challenge based on the credentials - let ext = RequestAuthenticationExtensions::builder().appid(format!("{}/app-id.json", &CONFIG.domain())).build(); - let (response, state) = WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?; + let ext = RequestAuthenticationExtensions::builder().appid(format!("{}/app-id.json", base_url)).build(); + let (response, state) = WebauthnConfig::load(base_url, origin).generate_challenge_authenticate_options(creds, Some(ext))?; // Save the challenge state for later validation TwoFactor::new(user_uuid.into(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?) @@ -397,7 +396,7 @@ pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> Json Ok(Json(serde_json::to_value(response.public_key)?)) } -pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut DbConn) -> EmptyResult { +pub async fn validate_webauthn_login(user_uuid: &str, response: &str, base_url: &str, origin: &str, conn: &mut DbConn) -> EmptyResult { let type_ = TwoFactorType::WebauthnLoginChallenge as i32; let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await { Some(tf) => { @@ -420,7 +419,7 @@ pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut // If the credential we received is migrated from U2F, enable the U2F compatibility //let use_u2f = registrations.iter().any(|r| r.migrated && r.credential.cred_id == rsp.raw_id.0); - let (cred_id, auth_data) = WebauthnConfig::load().authenticate_credential(&rsp, &state)?; + let (cred_id, auth_data) = WebauthnConfig::load(base_url, origin).authenticate_credential(&rsp, &state)?; for reg in &mut registrations { if ®.credential.cred_id == cred_id { From ac3c1d41b26b36ed4025551370fb77275a9c56d3 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:09:21 +0200 Subject: [PATCH 15/40] make web support hostinfo --- src/api/web.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/api/web.rs b/src/api/web.rs index d81bf9dc07..5f4c961d5b 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -5,7 +5,7 @@ use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, - auth::{decode_file_download, BaseURL}, + auth::{decode_file_download, HostInfo}, config::extract_url_host, error::Error, util::{Cached, SafeString}, @@ -63,14 +63,12 @@ fn web_index_head() -> EmptyResult { } #[get("/app-id.json")] -fn app_id(base_url: BaseURL) -> Cached<(ContentType, Json)> { +fn app_id(host_info: HostInfo) -> Cached<(ContentType, Json)> { let content_type = ContentType::new("application", "fido.trusted-apps+json"); // TODO_MAYBE: add an extractor for getting the origin, so we only have to do 1 lookup. - let origin = CONFIG.domain_origin(&extract_url_host(&base_url.base_url)) - // This should never fail, because every host with a domain entry - // should have a origin entry. - .expect("Configured domain has no origin entry"); + // Also I'm not sure if we shouldn't return all origins. + let origin = host_info.origin; Cached::long( ( From 6867099291b85566f557b86e43dc62cb0321e539 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:15:15 +0200 Subject: [PATCH 16/40] make headers use hostinfo --- src/auth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth.rs b/src/auth.rs index 214bf8556c..88ac71c32f 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -471,7 +471,7 @@ impl<'r> FromRequest<'r> for Headers { async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = request.headers(); - let base_url = try_outcome!(BaseURL::from_request(request).await).base_url; + let base_url = try_outcome!(HostInfo::from_request(request).await).base_url; let ip = match ClientIp::from_request(request).await { Outcome::Success(ip) => ip, _ => err_handler!("Error getting Client IP"), From 2670db15c53ca404289f8428eb4f032e529f4272 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:15:30 +0200 Subject: [PATCH 17/40] make accounts support multi-domains --- src/api/core/accounts.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index a97c4c31bc..5eda27a694 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -9,7 +9,7 @@ use crate::{ register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordOrOtpData, UpdateType, }, - auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers}, + auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers, HostInfo}, crypto, db::{models::*, DbConn}, mail, @@ -1042,6 +1042,7 @@ struct AuthRequestRequest { async fn post_auth_request( data: Json, headers: ClientHeaders, + host_info: HostInfo, mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { @@ -1076,13 +1077,13 @@ async fn post_auth_request( "creationDate": auth_request.creation_date.and_utc(), "responseDate": null, "requestApproved": false, - "origin": CONFIG.domain_origin(), + "origin": host_info.origin, "object": "auth-request" }))) } #[get("/auth-requests/")] -async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult { +async fn get_auth_request(uuid: &str, host_info: HostInfo, mut conn: DbConn) -> JsonResult { let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await { Some(auth_request) => auth_request, None => { @@ -1103,7 +1104,7 @@ async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult { "creationDate": auth_request.creation_date.and_utc(), "responseDate": response_date_utc, "requestApproved": auth_request.approved, - "origin": CONFIG.domain_origin(), + "origin": host_info.origin, "object":"auth-request" } ))) @@ -1122,6 +1123,7 @@ struct AuthResponseRequest { async fn put_auth_request( uuid: &str, data: Json, + host_info: HostInfo, mut conn: DbConn, ant: AnonymousNotify<'_>, nt: Notify<'_>, @@ -1158,14 +1160,14 @@ async fn put_auth_request( "creationDate": auth_request.creation_date.and_utc(), "responseDate": response_date_utc, "requestApproved": auth_request.approved, - "origin": CONFIG.domain_origin(), + "origin": host_info.origin, "object":"auth-request" } ))) } #[get("/auth-requests//response?")] -async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) -> JsonResult { +async fn get_auth_request_response(uuid: &str, code: &str, host_info: HostInfo, mut conn: DbConn) -> JsonResult { let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await { Some(auth_request) => auth_request, None => { @@ -1190,14 +1192,14 @@ async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) -> "creationDate": auth_request.creation_date.and_utc(), "responseDate": response_date_utc, "requestApproved": auth_request.approved, - "origin": CONFIG.domain_origin(), + "origin": host_info.origin, "object":"auth-request" } ))) } #[get("/auth-requests")] -async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult { +async fn get_auth_requests(headers: Headers, host_info: HostInfo, mut conn: DbConn) -> JsonResult { let auth_requests = AuthRequest::find_by_user(&headers.user.uuid, &mut conn).await; Ok(Json(json!({ @@ -1217,7 +1219,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult { "creationDate": request.creation_date.and_utc(), "responseDate": response_date_utc, "requestApproved": request.approved, - "origin": CONFIG.domain_origin(), + "origin": host_info.origin, "object":"auth-request" }) }).collect::>(), From 81dd47952b81c7b2b98d9c66e68217fb678f68fa Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:20:44 +0200 Subject: [PATCH 18/40] make ciphers work with multi-domains --- src/api/core/ciphers.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index b3dca3b681..924ed40ae4 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -113,7 +113,7 @@ async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { ciphers_json.push( - c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn) + c.to_json(&headers.base_url, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn, ()) .await, ); } @@ -160,7 +160,7 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json { let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { ciphers_json.push( - c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn) + c.to_json(&headers.base_url, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn, ()) .await, ); } @@ -183,7 +183,7 @@ async fn get_cipher(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResul err!("Cipher is not owned by user") } - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await)) } #[get("/ciphers//admin")] @@ -323,7 +323,7 @@ async fn post_ciphers(data: JsonUpcase, headers: Headers, mut conn: let mut cipher = Cipher::new(data.Type, data.Name.clone()); update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &nt, UpdateType::SyncCipherCreate).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await)) } /// Enforces the personal ownership policy on user-owned ciphers, if applicable. @@ -650,7 +650,7 @@ async fn put_cipher( update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &nt, UpdateType::SyncCipherUpdate).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await)) } #[post("/ciphers//partial", data = "")] @@ -694,7 +694,7 @@ async fn put_cipher_partial( // Update favorite cipher.set_favorite(Some(data.Favorite), &headers.user.uuid, &mut conn).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await)) } #[derive(Deserialize)] @@ -925,7 +925,7 @@ async fn share_cipher_by_uuid( update_cipher_from_data(&mut cipher, data.Cipher, headers, shared_to_collection, conn, nt, ut).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, conn, ()).await)) } /// v2 API for downloading an attachment. This just redirects the client to @@ -946,7 +946,7 @@ async fn get_attachment(uuid: &str, attachment_id: &str, headers: Headers, mut c } match Attachment::find_by_id(attachment_id, &mut conn).await { - Some(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))), + Some(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.base_url, ()))), Some(_) => err!("Attachment doesn't belong to cipher"), None => err!("Attachment doesn't exist"), } @@ -1006,7 +1006,7 @@ async fn post_attachment_v2( "AttachmentId": attachment_id, "Url": url, "FileUploadType": FileUploadType::Direct as i32, - response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await, + response_key: cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await, }))) } @@ -1233,7 +1233,7 @@ async fn post_attachment( let (cipher, mut conn) = save_attachment(attachment, uuid, data, &headers, conn, nt).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await)) } #[post("/ciphers//attachment-admin", format = "multipart/form-data", data = "")] @@ -1667,7 +1667,7 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon .await; } - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, conn, ()).await)) } async fn _restore_multiple_ciphers( From 3421dfcbf5894d4db27b0074efa84a1078ecc743 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:21:24 +0200 Subject: [PATCH 19/40] make emergency access work with multi-domains --- src/api/core/emergency_access.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index fb163849fd..029acb9ba5 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -603,11 +603,12 @@ async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn for c in ciphers { ciphers_json.push( c.to_json( - &headers.host, + &headers.base_url, &emergency_access.grantor_uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn, + (), ) .await, ); From ab96b26981612711c5436504b120d4cefdf503ae Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:22:09 +0200 Subject: [PATCH 20/40] make getting config work with multi-domains --- src/api/core/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 7712ea824a..3bd7c64199 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -190,7 +190,8 @@ fn version() -> Json<&'static str> { #[get("/config")] fn config() -> Json { - let domain = crate::CONFIG.domain(); + // TODO: maybe this should be extracted from the current request params + let domain = crate::CONFIG.main_domain(); let feature_states = parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags()); Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns From 901bf570bf8881a1fc145433e0c3ab5ecba44a9e Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:23:33 +0200 Subject: [PATCH 21/40] make organizations work with multi-domains --- src/api/core/organizations.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 1d841cda2a..0e5ba1cea6 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -765,20 +765,20 @@ struct OrgIdData { #[get("/ciphers/organization-details?")] async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> Json { Json(json!({ - "Data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &mut conn).await, + "Data": _get_org_details(&data.organization_id, &headers.base_url, &headers.user.uuid, &mut conn).await, "Object": "list", "ContinuationToken": null, })) } -async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &mut DbConn) -> Value { +async fn _get_org_details(org_id: &str, base_url: &str, user_uuid: &str, conn: &mut DbConn) -> Value { let ciphers = Cipher::find_by_org(org_id, conn).await; let cipher_sync_data = CipherSyncData::new(user_uuid, CipherSyncType::Organization, conn).await; let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { ciphers_json - .push(c.to_json(host, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await); + .push(c.to_json(base_url, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn, ()).await); } json!(ciphers_json) } @@ -2922,7 +2922,7 @@ async fn get_org_export(org_id: &str, headers: AdminHeaders, mut conn: DbConn) - "continuationToken": null, }, "ciphers": { - "data": convert_json_key_lcase_first(_get_org_details(org_id, &headers.host, &headers.user.uuid, &mut conn).await), + "data": convert_json_key_lcase_first(_get_org_details(org_id, &headers.base_url, &headers.user.uuid, &mut conn).await), "object": "list", "continuationToken": null, } @@ -2931,7 +2931,7 @@ async fn get_org_export(org_id: &str, headers: AdminHeaders, mut conn: DbConn) - // v2023.1.0 and newer response Json(json!({ "collections": convert_json_key_lcase_first(_get_org_collections(org_id, &mut conn).await), - "ciphers": convert_json_key_lcase_first(_get_org_details(org_id, &headers.host, &headers.user.uuid, &mut conn).await), + "ciphers": convert_json_key_lcase_first(_get_org_details(org_id, &headers.base_url, &headers.user.uuid, &mut conn).await), })) } } From df524c7139184d465a1c243adcda3175bf0e33bc Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:31:38 +0200 Subject: [PATCH 22/40] make PublicToken support multi-domains --- src/api/core/public.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 74f79a3ed2..14f2ab78d6 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -217,11 +217,13 @@ impl<'r> FromRequest<'r> for PublicToken { err_handler!("Token expired"); } // Check if claims.iss is host|claims.scope[0] - let host = match auth::Host::from_request(request).await { - Outcome::Success(host) => host, + let host_info = match auth::HostInfo::from_request(request).await { + Outcome::Success(host_info) => host_info, _ => err_handler!("Error getting Host"), }; - let complete_host = format!("{}|{}", host.host, claims.scope[0]); + // TODO check if this is fine + // using origin, because that's what they're generated with in auth.rs + let complete_host = format!("{}|{}", host_info.origin, claims.scope[0]); if complete_host != claims.iss { err_handler!("Token not issued by this server"); } From 7639a2b03d212097feba2cd012c2eaa872ad9210 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:33:07 +0200 Subject: [PATCH 23/40] make identity support multi-domains --- src/api/identity.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 9f3cd1bf89..0155bc2852 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -17,7 +17,7 @@ use crate::{ push::register_push_device, ApiResult, EmptyResult, JsonResult, JsonUpcase, }, - auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp}, + auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp, HostInfo}, db::{models::*, DbConn}, error::MapResult, mail, util, CONFIG, @@ -28,7 +28,7 @@ pub fn routes() -> Vec { } #[post("/connect/token", data = "")] -async fn login(data: Form, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult { +async fn login(data: Form, client_header: ClientHeaders, host_info: HostInfo, mut conn: DbConn) -> JsonResult { let data: ConnectData = data.into_inner(); let mut user_uuid: Option = None; @@ -48,7 +48,7 @@ async fn login(data: Form, client_header: ClientHeaders, mut conn: _check_is_some(&data.device_name, "device_name cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?; - _password_login(data, &mut user_uuid, &mut conn, &client_header.ip).await + _password_login(data, &mut user_uuid, &mut conn, &client_header.ip, &host_info.base_url, &host_info.origin).await } "client_credentials" => { _check_is_some(&data.client_id, "client_id cannot be blank")?; @@ -140,6 +140,8 @@ async fn _password_login( user_uuid: &mut Option, conn: &mut DbConn, ip: &ClientIp, + base_url: &str, + origin: &str, ) -> JsonResult { // Validate scope let scope = data.scope.as_ref().unwrap(); @@ -250,7 +252,7 @@ async fn _password_login( let (mut device, new_device) = get_device(&data, conn, &user).await; - let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, conn).await?; + let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, base_url, origin, conn).await?; if CONFIG.mail_enabled() && new_device { if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await { @@ -480,6 +482,8 @@ async fn twofactor_auth( data: &ConnectData, device: &mut Device, ip: &ClientIp, + base_url: &str, + origin: &str, conn: &mut DbConn, ) -> ApiResult> { let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await; @@ -497,7 +501,7 @@ async fn twofactor_auth( let twofactor_code = match data.two_factor_token { Some(ref code) => code, - None => err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, conn).await?, "2FA token not provided"), + None => err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, base_url, origin, conn).await?, "2FA token not provided"), }; let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled); @@ -511,7 +515,7 @@ async fn twofactor_auth( Some(TwoFactorType::Authenticator) => { authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await? } - Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?, + Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, base_url, origin, conn).await?, Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?, Some(TwoFactorType::Duo) => { duo::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, conn).await? @@ -527,7 +531,7 @@ async fn twofactor_auth( } _ => { err_json!( - _json_err_twofactor(&twofactor_ids, &user.uuid, conn).await?, + _json_err_twofactor(&twofactor_ids, &user.uuid, base_url, origin, conn).await?, "2FA Remember token not provided" ) } @@ -555,7 +559,7 @@ fn _selected_data(tf: Option) -> ApiResult { tf.map(|t| t.data).map_res("Two factor doesn't exist") } -async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbConn) -> ApiResult { +async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, base_url: &str, origin: &str, conn: &mut DbConn) -> ApiResult { let mut result = json!({ "error" : "invalid_grant", "error_description" : "Two factor required.", @@ -570,7 +574,7 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { - let request = webauthn::generate_webauthn_login(user_uuid, conn).await?; + let request = webauthn::generate_webauthn_login(user_uuid, base_url, origin, conn).await?; result["TwoFactorProviders2"][provider.to_string()] = request.0; } From 1dfc68ab8a374dbef253cc5d43aa459ba6269516 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:54:39 +0200 Subject: [PATCH 24/40] make auth support multi-domains --- src/auth.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 88ac71c32f..8a15a26fb2 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -17,16 +17,20 @@ const JWT_ALGORITHM: Algorithm = Algorithm::RS256; pub static DEFAULT_VALIDITY: Lazy = Lazy::new(|| Duration::hours(2)); static JWT_HEADER: Lazy
= Lazy::new(|| Header::new(JWT_ALGORITHM)); -pub static JWT_LOGIN_ISSUER: Lazy = Lazy::new(|| format!("{}|login", CONFIG.domain_origin())); -static JWT_INVITE_ISSUER: Lazy = Lazy::new(|| format!("{}|invite", CONFIG.domain_origin())); +fn jwt_origin() -> String { + extract_url_origin(&CONFIG.main_domain()) +} + +pub static JWT_LOGIN_ISSUER: Lazy = Lazy::new(|| format!("{}|login", jwt_origin())); +static JWT_INVITE_ISSUER: Lazy = Lazy::new(|| format!("{}|invite", jwt_origin())); static JWT_EMERGENCY_ACCESS_INVITE_ISSUER: Lazy = - Lazy::new(|| format!("{}|emergencyaccessinvite", CONFIG.domain_origin())); -static JWT_DELETE_ISSUER: Lazy = Lazy::new(|| format!("{}|delete", CONFIG.domain_origin())); -static JWT_VERIFYEMAIL_ISSUER: Lazy = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin())); -static JWT_ADMIN_ISSUER: Lazy = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin())); -static JWT_SEND_ISSUER: Lazy = Lazy::new(|| format!("{}|send", CONFIG.domain_origin())); -static JWT_ORG_API_KEY_ISSUER: Lazy = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin())); -static JWT_FILE_DOWNLOAD_ISSUER: Lazy = Lazy::new(|| format!("{}|file_download", CONFIG.domain_origin())); + Lazy::new(|| format!("{}|emergencyaccessinvite", jwt_origin())); +static JWT_DELETE_ISSUER: Lazy = Lazy::new(|| format!("{}|delete", jwt_origin())); +static JWT_VERIFYEMAIL_ISSUER: Lazy = Lazy::new(|| format!("{}|verifyemail", jwt_origin())); +static JWT_ADMIN_ISSUER: Lazy = Lazy::new(|| format!("{}|admin", jwt_origin())); +static JWT_SEND_ISSUER: Lazy = Lazy::new(|| format!("{}|send", jwt_origin())); +static JWT_ORG_API_KEY_ISSUER: Lazy = Lazy::new(|| format!("{}|api.organization", jwt_origin())); +static JWT_FILE_DOWNLOAD_ISSUER: Lazy = Lazy::new(|| format!("{}|file_download", jwt_origin())); static PRIVATE_RSA_KEY: OnceCell = OnceCell::new(); static PUBLIC_RSA_KEY: OnceCell = OnceCell::new(); @@ -422,7 +426,7 @@ impl<'r> FromRequest<'r> for HostInfo { let base_url_origin = format!("{protocol}://{host}"); return Outcome::Success(HostInfo { - base_url: base_url_origin, + base_url: base_url_origin.clone(), origin: base_url_origin, }); } @@ -440,7 +444,7 @@ impl<'r> FromRequest<'r> for ClientHeaders { type Error = &'static str; async fn from_request(request: &'r Request<'_>) -> Outcome { - let base_url = try_outcome!(Domain::from_request(request).await).base_url; + let base_url = try_outcome!(HostInfo::from_request(request).await).base_url; let ip = match ClientIp::from_request(request).await { Outcome::Success(ip) => ip, _ => err_handler!("Error getting Client IP"), From f20863096f02354ca4ffa3dd55b0eb00901624ba Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:55:43 +0200 Subject: [PATCH 25/40] fix issue in config --- src/config.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 1f62c22f51..791668b161 100644 --- a/src/config.rs +++ b/src/config.rs @@ -145,7 +145,8 @@ macro_rules! make_config { )+)+ config.domain_set = _domain_set; - config.domain_change_back = config.domain_change_back.split(',').map(|d| d.trim_end_matches('/')).fold(String::new(), |acc, d| { + // Remove slash from every domain + config.domain_change_back = config.domain_change_back.split(',').map(|d| d.trim_end_matches('/')).fold(String::new(), |mut acc, d| { acc.push_str(d); acc.push(','); acc From c0db0d8da0008085a39b1889fdc8e90294b98f2c Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:59:20 +0200 Subject: [PATCH 26/40] make clippy happy --- src/api/admin.rs | 2 +- src/api/core/two_factor/webauthn.rs | 4 ++-- src/api/web.rs | 1 - src/config.rs | 6 +++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 803fa39a61..f8b83581ad 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -99,7 +99,7 @@ const BASE_TEMPLATE: &str = "admin/base"; const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000"; fn admin_path() -> String { - format!("{}", ADMIN_PATH) + ADMIN_PATH.to_string() } #[derive(Debug)] diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index bf1460b803..2f8efaab5b 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -54,9 +54,9 @@ struct WebauthnConfig { impl WebauthnConfig { fn load(domain: &str, domain_origin: &str) -> Webauthn { Webauthn::new(Self { - rpid: Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(), + rpid: Url::parse(domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(), url: domain.to_string(), - origin: Url::parse(&domain_origin).unwrap(), + origin: Url::parse(domain_origin).unwrap(), }) } } diff --git a/src/api/web.rs b/src/api/web.rs index 5f4c961d5b..1920d749f4 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -6,7 +6,6 @@ use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, auth::{decode_file_download, HostInfo}, - config::extract_url_host, error::Error, util::{Cached, SafeString}, CONFIG, diff --git a/src/config.rs b/src/config.rs index 791668b161..0a9f80d6f4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -436,7 +436,7 @@ make_config! { domain_set: bool, false, def, false; /// Domain path |> Domain URL path (in https://example.com:8443/path, /path is the path) /// MUST be the same for all domains. - domain_path: String, false, auto, |c| extract_url_path(&c.domain_change_back.split(',').nth(0).expect("Missing domain")); + domain_path: String, false, auto, |c| extract_url_path(c.domain_change_back.split(',').next().expect("Missing domain")); /// Enable web vault web_vault_enabled: bool, false, def, true; @@ -1301,7 +1301,7 @@ impl Config { (extract_url_host(d), extract_url_origin(d)) }) .collect() - }).get(host).map(|h| h.clone()) + }).get(host).cloned() } pub fn host_to_domain(&self, host: &str) -> Option { @@ -1314,7 +1314,7 @@ impl Config { (extract_url_host(d), extract_url_path(d)) }) .collect() - }).get(host).map(|h| h.clone()) + }).get(host).cloned() } // Yes this is a base_url From 3a6677207795fe9dfb434674d6ff36d4229fb197 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 15:30:15 +0200 Subject: [PATCH 27/40] use single hashmap instead of two for domain lookups --- src/auth.rs | 1 + src/config.rs | 36 ++++++++++++++++++------------------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 8a15a26fb2..4a5b0b4e5e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -365,6 +365,7 @@ use crate::db::{ DbConn, }; +#[derive(Clone)] pub struct HostInfo { pub base_url: String, pub origin: String, diff --git a/src/config.rs b/src/config.rs index 0a9f80d6f4..b25767fc9c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,7 @@ use once_cell::sync::Lazy; use reqwest::Url; use crate::{ + auth::HostInfo, db::DbConnType, error::Error, util::{get_env, get_env_bool, parse_experimental_client_feature_flags}, @@ -49,8 +50,7 @@ macro_rules! make_config { _overrides: Vec, - domain_hostmap: OnceLock, - domain_origins: OnceLock, + domain_hostmap: OnceLock>, } #[derive(Clone, Default, Deserialize, Serialize)] @@ -347,8 +347,6 @@ macro_rules! make_config { } -type HostHashMap = HashMap; - //STRUCTURE: // /// Short description (without this they won't appear on the list) // group { @@ -1121,7 +1119,6 @@ impl Config { _env, _usr, _overrides, - domain_origins: OnceLock::new(), domain_hostmap: OnceLock::new(), }), }) @@ -1291,30 +1288,33 @@ impl Config { } } - pub fn domain_origin(&self, host: &str) -> Option { + + fn get_domain_hostmap(&self, host: &str) -> Option { // This is done to prevent deadlock, when read-locking an rwlock twice let domains = self.domain_change_back(); - self.inner.read().unwrap().domain_origins.get_or_init(|| { + self.inner.read().unwrap().domain_hostmap.get_or_init(|| { domains.split(',') .map(|d| { - (extract_url_host(d), extract_url_origin(d)) + let host_info = HostInfo { + base_url: d.to_string(), + origin: extract_url_origin(d), + }; + + (extract_url_host(d), host_info) }) .collect() }).get(host).cloned() } - pub fn host_to_domain(&self, host: &str) -> Option { - // This is done to prevent deadlock, when read-locking an rwlock twice - let domains = self.domain_change_back(); + pub fn domain_origin(&self, host: &str) -> Option { + self.get_domain_hostmap(host) + .map(|v| v.origin) + } - self.inner.read().unwrap().domain_hostmap.get_or_init(|| { - domains.split(',') - .map(|d| { - (extract_url_host(d), extract_url_path(d)) - }) - .collect() - }).get(host).cloned() + pub fn host_to_domain(&self, host: &str) -> Option { + self.get_domain_hostmap(host) + .map(|v| v.base_url) } // Yes this is a base_url From 12bdcd447d5e5174ee6bf814222a262f5e7e4418 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 15:32:50 +0200 Subject: [PATCH 28/40] clippy and format --- src/api/admin.rs | 7 ++++- src/api/core/ciphers.rs | 22 ++++++++++++--- src/api/core/organizations.rs | 5 ++-- src/api/core/two_factor/webauthn.rs | 32 +++++++++++++++++---- src/api/identity.rs | 23 ++++++++++++--- src/auth.rs | 16 +++++++---- src/config.rs | 44 ++++++++++++++++------------- src/util.rs | 2 +- 8 files changed, 107 insertions(+), 44 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index f8b83581ad..d2b55ea478 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -669,7 +669,12 @@ async fn get_ntp_time(has_http_access: bool) -> String { } #[get("/diagnostics")] -async fn diagnostics(_token: AdminToken, ip_header: IpHeader, host_info: HostInfo, mut conn: DbConn) -> ApiResult> { +async fn diagnostics( + _token: AdminToken, + ip_header: IpHeader, + host_info: HostInfo, + mut conn: DbConn, +) -> ApiResult> { use chrono::prelude::*; use std::net::ToSocketAddrs; diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 924ed40ae4..d9b5d544e2 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -113,8 +113,15 @@ async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { ciphers_json.push( - c.to_json(&headers.base_url, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn, ()) - .await, + c.to_json( + &headers.base_url, + &headers.user.uuid, + Some(&cipher_sync_data), + CipherSyncType::User, + &mut conn, + (), + ) + .await, ); } @@ -160,8 +167,15 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json { let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { ciphers_json.push( - c.to_json(&headers.base_url, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn, ()) - .await, + c.to_json( + &headers.base_url, + &headers.user.uuid, + Some(&cipher_sync_data), + CipherSyncType::User, + &mut conn, + (), + ) + .await, ); } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 0e5ba1cea6..4ad96a1d89 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -777,8 +777,9 @@ async fn _get_org_details(org_id: &str, base_url: &str, user_uuid: &str, conn: & let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { - ciphers_json - .push(c.to_json(base_url, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn, ()).await); + ciphers_json.push( + c.to_json(base_url, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn, ()).await, + ); } json!(ciphers_json) } diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 2f8efaab5b..96e33ba210 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -249,7 +249,12 @@ impl From for PublicKeyCredential { } #[post("/two-factor/webauthn", data = "")] -async fn activate_webauthn(data: JsonUpcase, headers: Headers, host_info: HostInfo, mut conn: DbConn) -> JsonResult { +async fn activate_webauthn( + data: JsonUpcase, + headers: Headers, + host_info: HostInfo, + mut conn: DbConn, +) -> JsonResult { let data: EnableWebauthnData = data.into_inner().data; let mut user = headers.user; @@ -272,8 +277,11 @@ async fn activate_webauthn(data: JsonUpcase, headers: Header }; // Verify the credentials with the saved state - let (credential, _data) = - WebauthnConfig::load(&host_info.base_url, &host_info.origin).register_credential(&data.DeviceResponse.into(), &state, |_| Ok(false))?; + let (credential, _data) = WebauthnConfig::load(&host_info.base_url, &host_info.origin).register_credential( + &data.DeviceResponse.into(), + &state, + |_| Ok(false), + )?; let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1; // TODO: Check for repeated ID's @@ -302,7 +310,12 @@ async fn activate_webauthn(data: JsonUpcase, headers: Header } #[put("/two-factor/webauthn", data = "")] -async fn activate_webauthn_put(data: JsonUpcase, headers: Headers, host_info: HostInfo, conn: DbConn) -> JsonResult { +async fn activate_webauthn_put( + data: JsonUpcase, + headers: Headers, + host_info: HostInfo, + conn: DbConn, +) -> JsonResult { activate_webauthn(data, headers, host_info, conn).await } @@ -385,7 +398,8 @@ pub async fn generate_webauthn_login(user_uuid: &str, base_url: &str, origin: &s // Generate a challenge based on the credentials let ext = RequestAuthenticationExtensions::builder().appid(format!("{}/app-id.json", base_url)).build(); - let (response, state) = WebauthnConfig::load(base_url, origin).generate_challenge_authenticate_options(creds, Some(ext))?; + let (response, state) = + WebauthnConfig::load(base_url, origin).generate_challenge_authenticate_options(creds, Some(ext))?; // Save the challenge state for later validation TwoFactor::new(user_uuid.into(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?) @@ -396,7 +410,13 @@ pub async fn generate_webauthn_login(user_uuid: &str, base_url: &str, origin: &s Ok(Json(serde_json::to_value(response.public_key)?)) } -pub async fn validate_webauthn_login(user_uuid: &str, response: &str, base_url: &str, origin: &str, conn: &mut DbConn) -> EmptyResult { +pub async fn validate_webauthn_login( + user_uuid: &str, + response: &str, + base_url: &str, + origin: &str, + conn: &mut DbConn, +) -> EmptyResult { let type_ = TwoFactorType::WebauthnLoginChallenge as i32; let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await { Some(tf) => { diff --git a/src/api/identity.rs b/src/api/identity.rs index 0155bc2852..55e2556f00 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -28,7 +28,12 @@ pub fn routes() -> Vec { } #[post("/connect/token", data = "")] -async fn login(data: Form, client_header: ClientHeaders, host_info: HostInfo, mut conn: DbConn) -> JsonResult { +async fn login( + data: Form, + client_header: ClientHeaders, + host_info: HostInfo, + mut conn: DbConn, +) -> JsonResult { let data: ConnectData = data.into_inner(); let mut user_uuid: Option = None; @@ -48,7 +53,8 @@ async fn login(data: Form, client_header: ClientHeaders, host_info: _check_is_some(&data.device_name, "device_name cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?; - _password_login(data, &mut user_uuid, &mut conn, &client_header.ip, &host_info.base_url, &host_info.origin).await + _password_login(data, &mut user_uuid, &mut conn, &client_header.ip, &host_info.base_url, &host_info.origin) + .await } "client_credentials" => { _check_is_some(&data.client_id, "client_id cannot be blank")?; @@ -501,7 +507,10 @@ async fn twofactor_auth( let twofactor_code = match data.two_factor_token { Some(ref code) => code, - None => err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, base_url, origin, conn).await?, "2FA token not provided"), + None => err_json!( + _json_err_twofactor(&twofactor_ids, &user.uuid, base_url, origin, conn).await?, + "2FA token not provided" + ), }; let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled); @@ -559,7 +568,13 @@ fn _selected_data(tf: Option) -> ApiResult { tf.map(|t| t.data).map_res("Two factor doesn't exist") } -async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, base_url: &str, origin: &str, conn: &mut DbConn) -> ApiResult { +async fn _json_err_twofactor( + providers: &[i32], + user_uuid: &str, + base_url: &str, + origin: &str, + conn: &mut DbConn, +) -> ApiResult { let mut result = json!({ "error" : "invalid_grant", "error_description" : "Two factor required.", diff --git a/src/auth.rs b/src/auth.rs index 4a5b0b4e5e..b904299b1e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -9,8 +9,8 @@ use openssl::rsa::Rsa; use serde::de::DeserializeOwned; use serde::ser::Serialize; -use crate::{error::Error, CONFIG}; use crate::config::extract_url_origin; +use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -394,11 +394,15 @@ impl<'r> FromRequest<'r> for HostInfo { // TODO fix error handling // This is probably a 421 misdirected request - let (base_url, origin) = CONFIG.host_to_domain(host).and_then(|base_url| { - Some((base_url, CONFIG.domain_origin(host)?)) - }).expect("This should not be merged like this!!!"); - - return Outcome::Success(HostInfo { base_url, origin }); + let (base_url, origin) = CONFIG + .host_to_domain(host) + .and_then(|base_url| Some((base_url, CONFIG.domain_origin(host)?))) + .expect("This should not be merged like this!!!"); + + return Outcome::Success(HostInfo { + base_url, + origin, + }); } else if let Some(referer) = headers.get_one("Referer") { return Outcome::Success(HostInfo { base_url: referer.to_string(), diff --git a/src/config.rs b/src/config.rs index b25767fc9c..f998045b50 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ -use std::{env::consts::EXE_SUFFIX, collections::HashMap}; use std::process::exit; -use std::sync::RwLock; use std::sync::OnceLock; +use std::sync::RwLock; +use std::{collections::HashMap, env::consts::EXE_SUFFIX}; use job_scheduler_ng::Schedule; use once_cell::sync::Lazy; @@ -1050,7 +1050,7 @@ fn generate_smtp_img_src(embed_images: bool, domains: &str) -> String { if embed_images { "cid:".to_string() } else { - let domain = domains.split(',').nth(0).expect("Domain missing"); + let domain = domains.split(',').next().expect("Domain missing"); format!("{domain}/vw_static/") } } @@ -1288,33 +1288,37 @@ impl Config { } } - fn get_domain_hostmap(&self, host: &str) -> Option { // This is done to prevent deadlock, when read-locking an rwlock twice let domains = self.domain_change_back(); - self.inner.read().unwrap().domain_hostmap.get_or_init(|| { - domains.split(',') - .map(|d| { - let host_info = HostInfo { - base_url: d.to_string(), - origin: extract_url_origin(d), - }; - - (extract_url_host(d), host_info) - }) - .collect() - }).get(host).cloned() + self.inner + .read() + .unwrap() + .domain_hostmap + .get_or_init(|| { + domains + .split(',') + .map(|d| { + let host_info = HostInfo { + base_url: d.to_string(), + origin: extract_url_origin(d), + }; + + (extract_url_host(d), host_info) + }) + .collect() + }) + .get(host) + .cloned() } pub fn domain_origin(&self, host: &str) -> Option { - self.get_domain_hostmap(host) - .map(|v| v.origin) + self.get_domain_hostmap(host).map(|v| v.origin) } pub fn host_to_domain(&self, host: &str) -> Option { - self.get_domain_hostmap(host) - .map(|v| v.base_url) + self.get_domain_hostmap(host).map(|v| v.base_url) } // Yes this is a base_url diff --git a/src/util.rs b/src/util.rs index b00fb205c7..c960b5cb90 100644 --- a/src/util.rs +++ b/src/util.rs @@ -17,7 +17,7 @@ use tokio::{ time::{sleep, Duration}, }; -use crate::{CONFIG, config::extract_url_host}; +use crate::{config::extract_url_host, CONFIG}; pub struct AppHeaders(); From fc78b6f4b3643a85bc020c041067ed52e6ee95cb Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 18:04:41 +0200 Subject: [PATCH 29/40] implement error handling for HostInfo extractor if host isn't an allowed domain, we default to the main domain. --- src/auth.rs | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index b904299b1e..57534769b4 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -9,7 +9,7 @@ use openssl::rsa::Rsa; use serde::de::DeserializeOwned; use serde::ser::Serialize; -use crate::config::extract_url_origin; +use crate::config::{extract_url_origin, extract_url_host}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -371,6 +371,17 @@ pub struct HostInfo { pub origin: String, } +fn get_host_info(host: &str) -> Option { + CONFIG + .host_to_domain(host) + .and_then(|base_url| Some((base_url, CONFIG.domain_origin(host)?))) + .and_then(|(base_url, origin)| Some(HostInfo { base_url, origin })) +} + +fn get_main_host() -> String { + extract_url_host(&CONFIG.main_domain()) +} + #[rocket::async_trait] impl<'r> FromRequest<'r> for HostInfo { type Error = &'static str; @@ -381,28 +392,20 @@ impl<'r> FromRequest<'r> for HostInfo { // Get host // TODO: UPDATE THIS SECTION if CONFIG.domain_set() { - let host = if let Some(host) = headers.get_one("X-Forwarded-Host") { - host + let host: Cow<'_, str> = if let Some(host) = headers.get_one("X-Forwarded-Host") { + host.into() } else if let Some(host) = headers.get_one("Host") { - host + host.into() } else { - // TODO fix error handling - // This is probably a 400 bad request, - // because http requests require the host header - todo!() + get_main_host().into() }; + + let host_info = get_host_info(host.as_ref()) + .unwrap_or_else(|| { + get_host_info(&get_main_host()).expect("Main domain doesn't have entry!") + }); - // TODO fix error handling - // This is probably a 421 misdirected request - let (base_url, origin) = CONFIG - .host_to_domain(host) - .and_then(|base_url| Some((base_url, CONFIG.domain_origin(host)?))) - .expect("This should not be merged like this!!!"); - - return Outcome::Success(HostInfo { - base_url, - origin, - }); + return Outcome::Success(host_info); } else if let Some(referer) = headers.get_one("Referer") { return Outcome::Success(HostInfo { base_url: referer.to_string(), @@ -852,6 +855,7 @@ impl<'r> FromRequest<'r> for OwnerHeaders { } } +use std::borrow::Cow; // // Client IP address detection // From d627b02c5fa1d19b1a372b027c8135cc71acfd69 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 18:08:42 +0200 Subject: [PATCH 30/40] remove admin_path function since it just returns a const --- src/api/admin.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index d2b55ea478..830f64f0cf 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -98,7 +98,7 @@ const BASE_TEMPLATE: &str = "admin/base"; const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000"; -fn admin_path() -> String { +fn admin_path() -> String { ADMIN_PATH.to_string() } @@ -125,9 +125,10 @@ impl<'r> FromRequest<'r> for IpHeader { } fn admin_url(base_url: &str) -> String { - format!("{}{}", base_url, admin_path()) + format!("{}{}", base_url, ADMIN_PATH) } + #[derive(Responder)] enum AdminResponse { #[response(status = 200)] @@ -197,7 +198,7 @@ fn post_admin_login(data: Form, cookies: &CookieJar<'_>, ip: ClientIp cookies.add(cookie); if let Some(redirect) = redirect { - Ok(Redirect::to(format!("{}{}", admin_path(), redirect))) + Ok(Redirect::to(format!("{}{}", ADMIN_PATH, redirect))) } else { Err(AdminResponse::Ok(render_admin_page())) } From 96261f1284ed5a468ac11644e2a3453dca9b97d3 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 18:26:56 +0200 Subject: [PATCH 31/40] remove breaking parameter from to_json methods --- src/api/core/ciphers.rs | 20 +++++++++----------- src/api/core/emergency_access.rs | 1 - src/api/core/organizations.rs | 2 +- src/api/web.rs | 3 +-- src/db/models/attachment.rs | 8 +++----- src/db/models/cipher.rs | 5 ++--- 6 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index d9b5d544e2..301dc29f11 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -119,7 +119,6 @@ async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json Some(&cipher_sync_data), CipherSyncType::User, &mut conn, - (), ) .await, ); @@ -173,7 +172,6 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json { Some(&cipher_sync_data), CipherSyncType::User, &mut conn, - (), ) .await, ); @@ -197,7 +195,7 @@ async fn get_cipher(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResul err!("Cipher is not owned by user") } - Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } #[get("/ciphers//admin")] @@ -337,7 +335,7 @@ async fn post_ciphers(data: JsonUpcase, headers: Headers, mut conn: let mut cipher = Cipher::new(data.Type, data.Name.clone()); update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &nt, UpdateType::SyncCipherCreate).await?; - Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } /// Enforces the personal ownership policy on user-owned ciphers, if applicable. @@ -664,7 +662,7 @@ async fn put_cipher( update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &nt, UpdateType::SyncCipherUpdate).await?; - Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } #[post("/ciphers//partial", data = "")] @@ -708,7 +706,7 @@ async fn put_cipher_partial( // Update favorite cipher.set_favorite(Some(data.Favorite), &headers.user.uuid, &mut conn).await?; - Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } #[derive(Deserialize)] @@ -939,7 +937,7 @@ async fn share_cipher_by_uuid( update_cipher_from_data(&mut cipher, data.Cipher, headers, shared_to_collection, conn, nt, ut).await?; - Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, conn, ()).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, conn).await)) } /// v2 API for downloading an attachment. This just redirects the client to @@ -960,7 +958,7 @@ async fn get_attachment(uuid: &str, attachment_id: &str, headers: Headers, mut c } match Attachment::find_by_id(attachment_id, &mut conn).await { - Some(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.base_url, ()))), + Some(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.base_url))), Some(_) => err!("Attachment doesn't belong to cipher"), None => err!("Attachment doesn't exist"), } @@ -1020,7 +1018,7 @@ async fn post_attachment_v2( "AttachmentId": attachment_id, "Url": url, "FileUploadType": FileUploadType::Direct as i32, - response_key: cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await, + response_key: cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await, }))) } @@ -1247,7 +1245,7 @@ async fn post_attachment( let (cipher, mut conn) = save_attachment(attachment, uuid, data, &headers, conn, nt).await?; - Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn, ()).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } #[post("/ciphers//attachment-admin", format = "multipart/form-data", data = "")] @@ -1681,7 +1679,7 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon .await; } - Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, conn, ()).await)) + Ok(Json(cipher.to_json(&headers.base_url, &headers.user.uuid, None, CipherSyncType::User, conn).await)) } async fn _restore_multiple_ciphers( diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 029acb9ba5..314bb616de 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -608,7 +608,6 @@ async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn Some(&cipher_sync_data), CipherSyncType::User, &mut conn, - (), ) .await, ); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 4ad96a1d89..9975ee5e19 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -778,7 +778,7 @@ async fn _get_org_details(org_id: &str, base_url: &str, user_uuid: &str, conn: & let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { ciphers_json.push( - c.to_json(base_url, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn, ()).await, + c.to_json(base_url, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await, ); } json!(ciphers_json) diff --git a/src/api/web.rs b/src/api/web.rs index 1920d749f4..8dc1c64938 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -65,8 +65,7 @@ fn web_index_head() -> EmptyResult { fn app_id(host_info: HostInfo) -> Cached<(ContentType, Json)> { let content_type = ContentType::new("application", "fido.trusted-apps+json"); - // TODO_MAYBE: add an extractor for getting the origin, so we only have to do 1 lookup. - // Also I'm not sure if we shouldn't return all origins. + // TODO Maybe return all available origins. let origin = host_info.origin; Cached::long( diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index a1f1604d1e..6d3df3693d 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -35,17 +35,15 @@ impl Attachment { format!("{}/{}/{}", CONFIG.attachments_folder(), self.cipher_uuid, self.id) } - // TODO: Change back - pub fn get_url(&self, base_url: &str, _parameter_to_break_existing_uses: ()) -> String { + pub fn get_url(&self, base_url: &str) -> String { let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone())); format!("{}/attachments/{}/{}?token={}", base_url, self.cipher_uuid, self.id, token) } - // TODO: Change back - pub fn to_json(&self, base_url: &str, _parameter_to_break_existing_uses: ()) -> Value { + pub fn to_json(&self, base_url: &str) -> Value { json!({ "Id": self.id, - "Url": self.get_url(base_url, ()), + "Url": self.get_url(base_url), "FileName": self.file_name, "Size": self.file_size.to_string(), "SizeName": crate::util::get_display_size(self.file_size), diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index f9192f6c24..096e198436 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -121,19 +121,18 @@ impl Cipher { cipher_sync_data: Option<&CipherSyncData>, sync_type: CipherSyncType, conn: &mut DbConn, - _parameter_to_break_existing_uses: (), ) -> Value { use crate::util::format_date; let mut attachments_json: Value = Value::Null; if let Some(cipher_sync_data) = cipher_sync_data { if let Some(attachments) = cipher_sync_data.cipher_attachments.get(&self.uuid) { - attachments_json = attachments.iter().map(|c| c.to_json(base_url, ())).collect(); + attachments_json = attachments.iter().map(|c| c.to_json(base_url)).collect(); } } else { let attachments = Attachment::find_by_cipher(&self.uuid, conn).await; if !attachments.is_empty() { - attachments_json = attachments.iter().map(|c| c.to_json(base_url, ())).collect() + attachments_json = attachments.iter().map(|c| c.to_json(base_url)).collect() } } From edcd2640d0d2e0b9e4be823ab2c70d44a84673f4 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 18:28:02 +0200 Subject: [PATCH 32/40] cargo clippy and cargo fmt --- src/api/core/ciphers.rs | 20 ++++---------------- src/api/core/organizations.rs | 5 ++--- src/auth.rs | 18 +++++++++--------- 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 301dc29f11..62c960ea98 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -113,14 +113,8 @@ async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { ciphers_json.push( - c.to_json( - &headers.base_url, - &headers.user.uuid, - Some(&cipher_sync_data), - CipherSyncType::User, - &mut conn, - ) - .await, + c.to_json(&headers.base_url, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn) + .await, ); } @@ -166,14 +160,8 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json { let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { ciphers_json.push( - c.to_json( - &headers.base_url, - &headers.user.uuid, - Some(&cipher_sync_data), - CipherSyncType::User, - &mut conn, - ) - .await, + c.to_json(&headers.base_url, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn) + .await, ); } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 9975ee5e19..4ff411396c 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -777,9 +777,8 @@ async fn _get_org_details(org_id: &str, base_url: &str, user_uuid: &str, conn: & let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { - ciphers_json.push( - c.to_json(base_url, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await, - ); + ciphers_json + .push(c.to_json(base_url, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await); } json!(ciphers_json) } diff --git a/src/auth.rs b/src/auth.rs index 57534769b4..c095a1aef0 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -9,7 +9,7 @@ use openssl::rsa::Rsa; use serde::de::DeserializeOwned; use serde::ser::Serialize; -use crate::config::{extract_url_origin, extract_url_host}; +use crate::config::{extract_url_host, extract_url_origin}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -372,10 +372,12 @@ pub struct HostInfo { } fn get_host_info(host: &str) -> Option { - CONFIG - .host_to_domain(host) - .and_then(|base_url| Some((base_url, CONFIG.domain_origin(host)?))) - .and_then(|(base_url, origin)| Some(HostInfo { base_url, origin })) + CONFIG.host_to_domain(host).and_then(|base_url| Some((base_url, CONFIG.domain_origin(host)?))).map( + |(base_url, origin)| HostInfo { + base_url, + origin, + }, + ) } fn get_main_host() -> String { @@ -399,11 +401,9 @@ impl<'r> FromRequest<'r> for HostInfo { } else { get_main_host().into() }; - + let host_info = get_host_info(host.as_ref()) - .unwrap_or_else(|| { - get_host_info(&get_main_host()).expect("Main domain doesn't have entry!") - }); + .unwrap_or_else(|| get_host_info(&get_main_host()).expect("Main domain doesn't have entry!")); return Outcome::Success(host_info); } else if let Some(referer) = headers.get_one("Referer") { From 09c0367571f13a5b8ad14b1050c07109e7b94cfb Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 18:46:14 +0200 Subject: [PATCH 33/40] re-add domain_origin field to configuration --- src/auth.rs | 2 +- src/config.rs | 26 +++++++++++++++++++++++++- src/util.rs | 2 +- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index c095a1aef0..400d222684 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -372,7 +372,7 @@ pub struct HostInfo { } fn get_host_info(host: &str) -> Option { - CONFIG.host_to_domain(host).and_then(|base_url| Some((base_url, CONFIG.domain_origin(host)?))).map( + CONFIG.host_to_domain(host).and_then(|base_url| Some((base_url, CONFIG.host_to_origin(host)?))).map( |(base_url, origin)| HostInfo { base_url, origin, diff --git a/src/config.rs b/src/config.rs index f998045b50..60c2a2fc03 100644 --- a/src/config.rs +++ b/src/config.rs @@ -432,6 +432,9 @@ make_config! { domain_change_back: String, true, def, "http://localhost".to_string(); /// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used. domain_set: bool, false, def, false; + /// Comma seperated list of domain origins |> Domain URL origin (in https://example.com:8443/path, https://example.com:8443 is the origin) + /// If specified manually, one entry needs to exist for every url in domain. + domain_origin: String, false, auto, |c| extract_origins(&c.domain_change_back); /// Domain path |> Domain URL path (in https://example.com:8443/path, /path is the path) /// MUST be the same for all domains. domain_path: String, false, auto, |c| extract_url_path(c.domain_change_back.split(',').next().expect("Missing domain")); @@ -741,6 +744,10 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } } + if cfg.domain_change_back.split(',').count() != cfg.domain_origin.split(',').count() { + err!("Each DOMAIN_ORIGIN entry corresponds to exactly one entry in DOMAIN."); + } + let whitelist = &cfg.signups_domains_whitelist; if !whitelist.is_empty() && whitelist.split(',').any(|d| d.trim().is_empty()) { err!("`SIGNUPS_DOMAINS_WHITELIST` contains empty tokens"); @@ -1012,6 +1019,23 @@ pub fn extract_url_origin(url: &str) -> String { } } +// urls should be comma-seperated +fn extract_origins(urls: &str) -> String { + let mut origins = urls.split(',') + .map(extract_url_origin) + // TODO add itertools as dependency maybe + .fold(String::new(), |mut acc, origin| { + acc.push_str(&origin); + acc.push(','); + acc + }); + + // Pop trailing comma + origins.pop(); + + origins +} + /// Extracts the path from a URL. /// All trailing '/' chars are trimmed, even if the path is a lone '/'. fn extract_url_path(url: &str) -> String { @@ -1313,7 +1337,7 @@ impl Config { .cloned() } - pub fn domain_origin(&self, host: &str) -> Option { + pub fn host_to_origin(&self, host: &str) -> Option { self.get_domain_hostmap(host).map(|v| v.origin) } diff --git a/src/util.rs b/src/util.rs index c960b5cb90..df182bdf43 100644 --- a/src/util.rs +++ b/src/util.rs @@ -130,7 +130,7 @@ impl Cors { fn get_allowed_origin(headers: &HeaderMap<'_>) -> Option { let origin = Cors::get_header(headers, "Origin"); - let domain_origin_opt = CONFIG.domain_origin(&extract_url_host(&origin)); + let domain_origin_opt = CONFIG.host_to_origin(&extract_url_host(&origin)); let safari_extension_origin = "file://"; let found_origin = { From 298cf8adcba0bfa1bc5a3bf56daa90335a5823f9 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 18:52:16 +0200 Subject: [PATCH 34/40] change back name of domain configuration option --- src/config.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/config.rs b/src/config.rs index 60c2a2fc03..4e4a966391 100644 --- a/src/config.rs +++ b/src/config.rs @@ -139,21 +139,21 @@ macro_rules! make_config { fn build(&self) -> ConfigItems { let mut config = ConfigItems::default(); - let _domain_set = self.domain_change_back.is_some(); + let _domain_set = self.domain.is_some(); $($( config.$name = make_config!{ @build self.$name.clone(), &config, $none_action, $($default)? }; )+)+ config.domain_set = _domain_set; // Remove slash from every domain - config.domain_change_back = config.domain_change_back.split(',').map(|d| d.trim_end_matches('/')).fold(String::new(), |mut acc, d| { + config.domain = config.domain.split(',').map(|d| d.trim_end_matches('/')).fold(String::new(), |mut acc, d| { acc.push_str(d); acc.push(','); acc }); // Remove trailing comma - config.domain_change_back.pop(); + config.domain.pop(); config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase(); config.org_creation_users = config.org_creation_users.trim().to_lowercase(); @@ -428,16 +428,15 @@ make_config! { settings { /// Comma seperated list of Domain URLs |> This needs to be set to the URL used to access the server, including /// 'http[s]://' and port, if it's different than the default. Some server functions don't work correctly without this value - // TODO: Change back, this is only done to break existing references - domain_change_back: String, true, def, "http://localhost".to_string(); + domain: String, true, def, "http://localhost".to_string(); /// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used. domain_set: bool, false, def, false; /// Comma seperated list of domain origins |> Domain URL origin (in https://example.com:8443/path, https://example.com:8443 is the origin) /// If specified manually, one entry needs to exist for every url in domain. - domain_origin: String, false, auto, |c| extract_origins(&c.domain_change_back); + domain_origin: String, false, auto, |c| extract_origins(&c.domain); /// Domain path |> Domain URL path (in https://example.com:8443/path, /path is the path) /// MUST be the same for all domains. - domain_path: String, false, auto, |c| extract_url_path(c.domain_change_back.split(',').next().expect("Missing domain")); + domain_path: String, false, auto, |c| extract_url_path(c.domain.split(',').next().expect("Missing domain")); /// Enable web vault web_vault_enabled: bool, false, def, true; @@ -682,7 +681,7 @@ make_config! { /// Embed images as email attachments. smtp_embed_images: bool, true, def, true; /// _smtp_img_src - _smtp_img_src: String, false, gen, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain_change_back); + _smtp_img_src: String, false, gen, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain); /// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting! smtp_debug: bool, false, def, false; /// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks! @@ -735,7 +734,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } } - let domains = cfg.domain_change_back.split(',').map(|d| d.to_string().to_lowercase()); + let domains = cfg.domain.split(',').map(|d| d.to_string().to_lowercase()); for dom in domains { if !dom.starts_with("http://") && !dom.starts_with("https://") { err!( @@ -744,7 +743,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } } - if cfg.domain_change_back.split(',').count() != cfg.domain_origin.split(',').count() { + if cfg.domain.split(',').count() != cfg.domain_origin.split(',').count() { err!("Each DOMAIN_ORIGIN entry corresponds to exactly one entry in DOMAIN."); } @@ -1314,7 +1313,7 @@ impl Config { fn get_domain_hostmap(&self, host: &str) -> Option { // This is done to prevent deadlock, when read-locking an rwlock twice - let domains = self.domain_change_back(); + let domains = self.domain(); self.inner .read() @@ -1348,7 +1347,7 @@ impl Config { // Yes this is a base_url // But the configuration precedent says, that we call this a domain. pub fn main_domain(&self) -> String { - self.domain_change_back().split(',').nth(0).expect("Missing domain").to_string() + self.domain().split(',').nth(0).expect("Missing domain").to_string() } } From 335984ee28a3b6a928d3cda6354c9b1d0a061093 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 18:53:45 +0200 Subject: [PATCH 35/40] cargo clippy and cargo fmt --- src/config.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/config.rs b/src/config.rs index 4e4a966391..5c4a9616e3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1019,8 +1019,9 @@ pub fn extract_url_origin(url: &str) -> String { } // urls should be comma-seperated -fn extract_origins(urls: &str) -> String { - let mut origins = urls.split(',') +fn extract_origins(urls: &str) -> String { + let mut origins = urls + .split(',') .map(extract_url_origin) // TODO add itertools as dependency maybe .fold(String::new(), |mut acc, origin| { @@ -1028,10 +1029,10 @@ fn extract_origins(urls: &str) -> String { acc.push(','); acc }); - + // Pop trailing comma origins.pop(); - + origins } From d1cb726996d6fa4e8e4aa4c40b0feabbd829d0fb Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 19:21:32 +0200 Subject: [PATCH 36/40] fix bug when extracing host from domain --- src/auth.rs | 28 +++++++++++++++++++--------- src/config.rs | 1 + 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 400d222684..b86c39254e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -365,7 +365,7 @@ use crate::db::{ DbConn, }; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct HostInfo { pub base_url: String, pub origin: String, @@ -393,7 +393,8 @@ impl<'r> FromRequest<'r> for HostInfo { // Get host // TODO: UPDATE THIS SECTION - if CONFIG.domain_set() { + let host_info = if CONFIG.domain_set() { + log::debug!("Using configured host info"); let host: Cow<'_, str> = if let Some(host) = headers.get_one("X-Forwarded-Host") { host.into() } else if let Some(host) = headers.get_one("Host") { @@ -403,15 +404,20 @@ impl<'r> FromRequest<'r> for HostInfo { }; let host_info = get_host_info(host.as_ref()) - .unwrap_or_else(|| get_host_info(&get_main_host()).expect("Main domain doesn't have entry!")); + .unwrap_or_else(|| { + log::debug!("Falling back to default domain, because {host} was not in domains."); + get_host_info(&get_main_host()).expect("Main domain doesn't have entry!") + }); - return Outcome::Success(host_info); + host_info } else if let Some(referer) = headers.get_one("Referer") { - return Outcome::Success(HostInfo { + log::debug!("Using referer host info"); + HostInfo { base_url: referer.to_string(), origin: extract_url_origin(referer), - }); + } } else { + log::debug!("Guessing host info with headers"); // Try to guess from the headers use std::env; @@ -433,11 +439,15 @@ impl<'r> FromRequest<'r> for HostInfo { let base_url_origin = format!("{protocol}://{host}"); - return Outcome::Success(HostInfo { + HostInfo { base_url: base_url_origin.clone(), origin: base_url_origin, - }); - } + } + }; + + log::debug!("Using host_info: {:?}", host_info); + + Outcome::Success(host_info) } } diff --git a/src/config.rs b/src/config.rs index 5c4a9616e3..97eb4b9a62 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1058,6 +1058,7 @@ pub fn extract_url_host(url: &str) -> String { }; if let Some(port) = u.port().map(|p| p.to_string()) { + host.push(':'); host.push_str(&port); } From 6375a20f2f3f9e747b32e3a9fbeda146ecdf82d4 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 19:22:58 +0200 Subject: [PATCH 37/40] cargo clippy and cargo fmt --- src/auth.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index b86c39254e..9af38a8f9c 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -403,11 +403,10 @@ impl<'r> FromRequest<'r> for HostInfo { get_main_host().into() }; - let host_info = get_host_info(host.as_ref()) - .unwrap_or_else(|| { - log::debug!("Falling back to default domain, because {host} was not in domains."); - get_host_info(&get_main_host()).expect("Main domain doesn't have entry!") - }); + let host_info = get_host_info(host.as_ref()).unwrap_or_else(|| { + log::debug!("Falling back to default domain, because {host} was not in domains."); + get_host_info(&get_main_host()).expect("Main domain doesn't have entry!") + }); host_info } else if let Some(referer) = headers.get_one("Referer") { From aceaf618100e7412eadd80d7984e754ca2d5068a Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 19:35:55 +0200 Subject: [PATCH 38/40] switch back to admin_path, since cookies break otherwise --- src/api/admin.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 830f64f0cf..d3007b18ac 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -124,8 +124,12 @@ impl<'r> FromRequest<'r> for IpHeader { } } -fn admin_url(base_url: &str) -> String { - format!("{}{}", base_url, ADMIN_PATH) +fn admin_path() -> String { + format!("{}{}", CONFIG.domain_path(), ADMIN_PATH) +} + +fn admin_url(origin: &str) -> String { + format!("{}{}", origin, admin_path()) } @@ -198,7 +202,7 @@ fn post_admin_login(data: Form, cookies: &CookieJar<'_>, ip: ClientIp cookies.add(cookie); if let Some(redirect) = redirect { - Ok(Redirect::to(format!("{}{}", ADMIN_PATH, redirect))) + Ok(Redirect::to(format!("{}{}", admin_path(), redirect))) } else { Err(AdminResponse::Ok(render_admin_page())) } @@ -731,7 +735,7 @@ async fn diagnostics( "uses_proxy": uses_proxy, "db_type": *DB_TYPE, "db_version": get_sql_server_version(&mut conn).await, - "admin_url": format!("{}/diagnostics", admin_url(&host_info.base_url)), + "admin_url": format!("{}/diagnostics", admin_url(&host_info.origin)), "overrides": &CONFIG.get_overrides().join(", "), "host_arch": std::env::consts::ARCH, "host_os": std::env::consts::OS, From fae770a6a272fbd8d707fc698032c36a679e2375 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Sat, 9 Sep 2023 19:50:16 +0200 Subject: [PATCH 39/40] remove some outdated comments / move import --- src/auth.rs | 3 +-- src/db/models/cipher.rs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 9af38a8f9c..29d907f37b 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -359,6 +359,7 @@ use rocket::{ outcome::try_outcome, request::{FromRequest, Outcome, Request}, }; +use std::borrow::Cow; use crate::db::{ models::{Collection, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException}, @@ -392,7 +393,6 @@ impl<'r> FromRequest<'r> for HostInfo { let headers = request.headers(); // Get host - // TODO: UPDATE THIS SECTION let host_info = if CONFIG.domain_set() { log::debug!("Using configured host info"); let host: Cow<'_, str> = if let Some(host) = headers.get_one("X-Forwarded-Host") { @@ -864,7 +864,6 @@ impl<'r> FromRequest<'r> for OwnerHeaders { } } -use std::borrow::Cow; // // Client IP address detection // diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 096e198436..29b24c1172 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -113,7 +113,6 @@ use crate::error::MapResult; /// Database methods impl Cipher { - // TODO: Change back pub async fn to_json( &self, base_url: &str, From c150818e191a431acf7402b8ef0a2463428f4a03 Mon Sep 17 00:00:00 2001 From: BlockListed <44610569+BlockListed@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:15:20 +0100 Subject: [PATCH 40/40] rebase and fix rebase issues --- src/api/admin.rs | 5 ----- src/api/core/sends.rs | 4 +--- src/api/core/two_factor/webauthn.rs | 17 +++++++++-------- src/api/identity.rs | 4 +++- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index d3007b18ac..66b3e01265 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -98,10 +98,6 @@ const BASE_TEMPLATE: &str = "admin/base"; const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000"; -fn admin_path() -> String { - ADMIN_PATH.to_string() -} - #[derive(Debug)] struct IpHeader(Option); @@ -132,7 +128,6 @@ fn admin_url(origin: &str) -> String { format!("{}{}", origin, admin_path()) } - #[derive(Responder)] enum AdminResponse { #[response(status = 200)] diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index a34d8b9728..5e1439588d 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -9,9 +9,7 @@ use rocket::serde::json::Json; use serde_json::Value; use crate::{ - api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, UpdateType}, - auth::{ClientIp, Headers, HostInfo}, - api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, UpdateType}, + api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, auth::{ClientIp, Headers, HostInfo}, db::{models::*, DbConn, DbPool}, util::{NumberOrString, SafeString}, diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 96e33ba210..b99d9a90c3 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -141,14 +141,15 @@ async fn generate_webauthn_challenge( .map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering .collect(); - let (challenge, state) = WebauthnConfig::load(&host_info.base_url, &host_info.origin).generate_challenge_register_options( - user.uuid.as_bytes().to_vec(), - user.email, - user.name, - Some(registrations), - None, - None, - )?; + let (challenge, state) = WebauthnConfig::load(&host_info.base_url, &host_info.origin) + .generate_challenge_register_options( + user.uuid.as_bytes().to_vec(), + user.email, + user.name, + Some(registrations), + None, + None, + )?; let type_ = TwoFactorType::WebauthnRegisterChallenge; TwoFactor::new(user.uuid, type_, serde_json::to_string(&state)?).save(&mut conn).await?; diff --git a/src/api/identity.rs b/src/api/identity.rs index 55e2556f00..7ce53a7bf1 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -524,7 +524,9 @@ async fn twofactor_auth( Some(TwoFactorType::Authenticator) => { authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await? } - Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, base_url, origin, conn).await?, + Some(TwoFactorType::Webauthn) => { + webauthn::validate_webauthn_login(&user.uuid, twofactor_code, base_url, origin, conn).await? + } Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?, Some(TwoFactorType::Duo) => { duo::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, conn).await?