1
0
Fork 1
Spiegel von https://github.com/dani-garcia/vaultwarden.git synchronisiert 2024-05-18 23:10:05 +02:00
Dieser Commit ist enthalten in:
BlockListed 2024-03-24 08:53:57 +08:00 committet von GitHub
Commit 21a3bee687
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: B5690EEEBB952194
17 geänderte Dateien mit 381 neuen und 165 gelöschten Zeilen

Datei anzeigen

@ -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,

Datei anzeigen

@ -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>>(),

Datei anzeigen

@ -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(

Datei anzeigen

@ -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,

Datei anzeigen

@ -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

Datei anzeigen

@ -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),
}))
}
}

Datei anzeigen

@ -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");
}

Datei anzeigen

@ -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)
})))
}

Datei anzeigen

@ -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 &reg.credential.cred_id == cred_id {

Datei anzeigen

@ -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;
}

Datei anzeigen

@ -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" ]
}]

Datei anzeigen

@ -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,

Datei anzeigen

@ -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::{

Datei anzeigen

@ -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),

Datei anzeigen

@ -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()
}
}

Datei anzeigen

@ -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,

Datei anzeigen

@ -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