aboutsummaryrefslogtreecommitdiff
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/admin.rs88
-rw-r--r--src/api/core/accounts.rs12
-rw-r--r--src/api/core/ciphers.rs139
-rw-r--r--src/api/core/folders.rs10
-rw-r--r--src/api/core/mod.rs29
-rw-r--r--src/api/core/organizations.rs58
-rw-r--r--src/api/core/sends.rs47
-rw-r--r--src/api/core/two_factor/authenticator.rs25
-rw-r--r--src/api/core/two_factor/duo.rs19
-rw-r--r--src/api/core/two_factor/email.rs9
-rw-r--r--src/api/core/two_factor/mod.rs8
-rw-r--r--src/api/core/two_factor/u2f.rs24
-rw-r--r--src/api/icons.rs119
-rw-r--r--src/api/identity.rs53
-rw-r--r--src/api/mod.rs9
-rw-r--r--src/api/notifications.rs32
-rw-r--r--src/api/web.rs8
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(&current_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)),
}
}