Skip to content

Commit

Permalink
proxy pictrs in request.rs (fixes #5270)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nutomic committed Jan 10, 2025
1 parent f040f91 commit 9fc02be
Show file tree
Hide file tree
Showing 8 changed files with 49 additions and 50 deletions.
16 changes: 15 additions & 1 deletion crates/api_common/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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,
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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())
Expand Down
13 changes: 6 additions & 7 deletions crates/api_common/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down
4 changes: 1 addition & 3 deletions crates/routes/src/images/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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>> {
Expand All @@ -136,7 +134,7 @@ 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?;

Expand Down
11 changes: 4 additions & 7 deletions crates/routes/src/images/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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() {
Expand All @@ -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)?;
Expand Down Expand Up @@ -89,17 +86,17 @@ 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?,
))
}
}

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());
Expand Down
13 changes: 7 additions & 6 deletions crates/routes/src/images/mod.rs
Original file line number Diff line number Diff line change
@@ -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()))
}
25 changes: 8 additions & 17 deletions crates/routes/src/images/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand All @@ -34,26 +33,24 @@ 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 {
return Err(LemmyErrorType::ImageUploadDisabled.into());
}

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?,
))
}

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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 => {
Expand Down
7 changes: 4 additions & 3 deletions crates/routes/src/images/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
10 changes: 4 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
);
Expand Down Expand Up @@ -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 || {
Expand All @@ -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(
Expand Down

0 comments on commit 9fc02be

Please sign in to comment.