//! # Base Request Handler //! //! Base handling code for all user requests use std::error::Error; use exif::In; use image::{GenericImageView, ImageFormat}; use serde::Serialize; use crate::api_data::http_error::HttpError; use crate::constants::PASSWORD_MIN_LENGTH; use crate::data::comment::Comment; use crate::data::conversation::ConvID; use crate::data::custom_emoji::CustomEmoji; use crate::data::error::{ExecError, Res, ResultBoxError}; use crate::data::group::GroupAccessLevel; use crate::data::group_id::GroupID; use crate::data::post::{Post, PostAccessLevel}; use crate::data::user::UserID; use crate::data::user_token::UserAccessToken; use crate::helpers::{account_helper, comments_helper, conversations_helper, custom_emojies_helper, friends_helper, groups_helper, posts_helper, user_helper, virtual_directory_helper}; use crate::helpers::virtual_directory_helper::VirtualDirType; use crate::routes::RequestResult; use crate::utils::pdf_utils::is_valid_pdf; use crate::utils::string_utils::{check_emoji_code, check_string_before_insert, check_url, remove_html_nodes}; use crate::utils::user_data_utils::{generate_new_user_data_file_name, prepare_file_creation, user_data_path}; use crate::utils::virtual_directories_utils; #[derive(Serialize)] struct SuccessMessage { success: String } pub struct PostFile { pub name: String, pub buff: actix_web::web::Bytes, } /// Single request body value pub enum RequestValue { String(String), File(PostFile), } pub trait BaseRequestHandler { /// Get a parameter of the request fn post_parameter_opt(&self, name: &str) -> Option<&RequestValue>; /// Set response fn set_response(&mut self, data: T) -> RequestResult; /// Set an error fn set_error(&mut self, error: HttpError); /// Get remote IP address fn remote_ip(&self) -> String; /// Current user access token fn user_access_token(&self) -> Option<&UserAccessToken>; /// Current user ID fn user_id_opt_ref(&self) -> Option<&UserID> { self.user_access_token().map(|u| &u.user_id) } /// Success message fn success(&mut self, message: &str) -> RequestResult { self.set_response(SuccessMessage { success: message.to_string() }) } /// Internal error response (500) fn internal_error(&mut self, error: Box) -> RequestResult { self.set_error(HttpError::internal_error("Internal server error.")); Err(error) } /// Bad request (400) fn bad_request(&mut self, message: String) -> RequestResult { self.set_error(HttpError::bad_request(&message)); Err(Box::new(ExecError::new(&message))) } /// Forbidden (401) /// /// I did not the HTTP official specs when I made this choice of using Unauthorized instead /// of Forbidden. Today it would be really complicated to come back... fn forbidden(&mut self, message: String) -> RequestResult { self.set_error(HttpError::forbidden(&message)); Err(Box::new(ExecError::new(&message))) } /// Not found (404) fn not_found(&mut self, message: String) -> RequestResult { self.set_error(HttpError::not_found(&message)); Err(Box::new(ExecError::new(&message))) } /// Conflict (409) fn conflict(&mut self, message: String) -> RequestResult { self.set_error(HttpError::new(409, &message)); Err(Box::new(ExecError::new(&message))) } /// Too many requests (429) fn too_many_requests(&mut self, message: &str) -> RequestResult { self.set_error(HttpError::new(429, message)); Err(Box::new(ExecError::new(message))) } /// If result is not OK, return a bad request fn ok_or_bad_request(&mut self, res: ResultBoxError, msg: &str) -> ResultBoxError { match res { Ok(e) => Ok(e), Err(err) => { println!("Error leading to bad request: {}", err); self.bad_request(msg.to_string())?; unreachable!() } } } /// If result is not OK, return a bad request fn ok_or_forbidden(&mut self, res: ResultBoxError, msg: &str) -> ResultBoxError { match res { Ok(e) => Ok(e), Err(err) => { println!("Error leading to access forbidden: {}", err); self.forbidden(msg.to_string())?; unreachable!() } } } /// If result is not OK, return a 404 not found error fn ok_or_not_found(&mut self, res: ResultBoxError, msg: &str) -> ResultBoxError { match res { Ok(e) => Ok(e), Err(err) => { println!("Error leading to 404 not found: {}", err); self.not_found(msg.to_string())?; unreachable!() } } } /// Unwrap an option, returning an error if none is returned fn some_or_internal_error(&mut self, opt: Option, msg: &str) -> Res { match opt { None => { self.internal_error(ExecError::boxed_new(msg))?; unreachable!() } Some(e) => Ok(e) } } /// Get a user ID, if available fn user_id_opt(&self) -> Option { self.user_id_opt_ref().map(|u| u.clone()) } /// Get user ID. This function assess that a user ID is available to continue fn user_id(&self) -> ResultBoxError { if let Some(id) = self.user_id_opt() { return Ok(id.clone()); } Err(ExecError::boxed_new("Could not get required user ID!")) } /// Get current user ID, return invalid user id value if there is none fn user_id_or_invalid(&self) -> UserID { self.user_id_opt().unwrap_or(UserID::invalid()) } /// Get user ID as a reference fn user_id_ref(&self) -> ResultBoxError<&UserID> { self.user_id_opt_ref().ok_or(ExecError::boxed_new("Could not get required user ID!")) } /// Check if a POST parameter was present in the request or not fn has_post_parameter(&self, name: &str) -> bool { self.post_parameter_opt(name).is_some() } /// Get a post parameter fn post_parameter(&mut self, name: &str) -> ResultBoxError<&RequestValue> { if !self.has_post_parameter(name) { self.bad_request(format!("POST parameter '{}' not found in request!", name))?; } Ok(self.post_parameter_opt(name).unwrap()) } /// Get a post string fn post_string(&mut self, name: &str) -> ResultBoxError { self.post_string_opt(name, 1, true) } /// Get a post string with a given name. If the value is not found, attempt to get the value /// with another name /// /// This function is useful to upgrade system fn post_string_with_fallback(&mut self, name: &str, fallback: &str) -> Res { if self.has_post_parameter(name) { self.post_string(name) } else { self.post_string(fallback) } } /// Get a post string, specifying minimum length fn post_string_opt(&mut self, name: &str, min_length: usize, required: bool) -> ResultBoxError { let param = self.post_parameter(name)?; match (¶m, required) { (RequestValue::String(s), _) => { if s.len() >= min_length { Ok(s.to_string()) } else { Err(self.bad_request(format!("'{}' is too short!", name)).unwrap_err()) } } (_, false) => Ok(String::new()), (_, true) => Err(self.bad_request(format!("'{}' is not a string!", name)).unwrap_err()), } } /// Check out whether a file was included in the request or not fn has_file(&self, name: &str) -> bool { self.post_parameter_opt(name) .map(|f| matches!(f, RequestValue::File(_))) .unwrap_or(false) } /// Get a file included in the request fn post_file(&mut self, name: &str) -> ResultBoxError<&PostFile> { if self.has_file(name) { if let RequestValue::File(f) = self.post_parameter(name)? { return Ok(f); } } else { self.bad_request(format!("File {} not included in request!", name))?; } unreachable!(); } /// Save an image in user data directory fn save_post_image(&mut self, name: &str, folder: &str, max_w: u32, max_h: u32) -> ResultBoxError { // Load image let file = self.post_file(name)?; let mut image = image::load_from_memory(file.buff.as_ref())?; if image.width() > max_w || image.height() > max_h { image = image.resize(max_w, max_h, image::imageops::FilterType::Nearest); } // Read EXIF information in case of JPEG image, if possible if let Ok(ImageFormat::Jpeg) = image::guess_format(file.buff.as_ref()) { let mut reader = std::io::BufReader::new(file.buff.as_ref()); if let Ok(exif_attr) = exif::get_exif_attr_from_jpeg(&mut reader) { let exif_reader = exif::Reader::new(); let exif = exif_reader.read_raw(exif_attr)?; if let Some(v) = exif.get_field(exif::Tag::Orientation, In::PRIMARY) { match v.value.get_uint(0) { Some(1) => { /* row 0 is top and column 0 is left */ } //Some(2) => println!("row 0 at top and column 0 at right"), Some(3) => { /* row 0 at bottom and column 0 at right */ image = image.rotate180() } //Some(4) => println!("row 0 at bottom and column 0 at left"), //Some(5) => println!("row 0 at left and column 0 at top"), Some(6) => { /* row 0 is right and column 0 is top */ image = image.rotate90(); } //Some(7) => println!("row 0 at right and column 0 at bottom"), Some(8) => { /* row 0 is left and column 0 is bottom */ image = image.rotate270(); } v => println!("Unhandled EXIF Orientation: {:?}", v), }; } } } // Determine image destination let target_user_data_folder = prepare_file_creation(&self.user_id()?, folder)?; let target_file_path = generate_new_user_data_file_name(target_user_data_folder.as_path(), "png")?; let target_sys_path = user_data_path(target_file_path.as_path()); // Save image image.save_with_format(target_sys_path, ImageFormat::Png)?; Ok(target_file_path.to_string_lossy().to_string()) } /// Save a pdf included in the request fn save_post_pdf(&mut self, name: &str, folder: &str) -> ResultBoxError { let file = self.post_file(name)?; if !is_valid_pdf(&file.buff)? { self.bad_request(format!("Invalid PDF specified in {} !", name))?; unreachable!(); } // Avoid memory warnings let copied_buff = file.buff.clone(); // Determine pdf file destination let target_user_data_folder = prepare_file_creation(self.user_id_ref()?, folder)?; let target_file_path = generate_new_user_data_file_name(target_user_data_folder.as_path(), "pdf")?; let target_sys_path = user_data_path(target_file_path.as_path()); std::fs::write(target_sys_path, &copied_buff.as_ref())?; Ok(target_file_path.to_string_lossy().to_string()) } /// Get an integer included in the POST request fn post_i64(&mut self, name: &str) -> ResultBoxError { Ok(self.post_string(name)?.parse::()?) } /// Get an optional number in the request. If none found, return a default value fn post_u64_opt(&mut self, name: &str, default: u64) -> ResultBoxError { if self.has_post_parameter(name) { Ok(self.post_string(name)?.parse::()?) } else { Ok(default) } } fn post_u64(&mut self, name: &str) -> ResultBoxError { Ok(self.post_string(name)?.parse::()?) } fn post_positive_u64_opt(&mut self, name: &str) -> Res> { match self.post_u64_opt(name, 0)? { 0 => Ok(None), val => Ok(Some(val)) } } /// Get a boolean included in a POST request fn post_bool(&mut self, name: &str) -> ResultBoxError { Ok(self.post_string(name)?.eq("true")) } /// Get an optional boolean included in post request fn post_bool_opt(&mut self, name: &str, default: bool) -> bool { self.post_bool(name).unwrap_or(default) } /// Get an email included in the request fn post_email(&mut self, name: &str) -> ResultBoxError { let mail = self.post_string(name)?; if !mailchecker::is_valid(&mail) { self.bad_request("Invalid email address!".to_string())?; } Ok(mail) } /// Get a list of integers included in the request fn post_numbers_list(&mut self, name: &str, min_len: usize) -> ResultBoxError> { let param = self.post_string_opt(name, min_len, min_len != 0)?; let mut list = vec![]; for split in param.split::<&str>(",") { if split.is_empty() { continue; } list.push(split.parse::()?); } if list.len() < min_len { self.bad_request(format!("Not enough entries in '{}'!", name))?; } Ok(list) } /// Get the ID of a user included in a POST request fn post_user_id(&mut self, name: &str) -> ResultBoxError { let user_id = UserID::new(self.post_u64(name)?); if user_id.id() < 1 { self.bad_request(format!("Invalid user specified in '{}'!", name))?; } if !user_helper::exists(&user_id)? { self.not_found(format!("User with ID {} not found!", user_id.id()))?; } Ok(user_id) } /// Get the ID of a friend included in a POST request /// /// *Note :* This function does not check whether the user exists or not before checking if the /// two users are friend because as it is not possible to be friend with a non existent person /// A single check is enough fn post_friend_id(&mut self, name: &str) -> ResultBoxError { let friend_id = UserID::new(self.post_u64(name)?); if !friends_helper::are_friend(&friend_id, self.user_id_ref()?)? { self.forbidden("You are not friend with this person!".to_string())?; } Ok(friend_id) } /// Get a virtual directory included in a POST request fn post_virtual_directory(&mut self, name: &str) -> ResultBoxError { let dir = self.post_string(name)?; if !virtual_directories_utils::check_virtual_directory(&dir) { self.bad_request(format!("Invalid virtual directory specified in '{}' !", name))?; } Ok(dir) } /// Get a string included in the request, with HTML codes removed fn post_string_without_html(&mut self, name: &str, min_length: usize, required: bool) -> ResultBoxError { Ok(remove_html_nodes(self.post_string_opt(name, min_length, required)?.as_str())) } /// Get an optional string included in the request, with HTML codes removed fn post_string_without_html_opt(&mut self, name: &str, min_length: usize) -> ResultBoxError> { if !self.has_post_parameter(name) { Ok(None) } else { Ok(Some(remove_html_nodes(self.post_string_opt(name, min_length, true)?.as_str()))) } } /// Get & return the ID of the conversation included in the POST request fn post_conv_id(&mut self, name: &str) -> ResultBoxError { let conv_id = self.post_u64(name)?; if !conversations_helper::does_user_belongs_to(&self.user_id()?, conv_id)? { self.forbidden(format!("You do not belong to conversation {} !", conv_id))?; } Ok(conv_id) } /// Get the ID fn post_group_id(&mut self, name: &str) -> ResultBoxError { let group_id = GroupID::new(self.post_u64(name)?); if !groups_helper::exists(&group_id)? { self.not_found("Specified group not found!".to_string())?; } Ok(group_id) } /// Get the ID of a group included in a request with a check for access level of current user fn post_group_id_with_access(&mut self, name: &str, min_level: GroupAccessLevel) -> ResultBoxError { let group_id = self.post_group_id(name)?; let access_level = groups_helper::get_access_level(&group_id, self.user_id_opt())?; if access_level == GroupAccessLevel::NO_ACCESS { self.not_found("Specified group not found!".to_string())?; } if access_level < min_level { self.forbidden("You do not have enough rights to perform what you intend to do on this group!".to_string())?; } Ok(group_id) } /// Get an URL included in the request fn post_url_opt(&mut self, name: &str, required: bool) -> ResultBoxError> { let url = self.post_string_opt(name, 0, required)?; if url.is_empty() && !required { Ok(None) } else { if !check_url(&url) { self.bad_request(format!("Invalid url specified in {} !", name))?; } Ok(Some(url)) } } /// Get an optional virtual directory included in the request fn post_checked_virtual_directory_opt(&mut self, name: &str, target_id: u64, target_type: VirtualDirType) -> ResultBoxError> { let dir = self.post_string_opt(name, 0, false)?; if dir.is_empty() { return Ok(None); } if !virtual_directory_helper::check_availability(&dir, target_id, target_type)? { self.forbidden("Requested virtual directory is not available!".to_string())?; } Ok(Some(dir)) } /// Get information about a post whose ID was specified in the request fn post_post_with_access(&mut self, name: &str, min_level: PostAccessLevel) -> ResultBoxError { let post_id = self.post_u64(name)?; let post = self.ok_or_not_found( posts_helper::get_single(post_id), "Requested post not found!", )?; if posts_helper::get_access_level(&post, &self.user_id_opt())? < min_level { self.forbidden("You are not allowed to access this post information!".to_string())?; } Ok(post) } /// Get information about a comment whose ID is specified in the request fn post_comment_with_access(&mut self, name: &str) -> ResultBoxError { let comment_id = self.post_u64(name)?; let comment = self.ok_or_not_found( comments_helper::get_single(comment_id), "Specified comment not found!", )?; if comment.user_id != self.user_id_or_invalid() { let post = posts_helper::get_single(comment.post_id)?; if posts_helper::get_access_level(&post, &self.user_id_opt())? == PostAccessLevel::NO_ACCESS { self.forbidden("You are not allowed to access this post information !".to_string())?; } } Ok(comment) } /// Get information about a comment specified in the request for which user has full access fn post_comment_with_full_access(&mut self, name: &str) -> ResultBoxError { let comment = self.post_comment_with_access(name)?; if comment.user_id != self.user_id()? { self.forbidden("You are not the owner of this comment!".to_string())?; } Ok(comment) } /// Get a content of a post and satinize it fn post_content(&mut self, name: &str, min_len: usize, required: bool) -> ResultBoxError { let content = self.post_string_opt(name, min_len, required)?; if content.contains("data:image") { self.forbidden("Please do not include inline images!".to_string())?; } if min_len > 0 && required && !check_string_before_insert(&content) { self.forbidden(format!("The content inside {} was rejected!", name))?; } Ok(remove_html_nodes(&content)) } /// Check the password of the current user fn need_user_password(&mut self, field: &str) -> ResultBoxError { let password = self.post_string_opt(field, PASSWORD_MIN_LENGTH, true)?; if !account_helper::check_user_password(self.user_id_ref()?, &password)? { self.forbidden("Invalid password!".to_string())?; } Ok(()) } /// Get an emoji shortcut included in a POST request fn post_emoji_shortcut(&mut self, field: &str) -> ResultBoxError { let emoji_shortcut = self.post_string(field)?; if !check_emoji_code(&emoji_shortcut) { self.bad_request("Invalid emoji shortcut code!".to_string())?; } Ok(emoji_shortcut) } /// Get information about an emoji included in a POST request fn post_emoji_id(&mut self, field: &str) -> ResultBoxError { let emoji_id = self.post_u64(field)?; let info = self.ok_or_not_found( custom_emojies_helper::get_single(emoji_id), "Requested emoji not found!", )?; if &info.user_id != self.user_id_ref()? { self.forbidden("You do not own this emoji!".to_string())?; } Ok(info) } }