proxy pictrs in request.rs (fixes #5270)

This commit is contained in:
Felix Ableitner 2025-01-10 11:08:24 +01:00
parent f040f9146c
commit 6123659711
8 changed files with 54 additions and 50 deletions

View file

@ -15,6 +15,9 @@ use std::sync::Arc;
pub struct LemmyContext { pub struct LemmyContext {
pool: ActualDbPool, pool: ActualDbPool,
client: Arc<ClientWithMiddleware>, client: Arc<ClientWithMiddleware>,
/// Pictrs requests must bypass proxy. Unfortunately no_proxy can only be set on ClientBuilder
/// and not on RequestBuilder, so we need a separate client here.
pictrs_client: Arc<ClientWithMiddleware>,
secret: Arc<Secret>, secret: Arc<Secret>,
rate_limit_cell: RateLimitCell, rate_limit_cell: RateLimitCell,
} }
@ -23,12 +26,14 @@ impl LemmyContext {
pub fn create( pub fn create(
pool: ActualDbPool, pool: ActualDbPool,
client: ClientWithMiddleware, client: ClientWithMiddleware,
pictrs_client: ClientWithMiddleware,
secret: Secret, secret: Secret,
rate_limit_cell: RateLimitCell, rate_limit_cell: RateLimitCell,
) -> LemmyContext { ) -> LemmyContext {
LemmyContext { LemmyContext {
pool, pool,
client: Arc::new(client), client: Arc::new(client),
pictrs_client: Arc::new(pictrs_client),
secret: Arc::new(secret), secret: Arc::new(secret),
rate_limit_cell, rate_limit_cell,
} }
@ -42,6 +47,9 @@ impl LemmyContext {
pub fn client(&self) -> &ClientWithMiddleware { pub fn client(&self) -> &ClientWithMiddleware {
&self.client &self.client
} }
pub fn pictrs_client(&self) -> &ClientWithMiddleware {
&self.pictrs_client
}
pub fn settings(&self) -> &'static Settings { pub fn settings(&self) -> &'static Settings {
&SETTINGS &SETTINGS
} }
@ -70,7 +78,13 @@ impl LemmyContext {
let rate_limit_cell = RateLimitCell::with_test_config(); let rate_limit_cell = RateLimitCell::with_test_config();
let context = LemmyContext::create(pool, client, secret, rate_limit_cell.clone()); let context = LemmyContext::create(
pool,
client.clone(),
client,
secret,
rate_limit_cell.clone(),
);
FederationConfig::builder() FederationConfig::builder()
.domain(context.settings().hostname.clone()) .domain(context.settings().hostname.clone())

View file

@ -319,7 +319,7 @@ struct PictrsPurgeResponse {
/// - It might not be an image /// - It might not be an image
/// - Pictrs might not be set up /// - Pictrs might not be set up
pub async fn purge_image_from_pictrs(image_url: &Url, context: &LemmyContext) -> LemmyResult<()> { pub async fn purge_image_from_pictrs(image_url: &Url, context: &LemmyContext) -> LemmyResult<()> {
is_image_content_type(context.client(), image_url).await?; is_image_content_type(context.pictrs_client(), image_url).await?;
let alias = image_url let alias = image_url
.path_segments() .path_segments()
@ -334,7 +334,7 @@ pub async fn purge_image_from_pictrs(image_url: &Url, context: &LemmyContext) ->
.api_key .api_key
.ok_or(LemmyErrorType::PictrsApiKeyNotProvided)?; .ok_or(LemmyErrorType::PictrsApiKeyNotProvided)?;
let response = context let response = context
.client() .pictrs_client()
.post(&purge_url) .post(&purge_url)
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.header("x-api-token", pictrs_api_key) .header("x-api-token", pictrs_api_key)
@ -361,7 +361,7 @@ pub async fn delete_image_from_pictrs(
pictrs_config.url, &delete_token, &alias pictrs_config.url, &delete_token, &alias
); );
context context
.client() .pictrs_client()
.delete(&url) .delete(&url)
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.send() .send()
@ -384,7 +384,6 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
}; };
// fetch remote non-pictrs images for persistent thumbnail link // fetch remote non-pictrs images for persistent thumbnail link
// TODO: should limit size once supported by pictrs
let fetch_url = format!( let fetch_url = format!(
"{}image/download?url={}&resize={}", "{}image/download?url={}&resize={}",
pictrs_config.url, pictrs_config.url,
@ -393,7 +392,7 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
); );
let res = context let res = context
.client() .pictrs_client()
.get(&fetch_url) .get(&fetch_url)
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.send() .send()
@ -439,7 +438,7 @@ pub async fn fetch_pictrs_proxied_image_details(
let proxy_url = format!("{pictrs_url}image/original?proxy={encoded_image_url}"); let proxy_url = format!("{pictrs_url}image/original?proxy={encoded_image_url}");
context context
.client() .pictrs_client()
.get(&proxy_url) .get(&proxy_url)
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.send() .send()
@ -450,7 +449,7 @@ pub async fn fetch_pictrs_proxied_image_details(
let details_url = format!("{pictrs_url}image/details/original?proxy={encoded_image_url}"); let details_url = format!("{pictrs_url}image/details/original?proxy={encoded_image_url}");
let res = context let res = context
.client() .pictrs_client()
.get(&details_url) .get(&details_url)
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.send() .send()

View file

@ -17,7 +17,6 @@ use lemmy_db_schema::{
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
use reqwest_middleware::ClientWithMiddleware;
pub async fn delete_site_icon( pub async fn delete_site_icon(
context: Data<LemmyContext>, context: Data<LemmyContext>,
@ -126,7 +125,6 @@ pub async fn delete_user_banner(
pub async fn delete_image( pub async fn delete_image(
data: Json<DeleteImageParams>, data: Json<DeleteImageParams>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
client: Data<ClientWithMiddleware>,
// require login // require login
_local_user_view: LocalUserView, _local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
@ -136,7 +134,12 @@ pub async fn delete_image(
pictrs_config.url, &data.token, &data.filename pictrs_config.url, &data.token, &data.filename
); );
client.delete(url).send().await?.error_for_status()?; context
.pictrs_client()
.delete(url)
.send()
.await?
.error_for_status()?;
LocalImage::delete_by_alias(&mut context.pool(), &data.filename).await?; LocalImage::delete_by_alias(&mut context.pool(), &data.filename).await?;

View file

@ -14,7 +14,6 @@ use lemmy_api_common::{
use lemmy_db_schema::source::{images::RemoteImage, local_site::LocalSite}; use lemmy_db_schema::source::{images::RemoteImage, local_site::LocalSite};
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
use reqwest_middleware::ClientWithMiddleware;
use url::Url; use url::Url;
pub async fn get_image( pub async fn get_image(
@ -23,7 +22,6 @@ pub async fn get_image(
req: HttpRequest, req: HttpRequest,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
client: Data<ClientWithMiddleware>,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<HttpResponse> {
// block access to images if instance is private // block access to images if instance is private
if local_user_view.is_none() { if local_user_view.is_none() {
@ -48,13 +46,12 @@ pub async fn get_image(
url url
}; };
do_get_image(processed_url, req, client).await do_get_image(processed_url, req, &context).await
} }
pub async fn image_proxy( pub async fn image_proxy(
Query(params): Query<ImageProxyParams>, Query(params): Query<ImageProxyParams>,
req: HttpRequest, req: HttpRequest,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Either<HttpResponse<()>, HttpResponse<BoxBody>>> { ) -> LemmyResult<Either<HttpResponse<()>, HttpResponse<BoxBody>>> {
let url = Url::parse(&params.url)?; let url = Url::parse(&params.url)?;
@ -89,7 +86,7 @@ pub async fn image_proxy(
} else { } else {
// Proxy the image data through Lemmy // Proxy the image data through Lemmy
Ok(Either::Right( Ok(Either::Right(
do_get_image(processed_url, req, client).await?, do_get_image(processed_url, req, &context).await?,
)) ))
} }
} }
@ -97,9 +94,9 @@ pub async fn image_proxy(
pub(super) async fn do_get_image( pub(super) async fn do_get_image(
url: String, url: String,
req: HttpRequest, req: HttpRequest,
client: Data<ClientWithMiddleware>, context: &LemmyContext,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<HttpResponse> {
let mut client_req = adapt_request(&req, url, client); let mut client_req = adapt_request(&req, url, context);
if let Some(addr) = req.head().peer_addr { if let Some(addr) = req.head().peer_addr {
client_req = client_req.header("X-Forwarded-For", addr.to_string()); client_req = client_req.header("X-Forwarded-For", addr.to_string());

View file

@ -1,21 +1,22 @@
use actix_web::web::*; use actix_web::web::*;
use lemmy_api_common::{context::LemmyContext, SuccessResponse}; use lemmy_api_common::{context::LemmyContext, SuccessResponse};
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
use reqwest_middleware::ClientWithMiddleware;
pub mod delete; pub mod delete;
pub mod download; pub mod download;
pub mod upload; pub mod upload;
mod utils; mod utils;
pub async fn pictrs_health( pub async fn pictrs_health(context: Data<LemmyContext>) -> LemmyResult<Json<SuccessResponse>> {
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
let pictrs_config = context.settings().pictrs()?; let pictrs_config = context.settings().pictrs()?;
let url = format!("{}healthz", pictrs_config.url); let url = format!("{}healthz", pictrs_config.url);
client.get(url).send().await?.error_for_status()?; context
.pictrs_client()
.get(url)
.send()
.await?
.error_for_status()?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -20,7 +20,6 @@ use lemmy_db_schema::{
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
use reqwest::Body; use reqwest::Body;
use reqwest_middleware::ClientWithMiddleware;
use std::time::Duration; use std::time::Duration;
use UploadType::*; use UploadType::*;
@ -34,7 +33,6 @@ pub async fn upload_image(
req: HttpRequest, req: HttpRequest,
body: Payload, body: Payload,
local_user_view: LocalUserView, local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<UploadImageResponse>> { ) -> LemmyResult<Json<UploadImageResponse>> {
if context.settings().pictrs()?.image_upload_disabled { if context.settings().pictrs()?.image_upload_disabled {
@ -42,7 +40,7 @@ pub async fn upload_image(
} }
Ok(Json( Ok(Json(
do_upload_image(req, body, Other, &local_user_view, client, &context).await?, do_upload_image(req, body, Other, &local_user_view, &context).await?,
)) ))
} }
@ -50,10 +48,9 @@ pub async fn upload_user_avatar(
req: HttpRequest, req: HttpRequest,
body: Payload, body: Payload,
local_user_view: LocalUserView, local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let image = do_upload_image(req, body, Avatar, &local_user_view, client, &context).await?; let image = do_upload_image(req, body, Avatar, &local_user_view, &context).await?;
delete_old_image(&local_user_view.person.avatar, &context).await?; delete_old_image(&local_user_view.person.avatar, &context).await?;
let form = PersonUpdateForm { let form = PersonUpdateForm {
@ -69,10 +66,9 @@ pub async fn upload_user_banner(
req: HttpRequest, req: HttpRequest,
body: Payload, body: Payload,
local_user_view: LocalUserView, local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let image = do_upload_image(req, body, Banner, &local_user_view, client, &context).await?; let image = do_upload_image(req, body, Banner, &local_user_view, &context).await?;
delete_old_image(&local_user_view.person.banner, &context).await?; delete_old_image(&local_user_view.person.banner, &context).await?;
let form = PersonUpdateForm { let form = PersonUpdateForm {
@ -89,13 +85,12 @@ pub async fn upload_community_icon(
query: Query<CommunityIdQuery>, query: Query<CommunityIdQuery>,
body: Payload, body: Payload,
local_user_view: LocalUserView, local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let community: Community = Community::read(&mut context.pool(), query.id).await?; let community: Community = Community::read(&mut context.pool(), query.id).await?;
is_mod_or_admin(&mut context.pool(), &local_user_view.person, community.id).await?; is_mod_or_admin(&mut context.pool(), &local_user_view.person, community.id).await?;
let image = do_upload_image(req, body, Avatar, &local_user_view, client, &context).await?; let image = do_upload_image(req, body, Avatar, &local_user_view, &context).await?;
delete_old_image(&community.icon, &context).await?; delete_old_image(&community.icon, &context).await?;
let form = CommunityUpdateForm { let form = CommunityUpdateForm {
@ -112,13 +107,12 @@ pub async fn upload_community_banner(
query: Query<CommunityIdQuery>, query: Query<CommunityIdQuery>,
body: Payload, body: Payload,
local_user_view: LocalUserView, local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let community: Community = Community::read(&mut context.pool(), query.id).await?; let community: Community = Community::read(&mut context.pool(), query.id).await?;
is_mod_or_admin(&mut context.pool(), &local_user_view.person, community.id).await?; is_mod_or_admin(&mut context.pool(), &local_user_view.person, community.id).await?;
let image = do_upload_image(req, body, Banner, &local_user_view, client, &context).await?; let image = do_upload_image(req, body, Banner, &local_user_view, &context).await?;
delete_old_image(&community.banner, &context).await?; delete_old_image(&community.banner, &context).await?;
let form = CommunityUpdateForm { let form = CommunityUpdateForm {
@ -134,13 +128,12 @@ pub async fn upload_site_icon(
req: HttpRequest, req: HttpRequest,
body: Payload, body: Payload,
local_user_view: LocalUserView, local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let site = Site::read_local(&mut context.pool()).await?; let site = Site::read_local(&mut context.pool()).await?;
let image = do_upload_image(req, body, Avatar, &local_user_view, client, &context).await?; let image = do_upload_image(req, body, Avatar, &local_user_view, &context).await?;
delete_old_image(&site.icon, &context).await?; delete_old_image(&site.icon, &context).await?;
let form = SiteUpdateForm { let form = SiteUpdateForm {
@ -156,13 +149,12 @@ pub async fn upload_site_banner(
req: HttpRequest, req: HttpRequest,
body: Payload, body: Payload,
local_user_view: LocalUserView, local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let site = Site::read_local(&mut context.pool()).await?; let site = Site::read_local(&mut context.pool()).await?;
let image = do_upload_image(req, body, Banner, &local_user_view, client, &context).await?; let image = do_upload_image(req, body, Banner, &local_user_view, &context).await?;
delete_old_image(&site.banner, &context).await?; delete_old_image(&site.banner, &context).await?;
let form = SiteUpdateForm { let form = SiteUpdateForm {
@ -179,13 +171,12 @@ pub async fn do_upload_image(
body: Payload, body: Payload,
upload_type: UploadType, upload_type: UploadType,
local_user_view: &LocalUserView, local_user_view: &LocalUserView,
client: Data<ClientWithMiddleware>,
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
) -> LemmyResult<UploadImageResponse> { ) -> LemmyResult<UploadImageResponse> {
let pictrs = context.settings().pictrs()?; let pictrs = context.settings().pictrs()?;
let image_url = format!("{}image", pictrs.url); let image_url = format!("{}image", pictrs.url);
let mut client_req = adapt_request(&req, image_url, client); let mut client_req = adapt_request(&req, image_url, &context);
client_req = match upload_type { client_req = match upload_type {
Avatar => { Avatar => {

View file

@ -11,17 +11,18 @@ use http::HeaderValue;
use lemmy_api_common::{context::LemmyContext, request::delete_image_from_pictrs}; use lemmy_api_common::{context::LemmyContext, request::delete_image_from_pictrs};
use lemmy_db_schema::{newtypes::DbUrl, source::images::LocalImage}; use lemmy_db_schema::{newtypes::DbUrl, source::images::LocalImage};
use lemmy_utils::{error::LemmyResult, REQWEST_TIMEOUT}; use lemmy_utils::{error::LemmyResult, REQWEST_TIMEOUT};
use reqwest_middleware::{ClientWithMiddleware, RequestBuilder}; use reqwest_middleware::RequestBuilder;
pub(super) fn adapt_request( pub(super) fn adapt_request(
request: &HttpRequest, request: &HttpRequest,
url: String, url: String,
client: Data<ClientWithMiddleware>, context: &LemmyContext,
) -> RequestBuilder { ) -> RequestBuilder {
// remove accept-encoding header so that pictrs doesn't compress the response // remove accept-encoding header so that pictrs doesn't compress the response
const INVALID_HEADERS: &[HeaderName] = &[ACCEPT_ENCODING, HOST]; const INVALID_HEADERS: &[HeaderName] = &[ACCEPT_ENCODING, HOST];
let client_request = client let client_request = context
.pictrs_client()
.request(convert_method(request.method()), url) .request(convert_method(request.method()), url)
.timeout(REQWEST_TIMEOUT); .timeout(REQWEST_TIMEOUT);

View file

@ -195,9 +195,13 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> {
let client = ClientBuilder::new(client_builder(&SETTINGS).build()?) let client = ClientBuilder::new(client_builder(&SETTINGS).build()?)
.with(TracingMiddleware::default()) .with(TracingMiddleware::default())
.build(); .build();
let pictrs_client = ClientBuilder::new(client_builder(&SETTINGS).no_proxy().build()?)
.with(TracingMiddleware::default())
.build();
let context = LemmyContext::create( let context = LemmyContext::create(
pool.clone(), pool.clone(),
client.clone(), client.clone(),
pictrs_client,
secret.clone(), secret.clone(),
rate_limit_cell.clone(), rate_limit_cell.clone(),
); );
@ -330,11 +334,6 @@ fn create_http_server(
.build() .build()
.map_err(|e| LemmyErrorType::Unknown(format!("Should always be buildable: {e}")))?; .map_err(|e| LemmyErrorType::Unknown(format!("Should always be buildable: {e}")))?;
// Pictrs cannot use proxy
let pictrs_client = ClientBuilder::new(client_builder(&SETTINGS).no_proxy().build()?)
.with(TracingMiddleware::default())
.build();
// Create Http server // Create Http server
let bind = (settings.bind, settings.port); let bind = (settings.bind, settings.port);
let server = HttpServer::new(move || { let server = HttpServer::new(move || {
@ -355,7 +354,6 @@ fn create_http_server(
.wrap(ErrorHandlers::new().default_handler(jsonify_plain_text_errors)) .wrap(ErrorHandlers::new().default_handler(jsonify_plain_text_errors))
.app_data(Data::new(context.clone())) .app_data(Data::new(context.clone()))
.app_data(Data::new(rate_limit_cell.clone())) .app_data(Data::new(rate_limit_cell.clone()))
.app_data(Data::new(pictrs_client.clone()))
.wrap(FederationMiddleware::new(federation_config.clone())) .wrap(FederationMiddleware::new(federation_config.clone()))
.wrap(SessionMiddleware::new(context.clone())) .wrap(SessionMiddleware::new(context.clone()))
.wrap(Condition::new( .wrap(Condition::new(