Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-05-18 23:10:05 +02:00
Merge c150818e19
into 2d98aa3045
Dieser Commit ist enthalten in:
Commit
21a3bee687
|
@ -12,6 +12,7 @@ use rocket::{
|
|||
Catcher, Route,
|
||||
};
|
||||
|
||||
use crate::auth::HostInfo;
|
||||
use crate::{
|
||||
api::{
|
||||
core::{log_event, two_factor},
|
||||
|
@ -97,10 +98,6 @@ 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)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct IpHeader(Option<String>);
|
||||
|
||||
|
@ -123,8 +120,12 @@ impl<'r> FromRequest<'r> for IpHeader {
|
|||
}
|
||||
}
|
||||
|
||||
fn admin_url() -> String {
|
||||
format!("{}{}", CONFIG.domain_origin(), admin_path())
|
||||
fn admin_path() -> String {
|
||||
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
|
||||
}
|
||||
|
||||
fn admin_url(origin: &str) -> String {
|
||||
format!("{}{}", origin, admin_path())
|
||||
}
|
||||
|
||||
#[derive(Responder)]
|
||||
|
@ -668,7 +669,12 @@ async fn get_ntp_time(has_http_access: bool) -> String {
|
|||
}
|
||||
|
||||
#[get("/diagnostics")]
|
||||
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) -> ApiResult<Html<String>> {
|
||||
async fn diagnostics(
|
||||
_token: AdminToken,
|
||||
ip_header: IpHeader,
|
||||
host_info: HostInfo,
|
||||
mut conn: DbConn,
|
||||
) -> ApiResult<Html<String>> {
|
||||
use chrono::prelude::*;
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
|
@ -724,7 +730,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(&host_info.origin)),
|
||||
"overrides": &CONFIG.get_overrides().join(", "),
|
||||
"host_arch": std::env::consts::ARCH,
|
||||
"host_os": std::env::consts::OS,
|
||||
|
|
|
@ -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<AuthRequestRequest>,
|
||||
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/<uuid>")]
|
||||
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<AuthResponseRequest>,
|
||||
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/<uuid>/response?<code>")]
|
||||
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::<Vec<Value>>(),
|
||||
|
|
|
@ -113,7 +113,7 @@ async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json<Value>
|
|||
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<Value> {
|
|||
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/<uuid>/admin")]
|
||||
|
@ -323,7 +323,7 @@ async fn post_ciphers(data: JsonUpcase<CipherData>, 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/<uuid>/partial", data = "<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/<uuid>/attachment-admin", format = "multipart/form-data", 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(
|
||||
|
|
|
@ -603,7 +603,7 @@ 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,
|
||||
|
|
|
@ -190,7 +190,8 @@ fn version() -> Json<&'static str> {
|
|||
|
||||
#[get("/config")]
|
||||
fn config() -> Json<Value> {
|
||||
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
|
||||
|
|
|
@ -765,20 +765,20 @@ struct OrgIdData {
|
|||
#[get("/ciphers/organization-details?<data..>")]
|
||||
async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||
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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use serde_json::Value;
|
|||
|
||||
use crate::{
|
||||
api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType},
|
||||
auth::{ClientIp, Headers, Host},
|
||||
auth::{ClientIp, Headers, HostInfo},
|
||||
db::{models::*, DbConn, DbPool},
|
||||
util::{NumberOrString, SafeString},
|
||||
CONFIG,
|
||||
|
@ -462,7 +462,7 @@ async fn post_access_file(
|
|||
send_id: &str,
|
||||
file_id: &str,
|
||||
data: JsonUpcase<SendAccessData>,
|
||||
host: Host,
|
||||
host_info: HostInfo,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
|
@ -517,7 +517,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)
|
||||
})))
|
||||
}
|
||||
|
||||
|
|
|
@ -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,13 +52,11 @@ struct WebauthnConfig {
|
|||
}
|
||||
|
||||
impl WebauthnConfig {
|
||||
fn load() -> Webauthn<Self> {
|
||||
let domain = CONFIG.domain();
|
||||
let domain_origin = CONFIG.domain_origin();
|
||||
fn load(domain: &str, domain_origin: &str) -> Webauthn<Self> {
|
||||
Webauthn::new(Self {
|
||||
rpid: Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(),
|
||||
url: domain,
|
||||
origin: Url::parse(&domain_origin).unwrap(),
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -128,6 +126,7 @@ async fn get_webauthn(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut
|
|||
async fn generate_webauthn_challenge(
|
||||
data: JsonUpcase<PasswordOrOtpData>,
|
||||
headers: Headers,
|
||||
host_info: HostInfo,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
let data: PasswordOrOtpData = data.into_inner().data;
|
||||
|
@ -142,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().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?;
|
||||
|
@ -250,7 +250,12 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
|
|||
}
|
||||
|
||||
#[post("/two-factor/webauthn", data = "<data>")]
|
||||
async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn activate_webauthn(
|
||||
data: JsonUpcase<EnableWebauthnData>,
|
||||
headers: Headers,
|
||||
host_info: HostInfo,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
let data: EnableWebauthnData = data.into_inner().data;
|
||||
let mut user = headers.user;
|
||||
|
||||
|
@ -273,8 +278,11 @@ async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Header
|
|||
};
|
||||
|
||||
// Verify the credentials with the saved state
|
||||
let (credential, _data) =
|
||||
WebauthnConfig::load().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
|
||||
|
@ -303,8 +311,13 @@ async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Header
|
|||
}
|
||||
|
||||
#[put("/two-factor/webauthn", data = "<data>")]
|
||||
async fn activate_webauthn_put(data: JsonUpcase<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
activate_webauthn(data, headers, conn).await
|
||||
async fn activate_webauthn_put(
|
||||
data: JsonUpcase<EnableWebauthnData>,
|
||||
headers: Headers,
|
||||
host_info: HostInfo,
|
||||
conn: DbConn,
|
||||
) -> JsonResult {
|
||||
activate_webauthn(data, headers, host_info, conn).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
@ -375,7 +388,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<Credential> =
|
||||
get_webauthn_registrations(user_uuid, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
||||
|
@ -385,8 +398,9 @@ 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 +411,13 @@ 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 +440,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 {
|
||||
|
|
|
@ -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,12 @@ pub fn routes() -> Vec<Route> {
|
|||
}
|
||||
|
||||
#[post("/connect/token", data = "<data>")]
|
||||
async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult {
|
||||
async fn login(
|
||||
data: Form<ConnectData>,
|
||||
client_header: ClientHeaders,
|
||||
host_info: HostInfo,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
let data: ConnectData = data.into_inner();
|
||||
|
||||
let mut user_uuid: Option<String> = None;
|
||||
|
@ -48,7 +53,8 @@ async fn login(data: Form<ConnectData>, 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 +146,8 @@ async fn _password_login(
|
|||
user_uuid: &mut Option<String>,
|
||||
conn: &mut DbConn,
|
||||
ip: &ClientIp,
|
||||
base_url: &str,
|
||||
origin: &str,
|
||||
) -> JsonResult {
|
||||
// Validate scope
|
||||
let scope = data.scope.as_ref().unwrap();
|
||||
|
@ -250,7 +258,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 +488,8 @@ async fn twofactor_auth(
|
|||
data: &ConnectData,
|
||||
device: &mut Device,
|
||||
ip: &ClientIp,
|
||||
base_url: &str,
|
||||
origin: &str,
|
||||
conn: &mut DbConn,
|
||||
) -> ApiResult<Option<String>> {
|
||||
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
||||
|
@ -497,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, 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 +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, 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 +542,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 +570,13 @@ fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
|
|||
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<Value> {
|
||||
async fn _json_err_twofactor(
|
||||
providers: &[i32],
|
||||
user_uuid: &str,
|
||||
base_url: &str,
|
||||
origin: &str,
|
||||
conn: &mut DbConn,
|
||||
) -> ApiResult<Value> {
|
||||
let mut result = json!({
|
||||
"error" : "invalid_grant",
|
||||
"error_description" : "Two factor required.",
|
||||
|
@ -570,7 +591,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;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use serde_json::Value;
|
|||
|
||||
use crate::{
|
||||
api::{core::now, ApiResult, EmptyResult},
|
||||
auth::decode_file_download,
|
||||
auth::{decode_file_download, HostInfo},
|
||||
error::Error,
|
||||
util::{Cached, SafeString},
|
||||
CONFIG,
|
||||
|
@ -62,9 +62,12 @@ fn web_index_head() -> EmptyResult {
|
|||
}
|
||||
|
||||
#[get("/app-id.json")]
|
||||
fn app_id() -> Cached<(ContentType, Json<Value>)> {
|
||||
fn app_id(host_info: HostInfo) -> Cached<(ContentType, Json<Value>)> {
|
||||
let content_type = ContentType::new("application", "fido.trusted-apps+json");
|
||||
|
||||
// TODO Maybe return all available origins.
|
||||
let origin = host_info.origin;
|
||||
|
||||
Cached::long(
|
||||
(
|
||||
content_type,
|
||||
|
@ -83,7 +86,7 @@ fn app_id() -> Cached<(ContentType, Json<Value>)> {
|
|||
// 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" ]
|
||||
}]
|
||||
|
|
123
src/auth.rs
123
src/auth.rs
|
@ -9,6 +9,7 @@ use openssl::rsa::Rsa;
|
|||
use serde::de::DeserializeOwned;
|
||||
use serde::ser::Serialize;
|
||||
|
||||
use crate::config::{extract_url_host, extract_url_origin};
|
||||
use crate::{error::Error, CONFIG};
|
||||
|
||||
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
|
||||
|
@ -16,16 +17,20 @@ const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
|
|||
pub static DEFAULT_VALIDITY: Lazy<TimeDelta> = Lazy::new(|| TimeDelta::try_hours(2).unwrap());
|
||||
static JWT_HEADER: Lazy<Header> = Lazy::new(|| Header::new(JWT_ALGORITHM));
|
||||
|
||||
pub static JWT_LOGIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|login", CONFIG.domain_origin()));
|
||||
static JWT_INVITE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|invite", CONFIG.domain_origin()));
|
||||
fn jwt_origin() -> String {
|
||||
extract_url_origin(&CONFIG.main_domain())
|
||||
}
|
||||
|
||||
pub static JWT_LOGIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|login", jwt_origin()));
|
||||
static JWT_INVITE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|invite", jwt_origin()));
|
||||
static JWT_EMERGENCY_ACCESS_INVITE_ISSUER: Lazy<String> =
|
||||
Lazy::new(|| format!("{}|emergencyaccessinvite", CONFIG.domain_origin()));
|
||||
static JWT_DELETE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|delete", CONFIG.domain_origin()));
|
||||
static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin()));
|
||||
static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin()));
|
||||
static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin()));
|
||||
static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin()));
|
||||
static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|file_download", CONFIG.domain_origin()));
|
||||
Lazy::new(|| format!("{}|emergencyaccessinvite", jwt_origin()));
|
||||
static JWT_DELETE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|delete", jwt_origin()));
|
||||
static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|verifyemail", jwt_origin()));
|
||||
static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", jwt_origin()));
|
||||
static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", jwt_origin()));
|
||||
static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", jwt_origin()));
|
||||
static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|file_download", jwt_origin()));
|
||||
|
||||
static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new();
|
||||
static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new();
|
||||
|
@ -355,29 +360,64 @@ use rocket::{
|
|||
outcome::try_outcome,
|
||||
request::{FromRequest, Outcome, Request},
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::db::{
|
||||
models::{Collection, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException},
|
||||
DbConn,
|
||||
};
|
||||
|
||||
pub struct Host {
|
||||
pub host: String,
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HostInfo {
|
||||
pub base_url: String,
|
||||
pub origin: String,
|
||||
}
|
||||
|
||||
fn get_host_info(host: &str) -> Option<HostInfo> {
|
||||
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,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn get_main_host() -> String {
|
||||
extract_url_host(&CONFIG.main_domain())
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for Host {
|
||||
impl<'r> FromRequest<'r> for HostInfo {
|
||||
type Error = &'static str;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = request.headers();
|
||||
|
||||
// Get host
|
||||
let host = if CONFIG.domain_set() {
|
||||
CONFIG.domain()
|
||||
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") {
|
||||
host.into()
|
||||
} else {
|
||||
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!")
|
||||
});
|
||||
|
||||
host_info
|
||||
} else if let Some(referer) = headers.get_one("Referer") {
|
||||
referer.to_string()
|
||||
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;
|
||||
|
||||
|
@ -397,17 +437,22 @@ impl<'r> FromRequest<'r> for Host {
|
|||
""
|
||||
};
|
||||
|
||||
format!("{protocol}://{host}")
|
||||
let base_url_origin = format!("{protocol}://{host}");
|
||||
|
||||
HostInfo {
|
||||
base_url: base_url_origin.clone(),
|
||||
origin: base_url_origin,
|
||||
}
|
||||
};
|
||||
|
||||
Outcome::Success(Host {
|
||||
host,
|
||||
})
|
||||
log::debug!("Using host_info: {:?}", host_info);
|
||||
|
||||
Outcome::Success(host_info)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientHeaders {
|
||||
pub host: String,
|
||||
pub base_url: String,
|
||||
pub device_type: i32,
|
||||
pub ip: ClientIp,
|
||||
}
|
||||
|
@ -417,7 +462,7 @@ impl<'r> FromRequest<'r> for ClientHeaders {
|
|||
type Error = &'static str;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let host = try_outcome!(Host::from_request(request).await).host;
|
||||
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"),
|
||||
|
@ -427,7 +472,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,
|
||||
base_url,
|
||||
device_type,
|
||||
ip,
|
||||
})
|
||||
|
@ -435,7 +480,7 @@ impl<'r> FromRequest<'r> for ClientHeaders {
|
|||
}
|
||||
|
||||
pub struct Headers {
|
||||
pub host: String,
|
||||
pub base_url: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub ip: ClientIp,
|
||||
|
@ -448,7 +493,7 @@ impl<'r> FromRequest<'r> for Headers {
|
|||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = request.headers();
|
||||
|
||||
let host = try_outcome!(Host::from_request(request).await).host;
|
||||
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"),
|
||||
|
@ -519,7 +564,7 @@ impl<'r> FromRequest<'r> for Headers {
|
|||
}
|
||||
|
||||
Outcome::Success(Headers {
|
||||
host,
|
||||
base_url,
|
||||
device,
|
||||
user,
|
||||
ip,
|
||||
|
@ -528,7 +573,7 @@ impl<'r> FromRequest<'r> for Headers {
|
|||
}
|
||||
|
||||
pub struct OrgHeaders {
|
||||
pub host: String,
|
||||
pub base_url: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user_type: UserOrgType,
|
||||
|
@ -584,7 +629,7 @@ impl<'r> FromRequest<'r> for OrgHeaders {
|
|||
};
|
||||
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
base_url: headers.base_url,
|
||||
device: headers.device,
|
||||
user,
|
||||
org_user_type: {
|
||||
|
@ -606,7 +651,7 @@ impl<'r> FromRequest<'r> for OrgHeaders {
|
|||
}
|
||||
|
||||
pub struct AdminHeaders {
|
||||
pub host: String,
|
||||
pub base_url: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user_type: UserOrgType,
|
||||
|
@ -623,7 +668,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,
|
||||
base_url: headers.base_url,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
org_user_type: headers.org_user_type,
|
||||
|
@ -639,7 +684,7 @@ impl<'r> FromRequest<'r> for AdminHeaders {
|
|||
impl From<AdminHeaders> for Headers {
|
||||
fn from(h: AdminHeaders) -> Headers {
|
||||
Headers {
|
||||
host: h.host,
|
||||
base_url: h.base_url,
|
||||
device: h.device,
|
||||
user: h.user,
|
||||
ip: h.ip,
|
||||
|
@ -670,7 +715,7 @@ fn get_col_id(request: &Request<'_>) -> Option<String> {
|
|||
/// and have access to the specific collection provided via the <col_id>/collections/collectionId.
|
||||
/// This does strict checking on the collection_id, ManagerHeadersLoose does not.
|
||||
pub struct ManagerHeaders {
|
||||
pub host: String,
|
||||
pub base_url: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user_type: UserOrgType,
|
||||
|
@ -699,7 +744,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
|
|||
}
|
||||
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
base_url: headers.base_url,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
org_user_type: headers.org_user_type,
|
||||
|
@ -714,7 +759,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
|
|||
impl From<ManagerHeaders> for Headers {
|
||||
fn from(h: ManagerHeaders) -> Headers {
|
||||
Headers {
|
||||
host: h.host,
|
||||
base_url: h.base_url,
|
||||
device: h.device,
|
||||
user: h.user,
|
||||
ip: h.ip,
|
||||
|
@ -725,7 +770,7 @@ impl From<ManagerHeaders> 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 base_url: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user: UserOrganization,
|
||||
|
@ -741,7 +786,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,
|
||||
base_url: headers.base_url,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
org_user: headers.org_user,
|
||||
|
@ -757,7 +802,7 @@ impl<'r> FromRequest<'r> for ManagerHeadersLoose {
|
|||
impl From<ManagerHeadersLoose> for Headers {
|
||||
fn from(h: ManagerHeadersLoose) -> Headers {
|
||||
Headers {
|
||||
host: h.host,
|
||||
base_url: h.base_url,
|
||||
device: h.device,
|
||||
user: h.user,
|
||||
ip: h.ip,
|
||||
|
@ -785,7 +830,7 @@ impl ManagerHeaders {
|
|||
}
|
||||
|
||||
Ok(ManagerHeaders {
|
||||
host: h.host,
|
||||
base_url: h.base_url,
|
||||
device: h.device,
|
||||
user: h.user,
|
||||
org_user_type: h.org_user_type,
|
||||
|
@ -795,7 +840,7 @@ impl ManagerHeaders {
|
|||
}
|
||||
|
||||
pub struct OwnerHeaders {
|
||||
pub host: String,
|
||||
pub base_url: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub ip: ClientIp,
|
||||
|
@ -809,7 +854,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,
|
||||
|
|
130
src/config.rs
130
src/config.rs
|
@ -1,12 +1,14 @@
|
|||
use std::env::consts::EXE_SUFFIX;
|
||||
use std::process::exit;
|
||||
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;
|
||||
use reqwest::Url;
|
||||
|
||||
use crate::{
|
||||
auth::HostInfo,
|
||||
db::DbConnType,
|
||||
error::Error,
|
||||
util::{get_env, get_env_bool, parse_experimental_client_feature_flags},
|
||||
|
@ -47,6 +49,8 @@ macro_rules! make_config {
|
|||
_usr: ConfigBuilder,
|
||||
|
||||
_overrides: Vec<String>,
|
||||
|
||||
domain_hostmap: OnceLock<HashMap<String, HostInfo>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, Serialize)]
|
||||
|
@ -141,7 +145,15 @@ macro_rules! make_config {
|
|||
)+)+
|
||||
config.domain_set = _domain_set;
|
||||
|
||||
config.domain = config.domain.trim_end_matches('/').to_string();
|
||||
// Remove slash from every domain
|
||||
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.pop();
|
||||
|
||||
config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase();
|
||||
config.org_creation_users = config.org_creation_users.trim().to_lowercase();
|
||||
|
@ -414,15 +426,17 @@ 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
|
||||
/// 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
|
||||
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;
|
||||
/// 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);
|
||||
/// 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);
|
||||
/// 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.split(',').next().expect("Missing domain"));
|
||||
/// Enable web vault
|
||||
web_vault_enabled: bool, false, def, true;
|
||||
|
||||
|
@ -720,11 +734,17 @@ 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.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'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.domain.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;
|
||||
|
@ -988,7 +1008,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) => {
|
||||
|
@ -998,6 +1018,24 @@ 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 {
|
||||
|
@ -1010,10 +1048,34 @@ 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(':');
|
||||
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(',').next().expect("Domain missing");
|
||||
format!("{domain}/vw_static/")
|
||||
}
|
||||
}
|
||||
|
@ -1082,6 +1144,7 @@ impl Config {
|
|||
_env,
|
||||
_usr,
|
||||
_overrides,
|
||||
domain_hostmap: OnceLock::new(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
@ -1249,6 +1312,45 @@ impl Config {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_domain_hostmap(&self, host: &str) -> Option<HostInfo> {
|
||||
// This is done to prevent deadlock, when read-locking an rwlock twice
|
||||
let domains = self.domain();
|
||||
|
||||
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 host_to_origin(&self, host: &str) -> Option<String> {
|
||||
self.get_domain_hostmap(host).map(|v| v.origin)
|
||||
}
|
||||
|
||||
pub fn host_to_domain(&self, host: &str) -> Option<String> {
|
||||
self.get_domain_hostmap(host).map(|v| v.base_url)
|
||||
}
|
||||
|
||||
// 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().split(',').nth(0).expect("Missing domain").to_string()
|
||||
}
|
||||
}
|
||||
|
||||
use handlebars::{
|
||||
|
|
|
@ -35,15 +35,15 @@ impl Attachment {
|
|||
format!("{}/{}/{}", CONFIG.attachments_folder(), self.cipher_uuid, self.id)
|
||||
}
|
||||
|
||||
pub fn get_url(&self, host: &str) -> 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={}", 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 {
|
||||
pub fn to_json(&self, base_url: &str) -> 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),
|
||||
|
|
|
@ -115,7 +115,7 @@ use crate::error::MapResult;
|
|||
impl Cipher {
|
||||
pub async fn to_json(
|
||||
&self,
|
||||
host: &str,
|
||||
base_url: &str,
|
||||
user_uuid: &str,
|
||||
cipher_sync_data: Option<&CipherSyncData>,
|
||||
sync_type: CipherSyncType,
|
||||
|
@ -126,12 +126,12 @@ impl Cipher {
|
|||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
52
src/mail.rs
52
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<String>) -> 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<String>) -> 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,
|
||||
|
|
16
src/util.rs
16
src/util.rs
|
@ -17,7 +17,7 @@ use tokio::{
|
|||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
use crate::CONFIG;
|
||||
use crate::{config::extract_url_host, CONFIG};
|
||||
|
||||
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<String> {
|
||||
let origin = Cors::get_header(headers, "Origin");
|
||||
let domain_origin = CONFIG.domain_origin();
|
||||
|
||||
let domain_origin_opt = CONFIG.host_to_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
|
||||
|
|
Laden …
In neuem Issue referenzieren