diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index 50897b95d..dc981f617 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -18,27 +18,13 @@ use lemmy_db_schema::source::{ local_site::LocalSite, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{error::LemmyResult, rate_limit::RateLimitCell, REQWEST_TIMEOUT}; +use lemmy_utils::{error::LemmyResult, REQWEST_TIMEOUT}; use reqwest::Body; -use reqwest_middleware::{ClientWithMiddleware, RequestBuilder}; +use reqwest_middleware::RequestBuilder; use serde::Deserialize; use std::time::Duration; use url::Url; -pub fn config(cfg: &mut ServiceConfig, client: ClientWithMiddleware, rate_limit: &RateLimitCell) { - cfg - .app_data(Data::new(client)) - .service( - resource("/pictrs/image") - .wrap(rate_limit.image()) - .route(post().to(upload)), - ) - // This has optional query params: /image/{filename}?format=jpg&thumbnail=256 - .service(resource("/pictrs/image/{filename}").route(get().to(full_res))) - .service(resource("/pictrs/image/delete/{token}/{filename}").route(get().to(delete))) - .service(resource("/pictrs/healthz").route(get().to(healthz))); -} - trait ProcessUrl { /// If thumbnail or format is given, this uses the pictrs process endpoint. /// Otherwise, it uses the normal pictrs url (IE image/original). @@ -46,7 +32,7 @@ trait ProcessUrl { } #[derive(Deserialize, Clone)] -struct PictrsGetParams { +pub struct PictrsGetParams { format: Option, thumbnail: Option, } @@ -99,15 +85,12 @@ impl ProcessUrl for ImageProxyParams { } } } -fn adapt_request( - request: &HttpRequest, - client: &ClientWithMiddleware, - url: String, -) -> RequestBuilder { +fn adapt_request(request: &HttpRequest, context: &LemmyContext, url: String) -> RequestBuilder { // remove accept-encoding header so that pictrs doesn't compress the response const INVALID_HEADERS: &[HeaderName] = &[ACCEPT_ENCODING, HOST]; - let client_request = client + let client_request = context + .client() .request(convert_method(request.method()), url) .timeout(REQWEST_TIMEOUT); @@ -124,19 +107,17 @@ fn adapt_request( }) } -async fn upload( +pub async fn upload_image( req: HttpRequest, body: Payload, // require login local_user_view: LocalUserView, - client: Data, context: Data, ) -> LemmyResult { - // TODO: check rate limit here let pictrs_config = context.settings().pictrs_config()?; let image_url = format!("{}image", pictrs_config.url); - let mut client_req = adapt_request(&req, &client, image_url); + let mut client_req = adapt_request(&req, &context, image_url); if let Some(addr) = req.head().peer_addr { client_req = client_req.header("X-Forwarded-For", addr.to_string()) @@ -169,11 +150,10 @@ async fn upload( Ok(HttpResponse::build(convert_status(status)).json(images)) } -async fn full_res( +pub async fn get_full_res_image( filename: Path, Query(params): Query, req: HttpRequest, - client: Data, context: Data, local_user_view: Option, ) -> LemmyResult { @@ -189,15 +169,11 @@ async fn full_res( let processed_url = params.process_url(name, &pictrs_config.url); - image(processed_url, req, &client).await + image(processed_url, req, &context).await } -async fn image( - url: String, - req: HttpRequest, - client: &ClientWithMiddleware, -) -> LemmyResult { - let mut client_req = adapt_request(&req, client, url); +async fn image(url: String, req: HttpRequest, context: &LemmyContext) -> LemmyResult { + let mut client_req = adapt_request(&req, context, url); if let Some(addr) = req.head().peer_addr { client_req = client_req.header("X-Forwarded-For", addr.to_string()); @@ -222,10 +198,9 @@ async fn image( Ok(client_res.body(BodyStream::new(res.bytes_stream()))) } -async fn delete( +pub async fn delete_image( components: Path<(String, String)>, req: HttpRequest, - client: Data, context: Data, // require login _local_user_view: LocalUserView, @@ -235,7 +210,7 @@ async fn delete( let pictrs_config = context.settings().pictrs_config()?; let url = format!("{}image/delete/{}/{}", pictrs_config.url, &token, &file); - let mut client_req = adapt_request(&req, &client, url); + let mut client_req = adapt_request(&req, &context, url); if let Some(addr) = req.head().peer_addr { client_req = client_req.header("X-Forwarded-For", addr.to_string()); @@ -248,15 +223,11 @@ async fn delete( Ok(HttpResponse::build(convert_status(res.status())).body(BodyStream::new(res.bytes_stream()))) } -async fn healthz( - req: HttpRequest, - client: Data, - context: Data, -) -> LemmyResult { +pub async fn pictrs_healthz(req: HttpRequest, context: Data) -> LemmyResult { let pictrs_config = context.settings().pictrs_config()?; let url = format!("{}healthz", pictrs_config.url); - let mut client_req = adapt_request(&req, &client, url); + let mut client_req = adapt_request(&req, &context, url); if let Some(addr) = req.head().peer_addr { client_req = client_req.header("X-Forwarded-For", addr.to_string()); @@ -270,7 +241,6 @@ async fn healthz( pub async fn image_proxy( Query(params): Query, req: HttpRequest, - client: Data, context: Data, ) -> LemmyResult, HttpResponse>> { let url = Url::parse(¶ms.url)?; @@ -291,7 +261,7 @@ pub async fn image_proxy( Ok(Either::Left(Redirect::to(url.to_string()).respond_to(&req))) } else { // Proxy the image data through Lemmy - Ok(Either::Right(image(processed_url, req, &client).await?)) + Ok(Either::Right(image(processed_url, req, &context).await?)) } } diff --git a/src/api_routes_v3.rs b/src/api_routes_v3.rs index eefaf5b87..56dde6db9 100644 --- a/src/api_routes_v3.rs +++ b/src/api_routes_v3.rs @@ -134,252 +134,262 @@ use lemmy_apub::api::{ search::search, user_settings_backup::{export_settings, import_settings}, }; -use lemmy_routes::images::image_proxy; +use lemmy_routes::images::{delete_image, get_full_res_image, image_proxy, pictrs_healthz, upload_image}; use lemmy_utils::rate_limit::RateLimitCell; // Deprecated, use api v4 instead. // When removing api v3, we also need to rewrite all links in database with // `/api/v3/image_proxy` to use `/api/v4/image_proxy` instead. pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { - cfg.service( - scope("/api/v3") - .route("/image_proxy", get().to(image_proxy)) - // Site - .service( - scope("/site") - .wrap(rate_limit.message()) - .route("", get().to(get_site_v3)) - // Admin Actions - .route("", post().to(create_site)) - .route("", put().to(update_site)) - .route("/block", post().to(user_block_instance)), - ) - .service( - resource("/modlog") - .wrap(rate_limit.message()) - .route(get().to(get_mod_log)), - ) - .service( - resource("/search") - .wrap(rate_limit.search()) - .route(get().to(search)), - ) - .service( - resource("/resolve_object") - .wrap(rate_limit.message()) - .route(get().to(resolve_object)), - ) - // Community - .service( - resource("/community") - .guard(guard::Post()) - .wrap(rate_limit.register()) - .route(post().to(create_community)), - ) - .service( - scope("/community") - .wrap(rate_limit.message()) - .route("", get().to(get_community)) - .route("", put().to(update_community)) - .route("/hide", put().to(hide_community)) - .route("/list", get().to(list_communities)) - .route("/follow", post().to(follow_community)) - .route("/block", post().to(user_block_community)) - .route("/delete", post().to(delete_community)) - // Mod Actions - .route("/remove", post().to(remove_community)) - .route("/transfer", post().to(transfer_community)) - .route("/ban_user", post().to(ban_from_community)) - .route("/mod", post().to(add_mod_to_community)), - ) - .service( - scope("/federated_instances") - .wrap(rate_limit.message()) - .route("", get().to(get_federated_instances)), - ) - // Post - .service( - // Handle POST to /post separately to add the post() rate limitter - resource("/post") - .guard(guard::Post()) - .wrap(rate_limit.post()) - .route(post().to(create_post)), - ) - .service( - scope("/post") - .wrap(rate_limit.message()) - .route("", get().to(get_post)) - .route("", put().to(update_post)) - .route("/delete", post().to(delete_post)) - .route("/remove", post().to(remove_post)) - .route("/mark_as_read", post().to(mark_post_as_read)) - .route("/hide", post().to(hide_post)) - .route("/lock", post().to(lock_post)) - .route("/feature", post().to(feature_post)) - .route("/list", get().to(list_posts)) - .route("/like", post().to(like_post)) - .route("/like/list", get().to(list_post_likes)) - .route("/save", put().to(save_post)) - .route("/report", post().to(create_post_report)) - .route("/report/resolve", put().to(resolve_post_report)) - .route("/report/list", get().to(list_post_reports)) - .route("/site_metadata", get().to(get_link_metadata)), - ) - // Comment - .service( - // Handle POST to /comment separately to add the comment() rate limitter - resource("/comment") - .guard(guard::Post()) - .wrap(rate_limit.comment()) - .route(post().to(create_comment)), - ) - .service( - scope("/comment") - .wrap(rate_limit.message()) - .route("", get().to(get_comment)) - .route("", put().to(update_comment)) - .route("/delete", post().to(delete_comment)) - .route("/remove", post().to(remove_comment)) - .route("/mark_as_read", post().to(mark_reply_as_read)) - .route("/distinguish", post().to(distinguish_comment)) - .route("/like", post().to(like_comment)) - .route("/like/list", get().to(list_comment_likes)) - .route("/save", put().to(save_comment)) - .route("/list", get().to(list_comments)) - .route("/report", post().to(create_comment_report)) - .route("/report/resolve", put().to(resolve_comment_report)) - .route("/report/list", get().to(list_comment_reports)), - ) - // Private Message - .service( - scope("/private_message") - .wrap(rate_limit.message()) - .route("/list", get().to(get_private_message)) - .route("", post().to(create_private_message)) - .route("", put().to(update_private_message)) - .route("/delete", post().to(delete_private_message)) - .route("/mark_as_read", post().to(mark_pm_as_read)) - .route("/report", post().to(create_pm_report)) - .route("/report/resolve", put().to(resolve_pm_report)) - .route("/report/list", get().to(list_pm_reports)), - ) - // User - .service( - // Account action, I don't like that it's in /user maybe /accounts - // Handle /user/register separately to add the register() rate limiter - resource("/user/register") - .guard(guard::Post()) - .wrap(rate_limit.register()) - .route(post().to(register)), - ) - // User - .service( - // Handle /user/login separately to add the register() rate limiter - // TODO: pretty annoying way to apply rate limits for register and login, we should - // group them under a common path so that rate limit is only applied once (eg under - // /account). - resource("/user/login") - .guard(guard::Post()) - .wrap(rate_limit.register()) - .route(post().to(login)), - ) - .service( - resource("/user/password_reset") - .wrap(rate_limit.register()) - .route(post().to(reset_password)), - ) - .service( - // Handle captcha separately - resource("/user/get_captcha") - .wrap(rate_limit.post()) - .route(get().to(get_captcha)), - ) - .service( - resource("/user/export_settings") - .wrap(rate_limit.import_user_settings()) - .route(get().to(export_settings)), - ) - .service( - resource("/user/import_settings") - .wrap(rate_limit.import_user_settings()) - .route(post().to(import_settings)), - ) - // TODO, all the current account related actions under /user need to get moved here eventually - .service( - scope("/account") - .wrap(rate_limit.message()) - .route("/list_media", get().to(list_media)), - ) - // User actions - .service( - scope("/user") - .wrap(rate_limit.message()) - .route("", get().to(read_person)) - .route("/mention", get().to(list_mentions)) - .route( - "/mention/mark_as_read", - post().to(mark_person_mention_as_read), - ) - .route("/replies", get().to(list_replies)) - // Admin action. I don't like that it's in /user - .route("/ban", post().to(ban_from_site)) - .route("/banned", get().to(list_banned_users)) - .route("/block", post().to(user_block_person)) - // TODO Account actions. I don't like that they're in /user maybe /accounts - .route("/logout", post().to(logout)) - .route("/delete_account", post().to(delete_account)) - .route("/password_change", post().to(change_password_after_reset)) - // TODO mark_all_as_read feels off being in this section as well - .route("/mark_all_as_read", post().to(mark_all_notifications_read)) - .route("/save_user_settings", put().to(save_user_settings)) - .route("/change_password", put().to(change_password)) - .route("/report_count", get().to(report_count)) - .route("/unread_count", get().to(unread_count)) - .route("/verify_email", post().to(verify_email)) - .route("/leave_admin", post().to(leave_admin)) - .route("/totp/generate", post().to(generate_totp_secret)) - .route("/totp/update", post().to(update_totp)) - .route("/list_logins", get().to(list_logins)) - .route("/validate_auth", get().to(validate_auth)), - ) - // Admin Actions - .service( - scope("/admin") - .wrap(rate_limit.message()) - .route("/add", post().to(add_admin)) - .route( - "/registration_application/count", - get().to(get_unread_registration_application_count), - ) - .route( - "/registration_application/list", - get().to(list_registration_applications), - ) - .route( - "/registration_application/approve", - put().to(approve_registration_application), - ) - .route( - "/registration_application", - get().to(get_registration_application), - ) - .route("/list_all_media", get().to(list_all_media)) - .service( - scope("/purge") - .route("/person", post().to(purge_person)) - .route("/community", post().to(purge_community)) - .route("/post", post().to(purge_post)) - .route("/comment", post().to(purge_comment)), - ), - ) - .service( - scope("/custom_emoji") - .wrap(rate_limit.message()) - .route("", post().to(create_custom_emoji)) - .route("", put().to(update_custom_emoji)) - .route("/delete", post().to(delete_custom_emoji)), - ), - ); + cfg + .service( + resource("/pictrs/image") + .wrap(rate_limit.image()) + .route(post().to(upload_image)), + ) + .service(resource("/pictrs/image/{filename}").route(get().to(get_full_res_image))) + .service(resource("/pictrs/image/delete/{token}/{filename}").route(get().to(delete_image))) + .service(resource("/pictrs/healthz").route(get().to(pictrs_healthz))) + .service( + scope("/api/v3") + .route("/image_proxy", get().to(image_proxy)) + // Site + .service( + scope("/site") + .wrap(rate_limit.message()) + .route("", get().to(get_site_v3)) + // Admin Actions + .route("", post().to(create_site)) + .route("", put().to(update_site)) + .route("/block", post().to(user_block_instance)), + ) + .service( + resource("/modlog") + .wrap(rate_limit.message()) + .route(get().to(get_mod_log)), + ) + .service( + resource("/search") + .wrap(rate_limit.search()) + .route(get().to(search)), + ) + .service( + resource("/resolve_object") + .wrap(rate_limit.message()) + .route(get().to(resolve_object)), + ) + // Community + .service( + resource("/community") + .guard(guard::Post()) + .wrap(rate_limit.register()) + .route(post().to(create_community)), + ) + .service( + scope("/community") + .wrap(rate_limit.message()) + .route("", get().to(get_community)) + .route("", put().to(update_community)) + .route("/hide", put().to(hide_community)) + .route("/list", get().to(list_communities)) + .route("/follow", post().to(follow_community)) + .route("/block", post().to(user_block_community)) + .route("/delete", post().to(delete_community)) + // Mod Actions + .route("/remove", post().to(remove_community)) + .route("/transfer", post().to(transfer_community)) + .route("/ban_user", post().to(ban_from_community)) + .route("/mod", post().to(add_mod_to_community)), + ) + .service( + scope("/federated_instances") + .wrap(rate_limit.message()) + .route("", get().to(get_federated_instances)), + ) + // Post + .service( + // Handle POST to /post separately to add the post() rate limitter + resource("/post") + .guard(guard::Post()) + .wrap(rate_limit.post()) + .route(post().to(create_post)), + ) + .service( + scope("/post") + .wrap(rate_limit.message()) + .route("", get().to(get_post)) + .route("", put().to(update_post)) + .route("/delete", post().to(delete_post)) + .route("/remove", post().to(remove_post)) + .route("/mark_as_read", post().to(mark_post_as_read)) + .route("/hide", post().to(hide_post)) + .route("/lock", post().to(lock_post)) + .route("/feature", post().to(feature_post)) + .route("/list", get().to(list_posts)) + .route("/like", post().to(like_post)) + .route("/like/list", get().to(list_post_likes)) + .route("/save", put().to(save_post)) + .route("/report", post().to(create_post_report)) + .route("/report/resolve", put().to(resolve_post_report)) + .route("/report/list", get().to(list_post_reports)) + .route("/site_metadata", get().to(get_link_metadata)), + ) + // Comment + .service( + // Handle POST to /comment separately to add the comment() rate limitter + resource("/comment") + .guard(guard::Post()) + .wrap(rate_limit.comment()) + .route(post().to(create_comment)), + ) + .service( + scope("/comment") + .wrap(rate_limit.message()) + .route("", get().to(get_comment)) + .route("", put().to(update_comment)) + .route("/delete", post().to(delete_comment)) + .route("/remove", post().to(remove_comment)) + .route("/mark_as_read", post().to(mark_reply_as_read)) + .route("/distinguish", post().to(distinguish_comment)) + .route("/like", post().to(like_comment)) + .route("/like/list", get().to(list_comment_likes)) + .route("/save", put().to(save_comment)) + .route("/list", get().to(list_comments)) + .route("/report", post().to(create_comment_report)) + .route("/report/resolve", put().to(resolve_comment_report)) + .route("/report/list", get().to(list_comment_reports)), + ) + // Private Message + .service( + scope("/private_message") + .wrap(rate_limit.message()) + .route("/list", get().to(get_private_message)) + .route("", post().to(create_private_message)) + .route("", put().to(update_private_message)) + .route("/delete", post().to(delete_private_message)) + .route("/mark_as_read", post().to(mark_pm_as_read)) + .route("/report", post().to(create_pm_report)) + .route("/report/resolve", put().to(resolve_pm_report)) + .route("/report/list", get().to(list_pm_reports)), + ) + // User + .service( + // Account action, I don't like that it's in /user maybe /accounts + // Handle /user/register separately to add the register() rate limiter + resource("/user/register") + .guard(guard::Post()) + .wrap(rate_limit.register()) + .route(post().to(register)), + ) + // User + .service( + // Handle /user/login separately to add the register() rate limiter + // TODO: pretty annoying way to apply rate limits for register and login, we should + // group them under a common path so that rate limit is only applied once (eg under + // /account). + resource("/user/login") + .guard(guard::Post()) + .wrap(rate_limit.register()) + .route(post().to(login)), + ) + .service( + resource("/user/password_reset") + .wrap(rate_limit.register()) + .route(post().to(reset_password)), + ) + .service( + // Handle captcha separately + resource("/user/get_captcha") + .wrap(rate_limit.post()) + .route(get().to(get_captcha)), + ) + .service( + resource("/user/export_settings") + .wrap(rate_limit.import_user_settings()) + .route(get().to(export_settings)), + ) + .service( + resource("/user/import_settings") + .wrap(rate_limit.import_user_settings()) + .route(post().to(import_settings)), + ) + // TODO, all the current account related actions under /user need to get moved here + // eventually + .service( + scope("/account") + .wrap(rate_limit.message()) + .route("/list_media", get().to(list_media)), + ) + // User actions + .service( + scope("/user") + .wrap(rate_limit.message()) + .route("", get().to(read_person)) + .route("/mention", get().to(list_mentions)) + .route( + "/mention/mark_as_read", + post().to(mark_person_mention_as_read), + ) + .route("/replies", get().to(list_replies)) + // Admin action. I don't like that it's in /user + .route("/ban", post().to(ban_from_site)) + .route("/banned", get().to(list_banned_users)) + .route("/block", post().to(user_block_person)) + // TODO Account actions. I don't like that they're in /user maybe /accounts + .route("/logout", post().to(logout)) + .route("/delete_account", post().to(delete_account)) + .route("/password_change", post().to(change_password_after_reset)) + // TODO mark_all_as_read feels off being in this section as well + .route("/mark_all_as_read", post().to(mark_all_notifications_read)) + .route("/save_user_settings", put().to(save_user_settings)) + .route("/change_password", put().to(change_password)) + .route("/report_count", get().to(report_count)) + .route("/unread_count", get().to(unread_count)) + .route("/verify_email", post().to(verify_email)) + .route("/leave_admin", post().to(leave_admin)) + .route("/totp/generate", post().to(generate_totp_secret)) + .route("/totp/update", post().to(update_totp)) + .route("/list_logins", get().to(list_logins)) + .route("/validate_auth", get().to(validate_auth)), + ) + // Admin Actions + .service( + scope("/admin") + .wrap(rate_limit.message()) + .route("/add", post().to(add_admin)) + .route( + "/registration_application/count", + get().to(get_unread_registration_application_count), + ) + .route( + "/registration_application/list", + get().to(list_registration_applications), + ) + .route( + "/registration_application/approve", + put().to(approve_registration_application), + ) + .route( + "/registration_application", + get().to(get_registration_application), + ) + .route("/list_all_media", get().to(list_all_media)) + .service( + scope("/purge") + .route("/person", post().to(purge_person)) + .route("/community", post().to(purge_community)) + .route("/post", post().to(purge_post)) + .route("/comment", post().to(purge_comment)), + ), + ) + .service( + scope("/custom_emoji") + .wrap(rate_limit.message()) + .route("", post().to(create_custom_emoji)) + .route("", put().to(update_custom_emoji)) + .route("/delete", post().to(delete_custom_emoji)), + ), + ); cfg.service( scope("/sitemap.xml") .wrap(rate_limit.message()) diff --git a/src/api_routes_v4.rs b/src/api_routes_v4.rs index a9f71c9da..ee88274c9 100644 --- a/src/api_routes_v4.rs +++ b/src/api_routes_v4.rs @@ -159,7 +159,13 @@ use lemmy_apub::api::{ search::search, user_settings_backup::{export_settings, import_settings}, }; -use lemmy_routes::images::image_proxy; +use lemmy_routes::images::{ + delete_image, + get_full_res_image, + image_proxy, + pictrs_healthz, + upload_image, +}; use lemmy_utils::rate_limit::RateLimitCell; pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { @@ -387,6 +393,17 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .wrap(rate_limit.register()) .route("/authenticate", post().to(authenticate_with_oauth)), ) - .route("/sitemap.xml", get().to(get_sitemap)), + .route("/sitemap.xml", get().to(get_sitemap)) + .service( + scope("/image") + .service( + resource("") + .wrap(rate_limit.image()) + .route(post().to(upload_image)), + ) + .route("/{filename}", get().to(get_full_res_image)) + .route("{token}/{filename}", delete().to(delete_image)) + .route("/healthz", get().to(pictrs_healthz)), + ), ); }