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