1use std::fmt::Debug;
5
6use hyper::{
7 header::HeaderValue, http::uri::PathAndQuery, Method, Request, Response, StatusCode, Uri,
8};
9use links_id::Id;
10use links_normalized::Normalized;
11use tokio::time::Instant;
12use tracing::{debug, field::Empty, instrument, trace};
13
14use crate::{
15 config::{Hsts, Redirector as Config},
16 stats::{ExtraStatisticInfo, Statistic},
17 store::Store,
18 util::{csp_hashes, include_html, SERVER_NAME},
19};
20
21#[instrument(level = "debug", name = "redirect-external", skip_all, fields(http.version = ?req.version(), http.host = %req.uri().host().unwrap_or_else(|| req.headers().get("host").map_or_else(|| "[unknown]", |h| h.to_str().unwrap_or("[unknown]"))), http.path = ?req.uri().path(), http.method = %req.method(), store = %store.backend_name(), time_ns = Empty, link = Empty, id = Empty, vanity = Empty, status_code = Empty))]
28pub async fn redirector<B: Debug + Send + 'static>(
29 req: Request<B>,
30 store: Store,
31 config: Config,
32 stat_info: ExtraStatisticInfo,
33) -> Result<Response<String>, anyhow::Error> {
34 let redirect_start = Instant::now();
35 trace!(?req);
36
37 let path = req.uri().path();
38 let mut res = Response::builder();
39
40 res = res.header("Referrer-Policy", "unsafe-url");
42 if config.send_server {
43 res = res.header("Server", SERVER_NAME);
44 }
45
46 if config.send_alt_svc {
47 res = res.header("Alt-Svc", "h2=\":443\"; ma=31536000");
48 }
49
50 res = match config.hsts {
51 Hsts::Disable => res,
52 Hsts::Enable(max_age) => {
53 res.header("Strict-Transport-Security", &format!("max-age={max_age}"))
54 }
55 Hsts::IncludeSubDomains(max_age) => res.header(
56 "Strict-Transport-Security",
57 &format!("max-age={max_age}; includeSubDomains"),
58 ),
59 Hsts::Preload(max_age) => res.header(
60 "Strict-Transport-Security",
61 &format!("max-age={max_age}; includeSubDomains; preload"),
62 ),
63 };
64
65 let id_or_vanity = path.trim_start_matches('/');
66
67 let (id, vanity) = if Id::is_valid(id_or_vanity) {
68 trace!("path is an ID");
69 (Some(Id::try_from(id_or_vanity)?), None)
70 } else {
71 let vanity = Normalized::new(id_or_vanity);
72 trace!("path is a vanity path, normalized to \"{}\"", &vanity);
73 (store.get_vanity(vanity.clone()).await?, Some(vanity))
74 };
75
76 let link = if let Some(id) = id {
77 store.get_redirect(id).await?
78 } else {
79 None
80 };
81
82 let res = if let Some(link) = link.clone() {
83 let link = link.into_string();
84
85 res = res.header("Location", &link);
86 res = res.header("Link-Id", &id.unwrap().to_string());
87
88 if config.send_csp {
89 res = res.header(
90 "Content-Security-Policy",
91 concat!(
92 "default-src 'none'; style-src ",
93 csp_hashes!("redirect", "style"),
94 "; sandbox allow-top-navigation"
95 ),
96 );
97 }
98
99 if req.method() == Method::GET {
100 res = res.status(StatusCode::FOUND);
101 } else {
102 res = res.status(StatusCode::TEMPORARY_REDIRECT);
103 }
104
105 res = res.header("Content-Type", "text/html; charset=UTF-8");
106 res.body(
107 include_html!("redirect")
108 .to_string()
109 .replace("{{LINK_URL}}", &link),
110 )?
111 } else {
112 res = res.status(StatusCode::NOT_FOUND);
113 res = res.header("Content-Type", "text/html; charset=UTF-8");
114
115 if config.send_csp {
116 res = res.header(
117 "Content-Security-Policy",
118 concat!(
119 "default-src 'none'; style-src ",
120 csp_hashes!("not-found", "style"),
121 "; sandbox allow-top-navigation"
122 ),
123 );
124 }
125
126 res.body(include_html!("not-found").to_string())?
127 };
128
129 let id = id.map(Into::into);
130 let vanity = vanity.map(Into::into);
131
132 let stats = Statistic::get_misc(
133 id.as_ref(),
134 stat_info.clone(),
135 res.status(),
136 config.statistics,
137 )
138 .chain(Statistic::from_req(id.as_ref(), &req, config.statistics))
139 .chain(Statistic::get_misc(
140 vanity.as_ref(),
141 stat_info,
142 res.status(),
143 config.statistics,
144 ))
145 .chain(Statistic::from_req(
146 vanity.as_ref(),
147 &req,
148 config.statistics,
149 ));
150
151 store.incr_statistics(stats);
152
153 let redirect_time = redirect_start.elapsed();
154
155 trace!(?res);
156 let span = tracing::Span::current();
157 span.record("time_ns", redirect_time.as_nanos());
158 span.record(
159 "link",
160 link.map_or_else(|| "[none]".to_string(), |link| link.to_string()),
161 );
162 span.record(
163 "id",
164 id.map_or_else(|| "[none]".to_string(), |id| id.to_string()),
165 );
166 span.record(
167 "vanity",
168 vanity.map_or_else(|| "[none]".to_string(), |vanity| vanity.to_string()),
169 );
170 span.record("status_code", res.status().as_u16());
171
172 debug!(
173 "External redirect processed in {:.6} seconds",
174 redirect_time.as_secs_f64()
175 );
176
177 Ok(res)
178}
179
180#[instrument(level = "debug", name = "redirect-https", skip_all, fields(http.version = ?req.version(), http.host = %req.uri().host().unwrap_or_else(|| req.headers().get("host").map_or_else(|| "[unknown]", |h| h.to_str().unwrap_or("[unknown]"))), http.path = ?req.uri().path(), http.method = %req.method(), time_ns = Empty, link = Empty, status_code = Empty))]
183pub async fn https_redirector<B: Debug + Send + 'static>(
184 req: Request<B>,
185 config: Config,
186) -> Result<Response<String>, anyhow::Error> {
187 let redirect_start = Instant::now();
188 trace!(?req);
189
190 let mut res = Response::builder();
192 res = res.header("Referrer-Policy", "no-referrer");
193 if config.send_server {
194 res = res.header("Server", SERVER_NAME);
195 }
196 if config.send_alt_svc {
197 res = res.header("Alt-Svc", "h2=\":443\"; ma=31536000");
198 }
199
200 let p_and_q = req.uri().path_and_query().map_or("/", PathAndQuery::as_str);
201 let (res, link) = if let Some(Ok(host)) = req.headers().get("host").map(HeaderValue::to_str) {
202 let link = Uri::builder()
203 .scheme("https")
204 .authority(host)
205 .path_and_query(p_and_q)
206 .build()?
207 .to_string();
208
209 res = res.header("Location", &link);
210
211 if req.method() == Method::GET {
212 res = res.status(StatusCode::FOUND);
213 } else {
214 res = res.status(StatusCode::TEMPORARY_REDIRECT);
215 }
216
217 if config.send_csp {
218 res = res.header(
219 "Content-Security-Policy",
220 concat!(
221 "default-src 'none'; style-src ",
222 csp_hashes!("https-redirect", "style"),
223 "; sandbox allow-top-navigation"
224 ),
225 );
226 }
227
228 res = res.header("Content-Type", "text/html; charset=UTF-8");
229 (
230 res.body(include_html!("https-redirect").to_string())?,
231 Some(link),
232 )
233 } else {
234 if config.send_csp {
235 res = res.header(
236 "Content-Security-Policy",
237 concat!(
238 "default-src 'none'; style-src ",
239 csp_hashes!("bad-request", "style"),
240 "; sandbox allow-top-navigation"
241 ),
242 );
243 }
244
245 res = res.status(StatusCode::BAD_REQUEST);
246 res = res.header("Content-Type", "text/html; charset=UTF-8");
247 (res.body(include_html!("bad-request").to_string())?, None)
248 };
249
250 let redirect_time = redirect_start.elapsed();
251
252 trace!(?res);
253 let span = tracing::Span::current();
254 span.record("time_ns", redirect_time.as_nanos());
255 span.record("link", link.unwrap_or_else(|| "[none]".to_string()));
256 span.record("status_code", res.status().as_u16());
257
258 debug!(
259 "HTTP-to-HTTPS redirect processed in {:.6} seconds",
260 redirect_time.as_secs_f64()
261 );
262
263 Ok(res)
264}