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