diff --git a/src/app.rs b/src/app.rs index 625ec2aa..83029694 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,6 +13,7 @@ use crate::common::AppData; use crate::config::Configuration; use crate::databases::database; use crate::services::category::{self, DbCategoryRepository}; +use crate::services::proxy; use crate::services::user::DbUserRepository; use crate::tracker::statistics_importer::StatisticsImporter; use crate::{mailer, routes, tracker}; @@ -49,10 +50,11 @@ pub async fn run(configuration: Configuration) -> Running { let tracker_statistics_importer = Arc::new(StatisticsImporter::new(cfg.clone(), tracker_service.clone(), database.clone()).await); let mailer_service = Arc::new(mailer::Service::new(cfg.clone()).await); - let image_cache_service = Arc::new(ImageCacheService::new(cfg.clone()).await); + let image_cache_service: Arc = Arc::new(ImageCacheService::new(cfg.clone()).await); let category_repository = Arc::new(DbCategoryRepository::new(database.clone())); let user_repository = Arc::new(DbUserRepository::new(database.clone())); let category_service = Arc::new(category::Service::new(category_repository.clone(), user_repository.clone())); + let proxy_service = Arc::new(proxy::Service::new(image_cache_service.clone(), user_repository.clone())); // Build app container @@ -67,6 +69,7 @@ pub async fn run(configuration: Configuration) -> Running { category_repository, user_repository, category_service, + proxy_service, )); // Start repeating task to import tracker torrent data and updating diff --git a/src/common.rs b/src/common.rs index 333c604c..4570d790 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,6 +5,7 @@ use crate::cache::image::manager::ImageCacheService; use crate::config::Configuration; use crate::databases::database::Database; use crate::services::category::{self, DbCategoryRepository}; +use crate::services::proxy; use crate::services::user::DbUserRepository; use crate::tracker::statistics_importer::StatisticsImporter; use crate::{mailer, tracker}; @@ -23,6 +24,7 @@ pub struct AppData { pub category_repository: Arc, pub user_repository: Arc, pub category_service: Arc, + pub proxy_service: Arc, } impl AppData { @@ -38,6 +40,7 @@ impl AppData { category_repository: Arc, user_repository: Arc, category_service: Arc, + proxy_service: Arc, ) -> AppData { AppData { cfg, @@ -50,6 +53,7 @@ impl AppData { category_repository, user_repository, category_service, + proxy_service, } } } diff --git a/src/lib.rs b/src/lib.rs index ae8132b0..03213e05 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ pub mod models; pub mod routes; pub mod services; pub mod tracker; +pub mod ui; pub mod upgrades; pub mod utils; diff --git a/src/routes/proxy.rs b/src/routes/proxy.rs index 0226c628..c985b53e 100644 --- a/src/routes/proxy.rs +++ b/src/routes/proxy.rs @@ -1,31 +1,11 @@ -use std::sync::Once; - use actix_web::http::StatusCode; use actix_web::{web, HttpRequest, HttpResponse, Responder}; -use bytes::Bytes; -use text_to_png::TextRenderer; use crate::cache::image::manager::Error; use crate::common::WebAppData; use crate::errors::ServiceResult; use crate::routes::API_VERSION; - -static ERROR_IMAGE_LOADER: Once = Once::new(); - -static mut ERROR_IMAGE_URL_IS_UNREACHABLE: Bytes = Bytes::new(); -static mut ERROR_IMAGE_URL_IS_NOT_AN_IMAGE: Bytes = Bytes::new(); -static mut ERROR_IMAGE_TOO_BIG: Bytes = Bytes::new(); -static mut ERROR_IMAGE_USER_QUOTA_MET: Bytes = Bytes::new(); -static mut ERROR_IMAGE_UNAUTHENTICATED: Bytes = Bytes::new(); - -const ERROR_IMG_FONT_SIZE: u8 = 16; -const ERROR_IMG_COLOR: &str = "Red"; - -const ERROR_IMAGE_URL_IS_UNREACHABLE_TEXT: &str = "Could not find image."; -const ERROR_IMAGE_URL_IS_NOT_AN_IMAGE_TEXT: &str = "Invalid image."; -const ERROR_IMAGE_TOO_BIG_TEXT: &str = "Image is too big."; -const ERROR_IMAGE_USER_QUOTA_MET_TEXT: &str = "Image proxy quota met."; -const ERROR_IMAGE_UNAUTHENTICATED_TEXT: &str = "Sign in to see image."; +use crate::ui::proxy::{load_error_images, map_error_to_image}; pub fn init(cfg: &mut web::ServiceConfig) { cfg.service( @@ -35,59 +15,46 @@ pub fn init(cfg: &mut web::ServiceConfig) { load_error_images(); } -fn generate_img_from_text(text: &str) -> Bytes { - let renderer = TextRenderer::default(); - - Bytes::from( - renderer - .render_text_to_png_data(text, ERROR_IMG_FONT_SIZE, ERROR_IMG_COLOR) - .unwrap() - .data, - ) -} - -fn load_error_images() { - ERROR_IMAGE_LOADER.call_once(|| unsafe { - ERROR_IMAGE_URL_IS_UNREACHABLE = generate_img_from_text(ERROR_IMAGE_URL_IS_UNREACHABLE_TEXT); - ERROR_IMAGE_URL_IS_NOT_AN_IMAGE = generate_img_from_text(ERROR_IMAGE_URL_IS_NOT_AN_IMAGE_TEXT); - ERROR_IMAGE_TOO_BIG = generate_img_from_text(ERROR_IMAGE_TOO_BIG_TEXT); - ERROR_IMAGE_USER_QUOTA_MET = generate_img_from_text(ERROR_IMAGE_USER_QUOTA_MET_TEXT); - ERROR_IMAGE_UNAUTHENTICATED = generate_img_from_text(ERROR_IMAGE_UNAUTHENTICATED_TEXT); - }); -} - /// Get the proxy image. /// /// # Errors /// /// This function will return `Ok` only for now. pub async fn get_proxy_image(req: HttpRequest, app_data: WebAppData, path: web::Path) -> ServiceResult { - // Check for optional user. - let opt_user = app_data.auth.get_user_compact_from_request(&req).await.ok(); - - let encoded_url = path.into_inner(); - let url = urlencoding::decode(&encoded_url).unwrap_or_default(); - - match app_data.image_cache_manager.get_image_by_url(&url, opt_user).await { - Ok(image_bytes) => Ok(HttpResponse::build(StatusCode::OK) - .content_type("image/png") - .append_header(("Cache-Control", "max-age=15552000")) - .body(image_bytes)), - Err(e) => unsafe { - // Handling status codes in the frontend other tan OK is quite a pain. - // Return OK for now. - let (_status_code, error_image_bytes): (StatusCode, Bytes) = match e { - Error::UrlIsUnreachable => (StatusCode::GATEWAY_TIMEOUT, ERROR_IMAGE_URL_IS_UNREACHABLE.clone()), - Error::UrlIsNotAnImage => (StatusCode::BAD_REQUEST, ERROR_IMAGE_URL_IS_NOT_AN_IMAGE.clone()), - Error::ImageTooBig => (StatusCode::BAD_REQUEST, ERROR_IMAGE_TOO_BIG.clone()), - Error::UserQuotaMet => (StatusCode::TOO_MANY_REQUESTS, ERROR_IMAGE_USER_QUOTA_MET.clone()), - Error::Unauthenticated => (StatusCode::UNAUTHORIZED, ERROR_IMAGE_UNAUTHENTICATED.clone()), - }; - + let user_id = app_data.auth.get_user_id_from_request(&req).await.ok(); + + match user_id { + Some(user_id) => { + // Get image URL from URL path + let encoded_image_url = path.into_inner(); + let image_url = urlencoding::decode(&encoded_image_url).unwrap_or_default(); + + match app_data.proxy_service.get_image_by_url(&image_url, &user_id).await { + Ok(image_bytes) => { + // Returns the cached image. + Ok(HttpResponse::build(StatusCode::OK) + .content_type("image/png") + .append_header(("Cache-Control", "max-age=15552000")) + .body(image_bytes)) + } + Err(e) => + // Returns an error image. + // Handling status codes in the frontend other tan OK is quite a pain. + // Return OK for now. + { + Ok(HttpResponse::build(StatusCode::OK) + .content_type("image/png") + .append_header(("Cache-Control", "no-cache")) + .body(map_error_to_image(&e))) + } + } + } + None => { + // Unauthenticated users can't see images. Ok(HttpResponse::build(StatusCode::OK) .content_type("image/png") .append_header(("Cache-Control", "no-cache")) - .body(error_image_bytes)) - }, + .body(map_error_to_image(&Error::Unauthenticated))) + } } } diff --git a/src/services/mod.rs b/src/services/mod.rs index 901b3b82..a52ba223 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,3 +1,4 @@ pub mod about; pub mod category; +pub mod proxy; pub mod user; diff --git a/src/services/proxy.rs b/src/services/proxy.rs new file mode 100644 index 00000000..45ba5b34 --- /dev/null +++ b/src/services/proxy.rs @@ -0,0 +1,46 @@ +//! Image cache proxy. +//! +//! The image cache proxy is a service that allows users to proxy images +//! through the server. +//! +//! Sample URL: +//! +//! +use std::sync::Arc; + +use bytes::Bytes; + +use super::user::DbUserRepository; +use crate::cache::image::manager::{Error, ImageCacheService}; +use crate::models::user::UserId; + +pub struct Service { + image_cache_service: Arc, + user_repository: Arc, +} + +impl Service { + #[must_use] + pub fn new(image_cache_service: Arc, user_repository: Arc) -> Self { + Self { + image_cache_service, + user_repository, + } + } + + /// It gets image by URL and caches it. + /// + /// # Errors + /// + /// It returns an error if: + /// + /// * The image URL is unreachable. + /// * The image URL is not an image. + /// * The image is too big. + /// * The user quota is met. + pub async fn get_image_by_url(&self, url: &str, user_id: &UserId) -> Result { + let user = self.user_repository.get_compact_user(user_id).await.ok(); + + self.image_cache_service.get_image_by_url(url, user).await + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 00000000..143a6381 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,2 @@ +//! User interface module. Presentation layer. +pub mod proxy; diff --git a/src/ui/proxy.rs b/src/ui/proxy.rs new file mode 100644 index 00000000..a744c5b7 --- /dev/null +++ b/src/ui/proxy.rs @@ -0,0 +1,56 @@ +use std::sync::Once; + +use bytes::Bytes; +use text_to_png::TextRenderer; + +use crate::cache::image::manager::Error; + +pub static ERROR_IMAGE_LOADER: Once = Once::new(); + +pub static mut ERROR_IMAGE_URL_IS_UNREACHABLE: Bytes = Bytes::new(); +pub static mut ERROR_IMAGE_URL_IS_NOT_AN_IMAGE: Bytes = Bytes::new(); +pub static mut ERROR_IMAGE_TOO_BIG: Bytes = Bytes::new(); +pub static mut ERROR_IMAGE_USER_QUOTA_MET: Bytes = Bytes::new(); +pub static mut ERROR_IMAGE_UNAUTHENTICATED: Bytes = Bytes::new(); + +const ERROR_IMG_FONT_SIZE: u8 = 16; +const ERROR_IMG_COLOR: &str = "Red"; + +const ERROR_IMAGE_URL_IS_UNREACHABLE_TEXT: &str = "Could not find image."; +const ERROR_IMAGE_URL_IS_NOT_AN_IMAGE_TEXT: &str = "Invalid image."; +const ERROR_IMAGE_TOO_BIG_TEXT: &str = "Image is too big."; +const ERROR_IMAGE_USER_QUOTA_MET_TEXT: &str = "Image proxy quota met."; +const ERROR_IMAGE_UNAUTHENTICATED_TEXT: &str = "Sign in to see image."; + +pub fn load_error_images() { + ERROR_IMAGE_LOADER.call_once(|| unsafe { + ERROR_IMAGE_URL_IS_UNREACHABLE = generate_img_from_text(ERROR_IMAGE_URL_IS_UNREACHABLE_TEXT); + ERROR_IMAGE_URL_IS_NOT_AN_IMAGE = generate_img_from_text(ERROR_IMAGE_URL_IS_NOT_AN_IMAGE_TEXT); + ERROR_IMAGE_TOO_BIG = generate_img_from_text(ERROR_IMAGE_TOO_BIG_TEXT); + ERROR_IMAGE_USER_QUOTA_MET = generate_img_from_text(ERROR_IMAGE_USER_QUOTA_MET_TEXT); + ERROR_IMAGE_UNAUTHENTICATED = generate_img_from_text(ERROR_IMAGE_UNAUTHENTICATED_TEXT); + }); +} + +pub fn map_error_to_image(error: &Error) -> Bytes { + unsafe { + match error { + Error::UrlIsUnreachable => ERROR_IMAGE_URL_IS_UNREACHABLE.clone(), + Error::UrlIsNotAnImage => ERROR_IMAGE_URL_IS_NOT_AN_IMAGE.clone(), + Error::ImageTooBig => ERROR_IMAGE_TOO_BIG.clone(), + Error::UserQuotaMet => ERROR_IMAGE_USER_QUOTA_MET.clone(), + Error::Unauthenticated => ERROR_IMAGE_UNAUTHENTICATED.clone(), + } + } +} + +fn generate_img_from_text(text: &str) -> Bytes { + let renderer = TextRenderer::default(); + + Bytes::from( + renderer + .render_text_to_png_data(text, ERROR_IMG_FONT_SIZE, ERROR_IMG_COLOR) + .unwrap() + .data, + ) +}