1
/*
2
 * This file is part of mailpot
3
 *
4
 * Copyright 2020 - Manos Pitsidianakis
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Affero General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Affero General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Affero General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 */
19

            
20
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
21

            
22
use super::*;
23

            
24
// from https://github.com/servo/rust-url/blob/master/url/src/parser.rs
25
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
26
const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
27
pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%');
28

            
29
pub trait IntoCrumb: TypedPath {
30
    fn to_crumb(&self) -> Cow<'static, str> {
31
        Cow::from(self.to_uri().to_string())
32
    }
33
}
34

            
35
impl<TP: TypedPath> IntoCrumb for TP {}
36

            
37
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
38
#[serde(untagged)]
39
pub enum ListPathIdentifier {
40
    Pk(#[serde(deserialize_with = "parse_int")] i64),
41
    Id(String),
42
}
43

            
44
fn parse_int<'de, T, D>(de: D) -> Result<T, D::Error>
45
where
46
    D: serde::Deserializer<'de>,
47
    T: std::str::FromStr,
48
    <T as std::str::FromStr>::Err: std::fmt::Display,
49
{
50
    use serde::Deserialize;
51
    String::deserialize(de)?
52
        .parse()
53
        .map_err(serde::de::Error::custom)
54
}
55

            
56
impl From<i64> for ListPathIdentifier {
57
    fn from(val: i64) -> Self {
58
        Self::Pk(val)
59
    }
60
}
61

            
62
impl From<String> for ListPathIdentifier {
63
    fn from(val: String) -> Self {
64
        Self::Id(val)
65
    }
66
}
67

            
68
impl std::fmt::Display for ListPathIdentifier {
69
    #[allow(clippy::unnecessary_to_owned)]
70
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71
        let id: Cow<'_, str> = match self {
72
            Self::Pk(id) => id.to_string().into(),
73
            Self::Id(id) => id.into(),
74
        };
75
        write!(f, "{}", utf8_percent_encode(&id, PATH_SEGMENT,))
76
    }
77
}
78

            
79
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
80
#[typed_path("/list/:id/")]
81
pub struct ListPath(pub ListPathIdentifier);
82

            
83
impl From<&DbVal<mailpot::models::MailingList>> for ListPath {
84
    fn from(val: &DbVal<mailpot::models::MailingList>) -> Self {
85
        Self(ListPathIdentifier::Id(val.id.clone()))
86
    }
87
}
88

            
89
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
90
#[typed_path("/list/:id/posts/:msgid/")]
91
pub struct ListPostPath(pub ListPathIdentifier, pub String);
92

            
93
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
94
#[typed_path("/list/:id/edit/")]
95
pub struct ListEditPath(pub ListPathIdentifier);
96

            
97
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
98
#[typed_path("/list/:id/edit/subscribers/")]
99
pub struct ListEditSubscribersPath(pub ListPathIdentifier);
100

            
101
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
102
#[typed_path("/list/:id/edit/candidates/")]
103
pub struct ListEditCandidatesPath(pub ListPathIdentifier);
104

            
105
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
106
#[typed_path("/settings/list/:id/")]
107
pub struct ListSettingsPath(pub ListPathIdentifier);
108

            
109
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
110
#[typed_path("/login/")]
111
pub struct LoginPath;
112

            
113
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
114
#[typed_path("/logout/")]
115
pub struct LogoutPath;
116

            
117
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
118
#[typed_path("/settings/")]
119
pub struct SettingsPath;
120

            
121
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
122
#[typed_path("/help/")]
123
pub struct HelpPath;
124

            
125
macro_rules! unit_impl {
126
    ($ident:ident, $ty:expr) => {
127
        pub fn $ident() -> Value {
128
            Value::from_safe_string($ty.to_crumb().to_string())
129
        }
130
    };
131
}
132

            
133
unit_impl!(login_path, LoginPath);
134
unit_impl!(logout_path, LogoutPath);
135
unit_impl!(settings_path, SettingsPath);
136
unit_impl!(help_path, HelpPath);
137

            
138
macro_rules! list_id_impl {
139
    ($ident:ident, $ty:tt) => {
140
        pub fn $ident(id: Value) -> std::result::Result<Value, Error> {
141
            if let Some(id) = id.as_str() {
142
                return Ok(Value::from_safe_string(
143
                    $ty(ListPathIdentifier::Id(id.to_string()))
144
                        .to_crumb()
145
                        .to_string(),
146
                ));
147
            }
148
            let pk = id.try_into()?;
149
            Ok(Value::from_safe_string(
150
                $ty(ListPathIdentifier::Pk(pk)).to_crumb().to_string(),
151
            ))
152
        }
153
    };
154
}
155

            
156
list_id_impl!(list_path, ListPath);
157
list_id_impl!(list_settings_path, ListSettingsPath);
158
list_id_impl!(list_edit_path, ListEditPath);
159
list_id_impl!(list_subscribers_path, ListEditSubscribersPath);
160
list_id_impl!(list_candidates_path, ListEditCandidatesPath);
161

            
162
pub fn list_post_path(id: Value, msg_id: Value) -> std::result::Result<Value, Error> {
163
    let Some(msg_id) = msg_id.as_str().map(|s| if s.starts_with('<') && s.ends_with('>') { s.to_string() } else {
164
        format!("<{s}>")
165
    }) else {
166
        return Err(Error::new(
167
                minijinja::ErrorKind::UnknownMethod,
168
                "Second argument of list_post_path must be a string."
169
        ));
170
    };
171

            
172
    if let Some(id) = id.as_str() {
173
        return Ok(Value::from_safe_string(
174
            ListPostPath(ListPathIdentifier::Id(id.to_string()), msg_id)
175
                .to_crumb()
176
                .to_string(),
177
        ));
178
    }
179
    let pk = id.try_into()?;
180
    Ok(Value::from_safe_string(
181
        ListPostPath(ListPathIdentifier::Pk(pk), msg_id)
182
            .to_crumb()
183
            .to_string(),
184
    ))
185
}
186

            
187
pub mod tsr {
188
    use std::{borrow::Cow, convert::Infallible};
189

            
190
    use axum::{
191
        http::Request,
192
        response::{IntoResponse, Redirect, Response},
193
        routing::{any, MethodRouter},
194
        Router,
195
    };
196
    use axum_extra::routing::{RouterExt as ExtraRouterExt, SecondElementIs, TypedPath};
197
    use http::{uri::PathAndQuery, StatusCode, Uri};
198
    use tower_service::Service;
199

            
200
    /// Extension trait that adds additional methods to [`Router`].
201
    pub trait RouterExt<S, B>: ExtraRouterExt<S, B> {
202
        /// Add a typed `GET` route to the router.
203
        ///
204
        /// The path will be inferred from the first argument to the handler
205
        /// function which must implement [`TypedPath`].
206
        ///
207
        /// See [`TypedPath`] for more details and examples.
208
        fn typed_get<H, T, P>(self, handler: H) -> Self
209
        where
210
            H: axum::handler::Handler<T, S, B>,
211
            T: SecondElementIs<P> + 'static,
212
            P: TypedPath;
213

            
214
        /// Add a typed `DELETE` route to the router.
215
        ///
216
        /// The path will be inferred from the first argument to the handler
217
        /// function which must implement [`TypedPath`].
218
        ///
219
        /// See [`TypedPath`] for more details and examples.
220
        fn typed_delete<H, T, P>(self, handler: H) -> Self
221
        where
222
            H: axum::handler::Handler<T, S, B>,
223
            T: SecondElementIs<P> + 'static,
224
            P: TypedPath;
225

            
226
        /// Add a typed `HEAD` route to the router.
227
        ///
228
        /// The path will be inferred from the first argument to the handler
229
        /// function which must implement [`TypedPath`].
230
        ///
231
        /// See [`TypedPath`] for more details and examples.
232
        fn typed_head<H, T, P>(self, handler: H) -> Self
233
        where
234
            H: axum::handler::Handler<T, S, B>,
235
            T: SecondElementIs<P> + 'static,
236
            P: TypedPath;
237

            
238
        /// Add a typed `OPTIONS` route to the router.
239
        ///
240
        /// The path will be inferred from the first argument to the handler
241
        /// function which must implement [`TypedPath`].
242
        ///
243
        /// See [`TypedPath`] for more details and examples.
244
        fn typed_options<H, T, P>(self, handler: H) -> Self
245
        where
246
            H: axum::handler::Handler<T, S, B>,
247
            T: SecondElementIs<P> + 'static,
248
            P: TypedPath;
249

            
250
        /// Add a typed `PATCH` route to the router.
251
        ///
252
        /// The path will be inferred from the first argument to the handler
253
        /// function which must implement [`TypedPath`].
254
        ///
255
        /// See [`TypedPath`] for more details and examples.
256
        fn typed_patch<H, T, P>(self, handler: H) -> Self
257
        where
258
            H: axum::handler::Handler<T, S, B>,
259
            T: SecondElementIs<P> + 'static,
260
            P: TypedPath;
261

            
262
        /// Add a typed `POST` route to the router.
263
        ///
264
        /// The path will be inferred from the first argument to the handler
265
        /// function which must implement [`TypedPath`].
266
        ///
267
        /// See [`TypedPath`] for more details and examples.
268
        fn typed_post<H, T, P>(self, handler: H) -> Self
269
        where
270
            H: axum::handler::Handler<T, S, B>,
271
            T: SecondElementIs<P> + 'static,
272
            P: TypedPath;
273

            
274
        /// Add a typed `PUT` route to the router.
275
        ///
276
        /// The path will be inferred from the first argument to the handler
277
        /// function which must implement [`TypedPath`].
278
        ///
279
        /// See [`TypedPath`] for more details and examples.
280
        fn typed_put<H, T, P>(self, handler: H) -> Self
281
        where
282
            H: axum::handler::Handler<T, S, B>,
283
            T: SecondElementIs<P> + 'static,
284
            P: TypedPath;
285

            
286
        /// Add a typed `TRACE` route to the router.
287
        ///
288
        /// The path will be inferred from the first argument to the handler
289
        /// function which must implement [`TypedPath`].
290
        ///
291
        /// See [`TypedPath`] for more details and examples.
292
        fn typed_trace<H, T, P>(self, handler: H) -> Self
293
        where
294
            H: axum::handler::Handler<T, S, B>,
295
            T: SecondElementIs<P> + 'static,
296
            P: TypedPath;
297

            
298
        /// Add another route to the router with an additional "trailing slash
299
        /// redirect" route.
300
        ///
301
        /// If you add a route _without_ a trailing slash, such as `/foo`, this
302
        /// method will also add a route for `/foo/` that redirects to
303
        /// `/foo`.
304
        ///
305
        /// If you add a route _with_ a trailing slash, such as `/bar/`, this
306
        /// method will also add a route for `/bar` that redirects to
307
        /// `/bar/`.
308
        ///
309
        /// This is similar to what axum 0.5.x did by default, except this
310
        /// explicitly adds another route, so trying to add a `/foo/`
311
        /// route after calling `.route_with_tsr("/foo", /* ... */)`
312
        /// will result in a panic due to route overlap.
313
        ///
314
        /// # Example
315
        ///
316
        /// ```
317
        /// use axum::{routing::get, Router};
318
        /// use axum_extra::routing::RouterExt;
319
        ///
320
        /// let app = Router::new()
321
        ///     // `/foo/` will redirect to `/foo`
322
        ///     .route_with_tsr("/foo", get(|| async {}))
323
        ///     // `/bar` will redirect to `/bar/`
324
        ///     .route_with_tsr("/bar/", get(|| async {}));
325
        /// # let _: Router = app;
326
        /// ```
327
        fn route_with_tsr(self, path: &str, method_router: MethodRouter<S, B>) -> Self
328
        where
329
            Self: Sized;
330

            
331
        /// Add another route to the router with an additional "trailing slash
332
        /// redirect" route.
333
        ///
334
        /// This works like [`RouterExt::route_with_tsr`] but accepts any
335
        /// [`Service`].
336
        fn route_service_with_tsr<T>(self, path: &str, service: T) -> Self
337
        where
338
            T: Service<Request<B>, Error = Infallible> + Clone + Send + 'static,
339
            T::Response: IntoResponse,
340
            T::Future: Send + 'static,
341
            Self: Sized;
342
    }
343

            
344
    impl<S, B> RouterExt<S, B> for Router<S, B>
345
    where
346
        B: axum::body::HttpBody + Send + 'static,
347
        S: Clone + Send + Sync + 'static,
348
    {
349
        fn typed_get<H, T, P>(mut self, handler: H) -> Self
350
        where
351
            H: axum::handler::Handler<T, S, B>,
352
            T: SecondElementIs<P> + 'static,
353
            P: TypedPath,
354
        {
355
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
356
            self = self.route(
357
                tsr_path.as_ref(),
358
                axum::routing::get(move |url| tsr_handler_into_async(url, tsr_handler)),
359
            );
360
            self = self.route(P::PATH, axum::routing::get(handler));
361
            self
362
        }
363

            
364
        fn typed_delete<H, T, P>(mut self, handler: H) -> Self
365
        where
366
            H: axum::handler::Handler<T, S, B>,
367
            T: SecondElementIs<P> + 'static,
368
            P: TypedPath,
369
        {
370
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
371
            self = self.route(
372
                tsr_path.as_ref(),
373
                axum::routing::delete(move |url| tsr_handler_into_async(url, tsr_handler)),
374
            );
375
            self = self.route(P::PATH, axum::routing::delete(handler));
376
            self
377
        }
378

            
379
        fn typed_head<H, T, P>(mut self, handler: H) -> Self
380
        where
381
            H: axum::handler::Handler<T, S, B>,
382
            T: SecondElementIs<P> + 'static,
383
            P: TypedPath,
384
        {
385
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
386
            self = self.route(
387
                tsr_path.as_ref(),
388
                axum::routing::head(move |url| tsr_handler_into_async(url, tsr_handler)),
389
            );
390
            self = self.route(P::PATH, axum::routing::head(handler));
391
            self
392
        }
393

            
394
        fn typed_options<H, T, P>(mut self, handler: H) -> Self
395
        where
396
            H: axum::handler::Handler<T, S, B>,
397
            T: SecondElementIs<P> + 'static,
398
            P: TypedPath,
399
        {
400
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
401
            self = self.route(
402
                tsr_path.as_ref(),
403
                axum::routing::options(move |url| tsr_handler_into_async(url, tsr_handler)),
404
            );
405
            self = self.route(P::PATH, axum::routing::options(handler));
406
            self
407
        }
408

            
409
        fn typed_patch<H, T, P>(mut self, handler: H) -> Self
410
        where
411
            H: axum::handler::Handler<T, S, B>,
412
            T: SecondElementIs<P> + 'static,
413
            P: TypedPath,
414
        {
415
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
416
            self = self.route(
417
                tsr_path.as_ref(),
418
                axum::routing::patch(move |url| tsr_handler_into_async(url, tsr_handler)),
419
            );
420
            self = self.route(P::PATH, axum::routing::patch(handler));
421
            self
422
        }
423

            
424
        fn typed_post<H, T, P>(mut self, handler: H) -> Self
425
        where
426
            H: axum::handler::Handler<T, S, B>,
427
            T: SecondElementIs<P> + 'static,
428
            P: TypedPath,
429
        {
430
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
431
            self = self.route(
432
                tsr_path.as_ref(),
433
                axum::routing::post(move |url| tsr_handler_into_async(url, tsr_handler)),
434
            );
435
            self = self.route(P::PATH, axum::routing::post(handler));
436
            self
437
        }
438

            
439
        fn typed_put<H, T, P>(mut self, handler: H) -> Self
440
        where
441
            H: axum::handler::Handler<T, S, B>,
442
            T: SecondElementIs<P> + 'static,
443
            P: TypedPath,
444
        {
445
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
446
            self = self.route(
447
                tsr_path.as_ref(),
448
                axum::routing::put(move |url| tsr_handler_into_async(url, tsr_handler)),
449
            );
450
            self = self.route(P::PATH, axum::routing::put(handler));
451
            self
452
        }
453

            
454
        fn typed_trace<H, T, P>(mut self, handler: H) -> Self
455
        where
456
            H: axum::handler::Handler<T, S, B>,
457
            T: SecondElementIs<P> + 'static,
458
            P: TypedPath,
459
        {
460
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
461
            self = self.route(
462
                tsr_path.as_ref(),
463
                axum::routing::trace(move |url| tsr_handler_into_async(url, tsr_handler)),
464
            );
465
            self = self.route(P::PATH, axum::routing::trace(handler));
466
            self
467
        }
468

            
469
        #[track_caller]
470
        fn route_with_tsr(mut self, path: &str, method_router: MethodRouter<S, B>) -> Self
471
        where
472
            Self: Sized,
473
        {
474
            validate_tsr_path(path);
475
            self = self.route(path, method_router);
476
            add_tsr_redirect_route(self, path)
477
        }
478

            
479
        #[track_caller]
480
        fn route_service_with_tsr<T>(mut self, path: &str, service: T) -> Self
481
        where
482
            T: Service<Request<B>, Error = Infallible> + Clone + Send + 'static,
483
            T::Response: IntoResponse,
484
            T::Future: Send + 'static,
485
            Self: Sized,
486
        {
487
            validate_tsr_path(path);
488
            self = self.route_service(path, service);
489
            add_tsr_redirect_route(self, path)
490
        }
491
    }
492

            
493
    #[track_caller]
494
    fn validate_tsr_path(path: &str) {
495
        if path == "/" {
496
            panic!("Cannot add a trailing slash redirect route for `/`")
497
        }
498
    }
499

            
500
    #[inline]
501
    fn add_tsr_redirect_route<S, B>(router: Router<S, B>, path: &str) -> Router<S, B>
502
    where
503
        B: axum::body::HttpBody + Send + 'static,
504
        S: Clone + Send + Sync + 'static,
505
    {
506
        async fn redirect_handler(uri: Uri) -> Response {
507
            let new_uri = map_path(uri, |path| {
508
                path.strip_suffix('/')
509
                    .map(Cow::Borrowed)
510
                    .unwrap_or_else(|| Cow::Owned(format!("{path}/")))
511
            });
512

            
513
            new_uri.map_or_else(
514
                || StatusCode::BAD_REQUEST.into_response(),
515
                |new_uri| Redirect::permanent(&new_uri.to_string()).into_response(),
516
            )
517
        }
518

            
519
        if let Some(path_without_trailing_slash) = path.strip_suffix('/') {
520
            router.route(path_without_trailing_slash, any(redirect_handler))
521
        } else {
522
            router.route(&format!("{path}/"), any(redirect_handler))
523
        }
524
    }
525

            
526
    #[inline]
527
    fn tsr_redirect_route(path: &'_ str) -> (Cow<'_, str>, fn(Uri) -> Response) {
528
        fn redirect_handler(uri: Uri) -> Response {
529
            let new_uri = map_path(uri, |path| {
530
                path.strip_suffix('/')
531
                    .map(Cow::Borrowed)
532
                    .unwrap_or_else(|| Cow::Owned(format!("{path}/")))
533
            });
534

            
535
            new_uri.map_or_else(
536
                || StatusCode::BAD_REQUEST.into_response(),
537
                |new_uri| Redirect::permanent(&new_uri.to_string()).into_response(),
538
            )
539
        }
540

            
541
        path.strip_suffix('/').map_or_else(
542
            || {
543
                (
544
                    Cow::Owned(format!("{path}/")),
545
                    redirect_handler as fn(Uri) -> Response,
546
                )
547
            },
548
            |path_without_trailing_slash| {
549
                (
550
                    Cow::Borrowed(path_without_trailing_slash),
551
                    redirect_handler as fn(Uri) -> Response,
552
                )
553
            },
554
        )
555
    }
556

            
557
    #[inline]
558
    async fn tsr_handler_into_async(u: Uri, h: fn(Uri) -> Response) -> Response {
559
        h(u)
560
    }
561

            
562
    /// Map the path of a `Uri`.
563
    ///
564
    /// Returns `None` if the `Uri` cannot be put back together with the new
565
    /// path.
566
    fn map_path<F>(original_uri: Uri, f: F) -> Option<Uri>
567
    where
568
        F: FnOnce(&str) -> Cow<'_, str>,
569
    {
570
        let mut parts = original_uri.into_parts();
571
        let path_and_query = parts.path_and_query.as_ref()?;
572

            
573
        let new_path = f(path_and_query.path());
574

            
575
        let new_path_and_query = if let Some(query) = &path_and_query.query() {
576
            format!("{new_path}?{query}").parse::<PathAndQuery>().ok()?
577
        } else {
578
            new_path.parse::<PathAndQuery>().ok()?
579
        };
580
        parts.path_and_query = Some(new_path_and_query);
581

            
582
        Uri::from_parts(parts).ok()
583
    }
584
}