vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit 2ac23b6a6c834ea173d3f06ac57d84ff45fe65f9
parent 8d6e4cb9584ab04d4e19aae669c57d48c5cf739b
Author: Zack Newman <zack@philomathiclife.com>
Date:   Sun, 14 Jan 2024 13:38:45 -0700

remove websockets

Diffstat:
Msrc/api/core/accounts.rs | 58+++++++++-------------------------------------------------
Msrc/api/core/ciphers.rs | 255+++++++++++++++----------------------------------------------------------------
Msrc/api/core/folders.rs | 30++++++------------------------
Msrc/api/core/mod.rs | 20++++++++------------
Msrc/api/core/organizations.rs | 32++++++++------------------------
Msrc/api/core/two_factor/duo.rs | 2+-
Msrc/api/core/two_factor/email.rs | 2+-
Msrc/api/core/two_factor/webauthn.rs | 5+----
Msrc/api/core/two_factor/yubikey.rs | 2+-
Msrc/api/mod.rs | 19++++---------------
Dsrc/api/notifications.rs | 496-------------------------------------------------------------------------------
Msrc/auth.rs | 2+-
Msrc/config.rs | 54++++++++++++++++++++++++++++++++++++------------------
Msrc/error.rs | 6+-----
Msrc/main.rs | 29+++++++++--------------------
15 files changed, 132 insertions(+), 880 deletions(-)

diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs @@ -1,7 +1,5 @@ use crate::{ - api::{ - EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordOrOtpData, UpdateType, - }, + api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordOrOtpData}, auth::{decode_delete, ClientHeaders, Headers}, db::{ models::{Cipher, Device, Folder, User, UserKdfType}, @@ -200,7 +198,6 @@ async fn post_password( data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { let pass_data: ChangePassData = data.into_inner().data; let mut user = headers.user; @@ -219,13 +216,7 @@ async fn post_password( String::from("get_public_keys"), ]), ); - - let save_result = user.save(&conn).await; - // Prevent logging out the client where the user requested this endpoint from. - // If you do logout the user it will causes issues at the client side. - // Adding the device uuid will prevent this. - nt.send_logout(&user, Some(headers.device.uuid)).await; - save_result + user.save(&conn).await } #[derive(Deserialize)] @@ -241,12 +232,7 @@ struct ChangeKdfData { } #[post("/accounts/kdf", data = "<data>")] -async fn post_kdf( - data: JsonUpcase<ChangeKdfData>, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> EmptyResult { +async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) -> EmptyResult { let kdf_data: ChangeKdfData = data.into_inner().data; let mut user = headers.user; if !user.check_valid_password(&kdf_data.MasterPasswordHash) { @@ -287,9 +273,7 @@ async fn post_kdf( true, None, ); - let save_result = user.save(&conn).await; - nt.send_logout(&user, Some(headers.device.uuid)).await; - save_result + user.save(&conn).await } #[derive(Deserialize)] @@ -312,12 +296,7 @@ struct KeyData { } #[post("/accounts/key", data = "<data>")] -async fn post_rotatekey( - data: JsonUpcase<KeyData>, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> EmptyResult { +async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn) -> EmptyResult { let key_data: KeyData = data.into_inner().data; if !headers .user @@ -353,19 +332,8 @@ async fn post_rotatekey( if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid { err!("The cipher is not owned by the user") } - // Prevent triggering cipher updates via WebSockets by settings UpdateType::None - // The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues. - // We force the users to logout after the user has been saved to try and prevent these issues. - update_cipher_from_data( - &mut saved_cipher, - cipher_data, - &headers, - false, - &conn, - &nt, - UpdateType::None, - ) - .await?; + update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, true) + .await?; } // Update user data @@ -373,12 +341,7 @@ async fn post_rotatekey( user.akey = key_data.Key; user.private_key = Some(key_data.PrivateKey); user.reset_security_stamp(); - let save_result = user.save(&conn).await; - // Prevent logging out the client where the user requested this endpoint from. - // If you do logout the user it will causes issues at the client side. - // Adding the device uuid will prevent this. - nt.send_logout(&user, Some(headers.device.uuid)).await; - save_result + user.save(&conn).await } #[post("/accounts/security-stamp", data = "<data>")] @@ -386,16 +349,13 @@ async fn post_sstamp( data: JsonUpcase<PasswordOrOtpData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { let otp_data: PasswordOrOtpData = data.into_inner().data; let mut user = headers.user; otp_data.validate(&user)?; Device::delete_all_by_user(&user.uuid, &conn).await?; user.reset_security_stamp(); - let save_result = user.save(&conn).await; - nt.send_logout(&user, None).await; - save_result + user.save(&conn).await } #[derive(Deserialize)] diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs @@ -1,6 +1,6 @@ use super::folders::FolderData; use crate::{ - api::{self, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordOrOtpData, UpdateType}, + api::{self, EmptyResult, JsonResult, JsonUpcase, PasswordOrOtpData}, auth::Headers, db::{ models::{ @@ -257,9 +257,8 @@ async fn post_ciphers_admin( data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> JsonResult { - post_ciphers_create(data, headers, conn, nt).await + post_ciphers_create(data, headers, conn).await } /// Called when creating a new org-owned cipher, or cloning a cipher (whether @@ -270,7 +269,6 @@ async fn post_ciphers_create( data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> JsonResult { let mut data: ShareCipherData = data.into_inner().data; // Check if there are one more more collections selected when this cipher is part of an organization. @@ -292,17 +290,12 @@ async fn post_ciphers_create( // line. Since this function only creates new ciphers (whether by cloning // or otherwise), we can just ignore this field entirely. data.Cipher.LastKnownRevisionDate = None; - share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn, &nt).await + share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn).await } /// Called when creating a new user-owned cipher. #[post("/ciphers", data = "<data>")] -async fn post_ciphers( - data: JsonUpcase<CipherData>, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> JsonResult { +async fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult { let mut data: CipherData = data.into_inner().data; // The web/browser clients set this field to null as expected, but the // mobile clients seem to set the invalid value `0001-01-01T00:00:00`, @@ -310,16 +303,7 @@ async fn post_ciphers( // needed when creating a new cipher, so just ignore it unconditionally. data.LastKnownRevisionDate = None; let mut cipher = Cipher::new(data.Type, data.Name.clone()); - update_cipher_from_data( - &mut cipher, - data, - &headers, - false, - &conn, - &nt, - UpdateType::SyncCipherCreate, - ) - .await?; + update_cipher_from_data(&mut cipher, data, &headers, false, &conn, false).await?; Ok(Json( cipher .to_json(&headers.user.uuid, None, CipherSyncType::User, &conn) @@ -355,13 +339,12 @@ pub async fn update_cipher_from_data( headers: &Headers, shared_to_collection: bool, conn: &DbConn, - nt: &Notify<'_>, - ut: UpdateType, + import_cipher: bool, ) -> EmptyResult { enforce_personal_ownership_policy(Some(&data), headers, conn).await?; // Check that the client isn't updating an existing cipher with stale data. // And only perform this check when not importing ciphers, else the date/time check will fail. - if ut != UpdateType::None { + if !import_cipher { if let Some(dt) = data.LastKnownRevisionDate { match NaiveDateTime::parse_from_str(&dt, "%+") { // ISO 8601 format @@ -466,16 +449,6 @@ pub async fn update_cipher_from_data( cipher .set_favorite(data.Favorite, &headers.user.uuid, conn) .await?; - if ut != UpdateType::None { - nt.send_cipher_update( - ut, - cipher, - &cipher.update_users_revision(conn).await, - &headers.device.uuid, - None, - ) - .await; - } Ok(()) } @@ -501,7 +474,6 @@ async fn post_ciphers_import( data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { enforce_personal_ownership_policy(None, &headers, &conn).await?; let data: ImportData = data.into_inner().data; @@ -527,20 +499,10 @@ async fn post_ciphers_import( let folder_uuid = relations_map.get(&index).map(|i| folders[*i].uuid.clone()); cipher_data.FolderId = folder_uuid; let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone()); - update_cipher_from_data( - &mut cipher, - cipher_data, - &headers, - false, - &conn, - &nt, - UpdateType::None, - ) - .await?; + update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn, true).await?; } let mut user = headers.user; user.update_revision(&conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user).await; Ok(()) } @@ -551,9 +513,8 @@ async fn put_cipher_admin( data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> JsonResult { - put_cipher(uuid, data, headers, conn, nt).await + put_cipher(uuid, data, headers, conn).await } #[post("/ciphers/<uuid>/admin", data = "<data>")] @@ -562,9 +523,8 @@ async fn post_cipher_admin( data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> JsonResult { - post_cipher(uuid, data, headers, conn, nt).await + post_cipher(uuid, data, headers, conn).await } #[post("/ciphers/<uuid>", data = "<data>")] @@ -573,9 +533,8 @@ async fn post_cipher( data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> JsonResult { - put_cipher(uuid, data, headers, conn, nt).await + put_cipher(uuid, data, headers, conn).await } #[put("/ciphers/<uuid>", data = "<data>")] @@ -584,7 +543,6 @@ async fn put_cipher( data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> JsonResult { let data: CipherData = data.into_inner().data; let Some(mut cipher) = Cipher::find_by_uuid(uuid, &conn).await else { @@ -600,16 +558,7 @@ async fn put_cipher( { err!("Cipher is not write accessible") } - update_cipher_from_data( - &mut cipher, - data, - &headers, - false, - &conn, - &nt, - UpdateType::SyncCipherUpdate, - ) - .await?; + update_cipher_from_data(&mut cipher, data, &headers, false, &conn, false).await?; Ok(Json( cipher .to_json(&headers.user.uuid, None, CipherSyncType::User, &conn) @@ -676,9 +625,8 @@ async fn put_collections_update( data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - post_collections_admin(uuid, data, headers, conn, nt).await + post_collections_admin(uuid, data, headers, conn).await } #[post("/ciphers/<uuid>/collections", data = "<data>")] @@ -687,9 +635,8 @@ async fn post_collections_update( data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - post_collections_admin(uuid, data, headers, conn, nt).await + post_collections_admin(uuid, data, headers, conn).await } #[put("/ciphers/<uuid>/collections-admin", data = "<data>")] @@ -698,9 +645,8 @@ async fn put_collections_admin( data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - post_collections_admin(uuid, data, headers, conn, nt).await + post_collections_admin(uuid, data, headers, conn).await } #[post("/ciphers/<uuid>/collections-admin", data = "<data>")] @@ -709,7 +655,6 @@ async fn post_collections_admin( data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { let data: CollectionsAdminData = data.into_inner().data; let Some(cipher) = Cipher::find_by_uuid(uuid, &conn).await else { @@ -750,14 +695,6 @@ async fn post_collections_admin( } } } - nt.send_cipher_update( - UpdateType::SyncCipherUpdate, - &cipher, - &cipher.update_users_revision(&conn).await, - &headers.device.uuid, - Some(Vec::from_iter(posted_collections)), - ) - .await; Ok(()) } @@ -774,10 +711,9 @@ async fn post_cipher_share( data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> JsonResult { let data: ShareCipherData = data.into_inner().data; - share_cipher_by_uuid(uuid, data, &headers, &conn, &nt).await + share_cipher_by_uuid(uuid, data, &headers, &conn).await } #[put("/ciphers/<uuid>/share", data = "<data>")] @@ -786,10 +722,9 @@ async fn put_cipher_share( data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> JsonResult { let data: ShareCipherData = data.into_inner().data; - share_cipher_by_uuid(uuid, data, &headers, &conn, &nt).await + share_cipher_by_uuid(uuid, data, &headers, &conn).await } #[derive(Deserialize)] @@ -804,7 +739,6 @@ async fn put_cipher_share_selected( data: JsonUpcase<ShareSelectedCipherData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { let mut data: ShareSelectedCipherData = data.into_inner().data; if data.Ciphers.is_empty() { @@ -824,7 +758,7 @@ async fn put_cipher_share_selected( CollectionIds: data.CollectionIds.clone(), }; match shared_cipher_data.Cipher.Id.take() { - Some(id) => share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn, &nt).await?, + Some(id) => share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn).await?, None => err!("Request missing ids field"), }; } @@ -836,7 +770,6 @@ async fn share_cipher_by_uuid( data: ShareCipherData, headers: &Headers, conn: &DbConn, - nt: &Notify<'_>, ) -> JsonResult { let mut cipher = match Cipher::find_by_uuid(uuid, conn).await { Some(cipher) => { @@ -870,20 +803,13 @@ async fn share_cipher_by_uuid( } } }; - // When LastKnownRevisionDate is None, it is a new cipher, so send CipherCreate. - let ut = if data.Cipher.LastKnownRevisionDate.is_some() { - UpdateType::SyncCipherUpdate - } else { - UpdateType::SyncCipherCreate - }; update_cipher_from_data( &mut cipher, data.Cipher, headers, shared_to_collection, conn, - nt, - ut, + false, ) .await?; Ok(Json( @@ -1017,58 +943,33 @@ fn delete_attachment_admin(uuid: &str, attachment_id: &str, _headers: Headers) - } #[post("/ciphers/<uuid>/delete")] -async fn delete_cipher_post( - uuid: &str, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &conn, false, &nt).await +async fn delete_cipher_post(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult { + _delete_cipher_by_uuid(uuid, &headers, &conn, false).await } #[post("/ciphers/<uuid>/delete-admin")] -async fn delete_cipher_post_admin( - uuid: &str, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &conn, false, &nt).await +async fn delete_cipher_post_admin(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult { + _delete_cipher_by_uuid(uuid, &headers, &conn, false).await } #[put("/ciphers/<uuid>/delete")] -async fn delete_cipher_put( - uuid: &str, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &conn, true, &nt).await +async fn delete_cipher_put(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult { + _delete_cipher_by_uuid(uuid, &headers, &conn, true).await } #[put("/ciphers/<uuid>/delete-admin")] -async fn delete_cipher_put_admin( - uuid: &str, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &conn, true, &nt).await +async fn delete_cipher_put_admin(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult { + _delete_cipher_by_uuid(uuid, &headers, &conn, true).await } #[delete("/ciphers/<uuid>")] -async fn delete_cipher(uuid: &str, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &conn, false, &nt).await +async fn delete_cipher(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult { + _delete_cipher_by_uuid(uuid, &headers, &conn, false).await } #[delete("/ciphers/<uuid>/admin")] -async fn delete_cipher_admin( - uuid: &str, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &conn, false, &nt).await +async fn delete_cipher_admin(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult { + _delete_cipher_by_uuid(uuid, &headers, &conn, false).await } #[delete("/ciphers", data = "<data>")] @@ -1076,9 +977,8 @@ async fn delete_cipher_selected( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, false, nt).await // permanent delete + _delete_multiple_ciphers(data, headers, conn, false).await // permanent delete } #[post("/ciphers/delete", data = "<data>")] @@ -1086,9 +986,8 @@ async fn delete_cipher_selected_post( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, false, nt).await // permanent delete + _delete_multiple_ciphers(data, headers, conn, false).await // permanent delete } #[put("/ciphers/delete", data = "<data>")] @@ -1096,9 +995,8 @@ async fn delete_cipher_selected_put( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, true, nt).await // soft delete + _delete_multiple_ciphers(data, headers, conn, true).await // soft delete } #[delete("/ciphers/admin", data = "<data>")] @@ -1106,9 +1004,8 @@ async fn delete_cipher_selected_admin( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, false, nt).await // permanent delete + _delete_multiple_ciphers(data, headers, conn, false).await // permanent delete } #[post("/ciphers/delete-admin", data = "<data>")] @@ -1116,9 +1013,8 @@ async fn delete_cipher_selected_post_admin( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, false, nt).await // permanent delete + _delete_multiple_ciphers(data, headers, conn, false).await // permanent delete } #[put("/ciphers/delete-admin", data = "<data>")] @@ -1126,29 +1022,18 @@ async fn delete_cipher_selected_put_admin( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, true, nt).await // soft delete + _delete_multiple_ciphers(data, headers, conn, true).await // soft delete } #[put("/ciphers/<uuid>/restore")] -async fn restore_cipher_put( - uuid: &str, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> JsonResult { - _restore_cipher_by_uuid(uuid, &headers, &conn, &nt).await +async fn restore_cipher_put(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult { + _restore_cipher_by_uuid(uuid, &headers, &conn).await } #[put("/ciphers/<uuid>/restore-admin")] -async fn restore_cipher_put_admin( - uuid: &str, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> JsonResult { - _restore_cipher_by_uuid(uuid, &headers, &conn, &nt).await +async fn restore_cipher_put_admin(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult { + _restore_cipher_by_uuid(uuid, &headers, &conn).await } #[put("/ciphers/restore", data = "<data>")] @@ -1156,9 +1041,8 @@ async fn restore_cipher_selected( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> JsonResult { - _restore_multiple_ciphers(data, &headers, &conn, &nt).await + _restore_multiple_ciphers(data, &headers, &conn).await } #[derive(Deserialize)] @@ -1173,7 +1057,6 @@ async fn move_cipher_selected( data: JsonUpcase<MoveCipherData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { let data = data.into_inner().data; let user_uuid = headers.user.uuid; @@ -1198,14 +1081,6 @@ async fn move_cipher_selected( cipher .move_to_folder(data.FolderId.clone(), &user_uuid, &conn) .await?; - nt.send_cipher_update( - UpdateType::SyncCipherUpdate, - &cipher, - &[user_uuid.clone()], - &headers.device.uuid, - None, - ) - .await; } Ok(()) } @@ -1215,9 +1090,8 @@ async fn move_cipher_selected_put( data: JsonUpcase<MoveCipherData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - move_cipher_selected(data, headers, conn, nt).await + move_cipher_selected(data, headers, conn).await } #[derive(FromForm)] @@ -1232,7 +1106,6 @@ async fn delete_all( data: JsonUpcase<PasswordOrOtpData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { let data: PasswordOrOtpData = data.into_inner().data; let mut user = headers.user; @@ -1244,7 +1117,6 @@ async fn delete_all( Some(user_org) => { if user_org.atype == UserOrgType::Owner { Cipher::delete_all_by_organization(&org_data.org_id, &conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user).await; Ok(()) } else { err!("You don't have permission to purge the organization vault"); @@ -1262,7 +1134,6 @@ async fn delete_all( f.delete(&conn).await?; } user.update_revision(&conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user).await; Ok(()) } } @@ -1272,7 +1143,6 @@ async fn _delete_cipher_by_uuid( headers: &Headers, conn: &DbConn, soft_delete: bool, - nt: &Notify<'_>, ) -> EmptyResult { let Some(mut cipher) = Cipher::find_by_uuid(uuid, conn).await else { err!("Cipher doesn't exist") @@ -1286,24 +1156,8 @@ async fn _delete_cipher_by_uuid( if soft_delete { cipher.deleted_at = Some(Utc::now().naive_utc()); cipher.save(conn).await?; - nt.send_cipher_update( - UpdateType::SyncCipherUpdate, - &cipher, - &cipher.update_users_revision(conn).await, - &headers.device.uuid, - None, - ) - .await; } else { cipher.delete(conn).await?; - nt.send_cipher_update( - UpdateType::SyncCipherDelete, - &cipher, - &cipher.update_users_revision(conn).await, - &headers.device.uuid, - None, - ) - .await; } Ok(()) } @@ -1313,7 +1167,6 @@ async fn _delete_multiple_ciphers( headers: Headers, conn: DbConn, soft_delete: bool, - nt: Notify<'_>, ) -> EmptyResult { let data: Value = data.into_inner().data; let uuids = match data.get("Ids") { @@ -1324,21 +1177,14 @@ async fn _delete_multiple_ciphers( None => err!("Request missing ids field"), }; for uuid in uuids { - if let error @ Err(_) = - _delete_cipher_by_uuid(uuid, &headers, &conn, soft_delete, &nt).await - { + if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, soft_delete).await { return error; }; } Ok(()) } -async fn _restore_cipher_by_uuid( - uuid: &str, - headers: &Headers, - conn: &DbConn, - nt: &Notify<'_>, -) -> JsonResult { +async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn) -> JsonResult { let Some(mut cipher) = Cipher::find_by_uuid(uuid, conn).await else { err!("Cipher doesn't exist") }; @@ -1350,14 +1196,6 @@ async fn _restore_cipher_by_uuid( } cipher.deleted_at = None; cipher.save(conn).await?; - nt.send_cipher_update( - UpdateType::SyncCipherUpdate, - &cipher, - &cipher.update_users_revision(conn).await, - &headers.device.uuid, - None, - ) - .await; Ok(Json( cipher .to_json(&headers.user.uuid, None, CipherSyncType::User, conn) @@ -1369,7 +1207,6 @@ async fn _restore_multiple_ciphers( data: JsonUpcase<Value>, headers: &Headers, conn: &DbConn, - nt: &Notify<'_>, ) -> JsonResult { let data: Value = data.into_inner().data; let uuids = match data.get("Ids") { @@ -1381,7 +1218,7 @@ async fn _restore_multiple_ciphers( }; let mut ciphers: Vec<Value> = Vec::new(); for uuid in uuids { - match _restore_cipher_by_uuid(uuid, headers, conn, nt).await { + match _restore_cipher_by_uuid(uuid, headers, conn).await { Ok(json) => ciphers.push(json.into_inner()), err => return err, } diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs @@ -1,5 +1,5 @@ use crate::{ - api::{EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, + api::{EmptyResult, JsonResult, JsonUpcase}, auth::Headers, db::{models::Folder, DbConn}, }; @@ -47,17 +47,10 @@ pub struct FolderData { } #[post("/folders", data = "<data>")] -async fn post_folders( - data: JsonUpcase<FolderData>, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> JsonResult { +async fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn) -> JsonResult { let data: FolderData = data.into_inner().data; let mut folder = Folder::new(headers.user.uuid, data.Name); folder.save(&conn).await?; - nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid) - .await; Ok(Json(folder.to_json())) } @@ -67,9 +60,8 @@ async fn post_folder( data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> JsonResult { - put_folder(uuid, data, headers, conn, nt).await + put_folder(uuid, data, headers, conn).await } #[put("/folders/<uuid>", data = "<data>")] @@ -78,7 +70,6 @@ async fn put_folder( data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, - nt: Notify<'_>, ) -> JsonResult { let data: FolderData = data.into_inner().data; let Some(mut folder) = Folder::find_by_uuid(uuid, &conn).await else { @@ -89,23 +80,16 @@ async fn put_folder( } folder.name = data.Name; folder.save(&conn).await?; - nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid) - .await; Ok(Json(folder.to_json())) } #[post("/folders/<uuid>/delete")] -async fn delete_folder_post( - uuid: &str, - headers: Headers, - conn: DbConn, - nt: Notify<'_>, -) -> EmptyResult { - delete_folder(uuid, headers, conn, nt).await +async fn delete_folder_post(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult { + delete_folder(uuid, headers, conn).await } #[delete("/folders/<uuid>")] -async fn delete_folder(uuid: &str, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { +async fn delete_folder(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult { let Some(folder) = Folder::find_by_uuid(uuid, &conn).await else { err!("Invalid folder") }; @@ -114,7 +98,5 @@ async fn delete_folder(uuid: &str, headers: Headers, conn: DbConn, nt: Notify<'_ } // Delete the actual folder entry folder.delete(&conn).await?; - nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.uuid) - .await; Ok(()) } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs @@ -137,15 +137,13 @@ const fn version() -> Json<&'static str> { #[get("/config")] fn config() -> Json<Value> { - let domain = &config_file::get_config().domain; + let domain = &config_file::get_config().domain_url(); Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns // This means they expect a version that closely matches the Bitwarden server version // We should make sure that we keep this updated when we support the new server features - // Version history: - // - Individual cipher key encryption: 2023.9.1 - "version": "2023.9.1", - "gitHash": option_env!("GIT_REV"), + "version": "2023.12.1", + "gitHash": "", "server": { "name": "Vaultwarden", "url": "https://github.com/dani-garcia/vaultwarden", @@ -155,15 +153,13 @@ fn config() -> Json<Value> { "vault": domain, "api": format!("{domain}/api"), "identity": format!("{domain}/identity"), - "notifications": format!("{domain}/notifications"), - "sso": "", + "notifications": "", + "sso": "" }, "featureStates": { - // Any feature flags that we want the clients to use - // Can check the enabled ones at: - // https://vault.bitwarden.com/api/config - "fido2-vault-credentials": true, // Passkey support - "autofill-v2": false, // Disabled because it is causing issues https://github.com/dani-garcia/vaultwarden/discussions/4052 + "autofill-overlay": true, + "autofill-v2": true, + "fido2-vault-credentials": true }, "object": "config", })) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs @@ -1,8 +1,8 @@ use crate::{ api::{ core::{CipherSyncData, CipherSyncType}, - EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, Notify, NumberOrString, - PasswordOrOtpData, UpdateType, + EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, NumberOrString, + PasswordOrOtpData, }, auth::{self, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, db::{ @@ -950,12 +950,11 @@ async fn bulk_delete_user( data: JsonUpcase<OrgBulkIds>, headers: AdminHeaders, conn: DbConn, - nt: Notify<'_>, ) -> Json<Value> { let data: OrgBulkIds = data.into_inner().data; let mut bulk_response = Vec::new(); for org_user_id in data.Ids { - let err_msg = match _delete_user(org_id, &org_user_id, &headers, &conn, &nt).await { + let err_msg = match _delete_user(org_id, &org_user_id, &headers, &conn).await { Ok(()) => String::new(), Err(e) => format!("{e:?}"), }; @@ -980,9 +979,8 @@ async fn delete_user( org_user_id: &str, headers: AdminHeaders, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - _delete_user(org_id, org_user_id, &headers, &conn, &nt).await + _delete_user(org_id, org_user_id, &headers, &conn).await } #[post("/organizations/<org_id>/users/<org_user_id>/delete")] @@ -991,9 +989,8 @@ async fn post_delete_user( org_user_id: &str, headers: AdminHeaders, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { - _delete_user(org_id, org_user_id, &headers, &conn, &nt).await + _delete_user(org_id, org_user_id, &headers, &conn).await } async fn _delete_user( @@ -1001,7 +998,6 @@ async fn _delete_user( org_user_id: &str, headers: &AdminHeaders, conn: &DbConn, - nt: &Notify<'_>, ) -> EmptyResult { let Some(user_to_delete) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await @@ -1021,9 +1017,6 @@ async fn _delete_user( err!("Can't delete the last owner") } } - if let Some(user) = User::find_by_uuid(&user_to_delete.user_uuid, conn).await { - nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; - } user_to_delete.delete(conn).await } @@ -1092,7 +1085,6 @@ async fn post_org_import( data: JsonUpcase<ImportData>, headers: AdminHeaders, conn: DbConn, - nt: Notify<'_>, ) -> EmptyResult { let data: ImportData = data.into_inner().data; let org_id = query.organization_id; @@ -1122,17 +1114,9 @@ async fn post_org_import( let mut ciphers = Vec::new(); for cipher_data in data.Ciphers { let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone()); - update_cipher_from_data( - &mut cipher, - cipher_data, - &headers, - false, - &conn, - &nt, - UpdateType::None, - ) - .await - .ok(); + update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn, true) + .await + .ok(); ciphers.push(cipher); } // Assign the collections diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs @@ -6,7 +6,7 @@ use crate::{ use rocket::Route; pub fn routes() -> Vec<Route> { - routes![get_duo, activate_duo, activate_duo_put,] + routes![activate_duo, activate_duo_put, get_duo] } const DUO_DISABLED_MSG: &str = "Duo is disabled."; #[allow(unused_variables, clippy::needless_pass_by_value)] diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs @@ -6,7 +6,7 @@ use crate::{ use rocket::Route; pub fn routes() -> Vec<Route> { - routes![get_email, send_email_login, send_email, email,] + routes![email, get_email, send_email, send_email_login] } #[derive(Deserialize)] diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs @@ -27,10 +27,7 @@ pub fn routes() -> Vec<Route> { } fn build_webauthn() -> Result<Webauthn, WebauthnError> { WebauthnBuilder::new( - config::get_config() - .domain - .domain() - .expect("a valid domain"), + config::get_config().domain(), &Url::parse(&config::get_config().domain_origin()).expect("a valid URL"), )? .build() diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs @@ -6,7 +6,7 @@ use crate::{ use rocket::Route; pub fn routes() -> Vec<Route> { - routes![generate_yubikey, activate_yubikey, activate_yubikey_put,] + routes![activate_yubikey, activate_yubikey_put, generate_yubikey] } #[derive(Deserialize)] diff --git a/src/api/mod.rs b/src/api/mod.rs @@ -2,23 +2,12 @@ mod admin; pub mod core; mod icons; mod identity; -mod notifications; mod web; pub use crate::api::{ - admin::catchers as admin_catchers, - admin::routes as admin_routes, - core::catchers as core_catchers, - core::events_routes as core_events_routes, - core::routes as core_routes, - icons::routes as icons_routes, - identity::routes as identity_routes, - notifications::routes as notifications_routes, - notifications::{ - init_ws_anonymous_subscriptions, init_ws_users, start_notification_server, - ws_anonymous_subscriptions, Notify, UpdateType, - }, - web::catchers as web_catchers, - web::routes as web_routes, + admin::catchers as admin_catchers, admin::routes as admin_routes, + core::catchers as core_catchers, core::events_routes as core_events_routes, + core::routes as core_routes, icons::routes as icons_routes, + identity::routes as identity_routes, web::catchers as web_catchers, web::routes as web_routes, }; use crate::db::models::User; use crate::error::Error; diff --git a/src/api/notifications.rs b/src/api/notifications.rs @@ -1,496 +0,0 @@ -use crate::{ - auth::{self, ClientIp, WsAccessTokenHeader}, - db::models::{Cipher, Folder, User}, - Error, -}; -use chrono::{NaiveDateTime, Utc}; -use core::convert; -use rmpv::Value; -use rocket::{futures::StreamExt, Route}; -use std::sync::OnceLock; -use std::{sync::Arc, time::Duration}; -use tokio::sync::mpsc::{channel, Sender}; -use tokio::time; -use tokio_tungstenite::tungstenite::Message; -static WS_USERS: OnceLock<Arc<WebSocketUsers>> = OnceLock::new(); -#[inline] -pub fn init_ws_users() { - if WS_USERS - .set(Arc::new(WebSocketUsers { - map: Arc::new(dashmap::DashMap::new()), - })) - .is_err() - { - panic!("WS_USERS must be initialized only once") - } -} -#[inline] -fn ws_users() -> &'static Arc<WebSocketUsers> { - WS_USERS - .get() - .expect("WS_USERS should be initialized in main") -} -static WS_ANONYMOUS_SUBSCRIPTIONS: OnceLock<Arc<AnonymousWebSocketSubscriptions>> = OnceLock::new(); -#[inline] -pub fn init_ws_anonymous_subscriptions() { - if WS_ANONYMOUS_SUBSCRIPTIONS - .set(Arc::new(AnonymousWebSocketSubscriptions { - map: Arc::new(dashmap::DashMap::new()), - })) - .is_err() - { - panic!("WS_ANONYMOUS_SUBSCRIPTIONS must only be initialized once") - } -} -#[inline] -pub fn ws_anonymous_subscriptions() -> &'static Arc<AnonymousWebSocketSubscriptions> { - WS_ANONYMOUS_SUBSCRIPTIONS - .get() - .expect("WS_ANONYMOUS_SUBSCRIPTIONS should be initialized in main") -} - -pub fn routes() -> Vec<Route> { - routes![anonymous_websockets_hub, websockets_hub] -} - -#[derive(FromForm)] -struct WsAccessToken { - access_token: Option<String>, -} - -struct WSEntryMapGuard { - users: Arc<WebSocketUsers>, - user_uuid: String, - entry_uuid: uuid::Uuid, -} - -impl WSEntryMapGuard { - fn new(users: Arc<WebSocketUsers>, user_uuid: String, entry_uuid: uuid::Uuid) -> Self { - Self { - users, - user_uuid, - entry_uuid, - } - } -} - -impl Drop for WSEntryMapGuard { - fn drop(&mut self) { - if let Some(mut entry) = self.users.map.get_mut(&self.user_uuid) { - entry.retain(|tup| tup.0 != self.entry_uuid); - } - } -} - -struct WSAnonymousEntryMapGuard { - subscriptions: Arc<AnonymousWebSocketSubscriptions>, - token: String, -} - -impl WSAnonymousEntryMapGuard { - fn new(subscriptions: Arc<AnonymousWebSocketSubscriptions>, token: String) -> Self { - Self { - subscriptions, - token, - } - } -} - -impl Drop for WSAnonymousEntryMapGuard { - fn drop(&mut self) { - self.subscriptions.map.remove(&self.token); - } -} - -#[get("/hub?<data..>")] -fn websockets_hub<'r>( - ws: rocket_ws::WebSocket, - data: WsAccessToken, - _ip: ClientIp, - header_token: WsAccessTokenHeader, -) -> Result<rocket_ws::Stream!['r], Error> { - let token = if let Some(token) = data.access_token { - token - } else if let Some(token) = header_token.access_token { - token - } else { - err_code!("Invalid claim", 401) - }; - - let Ok(claims) = auth::decode_login(&token) else { - err_code!("Invalid token", 401) - }; - let (mut rx, guard) = { - let users = Arc::clone(ws_users()); - // Add a channel to send messages to this client to the map - let entry_uuid = uuid::Uuid::new_v4(); - let (tx, rx) = channel::<Message>(100); - users - .map - .entry(claims.sub.clone()) - .or_default() - .push((entry_uuid, tx)); - - // Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map - (rx, WSEntryMapGuard::new(users, claims.sub, entry_uuid)) - }; - Ok({ - rocket_ws::Stream! { ws => { - let mut ws_copy = ws; - let _guard = guard; - let mut interval = time::interval(Duration::from_secs(15)); - loop { - tokio::select! { - res = ws_copy.next() => { - match res { - Some(Ok(message)) => { - match message { - // Respond to any pings - Message::Ping(ping) => yield Message::Pong(ping), - Message::Pong(_) => {/* Ignored */}, - // We should receive an initial message with the protocol and version, and we will reply to it - Message::Text(ref message) => { - let msg = message.strip_suffix(char::from(RECORD_SEPARATOR)).unwrap_or(message); - if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) { - yield Message::binary(INITIAL_RESPONSE); - continue; - } - } - // Prevent sending anything back when a `Close` Message is received. - // Just break the loop - Message::Close(_) => break, - // Just echo anything else the client sends - _ => yield message, - } - } - _ => break, - } - } - res = rx.recv() => { - match res { - Some(res) => yield res, - None => break, - } - } - _ = interval.tick() => yield Message::Ping(create_ping()) - } - } - }} - }) -} -#[get("/anonymous-hub?<token..>")] -fn anonymous_websockets_hub<'r>( - ws: rocket_ws::WebSocket, - token: String, - _ip: ClientIp, -) -> rocket_ws::Stream!['r] { - let (mut rx, guard) = { - let subscriptions = Arc::clone(ws_anonymous_subscriptions()); - // Add a channel to send messages to this client to the map - let (tx, rx) = channel::<Message>(100); - subscriptions.map.insert(token.clone(), tx); - // Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map - (rx, WSAnonymousEntryMapGuard::new(subscriptions, token)) - }; - rocket_ws::Stream! { ws => { - let mut ws_copy = ws; - let _guard = guard; - let mut interval = time::interval(Duration::from_secs(15)); - loop { - tokio::select! { - res = ws_copy.next() => { - match res { - Some(Ok(message)) => { - match message { - // Respond to any pings - Message::Ping(ping) => yield Message::Pong(ping), - Message::Pong(_) => {/* Ignored */}, - // We should receive an initial message with the protocol and version, and we will reply to it - Message::Text(ref message) => { - let msg = message.strip_suffix(char::from(RECORD_SEPARATOR)).unwrap_or(message); - if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) { - yield Message::binary(INITIAL_RESPONSE); - continue; - } - } - // Prevent sending anything back when a `Close` Message is received. - // Just break the loop - Message::Close(_) => break, - // Just echo anything else the client sends - _ => yield message, - } - } - _ => break, - } - } - res = rx.recv() => { - match res { - Some(res) => yield res, - None => break, - } - } - _ = interval.tick() => yield Message::Ping(create_ping()) - } - } - }} -} -fn serialize(val: &Value) -> Vec<u8> { - use rmpv::encode::write_value; - let mut buf = Vec::new(); - write_value(&mut buf, val).expect("Error encoding MsgPack"); - // Add size bytes at the start - // Extracted from BinaryMessageFormat.js - let mut size: usize = buf.len(); - let mut len_buf: Vec<u8> = Vec::new(); - loop { - let mut size_part = size & 0x7f; - size >>= 7i32; - if size > 0 { - size_part |= 0x80; - } - len_buf.push(u8::try_from(size_part).unwrap()); - if size == 0 { - break; - } - } - len_buf.append(&mut buf); - len_buf -} -#[allow(clippy::big_endian_bytes)] -fn serialize_date(date: NaiveDateTime) -> Value { - let seconds: i64 = date.timestamp(); - let nanos: i64 = date.timestamp_subsec_nanos().into(); - let timestamp = nanos << 34i32 | seconds; - let bs = timestamp.to_be_bytes(); - // -1 is Timestamp - // https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type - Value::Ext(-1, bs.to_vec()) -} - -fn convert_option<T: Into<Value>>(option: Option<T>) -> Value { - option.map_or(Value::Nil, convert::Into::into) -} - -const RECORD_SEPARATOR: u8 = 0x1e; -const INITIAL_RESPONSE: [u8; 3] = [0x7b, 0x7d, RECORD_SEPARATOR]; // {, }, <RS> - -#[derive(Deserialize, Copy, Clone, Eq, PartialEq)] -struct InitialMessage<'a> { - protocol: &'a str, - version: i32, -} - -static INITIAL_MESSAGE: InitialMessage<'static> = InitialMessage { - protocol: "messagepack", - version: 1, -}; - -// We attach the UUID to the sender so we can differentiate them when we need to remove them from the Vec -type UserSenders = (uuid::Uuid, Sender<Message>); -#[derive(Clone)] -pub struct WebSocketUsers { - map: Arc<dashmap::DashMap<String, Vec<UserSenders>>>, -} - -impl WebSocketUsers { - async fn send_update(&self, user_uuid: &str, data: &[u8]) { - if let Some(user) = self.map.get(user_uuid).map(|v| v.clone()) { - for tup in user { - if let Err(e) = tup.1.send(Message::binary(data)).await { - error!("Error sending WS update {e}"); - } - } - } - } - - // NOTE: The last modified date needs to be updated before calling these methods - pub async fn send_user_update(&self, ut: UpdateType, user: &User) { - let data = create_update( - vec![ - ("UserId".into(), user.uuid.clone().into()), - ("Date".into(), serialize_date(user.updated_at)), - ], - ut, - None, - ); - self.send_update(&user.uuid, &data).await; - } - - pub async fn send_logout(&self, user: &User, acting_device_uuid: Option<String>) { - let data = create_update( - vec![ - ("UserId".into(), user.uuid.clone().into()), - ("Date".into(), serialize_date(user.updated_at)), - ], - UpdateType::LogOut, - acting_device_uuid.clone(), - ); - self.send_update(&user.uuid, &data).await; - } - - pub async fn send_folder_update( - &self, - ut: UpdateType, - folder: &Folder, - acting_device_uuid: &String, - ) { - let data = create_update( - vec![ - ("Id".into(), folder.uuid.clone().into()), - ("UserId".into(), folder.user_uuid.clone().into()), - ("RevisionDate".into(), serialize_date(folder.updated_at)), - ], - ut, - Some(acting_device_uuid.into()), - ); - self.send_update(&folder.user_uuid, &data).await; - } - - pub async fn send_cipher_update( - &self, - ut: UpdateType, - cipher: &Cipher, - user_uuids: &[String], - acting_device_uuid: &String, - collection_uuids: Option<Vec<String>>, - ) { - let org_uuid = convert_option(cipher.organization_uuid.clone()); - // Depending if there are collections provided or not, we need to have different values for the following variables. - // The user_uuid should be `null`, and the revision date should be set to now, else the clients won't sync the collection change. - let (user_uuid, collection_uuids, revision_date) = collection_uuids.map_or_else( - || { - ( - convert_option(cipher.user_uuid.clone()), - Value::Nil, - serialize_date(cipher.updated_at), - ) - }, - |col_uuids| { - ( - Value::Nil, - Value::Array( - col_uuids - .into_iter() - .map(convert::Into::into) - .collect::<Vec<rmpv::Value>>(), - ), - serialize_date(Utc::now().naive_utc()), - ) - }, - ); - let data = create_update( - vec![ - ("Id".into(), cipher.uuid.clone().into()), - ("UserId".into(), user_uuid), - ("OrganizationId".into(), org_uuid), - ("CollectionIds".into(), collection_uuids), - ("RevisionDate".into(), revision_date), - ], - ut, - Some(acting_device_uuid.into()), - ); - - for uuid in user_uuids { - self.send_update(uuid, &data).await; - } - } -} - -#[derive(Clone)] -pub struct AnonymousWebSocketSubscriptions { - map: Arc<dashmap::DashMap<String, Sender<Message>>>, -} - -/* Message Structure -[ - 1, // MessageType.Invocation - {}, // Headers (map) - null, // InvocationId - "ReceiveMessage", // Target - [ // Arguments - { - "ContextId": acting_device_uuid || Nil, - "Type": ut as i32, - "Payload": {} - } - ] -] -*/ -fn create_update( - payload: Vec<(Value, Value)>, - ut: UpdateType, - acting_device_uuid: Option<String>, -) -> Vec<u8> { - use rmpv::Value as V; - let value = V::Array(vec![ - 1i32.into(), - V::Map(vec![]), - V::Nil, - "ReceiveMessage".into(), - V::Array(vec![V::Map(vec![ - ( - "ContextId".into(), - acting_device_uuid.map_or(V::Nil, convert::Into::into), - ), - ("Type".into(), (i32::from(ut)).into()), - ("Payload".into(), payload.into()), - ])]), - ]); - serialize(&value) -} - -fn create_ping() -> Vec<u8> { - serialize(&Value::Array(vec![6i32.into()])) -} - -#[allow(dead_code)] -#[derive(Copy, Clone, Eq, PartialEq)] -pub enum UpdateType { - SyncCipherUpdate = 0, - SyncCipherCreate = 1, - SyncLoginDelete = 2, - SyncFolderDelete = 3, - SyncCiphers = 4, - SyncVault = 5, - SyncOrgKeys = 6, - SyncFolderCreate = 7, - SyncFolderUpdate = 8, - SyncCipherDelete = 9, - SyncSettings = 10, - LogOut = 11, - SyncSendCreate = 12, - SyncSendUpdate = 13, - SyncSendDelete = 14, - AuthRequest = 15, - AuthRequestResponse = 16, - None = 100, -} -impl From<UpdateType> for i32 { - fn from(value: UpdateType) -> Self { - match value { - UpdateType::SyncCipherUpdate => 0i32, - UpdateType::SyncCipherCreate => 1i32, - UpdateType::SyncLoginDelete => 2i32, - UpdateType::SyncFolderDelete => 3i32, - UpdateType::SyncCiphers => 4i32, - UpdateType::SyncVault => 5i32, - UpdateType::SyncOrgKeys => 6i32, - UpdateType::SyncFolderCreate => 7i32, - UpdateType::SyncFolderUpdate => 8i32, - UpdateType::SyncCipherDelete => 9i32, - UpdateType::SyncSettings => 10i32, - UpdateType::LogOut => 11i32, - UpdateType::SyncSendCreate => 12i32, - UpdateType::SyncSendUpdate => 13i32, - UpdateType::SyncSendDelete => 14i32, - UpdateType::AuthRequest => 15i32, - UpdateType::AuthRequestResponse => 16i32, - UpdateType::None => 100i32, - } - } -} - -pub type Notify<'a> = &'a rocket::State<Arc<WebSocketUsers>>; -pub fn start_notification_server() -> Arc<WebSocketUsers> { - Arc::clone(ws_users()) -} diff --git a/src/auth.rs b/src/auth.rs @@ -284,7 +284,7 @@ impl<'r> FromRequest<'r> for Host { type Error = &'static str; async fn from_request(_: &'r Request<'_>) -> Outcome<Self, Self::Error> { Outcome::Success(Self { - host: config::get_config().domain.to_string(), + host: config::get_config().domain_url().to_owned(), }) } } diff --git a/src/config.rs b/src/config.rs @@ -1,5 +1,6 @@ use core::fmt::{self, Display, Formatter}; use core::num::NonZeroU8; +use core::str; use rocket::config::{CipherSuite, LogLevel, TlsConfig}; use rocket::data::{Limits, ToByteUnit}; use std::error; @@ -90,7 +91,7 @@ pub struct Config { pub database_max_conns: NonZeroU8, pub database_timeout: u16, pub db_connection_retries: NonZeroU8, - pub domain: Url, + domain: Url, pub password_iterations: u32, pub rocket: rocket::Config, pub web_vault_enabled: bool, @@ -132,19 +133,23 @@ impl Config { if let Some(count) = config_file.workers { rocket.workers = usize::from(count.get()); } - let domain = Url::parse( - format!( - "https://{}{}", - config_file.domain, - if config_file.port == 443 { - String::new() - } else { - format!(":{}", config_file.port) - } - ) - .as_str(), - )?; - if domain.domain().is_none() { + let url = format!( + "https://{}{}", + config_file.domain, + if config_file.port == 443 { + String::new() + } else { + format!(":{}", config_file.port) + } + ); + let domain = Url::parse(url.as_str())?; + if domain + .domain() + // We only allow domains in the config file. + // Note currently this check is overly conservative and + // disallows any domains that `Url` will encode in Punycode. + .map_or(true, |dom| !dom.eq_ignore_ascii_case(&config_file.domain)) + { return Err(ConfigErr::BadDomain); } Ok(Self { @@ -176,12 +181,25 @@ impl Config { pub const DATABASE_URL: &'static str = "data/db.sqlite3"; pub const PRIVATE_ED25519_KEY: &'static str = "data/ed25519_key.pem"; pub const WEB_VAULT_FOLDER: &'static str = "web-vault/"; + #[allow(clippy::arithmetic_side_effects, clippy::string_slice)] #[inline] - pub fn domain_origin(&self) -> String { - self.domain.origin().ascii_serialization() + pub fn domain_url(&self) -> &str { + let val = self.domain.as_str(); + // The last Unicode scalar value is '/' which is a + // single UTF-8 code unit, and we want to remove that. + // Note if this changes in the future such that the last + // Unicode scalar value is encoded using more than one + // UTF-8 code unit, then this will panic. + // Additionally if `len` is somehow 0, indexing will panic + // making this memory and logic safe. + &val[..val.len() - 1] } #[inline] - pub fn domain_path(&self) -> &str { - self.domain.path().trim_end_matches('/') + pub fn domain(&self) -> &str { + self.domain.domain().expect("impossible to error") + } + #[inline] + pub fn domain_origin(&self) -> String { + self.domain.origin().ascii_serialization() } } diff --git a/src/error.rs b/src/error.rs @@ -47,7 +47,6 @@ use rocket::error::Error as RocketErr; use serde_json::{Error as SerdeErr, Value}; use std::io::Error as IoErr; use std::time::SystemTimeError as TimeErr; -use tokio_tungstenite::tungstenite::Error as TungstError; use webauthn_rs::prelude::WebauthnError as WebauthnErr; #[derive(Serialize)] @@ -79,7 +78,6 @@ make_error! { DieselCon(DieselConErr): _has_source, _api_error, Webauthn(WebauthnErr): _has_source, _api_error, - WebSocket(TungstError): _has_source, _api_error, } // Error struct // Contains a String error message, meant for the user and an enum variant, with an error of different types. @@ -108,7 +106,6 @@ make_error! { DieselCon(DieselConErr): _has_source, _api_error, Webauthn(WebauthnErr): _has_source, _api_error, - WebSocket(TungstError): _has_source, _api_error, } #[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] impl From<Infallible> for Error { @@ -142,8 +139,7 @@ impl Debug for Error { | ErrorKind::OpenSSL(_) | ErrorKind::Rocket(_) | ErrorKind::DieselCon(_) - | ErrorKind::Webauthn(_) - | ErrorKind::WebSocket(_) => unreachable!(), + | ErrorKind::Webauthn(_) => unreachable!(), }, } } diff --git a/src/main.rs b/src/main.rs @@ -57,7 +57,6 @@ // We want to keep this as low as possible, but not higher then 128. // If you go above 128 it will cause rust-analyzer to fail, #![recursion_limit = "103"] -extern crate alloc; #[macro_use] extern crate diesel; #[macro_use] @@ -75,7 +74,6 @@ mod crypto; mod db; mod priv_sep; mod util; -use alloc::sync::Arc; use config::Config; pub use error::{Error, MapResult}; use std::env; @@ -124,8 +122,6 @@ fn static_init() { ) }); auth::init_values(); - api::init_ws_users(); - api::init_ws_anonymous_subscriptions(); } #[allow(clippy::exit)] @@ -160,24 +156,17 @@ async fn create_db_pool() -> db::DbPool { } async fn launch_rocket(pool: db::DbPool) -> Result<(), Error> { - let basepath = config::get_config().domain_path(); let instance = rocket::custom(&config::get_config().rocket) - .mount([basepath, "/"].concat(), api::web_routes()) - .mount([basepath, "/admin"].concat(), api::admin_routes()) - .mount([basepath, "/api"].concat(), api::core_routes()) - .mount([basepath, "/events"].concat(), api::core_events_routes()) - .mount([basepath, "/icons"].concat(), api::icons_routes()) - .mount([basepath, "/identity"].concat(), api::identity_routes()) - .mount( - [basepath, "/notifications"].concat(), - api::notifications_routes(), - ) - .register([basepath, "/"].concat(), api::web_catchers()) - .register([basepath, "/admin"].concat(), api::admin_catchers()) - .register([basepath, "/api"].concat(), api::core_catchers()) + .mount("/", api::web_routes()) + .mount("/admin", api::admin_routes()) + .mount("/api", api::core_routes()) + .mount("/events", api::core_events_routes()) + .mount("/icons", api::icons_routes()) + .mount("/identity", api::identity_routes()) + .register("/", api::web_catchers()) + .register("/admin", api::admin_catchers()) + .register("/api", api::core_catchers()) .manage(pool) - .manage(api::start_notification_server()) - .manage(Arc::clone(api::ws_anonymous_subscriptions())) .attach(util::AppHeaders) .attach(util::Cors) .ignite()