Skip to main content

bobashare_web/views/
mod.rs

1//! Frontend views (as opposed to REST API)
2
3// The ErrorResponse is actually the same size as Response, and there's no
4// consumer of this api that would be "infected" by this large error value, so
5// this lint isn't necessary.
6//
7// See https://github.com/rust-lang/rust-clippy/issues/10211
8//
9// This is changed from expect to allow as the false positive doesn't appear on
10// arm targets.
11#![allow(clippy::result_large_err)]
12
13use std::path::Path;
14
15use askama::Template;
16use axum::{
17    http,
18    response::{Html, IntoResponse, Response},
19    routing::get,
20    Router,
21};
22use chrono::TimeDelta;
23use http::header::{HeaderName, HeaderValue};
24use hyper::StatusCode;
25use tower::ServiceBuilder;
26use tower_http::set_header::SetResponseHeaderLayer;
27use tracing::{event, Level};
28use url::Url;
29
30use crate::AppState;
31
32pub mod about;
33pub mod display;
34pub mod filters;
35pub mod upload;
36
37mod prelude {
38    pub use super::CurrentNavigation;
39}
40
41// 's is for &AppState
42// TODO: should this be Copy
43#[derive(Debug, Clone)]
44pub struct TemplateState<'s> {
45    version: &'static str,
46    instance_name: &'s str,
47    base_url: &'s Url,
48    max_file_size: u64,
49    max_expiry: Option<TimeDelta>,
50    extra_footer_text: Option<&'s str>,
51    about_page: Option<&'s Path>,
52
53    // None if the current page is not a navbar item
54    current_navigation: Option<CurrentNavigation>,
55}
56impl<'s> From<&'s AppState> for TemplateState<'s> {
57    fn from(state: &'s AppState) -> Self {
58        Self {
59            version: env!("CARGO_PKG_VERSION"),
60            instance_name: &state.instance_name,
61            base_url: &state.base_url,
62            max_file_size: state.max_file_size,
63            max_expiry: state.max_expiry,
64            extra_footer_text: state.extra_footer_text.as_deref(),
65            about_page: state.about_page.as_deref(),
66            current_navigation: None, // will be set to Some in individual handlers
67        }
68    }
69}
70
71// which page is current navigated to, for navbar formatting
72#[derive(Debug, Clone, Copy)]
73#[non_exhaustive]
74pub enum CurrentNavigation {
75    Upload,
76    Paste,
77    About,
78}
79
80#[derive(Template)]
81#[template(path = "error.html.jinja", blocks = ["title"])]
82pub struct ErrorTemplate<'s> {
83    pub state: TemplateState<'s>,
84    pub code: StatusCode,
85    pub message: String,
86}
87
88pub struct ErrorResponse(Response);
89impl From<ErrorTemplate<'_>> for ErrorResponse {
90    fn from(tmpl: ErrorTemplate) -> Self {
91        let error_msg = &tmpl.message;
92        match tmpl.render() {
93            Ok(s) => {
94                let status_num = tmpl.code.as_u16();
95                if tmpl.code.is_server_error() {
96                    event!(Level::ERROR, status = status_num, error_msg);
97                } else if tmpl.code.is_client_error() {
98                    event!(Level::WARN, status = status_num, error_msg);
99                } else {
100                    event!(Level::INFO, status = status_num, error_msg);
101                }
102                Self((tmpl.code, Html(s)).into_response())
103            }
104            Err(e) => {
105                let status = tmpl.code.as_u16();
106                event!(Level::ERROR, status, error_msg, render_error = ?e, "error rendering error page template, so HTTP 500 returned:");
107                Self(
108                    (
109                        StatusCode::INTERNAL_SERVER_ERROR,
110                        format!("internal error rendering error page template: {:?}", e),
111                    )
112                        .into_response(),
113                )
114            }
115        }
116    }
117}
118impl From<askama::Error> for ErrorResponse {
119    fn from(err: askama::Error) -> Self {
120        event!(Level::ERROR, render_error = ?err, "error rendering template");
121        Self(
122            (
123                StatusCode::INTERNAL_SERVER_ERROR,
124                format!("internal error rendering template: {:?}", err),
125            )
126                .into_response(),
127        )
128    }
129}
130impl IntoResponse for ErrorResponse {
131    fn into_response(self) -> axum::response::Response {
132        self.0
133    }
134}
135
136pub(crate) fn render_template<T: askama::Template>(tmpl: T) -> Result<Response, ErrorResponse> {
137    let rendered = tmpl.render()?;
138    Ok((StatusCode::OK, Html(rendered)).into_response())
139}
140
141pub fn router() -> Router<&'static AppState> {
142    let x_robots_tag_no_index = SetResponseHeaderLayer::overriding(
143        HeaderName::from_static("x-robots-tag"),
144        HeaderValue::from_static("noindex"),
145    );
146    let csp_normal = SetResponseHeaderLayer::overriding(
147        http::header::CONTENT_SECURITY_POLICY,
148        HeaderValue::from_static("default-src 'self'; img-src 'self' data:;"),
149    );
150    let csp_sandbox = SetResponseHeaderLayer::overriding(
151        http::header::CONTENT_SECURITY_POLICY,
152        HeaderValue::from_static("sandbox; default-src 'none';"),
153    );
154
155    Router::new()
156        .route("/", get(upload::upload))
157        .route("/paste/", get(upload::paste))
158        .route("/about/", get(about::about))
159        .route(
160            "/{id}",
161            get(display::display).layer(
162                ServiceBuilder::new()
163                    .layer(x_robots_tag_no_index.clone())
164                    .layer(csp_normal),
165            ),
166        )
167        .route(
168            "/raw/{id}",
169            get(display::raw).layer(
170                ServiceBuilder::new()
171                    .layer(x_robots_tag_no_index.clone())
172                    .layer(csp_sandbox),
173            ),
174        )
175}