diff options
Diffstat (limited to 'src/api')
-rw-r--r-- | src/api/admin.rs | 88 | ||||
-rw-r--r-- | src/api/core/accounts.rs | 12 | ||||
-rw-r--r-- | src/api/core/ciphers.rs | 139 | ||||
-rw-r--r-- | src/api/core/folders.rs | 10 | ||||
-rw-r--r-- | src/api/core/mod.rs | 29 | ||||
-rw-r--r-- | src/api/core/organizations.rs | 58 | ||||
-rw-r--r-- | src/api/core/sends.rs | 47 | ||||
-rw-r--r-- | src/api/core/two_factor/authenticator.rs | 25 | ||||
-rw-r--r-- | src/api/core/two_factor/duo.rs | 19 | ||||
-rw-r--r-- | src/api/core/two_factor/email.rs | 9 | ||||
-rw-r--r-- | src/api/core/two_factor/mod.rs | 8 | ||||
-rw-r--r-- | src/api/core/two_factor/u2f.rs | 24 | ||||
-rw-r--r-- | src/api/icons.rs | 119 | ||||
-rw-r--r-- | src/api/identity.rs | 53 | ||||
-rw-r--r-- | src/api/mod.rs | 9 | ||||
-rw-r--r-- | src/api/notifications.rs | 32 | ||||
-rw-r--r-- | src/api/web.rs | 8 |
17 files changed, 335 insertions, 354 deletions
diff --git a/src/api/admin.rs b/src/api/admin.rs index d484407a..6e168e7a 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -1,9 +1,8 @@ use once_cell::sync::Lazy; use serde::de::DeserializeOwned; use serde_json::Value; -use std::{env, process::Command, time::Duration}; +use std::{env, time::Duration}; -use reqwest::{blocking::Client, header::USER_AGENT}; use rocket::{ http::{Cookie, Cookies, SameSite}, request::{self, FlashMessage, Form, FromRequest, Outcome, Request}, @@ -19,7 +18,7 @@ use crate::{ db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, error::{Error, MapResult}, mail, - util::{format_naive_datetime_local, get_display_size, is_running_in_docker}, + util::{format_naive_datetime_local, get_display_size, get_reqwest_client, is_running_in_docker}, CONFIG, }; @@ -64,12 +63,8 @@ static DB_TYPE: Lazy<&str> = Lazy::new(|| { .unwrap_or("Unknown") }); -static CAN_BACKUP: Lazy<bool> = Lazy::new(|| { - DbConnType::from_url(&CONFIG.database_url()) - .map(|t| t == DbConnType::sqlite) - .unwrap_or(false) - && Command::new("sqlite3").arg("-version").status().is_ok() -}); +static CAN_BACKUP: Lazy<bool> = + Lazy::new(|| DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false)); #[get("/")] fn admin_disabled() -> &'static str { @@ -142,7 +137,12 @@ fn admin_url(referer: Referer) -> String { fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> { // If there is an error, show it let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg())); - let json = json!({"page_content": "admin/login", "version": VERSION, "error": msg, "urlpath": CONFIG.domain_path()}); + let json = json!({ + "page_content": "admin/login", + "version": VERSION, + "error": msg, + "urlpath": CONFIG.domain_path() + }); // Return the page let text = CONFIG.render_template(BASE_TEMPLATE, &json)?; @@ -166,10 +166,7 @@ fn post_admin_login( // If the token is invalid, redirect to login page if !_validate_token(&data.token) { error!("Invalid admin token. IP: {}", ip.ip); - Err(Flash::error( - Redirect::to(admin_url(referer)), - "Invalid admin token, please try again.", - )) + Err(Flash::error(Redirect::to(admin_url(referer)), "Invalid admin token, please try again.")) } else { // If the token received is valid, generate JWT and save it as a cookie let claims = generate_admin_claims(); @@ -329,7 +326,8 @@ fn get_users_json(_token: AdminToken, conn: DbConn) -> Json<Value> { fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { let users = User::get_all(&conn); let dt_fmt = "%Y-%m-%d %H:%M:%S %Z"; - let users_json: Vec<Value> = users.iter() + let users_json: Vec<Value> = users + .iter() .map(|u| { let mut usr = u.to_json(&conn); usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &conn)); @@ -339,7 +337,7 @@ fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { usr["created_at"] = json!(format_naive_datetime_local(&u.created_at, dt_fmt)); usr["last_active"] = match u.last_active(&conn) { Some(dt) => json!(format_naive_datetime_local(&dt, dt_fmt)), - None => json!("Never") + None => json!("Never"), }; usr }) @@ -424,7 +422,6 @@ fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, conn: D user_to_edit.save(&conn) } - #[post("/users/update_revision")] fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult { User::update_all_revisions(&conn) @@ -433,7 +430,8 @@ fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult { #[get("/organizations/overview")] fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { let organizations = Organization::get_all(&conn); - let organizations_json: Vec<Value> = organizations.iter() + let organizations_json: Vec<Value> = organizations + .iter() .map(|o| { let mut org = o.to_json(); org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn)); @@ -470,26 +468,15 @@ struct GitCommit { } fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> { - let github_api = Client::builder().build()?; + let github_api = get_reqwest_client(); - Ok(github_api - .get(url) - .timeout(Duration::from_secs(10)) - .header(USER_AGENT, "Bitwarden_RS") - .send()? - .error_for_status()? - .json::<T>()?) + Ok(github_api.get(url).timeout(Duration::from_secs(10)).send()?.error_for_status()?.json::<T>()?) } fn has_http_access() -> bool { - let http_access = Client::builder().build().unwrap(); - - match http_access - .head("https://github.com/dani-garcia/bitwarden_rs") - .timeout(Duration::from_secs(10)) - .header(USER_AGENT, "Bitwarden_RS") - .send() - { + let http_access = get_reqwest_client(); + + match http_access.head("https://github.com/dani-garcia/vaultwarden").timeout(Duration::from_secs(10)).send() { Ok(r) => r.status().is_success(), _ => false, } @@ -502,9 +489,16 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu use std::net::ToSocketAddrs; // Get current running versions - let vault_version_path = format!("{}/{}", CONFIG.web_vault_folder(), "version.json"); - let vault_version_str = read_file_string(&vault_version_path)?; - let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?; + let web_vault_version: WebVaultVersion = + match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "bwrs-version.json")) { + Ok(s) => serde_json::from_str(&s)?, + _ => match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "version.json")) { + Ok(s) => serde_json::from_str(&s)?, + _ => WebVaultVersion { + version: String::from("Version file missing"), + }, + }, + }; // Execute some environment checks let running_within_docker = is_running_in_docker(); @@ -524,11 +518,11 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu // TODO: Maybe we need to cache this using a LazyStatic or something. Github only allows 60 requests per hour, and we use 3 here already. let (latest_release, latest_commit, latest_web_build) = if has_http_access { ( - match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bitwarden_rs/releases/latest") { + match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest") { Ok(r) => r.tag_name, _ => "-".to_string(), }, - match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/bitwarden_rs/commits/master") { + match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main") { Ok(mut c) => { c.sha.truncate(8); c.sha @@ -540,7 +534,9 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu if running_within_docker { "-".to_string() } else { - match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest") { + match get_github_api::<GitRelease>( + "https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest", + ) { Ok(r) => r.tag_name.trim_start_matches('v').to_string(), _ => "-".to_string(), } @@ -552,14 +548,15 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu let ip_header_name = match &ip_header.0 { Some(h) => h, - _ => "" + _ => "", }; let diagnostics_json = json!({ "dns_resolved": dns_resolved, - "web_vault_version": web_vault_version.version, "latest_release": latest_release, "latest_commit": latest_commit, + "web_vault_enabled": &CONFIG.web_vault_enabled(), + "web_vault_version": web_vault_version.version, "latest_web_build": latest_web_build, "running_within_docker": running_within_docker, "has_http_access": has_http_access, @@ -571,6 +568,7 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu "db_type": *DB_TYPE, "db_version": get_sql_server_version(&conn), "admin_url": format!("{}/diagnostics", admin_url(Referer(None))), + "server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(), "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference }); @@ -596,11 +594,11 @@ fn delete_config(_token: AdminToken) -> EmptyResult { } #[post("/config/backup_db")] -fn backup_db(_token: AdminToken) -> EmptyResult { +fn backup_db(_token: AdminToken, conn: DbConn) -> EmptyResult { if *CAN_BACKUP { - backup_database() + backup_database(&conn) } else { - err!("Can't back up current DB (either it's not SQLite or the 'sqlite' binary is not present)"); + err!("Can't back up current DB (Only SQLite supports this feature)"); } } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 6e45a947..3888075b 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -95,7 +95,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult { } None => { // Order is important here; the invitation check must come first - // because the bitwarden_rs admin can invite anyone, regardless + // because the vaultwarden admin can invite anyone, regardless // of other signup restrictions. if Invitation::take(&data.Email, &conn) || CONFIG.is_signup_allowed(&data.Email) { User::new(data.Email.clone()) @@ -320,15 +320,7 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt: err!("The cipher is not owned by the user") } - update_cipher_from_data( - &mut saved_cipher, - cipher_data, - &headers, - false, - &conn, - &nt, - UpdateType::CipherUpdate, - )? + update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &nt, UpdateType::CipherUpdate)? } // Update user data diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 7b0de205..0f655f76 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -13,7 +13,7 @@ use crate::{ api::{self, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType}, auth::Headers, crypto, - db::{models::*, DbConn}, + db::{models::*, DbConn, DbPool}, CONFIG, }; @@ -25,7 +25,7 @@ pub fn routes() -> Vec<Route> { // whether the user is an owner/admin of the relevant org, and if so, // allows the operation unconditionally. // - // bitwarden_rs factors in the org owner/admin status as part of + // vaultwarden factors in the org owner/admin status as part of // determining the write accessibility of a cipher, so most // admin/non-admin implementations can be shared. routes![ @@ -77,6 +77,15 @@ pub fn routes() -> Vec<Route> { ] } +pub fn purge_trashed_ciphers(pool: DbPool) { + debug!("Purging trashed ciphers"); + if let Ok(conn) = pool.get() { + Cipher::purge_trash(&conn); + } else { + error!("Failed to get DB connection while purging trashed ciphers") + } +} + #[derive(FromForm, Default)] struct SyncData { #[form(field = "excludeDomains")] @@ -91,24 +100,18 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> Json<Value> { let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect(); let collections = Collection::find_by_user_uuid(&headers.user.uuid, &conn); - let collections_json: Vec<Value> = collections.iter() - .map(|c| c.to_json_details(&headers.user.uuid, &conn)) - .collect(); + let collections_json: Vec<Value> = + collections.iter().map(|c| c.to_json_details(&headers.user.uuid, &conn)).collect(); let policies = OrgPolicy::find_by_user(&headers.user.uuid, &conn); let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect(); let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn); - let ciphers_json: Vec<Value> = ciphers - .iter() - .map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)) - .collect(); + let ciphers_json: Vec<Value> = + ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect(); let sends = Send::find_by_user(&headers.user.uuid, &conn); - let sends_json: Vec<Value> = sends - .iter() - .map(|s| s.to_json()) - .collect(); + let sends_json: Vec<Value> = sends.iter().map(|s| s.to_json()).collect(); let domains_json = if data.exclude_domains { Value::Null @@ -124,6 +127,7 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> Json<Value> { "Ciphers": ciphers_json, "Domains": domains_json, "Sends": sends_json, + "unofficialServer": true, "Object": "sync" })) } @@ -132,10 +136,8 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> Json<Value> { fn get_ciphers(headers: Headers, conn: DbConn) -> Json<Value> { let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn); - let ciphers_json: Vec<Value> = ciphers - .iter() - .map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)) - .collect(); + let ciphers_json: Vec<Value> = + ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect(); Json(json!({ "Data": ciphers_json, @@ -236,7 +238,7 @@ fn post_ciphers_create(data: JsonUpcase<ShareCipherData>, headers: Headers, conn // Check if there are one more more collections selected when this cipher is part of an organization. // err if this is not the case before creating an empty cipher. - if data.Cipher.OrganizationId.is_some() && data.CollectionIds.is_empty() { + if data.Cipher.OrganizationId.is_some() && data.CollectionIds.is_empty() { err!("You must select at least one collection."); } @@ -278,17 +280,12 @@ fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, nt /// allowed to delete or share such ciphers to an org, however. /// /// Ref: https://bitwarden.com/help/article/policies/#personal-ownership -fn enforce_personal_ownership_policy( - data: &CipherData, - headers: &Headers, - conn: &DbConn -) -> EmptyResult { +fn enforce_personal_ownership_policy(data: &CipherData, headers: &Headers, conn: &DbConn) -> EmptyResult { if data.OrganizationId.is_none() { let user_uuid = &headers.user.uuid; let policy_type = OrgPolicyType::PersonalOwnership; if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, conn) { - err!("Due to an Enterprise Policy, you are restricted from \ - saving items to your personal vault.") + err!("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.") } } Ok(()) @@ -307,11 +304,12 @@ pub fn update_cipher_from_data( // Check that the client isn't updating an existing cipher with stale data. if let Some(dt) = data.LastKnownRevisionDate { - match NaiveDateTime::parse_from_str(&dt, "%+") { // ISO 8601 format - Err(err) => - warn!("Error parsing LastKnownRevisionDate '{}': {}", dt, err), - Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 => - err!("The client copy of this cipher is out of date. Resync the client and try again."), + match NaiveDateTime::parse_from_str(&dt, "%+") { + // ISO 8601 format + Err(err) => warn!("Error parsing LastKnownRevisionDate '{}': {}", dt, err), + Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 => { + err!("The client copy of this cipher is out of date. Resync the client and try again.") + } Ok(_) => (), } } @@ -384,12 +382,9 @@ pub fn update_cipher_from_data( // But, we at least know we do not need to store and return this specific key. fn _clean_cipher_data(mut json_data: Value) -> Value { if json_data.is_array() { - json_data.as_array_mut() - .unwrap() - .iter_mut() - .for_each(|ref mut f| { - f.as_object_mut().unwrap().remove("Response"); - }); + json_data.as_array_mut().unwrap().iter_mut().for_each(|ref mut f| { + f.as_object_mut().unwrap().remove("Response"); + }); }; json_data } @@ -411,13 +406,13 @@ pub fn update_cipher_from_data( data["Uris"] = _clean_cipher_data(data["Uris"].clone()); } data - }, + } None => err!("Data missing"), }; cipher.name = data.Name; cipher.notes = data.Notes; - cipher.fields = data.Fields.map(|f| _clean_cipher_data(f).to_string() ); + cipher.fields = data.Fields.map(|f| _clean_cipher_data(f).to_string()); cipher.data = type_data.to_string(); cipher.password_history = data.PasswordHistory.map(|f| f.to_string()); @@ -592,11 +587,8 @@ fn post_collections_admin( } let posted_collections: HashSet<String> = data.CollectionIds.iter().cloned().collect(); - let current_collections: HashSet<String> = cipher - .get_collections(&headers.user.uuid, &conn) - .iter() - .cloned() - .collect(); + let current_collections: HashSet<String> = + cipher.get_collections(&headers.user.uuid, &conn).iter().cloned().collect(); for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid(&collection, &conn) { @@ -832,24 +824,25 @@ fn post_attachment( let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10])); let path = base_path.join(&file_name); - let size = match field.data.save().memory_threshold(0).size_limit(size_limit).with_path(path.clone()) { - SaveResult::Full(SavedData::File(_, size)) => size as i32, - SaveResult::Full(other) => { - std::fs::remove_file(path).ok(); - error = Some(format!("Attachment is not a file: {:?}", other)); - return; - } - SaveResult::Partial(_, reason) => { - std::fs::remove_file(path).ok(); - error = Some(format!("Attachment size limit exceeded with this file: {:?}", reason)); - return; - } - SaveResult::Error(e) => { - std::fs::remove_file(path).ok(); - error = Some(format!("Error: {:?}", e)); - return; - } - }; + let size = + match field.data.save().memory_threshold(0).size_limit(size_limit).with_path(path.clone()) { + SaveResult::Full(SavedData::File(_, size)) => size as i32, + SaveResult::Full(other) => { + std::fs::remove_file(path).ok(); + error = Some(format!("Attachment is not a file: {:?}", other)); + return; + } + SaveResult::Partial(_, reason) => { + std::fs::remove_file(path).ok(); + error = Some(format!("Attachment size limit exceeded with this file: {:?}", reason)); + return; + } + SaveResult::Error(e) => { + std::fs::remove_file(path).ok(); + error = Some(format!("Error: {:?}", e)); + return; + } + }; let mut attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size); attachment.akey = attachment_key.clone(); @@ -984,12 +977,22 @@ fn delete_cipher_selected_admin(data: JsonUpcase<Value>, headers: Headers, conn: } #[post("/ciphers/delete-admin", data = "<data>")] -fn delete_cipher_selected_post_admin(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { +fn delete_cipher_selected_post_admin( + data: JsonUpcase<Value>, + headers: Headers, + conn: DbConn, + nt: Notify, +) -> EmptyResult { delete_cipher_selected_post(data, headers, conn, nt) } #[put("/ciphers/delete-admin", data = "<data>")] -fn delete_cipher_selected_put_admin(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { +fn delete_cipher_selected_put_admin( + data: JsonUpcase<Value>, + headers: Headers, + conn: DbConn, + nt: Notify, +) -> EmptyResult { delete_cipher_selected_put(data, headers, conn, nt) } @@ -1140,7 +1143,13 @@ fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, soft_del Ok(()) } -fn _delete_multiple_ciphers(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, soft_delete: bool, nt: Notify) -> EmptyResult { +fn _delete_multiple_ciphers( + data: JsonUpcase<Value>, + headers: Headers, + conn: DbConn, + soft_delete: bool, + nt: Notify, +) -> EmptyResult { let data: Value = data.into_inner().data; let uuids = match data.get("Ids") { @@ -1192,7 +1201,7 @@ fn _restore_multiple_ciphers(data: JsonUpcase<Value>, headers: &Headers, conn: & for uuid in uuids { match _restore_cipher_by_uuid(uuid, headers, conn, nt) { Ok(json) => ciphers.push(json.into_inner()), - err => return err + err => return err, } } diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index 2779fe61..57ec7f18 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -8,15 +8,7 @@ use crate::{ }; pub fn routes() -> Vec<rocket::Route> { - routes![ - get_folders, - get_folder, - post_folders, - post_folder, - put_folder, - delete_folder_post, - delete_folder, - ] + routes![get_folders, get_folder, post_folders, post_folder, put_folder, delete_folder_post, delete_folder,] } #[get("/folders")] diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 36e83f0e..6f9db9bc 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -2,20 +2,15 @@ mod accounts; mod ciphers; mod folders; mod organizations; -pub mod two_factor; mod sends; +pub mod two_factor; -pub use sends::start_send_deletion_scheduler; +pub use ciphers::purge_trashed_ciphers; +pub use sends::purge_sends; pub fn routes() -> Vec<Route> { - let mut mod_routes = routes![ - clear_device_token, - put_device_token, - get_eq_domains, - post_eq_domains, - put_eq_domains, - hibp_breach, - ]; + let mut mod_routes = + routes![clear_device_token, put_device_token, get_eq_domains, post_eq_domains, put_eq_domains, hibp_breach,]; let mut routes = Vec::new(); routes.append(&mut accounts::routes()); @@ -32,9 +27,9 @@ pub fn routes() -> Vec<Route> { // // Move this somewhere else // +use rocket::response::Response; use rocket::Route; use rocket_contrib::json::Json; -use rocket::response::Response; use serde_json::Value; use crate::{ @@ -42,6 +37,7 @@ use crate::{ auth::Headers, db::DbConn, error::Error, + util::get_reqwest_client, }; #[put("/devices/identifier/<uuid>/clear-token")] @@ -146,22 +142,15 @@ fn put_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbC #[get("/hibp/breach?<username>")] fn hibp_breach(username: String) -> JsonResult { - let user_agent = "Bitwarden_RS"; let url = format!( "https://haveibeenpwned.com/api/v3/breachedaccount/{}?truncateResponse=false&includeUnverified=false", username ); - use reqwest::{blocking::Client, header::USER_AGENT}; - if let Some(api_key) = crate::CONFIG.hibp_api_key() { - let hibp_client = Client::builder().build()?; + let hibp_client = get_reqwest_client(); - let res = hibp_client - .get(&url) - .header(USER_AGENT, user_agent) - .header("hibp-api-key", api_key) - .send()?; + let res = hibp_client.get(&url).header("hibp-api-key", api_key).send()?; // If we get a 404, return a 404, it means no breached accounts if res.status() == 404 { diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 5698c187..cfe3932e 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -5,7 +5,7 @@ use serde_json::Value; use crate::{ api::{EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, Notify, NumberOrString, PasswordData, UpdateType}, - auth::{decode_invite, AdminHeaders, Headers, OwnerHeaders, ManagerHeaders, ManagerHeadersLoose}, + auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, db::{models::*, DbConn}, mail, CONFIG, }; @@ -333,7 +333,12 @@ fn post_organization_collection_delete_user( } #[delete("/organizations/<org_id>/collections/<col_id>")] -fn delete_organization_collection(org_id: String, col_id: String, _headers: ManagerHeaders, conn: DbConn) -> EmptyResult { +fn delete_organization_collection( + org_id: String, + col_id: String, + _headers: ManagerHeaders, + conn: DbConn, +) -> EmptyResult { match Collection::find_by_uuid(&col_id, &conn) { None => err!("Collection not found"), Some(collection) => { @@ -426,9 +431,7 @@ fn put_collection_users( continue; } - CollectionUser::save(&user.user_uuid, &coll_id, - d.ReadOnly, d.HidePasswords, - &conn)?; + CollectionUser::save(&user.user_uuid, &coll_id, d.ReadOnly, d.HidePasswords, &conn)?; } Ok(()) @@ -443,10 +446,8 @@ struct OrgIdData { #[get("/ciphers/organization-details?<data..>")] fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> Json<Value> { let ciphers = Cipher::find_by_org(&data.organization_id, &conn); - let ciphers_json: Vec<Value> = ciphers - .iter() - .map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)) - .collect(); + let ciphers_json: Vec<Value> = + ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect(); Json(json!({ "Data": ciphers_json, @@ -544,9 +545,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) { None => err!("Collection not found in Organization"), Some(collection) => { - CollectionUser::save(&user.uuid, &collection.uuid, - col.ReadOnly, col.HidePasswords, - &conn)?; + CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, col.HidePasswords, &conn)?; } } } @@ -801,9 +800,13 @@ fn edit_user( match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) { None => err!("Collection not found in Organization"), Some(collection) => { - CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid, - col.ReadOnly, col.HidePasswords, - &conn)?; + CollectionUser::save( + &user_to_edit.user_uuid, + &collection.uuid, + col.ReadOnly, + col.HidePasswords, + &conn, + )?; } } } @@ -899,16 +902,8 @@ fn post_org_import( .into_iter() .map(|cipher_data| { 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::CipherCreate, - ) - .ok(); + update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn, &nt, UpdateType::CipherCreate) + .ok(); cipher }) .collect(); @@ -989,7 +984,13 @@ struct PolicyData { } #[put("/organizations/<org_id>/policies/<pol_type>", data = "<data>")] -fn put_policy(org_id: String, pol_type: i32, data: Json<PolicyData>, _headers: AdminHeaders, conn: DbConn) -> JsonResult { +fn put_policy( + org_id: String, + pol_type: i32, + data: Json<PolicyData>, + _headers: AdminHeaders, + conn: DbConn, +) -> JsonResult { let data: PolicyData = data.into_inner(); let pol_type_enum = match OrgPolicyType::from_i32(pol_type) { @@ -1127,8 +1128,7 @@ fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Headers, con // If user is not part of the organization, but it exists } else if UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &conn).is_none() { - if let Some (user) = User::find_by_mail(&user_data.Email, &conn) { - + if let Some(user) = User::find_by_mail(&user_data.Email, &conn) { let user_org_status = if CONFIG.mail_enabled() { UserOrgStatus::Invited as i32 } else { @@ -1164,7 +1164,7 @@ fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Headers, con // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) if data.OverwriteExisting { for user_org in UserOrganization::find_by_org_and_type(&org_id, UserOrgType::User as i32, &conn) { - if let Some (user_email) = User::find_by_uuid(&user_org.user_uuid, &conn).map(|u| u.email) { + if let Some(user_email) = User::find_by_uuid(&user_org.user_uuid, &conn).map(|u| u.email) { if !data.Users.iter().any(|u| u.Email == user_email) { user_org.delete(&conn)?; } diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index ec6809a2..4cedf055 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -9,39 +9,23 @@ use serde_json::Value; use crate::{ api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, auth::{Headers, Host}, - db::{models::*, DbConn}, + db::{models::*, DbConn, DbPool}, CONFIG, }; const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available"; pub fn routes() -> Vec<rocket::Route> { - routes![ - post_send, - post_send_file, - post_access, - post_access_file, - put_send, - delete_send, - put_remove_password - ] + routes![post_send, post_send_file, post_access, post_access_file, put_send, delete_send, put_remove_password] } -pub fn start_send_deletion_scheduler(pool: crate::db::DbPool) { - std::thread::spawn(move || { - loop { - if let Ok(conn) = pool.get() { - info!("Initiating send deletion"); - for send in Send::find_all(&conn) { - if chrono::Utc::now().naive_utc() >= send.deletion_date { - send.delete(&conn).ok(); - } - } - } - - std::thread::sleep(std::time::Duration::from_secs(3600)); - } - }); +pub fn purge_sends(pool: DbPool) { + debug!("Purging sends"); + if let Ok(conn) = pool.get() { + Send::purge(&conn); + } else { + error!("Failed to get DB connection while purging sends") + } } #[derive(Deserialize)] @@ -179,13 +163,7 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn None => err!("No model entry present"), }; - let size = match data_entry - .data - .save() - .memory_threshold(0) - .size_limit(size_limit) - .with_path(&file_path) - { + let size = match data_entry.data.save().memory_threshold(0).size_limit(size_limit).with_path(&file_path) { SaveResult::Full(SavedData::File(_, size)) => size as i32, SaveResult::Full(other) => { std::fs::remove_file(&file_path).ok(); @@ -206,10 +184,7 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn if let Some(o) = data_value.as_object_mut() { o.insert(String::from("Id"), Value::String(file_id)); o.insert(String::from("Size"), Value::Number(size.into())); - o.insert( - String::from("SizeName"), - Value::String(crate::util::get_display_size(size)), - ); + o.insert(String::from("SizeName"), Value::String(crate::util::get_display_size(size))); } send.data = serde_json::to_string(&data_value)?; diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs index f4bd5df5..2d076b27 100644 --- a/src/api/core/two_factor/authenticator.rs +++ b/src/api/core/two_factor/authenticator.rs @@ -17,11 +17,7 @@ use crate::{ pub use crate::config::CONFIG; pub fn routes() -> Vec<Route> { - routes![ - generate_authenticator, - activate_authenticator, - activate_authenticator_put, - ] + routes![generate_authenticator, activate_authenticator, activate_authenticator_put,] } #[post("/two-factor/get-authenticator", data = "<data>")] @@ -141,7 +137,7 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, ip: &Cl // The amount of steps back and forward in time // Also check if we need to disable time drifted TOTP codes. // If that is the case, we set the steps to 0 so only the current TOTP is valid. - let steps: i64 = if CONFIG.authenticator_disable_time_drift() { 0 } else { 1 }; + let steps = !CONFIG.authenticator_disable_time_drift() as i64; for step in -steps..=steps { let time_step = current_timestamp / 30i64 + step; @@ -163,22 +159,11 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, ip: &Cl twofactor.save(&conn)?; return Ok(()); } else if generated == totp_code && time_step <= twofactor.last_used as i64 { - warn!( - "This or a TOTP code within {} steps back and forward has already been used!", - steps - ); - err!(format!( - "Invalid TOTP code! Server time: {} IP: {}", - current_time.format("%F %T UTC"), - ip.ip - )); + warn!("This or a TOTP code within {} steps back and forward has already been used!", steps); + err!(format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip)); } } // Else no valide code received, deny access - err!(format!( - "Invalid TOTP code! Server time: {} IP: {}", - current_time.format("%F %T UTC"), - ip.ip - )); + err!(format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip)); } diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index 18eda4b2..5ca87085 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -12,6 +12,7 @@ use crate::{ DbConn, }, error::MapResult, + util::get_reqwest_client, CONFIG, }; @@ -59,7 +60,11 @@ impl DuoData { ik.replace_range(digits.., replaced); sk.replace_range(digits.., replaced); - Self { host, ik, sk } + Self { + host, + ik, + sk, + } } } @@ -185,9 +190,7 @@ fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbC } fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult { - const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)"; - - use reqwest::{blocking::Client, header::*, Method}; + use reqwest::{header, Method}; use std::str::FromStr; // https://duo.com/docs/authapi#api-details @@ -199,11 +202,13 @@ fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> Em let m = Method::from_str(method).unwrap_or_default(); - Client::new() + let client = get_reqwest_client(); + + client .request(m, &url) .basic_auth(username, Some(password)) - .header(USER_AGENT, AGENT) - .header(DATE, date) + .header(header::USER_AGENT, "vaultwarden:Duo/1.0 (Rust)") + .header(header::DATE, date) .send()? .error_for_status()?; diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 6aa6e013..c47f9498 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -125,11 +125,7 @@ fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) - let twofactor_data = EmailTokenData::new(data.Email, generated_token); // Uses EmailVerificationChallenge as type to show that it's not verified yet. - let twofactor = TwoFactor::new( - user.uuid, - TwoFactorType::EmailVerificationChallenge, - twofactor_data.to_json(), - ); + let twofactor = TwoFactor::new(user.uuid, TwoFactorType::EmailVerificationChallenge, twofactor_data.to_json()); twofactor.save(&conn)?; mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?)?; @@ -186,7 +182,8 @@ fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonRes /// Validate the email code when used as TwoFactor token mechanism pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &DbConn) -> EmptyResult { let mut email_data = EmailTokenData::from_json(&data)?; - let mut twofactor = TwoFactor::find_by_user_and_type(&user_uuid, TwoFactorType::Email as i32, &conn).map_res("Two factor not found")?; + let mut twofactor = TwoFactor::find_by_user_and_type(&user_uuid, TwoFactorType::Email as i32, &conn) + .map_res("Two factor not found")?; let issued_token = match &email_data.last_token { Some(t) => t, _ => err!("No token available"), diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index a3dfd319..0d0d2bd2 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -20,13 +20,7 @@ pub mod u2f; pub mod yubikey; pub fn routes() -> Vec<Route> { - let mut routes = routes![ - get_twofactor, - get_recover, - recover, - disable_twofactor, - disable_twofactor_put, - ]; + let mut routes = routes![get_twofactor, get_recover, recover, disable_twofactor, disable_twofactor_put,]; routes.append(&mut authenticator::routes()); routes.append(&mut duo::routes()); diff --git a/src/api/core/two_factor/u2f.rs b/src/api/core/two_factor/u2f.rs index f841240b..3455beab 100644 --- a/src/api/core/two_factor/u2f.rs +++ b/src/api/core/two_factor/u2f.rs @@ -28,13 +28,7 @@ static APP_ID: Lazy<String> = Lazy::new(|| format!("{}/app-id.json", &CONFIG.dom static U2F: Lazy<U2f> = Lazy::new(|| U2f::new(APP_ID.clone())); pub fn routes() -> Vec<Route> { - routes![ - generate_u2f, - generate_u2f_challenge, - activate_u2f, - activate_u2f_put, - delete_u2f, - ] + routes![generate_u2f, generate_u2f_challenge, activate_u2f, activate_u2f_put, delete_u2f,] } #[post("/two-factor/get-u2f", data = "<data>")] @@ -161,10 +155,7 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?; - let error_code = response - .error_code - .clone() - .map_or("0".into(), NumberOrString::into_string); + let error_code = response.error_code.clone().map_or("0".into(), NumberOrString::into_string); if error_code != "0" { err!("Error registering U2F token") @@ -300,20 +291,13 @@ fn _old_parse_registrations(registations: &str) -> Vec<Registration> { let regs: Vec<Value> = serde_json::from_str(registations).expect("Can't parse Registration data"); - regs.into_iter() - .map(|r| serde_json::from_value(r).unwrap()) - .map(|Helper(r)| r) - .collect() + regs.into_iter().map(|r| serde_json::from_value(r).unwrap()).map(|Helper(r)| r).collect() } pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> { let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn); - let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)? - .1 - .into_iter() - .map(|r| r.reg) - .collect(); + let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)?.1.into_iter().map(|r| r.reg).collect(); if registrations.is_empty() { err!("No U2F devices registered") diff --git a/src/api/icons.rs b/src/api/icons.rs index 747569c6..33849bbb 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -12,14 +12,16 @@ use regex::Regex; use reqwest::{blocking::Client, blocking::Response, header, Url}; use rocket::{http::ContentType, http::Cookie, response::Content, Route}; -use crate::{error::Error, util::Cached, CONFIG}; +use crate::{ + error::Error, + util::{get_reqwest_client_builder, Cached}, + CONFIG, +}; pub fn routes() -> Vec<Route> { routes![icon] } -const ALLOWED_CHARS: &str = "_-."; - static CLIENT: Lazy<Client> = Lazy::new(|| { // Generate the default headers let mut default_headers = header::HeaderMap::new(); @@ -27,31 +29,47 @@ static CLIENT: Lazy<Client> = Lazy::new(|| { default_headers.insert(header::ACCEPT_LANGUAGE, header::HeaderValue::from_static("en-US,en;q=0.8")); default_headers.insert(header::CACHE_CONTROL, header::HeaderValue::from_static("no-cache")); default_headers.insert(header::PRAGMA, header::HeaderValue::from_static("no-cache")); - default_headers.insert(header::ACCEPT, header::HeaderValue::from_static("text/html,application/xhtml+xml,application/xml; q=0.9,image/webp,image/apng,*/*;q=0.8")); + default_headers.insert( + header::ACCEPT, + header::HeaderValue::from_static( + "text/html,application/xhtml+xml,application/xml; q=0.9,image/webp,image/apng,*/*;q=0.8", + ), + ); // Reuse the client between requests - Client::builder() + get_reqwest_client_builder() .timeout(Duration::from_secs(CONFIG.icon_download_timeout())) .default_headers(default_headers) .build() - .unwrap() + .expect("Failed to build icon client") }); // Build Regex only once since this takes a lot of time. static ICON_REL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)icon$|apple.*icon").unwrap()); +static ICON_REL_BLACKLIST: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)mask-icon").unwrap()); static ICON_SIZE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap()); // Special HashMap which holds the user defined Regex to speedup matching the regex. static ICON_BLACKLIST_REGEX: Lazy<RwLock<HashMap<String, Regex>>> = Lazy::new(|| RwLock::new(HashMap::new())); #[get("/<domain>/icon.png")] -fn icon(domain: String) -> Option<Cached<Content<Vec<u8>>>> { +fn icon(domain: String) -> Cached<Content<Vec<u8>>> { + const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png"); + if !is_valid_domain(&domain) { warn!("Invalid domain: {}", domain); - return None; + return Cached::ttl( + Content(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()), + CONFIG.icon_cache_negttl(), + ); } - get_icon(&domain).map(|icon| Cached::ttl(Content(ContentType::new("image", "x-icon"), icon), CONFIG.icon_cache_ttl())) + match get_icon(&domain) { + Some((icon, icon_type)) => { + Cached::ttl(Content(ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl()) + } + _ => Cached::ttl(Content(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()), CONFIG.icon_cache_negttl()), + } } /// Returns if the domain provided is valid or not. @@ -59,6 +77,8 @@ fn icon(domain: String) -> Option<Cached<Content<Vec<u8>>>> { /// This does some manual checks and makes use of Url to do some basic checking. /// domains can't be larger then 63 characters (not counting multiple subdomains) according to the RFC's, but we limit the total size to 255. fn is_valid_domain(domain: &str) -> bool { + const ALLOWED_CHARS: &str = "_-."; + // If parsing the domain fails using Url, it will not work with reqwest. if let Err(parse_error) = Url::parse(format!("https://{}", domain).as_str()) { debug!("Domain parse error: '{}' - {:?}", domain, parse_error); @@ -69,7 +89,10 @@ fn is_valid_domain(domain: &str) -> bool { || domain.starts_with('-') || domain.ends_with('-') { - debug!("Domain validation error: '{}' is either empty, contains '..', starts with an '.', starts or ends with a '-'", domain); + debug!( + "Domain validation error: '{}' is either empty, contains '..', starts with an '.', starts or ends with a '-'", + domain + ); return false; } else if domain.len() > 255 { debug!("Domain validation error: '{}' exceeds 255 characters", domain); @@ -238,7 +261,7 @@ fn is_domain_blacklisted(domain: &str) -> bool { is_blacklisted } -fn get_icon(domain: &str) -> Option<Vec<u8>> { +fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> { let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain); // Check for expiration of negatively cached copy @@ -247,7 +270,11 @@ fn get_icon(domain: &str) -> Option<Vec<u8>> { } if let Some(icon) = get_cached_icon(&path) { - return Some(icon); + let icon_type = match get_icon_type(&icon) { + Some(x) => x, + _ => "x-icon", + }; + return Some((icon, icon_type.to_string())); } if CONFIG.disable_icon_download() { @@ -256,9 +283,9 @@ fn get_icon(domain: &str) -> Option<Vec<u8>> { // Get the icon, or None in case of error match download_icon(&domain) { - Ok(icon) => { + Ok((icon, icon_type)) => { save_icon(&path, &icon); - Some(icon) + Some((icon, icon_type.unwrap_or("x-icon").to_string())) } Err(e) => { error!("Error downloading icon: {:?}", e); @@ -319,7 +346,6 @@ fn icon_is_expired(path: &str) -> bool { expired.unwrap_or(true) } -#[derive(Debug)] struct Icon { priority: u8, href: String, @@ -327,12 +353,20 @@ struct Icon { impl Icon { const fn new(priority: u8, href: String) -> Self { - Self { href, priority } + Self { + href, + priority, + } } } fn get_favicons_node(node: &std::rc::Rc<markup5ever_rcdom::Node>, icons: &mut Vec<Icon>, url: &Url) { - if let markup5ever_rcdom::NodeData::Element { name, attrs, .. } = &node.data { + if let markup5ever_rcdom::NodeData::Element { + name, + attrs, + .. + } = &node.data + { if name.local.as_ref() == "link" { let mut has_rel = false; let mut href = None; @@ -343,7 +377,8 @@ fn get_favicons_node(node: &std::rc::Rc<markup5ever_rcdom::Node>, icons: &mut Ve let attr_name = attr.name.local.as_ref(); let attr_value = attr.value.as_ref(); - if attr_name == "rel" && ICON_REL_REGEX.is_match(attr_value) { + if attr_name == "rel" && ICON_REL_REGEX.is_match(attr_value) && !ICON_REL_BLACKLIST.is_match(attr_value) + { has_rel = true; } else if attr_name == "href" { href = Some(attr_value); @@ -486,10 +521,10 @@ fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> { iconlist.sort_by_key(|x| x.priority); // There always is an icon in the list, so no need to check if it exists, and just return the first one - Ok(IconUrlResult{ + Ok(IconUrlResult { iconlist, cookies: cookie_str, - referer + referer, }) } @@ -510,9 +545,7 @@ fn get_page_with_cookies(url: &str, cookie_str: &str, referer: &str) -> Result<R client = client.header("Referer", referer) } - client.send()? - .error_for_status() - .map_err(Into::into) + client.send()?.error_for_status().map_err(Into::into) } /// Returns a Integer with the priority of the type of the icon which to prefer. @@ -594,7 +627,7 @@ fn parse_sizes(sizes: Option<&str>) -> (u16, u16) { (width, height) } -fn download_icon(domain: &str) -> Result<Vec<u8>, Error> { +fn download_icon(domain: &str) -> Result<(Vec<u8>, Option<&str>), Error> { if is_domain_blacklisted(domain) { err!("Domain is blacklisted", domain) } @@ -602,6 +635,7 @@ fn download_icon(domain: &str) -> Result<Vec<u8>, Error> { let icon_result = get_icon_url(&domain)?; let mut buffer = Vec::new(); + let mut icon_type: Option<&str> = None; use data_url::DataUrl; @@ -613,29 +647,43 @@ fn download_icon(domain: &str) -> Result<Vec<u8>, Error> { Ok((body, _fragment)) => { // Also check if the size is atleast 67 bytes, which seems to be the smallest png i could create if body.len() >= 67 { + // Check if the icon type is allowed, else try an icon from the list. + icon_type = get_icon_type(&body); + if icon_type.is_none() { + debug!("Icon from {} data:image uri, is not a valid image type", domain); + continue; + } + info!("Extracted icon from data:image uri for {}", domain); buffer = body; break; } } - _ => warn!("data uri is invalid"), + _ => warn!("Extracted icon from data:image uri is invalid"), }; } else { match get_page_with_cookies(&icon.href, &icon_result.cookies, &icon_result.referer) { Ok(mut res) => { - info!("Downloaded icon from {}", icon.href); res.copy_to(&mut buffer)?; + // Check if the icon type is allowed, else try an icon from the list. + icon_type = get_icon_type(&buffer); + if icon_type.is_none() { + buffer.clear(); + debug!("Icon from {}, is not a valid image type", icon.href); + continue; + } + info!("Downloaded icon from {}", icon.href); break; - }, + } _ => warn!("Download failed for {}", icon.href), }; } } if buffer.is_empty() { - err!("Empty response") + err!("Empty response downloading icon") } - Ok(buffer) + Ok((buffer, icon_type)) } fn save_icon(path: &str, icon: &[u8]) { @@ -647,7 +695,18 @@ fn save_icon(path: &str, icon: &[u8]) { create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache"); } Err(e) => { - info!("Icon save error: {:?}", e); + warn!("Icon save error: {:?}", e); } } } + +fn get_icon_type(bytes: &[u8]) -> Option<&'static str> { + match bytes { + [137, 80, 78, 71, ..] => Some("png"), + [0, 0, 1, 0, ..] => Some("x-icon"), + [82, 73, 70, 70, ..] => Some("webp"), + [255, 216, 255, ..] => Some("jpeg"), + [66, 77, ..] => Some("bmp"), + _ => None, + } +} diff --git a/src/api/identity.rs b/src/api/identity.rs index dcfe607a..31f686c2 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -72,7 +72,8 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult { "Kdf": user.client_kdf_type, "KdfIterations": user.client_kdf_iter, "ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing - "scope": "api offline_access" + "scope": "api offline_access", + "unofficialServer": true, }))) } @@ -87,34 +88,28 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult let username = data.username.as_ref().unwrap(); let user = match User::find_by_mail(username, &conn) { Some(user) => user, - None => err!( - "Username or password is incorrect. Try again", - format!("IP: {}. Username: {}.", ip.ip, username) - ), + None => err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)), }; // Check password let password = data.password.as_ref().unwrap(); if !user.check_valid_password(password) { - err!( - "Username or password is incorrect. Try again", - format!("IP: {}. Username: {}.", ip.ip, username) - ) + err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)) } // Check if the user is disabled if !user.enabled { - err!( - "This user has been disabled", - format!("IP: {}. Username: {}.", ip.ip, username) - ) + err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username)) } let now = Local::now(); if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() { let now = now.naive_utc(); - if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() > CONFIG.signups_verify_resend_time() as i64 { + if user.last_verifying_at.is_none() + || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() + > CONFIG.signups_verify_resend_time() as i64 + { let resend_limit = CONFIG.signups_verify_resend_limit() as i32; if resend_limit == 0 || user.login_verify_count < resend_limit { // We want to send another email verification if we require signups to verify @@ -134,10 +129,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult } // We still want the login to fail until they actually verified the email address - err!( - "Please verify your email before trying again.", - format!("IP: {}. Username: {}.", ip.ip, username) - ) + err!("Please verify your email before trying again.", format!("IP: {}. Username: {}.", ip.ip, username)) } let (mut device, new_device) = get_device(&data, &conn, &user); @@ -168,11 +160,12 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult "Key": user.akey, "PrivateKey": user.private_key, //"TwoFactorToken": "11122233333444555666777888999" - + "Kdf": user.client_kdf_type, "KdfIterations": user.client_kdf_iter, "ResetMasterPassword": false,// TODO: Same as above - "scope": "api offline_access" + "scope": "api offline_access", + "unofficialServer": true, }); if let Some(token) = twofactor_token { @@ -234,9 +227,7 @@ fn twofactor_auth( None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA token not provided"), }; - let selected_twofactor = twofactors - .into_iter() - .find(|tf| tf.atype == selected_id && tf.enabled); + let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled); use crate::api::core::two_factor as _tf; use crate::crypto::ct_eq; @@ -245,18 +236,26 @@ fn twofactor_auth( let mut remember = data.two_factor_remember.unwrap_or(0); match TwoFactorType::from_i32(selected_id) { - Some(TwoFactorType::Authenticator) => _tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, ip, conn)?, + Some(TwoFactorType::Authenticator) => { + _tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, ip, conn)? + } Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn)?, Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?)?, - Some(TwoFactorType::Duo) => _tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?, - Some(TwoFactorType::Email) => _tf::email::validate_email_code_str(user_uuid, twofactor_code, &selected_data?, conn)?, + Some(TwoFactorType::Duo) => { + _tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)? + } + Some(TwoFactorType::Email) => { + _tf::email::validate_email_code_str(user_uuid, twofactor_code, &selected_data?, conn)? + } Some(TwoFactorType::Remember) => { match device.twofactor_remember { Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => { remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time } - _ => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA Remember token not provided"), + _ => { + err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA Remember token not provided") + } } } _ => err!("Invalid two factor provider"), diff --git a/src/api/mod.rs b/src/api/mod.rs index 840c65ff..7312aeec 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -10,8 +10,9 @@ use serde_json::Value; pub use crate::api::{ admin::routes as admin_routes, + core::purge_sends, + core::purge_trashed_ciphers, core::routes as core_routes, - core::start_send_deletion_scheduler, icons::routes as icons_routes, identity::routes as identity_routes, notifications::routes as notifications_routes, @@ -54,9 +55,9 @@ impl NumberOrString { use std::num::ParseIntError as PIE; match self { NumberOrString::Number(n) => Ok(n), - NumberOrString::String(s) => s - .parse() - .map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())), + NumberOrString::String(s) => { + s.parse().map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())) + } } } } diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 8876937d..a64ea9d8 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -4,12 +4,7 @@ use rocket::Route; use rocket_contrib::json::Json; use serde_json::Value as JsonValue; -use crate::{ - api::EmptyResult, - auth::Headers, - db::DbConn, - Error, CONFIG, -}; +use crate::{api::EmptyResult, auth::Headers, db::DbConn, Error, CONFIG}; pub fn routes() -> Vec<Route> { routes![negotiate, websockets_err] @@ -19,12 +14,16 @@ static SHOW_WEBSOCKETS_MSG: AtomicBool = AtomicBool::new(true); #[get("/hub")] fn websockets_err() -> EmptyResult { - if CONFIG.websocket_enabled() && SHOW_WEBSOCKETS_MSG.compare_exchange(true, false, Ordering::Relaxed, Ordering::Relaxed).is_ok() { - err!(" + if CONFIG.websocket_enabled() + && SHOW_WEBSOCKETS_MSG.compare_exchange(true, false, Ordering::Relaxed, Ordering::Relaxed).is_ok() + { + err!( + " ########################################################### '/notifications/hub' should be proxied to the websocket server or notifications won't work. Go to the Wiki for more info, or disable WebSockets setting WEBSOCKET_ENABLED=false. - ###########################################################################################\n") + ###########################################################################################\n" + ) } else { Err(Error::empty()) } @@ -204,9 +203,7 @@ impl Handler for WsHandler { let handler_insert = self.out.clone(); let handler_update = self.out.clone(); - self.users - .map - .upsert(user_uuid, || vec![handler_insert], |ref mut v| v.push(handler_update)); + self.users.map.upsert(user_uuid, || vec![handler_insert], |ref mut v| v.push(handler_update)); // Schedule a ping to keep the connection alive self.out.timeout(PING_MS, PING) @@ -216,7 +213,11 @@ impl Handler for WsHandler { if let Message::Text(text) = msg.clone() { let json = &text[..text.len() - 1]; // Remove last char - if let Ok(InitialMessage { protocol, version }) = from_str::<InitialMessage>(json) { + if let Ok(InitialMessage { + protocol, + version, + }) = from_str::<InitialMessage>(json) + { if &protocol == "messagepack" && version == 1 { return self.out.send(&INITIAL_RESPONSE[..]); // Respond to initial message } @@ -295,10 +296,7 @@ impl WebSocketUsers { // NOTE: The last modified date needs to be updated before calling these methods pub 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)), - ], + vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))], ut, ); diff --git a/src/api/web.rs b/src/api/web.rs index 90e572a8..29c64ae4 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -83,11 +83,15 @@ fn static_files(filename: String) -> Result<Content<&'static [u8]>, Error> { "hibp.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/hibp.png"))), "bootstrap.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))), - "bootstrap-native.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))), + "bootstrap-native.js" => { + Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))) + } "identicon.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))), "datatables.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))), "datatables.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))), - "jquery-3.5.1.slim.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.5.1.slim.js"))), + "jquery-3.5.1.slim.js" => { + Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.5.1.slim.js"))) + } _ => err!(format!("Static file not found: {}", filename)), } } |