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 {
pool: ActualDbPool,
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>,
rate_limit_cell: RateLimitCell,
}
@ -23,12 +26,14 @@ impl LemmyContext {
pub fn create(
pool: ActualDbPool,
client: ClientWithMiddleware,
pictrs_client: ClientWithMiddleware,
secret: Secret,
rate_limit_cell: RateLimitCell,
) -> LemmyContext {
LemmyContext {
pool,
client: Arc::new(client),
pictrs_client: Arc::new(pictrs_client),
secret: Arc::new(secret),
rate_limit_cell,
}
@ -42,6 +47,9 @@ impl LemmyContext {
pub fn client(&self) -> &ClientWithMiddleware {
&self.client
}
pub fn pictrs_client(&self) -> &ClientWithMiddleware {
&self.pictrs_client
}
pub fn settings(&self) -> &'static Settings {
&SETTINGS
}
@ -70,7 +78,13 @@ impl LemmyContext {
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()
.domain(context.settings().hostname.clone())

View file

@ -319,7 +319,7 @@ struct PictrsPurgeResponse {
/// - It might not be an image
/// - Pictrs might not be set up
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
.path_segments()
@ -334,7 +334,7 @@ pub async fn purge_image_from_pictrs(image_url: &Url, context: &LemmyContext) ->
.api_key
.ok_or(LemmyErrorType::PictrsApiKeyNotProvided)?;
let response = context
.client()
.pictrs_client()
.post(&purge_url)
.timeout(REQWEST_TIMEOUT)
.header("x-api-token", pictrs_api_key)
@ -361,7 +361,7 @@ pub async fn delete_image_from_pictrs(
pictrs_config.url, &delete_token, &alias
);
context
.client()
.pictrs_client()
.delete(&url)
.timeout(REQWEST_TIMEOUT)
.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
// TODO: should limit size once supported by pictrs
let fetch_url = format!(
"{}image/download?url={}&resize={}",
pictrs_config.url,
@ -393,7 +392,7 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
);
let res = context
.client()
.pictrs_client()
.get(&fetch_url)
.timeout(REQWEST_TIMEOUT)
.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}");
context
.client()
.pictrs_client()
.get(&proxy_url)
.timeout(REQWEST_TIMEOUT)
.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 res = context
.client()
.pictrs_client()
.get(&details_url)
.timeout(REQWEST_TIMEOUT)
.send()

View file

@ -17,7 +17,6 @@ use lemmy_db_schema::{
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;
use reqwest_middleware::ClientWithMiddleware;
pub async fn delete_site_icon(
context: Data<LemmyContext>,
@ -126,7 +125,6 @@ pub async fn delete_user_banner(
pub async fn delete_image(
data: Json<DeleteImageParams>,
context: Data<LemmyContext>,
client: Data<ClientWithMiddleware>,
// require login
_local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
@ -136,7 +134,12 @@ pub async fn delete_image(
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?;

View file

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

View file

@ -1,21 +1,22 @@
use actix_web::web::*;
use lemmy_api_common::{context::LemmyContext, SuccessResponse};
use lemmy_utils::error::LemmyResult;
use reqwest_middleware::ClientWithMiddleware;
pub mod delete;
pub mod download;
pub mod upload;
mod utils;
pub async fn pictrs_health(
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
pub async fn pictrs_health(context: Data<LemmyContext>) -> LemmyResult<Json<SuccessResponse>> {
let pictrs_config = context.settings().pictrs()?;
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()))
}

View file

@ -20,7 +20,6 @@ use lemmy_db_schema::{
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;
use reqwest::Body;
use reqwest_middleware::ClientWithMiddleware;
use std::time::Duration;
use UploadType::*;
@ -34,7 +33,6 @@ pub async fn upload_image(
req: HttpRequest,
body: Payload,
local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<UploadImageResponse>> {
if context.settings().pictrs()?.image_upload_disabled {
@ -42,7 +40,7 @@ pub async fn upload_image(
}
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,
body: Payload,
local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>,
) -> 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?;
let form = PersonUpdateForm {
@ -69,10 +66,9 @@ pub async fn upload_user_banner(
req: HttpRequest,
body: Payload,
local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>,
) -> 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?;
let form = PersonUpdateForm {
@ -89,13 +85,12 @@ pub async fn upload_community_icon(
query: Query<CommunityIdQuery>,
body: Payload,
local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
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?;
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?;
let form = CommunityUpdateForm {
@ -112,13 +107,12 @@ pub async fn upload_community_banner(
query: Query<CommunityIdQuery>,
body: Payload,
local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
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?;
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?;
let form = CommunityUpdateForm {
@ -134,13 +128,12 @@ pub async fn upload_site_icon(
req: HttpRequest,
body: Payload,
local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
is_admin(&local_user_view)?;
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?;
let form = SiteUpdateForm {
@ -156,13 +149,12 @@ pub async fn upload_site_banner(
req: HttpRequest,
body: Payload,
local_user_view: LocalUserView,
client: Data<ClientWithMiddleware>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
is_admin(&local_user_view)?;
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?;
let form = SiteUpdateForm {
@ -179,13 +171,12 @@ pub async fn do_upload_image(
body: Payload,
upload_type: UploadType,
local_user_view: &LocalUserView,
client: Data<ClientWithMiddleware>,
context: &Data<LemmyContext>,
) -> LemmyResult<UploadImageResponse> {
let pictrs = context.settings().pictrs()?;
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 {
Avatar => {

View file

@ -11,17 +11,18 @@ use http::HeaderValue;
use lemmy_api_common::{context::LemmyContext, request::delete_image_from_pictrs};
use lemmy_db_schema::{newtypes::DbUrl, source::images::LocalImage};
use lemmy_utils::{error::LemmyResult, REQWEST_TIMEOUT};
use reqwest_middleware::{ClientWithMiddleware, RequestBuilder};
use reqwest_middleware::RequestBuilder;
pub(super) fn adapt_request(
request: &HttpRequest,
url: String,
client: Data<ClientWithMiddleware>,
context: &LemmyContext,
) -> 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
.pictrs_client()
.request(convert_method(request.method()), url)
.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()?)
.with(TracingMiddleware::default())
.build();
let pictrs_client = ClientBuilder::new(client_builder(&SETTINGS).no_proxy().build()?)
.with(TracingMiddleware::default())
.build();
let context = LemmyContext::create(
pool.clone(),
client.clone(),
pictrs_client,
secret.clone(),
rate_limit_cell.clone(),
);
@ -330,11 +334,6 @@ fn create_http_server(
.build()
.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
let bind = (settings.bind, settings.port);
let server = HttpServer::new(move || {
@ -355,7 +354,6 @@ fn create_http_server(
.wrap(ErrorHandlers::new().default_handler(jsonify_plain_text_errors))
.app_data(Data::new(context.clone()))
.app_data(Data::new(rate_limit_cell.clone()))
.app_data(Data::new(pictrs_client.clone()))
.wrap(FederationMiddleware::new(federation_config.clone()))
.wrap(SessionMiddleware::new(context.clone()))
.wrap(Condition::new(