]> Untitled Git - lemmy.git/blob - crates/routes/src/images.rs
Dont pass accept-encoding header to pictrs (ref #1734) (#1738)
[lemmy.git] / crates / routes / src / images.rs
1 use actix_http::http::header::ACCEPT_ENCODING;
2 use actix_web::{body::BodyStream, http::StatusCode, web::Data, *};
3 use anyhow::anyhow;
4 use awc::Client;
5 use lemmy_utils::{claims::Claims, rate_limit::RateLimit, settings::structs::Settings, LemmyError};
6 use serde::{Deserialize, Serialize};
7 use std::time::Duration;
8
9 pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
10   let client = Client::builder()
11     .header("User-Agent", "pict-rs-frontend, v0.1.0")
12     .timeout(Duration::from_secs(30))
13     .finish();
14
15   cfg
16     .app_data(Data::new(client))
17     .service(
18       web::resource("/pictrs/image")
19         .wrap(rate_limit.image())
20         .route(web::post().to(upload)),
21     )
22     // This has optional query params: /image/{filename}?format=jpg&thumbnail=256
23     .service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res)))
24     .service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete)));
25 }
26
27 #[derive(Debug, Serialize, Deserialize)]
28 struct Image {
29   file: String,
30   delete_token: String,
31 }
32
33 #[derive(Debug, Serialize, Deserialize)]
34 struct Images {
35   msg: String,
36   files: Option<Vec<Image>>,
37 }
38
39 #[derive(Deserialize)]
40 struct PictrsParams {
41   format: Option<String>,
42   thumbnail: Option<String>,
43 }
44
45 async fn upload(
46   req: HttpRequest,
47   body: web::Payload,
48   client: web::Data<Client>,
49 ) -> Result<HttpResponse, Error> {
50   // TODO: check rate limit here
51   let jwt = req
52     .cookie("jwt")
53     .expect("No auth header for picture upload");
54
55   if Claims::decode(jwt.value()).is_err() {
56     return Ok(HttpResponse::Unauthorized().finish());
57   };
58
59   let mut client_req = client.request_from(format!("{}/image", pictrs_url()?), req.head());
60   // remove content-encoding header so that pictrs doesnt send gzipped response
61   client_req.headers_mut().remove(ACCEPT_ENCODING);
62
63   if let Some(addr) = req.head().peer_addr {
64     client_req = client_req.insert_header(("X-Forwarded-For", addr.to_string()))
65   };
66
67   let mut res = client_req
68     .send_stream(body)
69     .await
70     .map_err(error::ErrorBadRequest)?;
71
72   let images = res.json::<Images>().await.map_err(error::ErrorBadRequest)?;
73
74   Ok(HttpResponse::build(res.status()).json(images))
75 }
76
77 async fn full_res(
78   filename: web::Path<String>,
79   web::Query(params): web::Query<PictrsParams>,
80   req: HttpRequest,
81   client: web::Data<Client>,
82 ) -> Result<HttpResponse, Error> {
83   let name = &filename.into_inner();
84
85   // If there are no query params, the URL is original
86   let url = if params.format.is_none() && params.thumbnail.is_none() {
87     format!("{}/image/original/{}", pictrs_url()?, name,)
88   } else {
89     // Use jpg as a default when none is given
90     let format = params.format.unwrap_or_else(|| "jpg".to_string());
91
92     let mut url = format!("{}/image/process.{}?src={}", pictrs_url()?, format, name,);
93
94     if let Some(size) = params.thumbnail {
95       url = format!("{}&thumbnail={}", url, size,);
96     }
97     url
98   };
99
100   image(url, req, client).await
101 }
102
103 async fn image(
104   url: String,
105   req: HttpRequest,
106   client: web::Data<Client>,
107 ) -> Result<HttpResponse, Error> {
108   let mut client_req = client.request_from(url, req.head());
109   client_req.headers_mut().remove(ACCEPT_ENCODING);
110
111   if let Some(addr) = req.head().peer_addr {
112     client_req = client_req.insert_header(("X-Forwarded-For", addr.to_string()))
113   };
114
115   let res = client_req
116     .no_decompress()
117     .send()
118     .await
119     .map_err(error::ErrorBadRequest)?;
120
121   if res.status() == StatusCode::NOT_FOUND {
122     return Ok(HttpResponse::NotFound().finish());
123   }
124
125   let mut client_res = HttpResponse::build(res.status());
126
127   for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") {
128     client_res.insert_header((name.clone(), value.clone()));
129   }
130
131   Ok(client_res.body(BodyStream::new(res)))
132 }
133
134 async fn delete(
135   components: web::Path<(String, String)>,
136   req: HttpRequest,
137   client: web::Data<Client>,
138 ) -> Result<HttpResponse, Error> {
139   let (token, file) = components.into_inner();
140
141   let url = format!("{}/image/delete/{}/{}", pictrs_url()?, &token, &file);
142
143   let mut client_req = client.request_from(url, req.head());
144   client_req.headers_mut().remove(ACCEPT_ENCODING);
145
146   if let Some(addr) = req.head().peer_addr {
147     client_req = client_req.insert_header(("X-Forwarded-For", addr.to_string()))
148   };
149
150   let res = client_req
151     .no_decompress()
152     .send()
153     .await
154     .map_err(error::ErrorBadRequest)?;
155
156   Ok(HttpResponse::build(res.status()).body(BodyStream::new(res)))
157 }
158
159 fn pictrs_url() -> Result<String, LemmyError> {
160   Settings::get()
161     .pictrs_url
162     .ok_or_else(|| anyhow!("images_disabled").into())
163 }