atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
d604371299fbbc6fbf489e054ac9390a97b7ac55
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/middleware.rsRust168 lines5.6 KB
1
use std::io::Write as _;
2
3
use axum::{
4
    body::Body,
5
    extract::Request,
6
    http::{HeaderValue, StatusCode, Version, header},
7
    middleware::Next,
8
    response::{IntoResponse, Response},
9
};
10
use flate2::{Compression, write::GzEncoder};
11
12
use serde_json::Value;
13
14
use crate::{
15
    headers::{X_OBSERVED_BY, X_POWERED_BY},
16
    locale::Locale,
17
    problem::Problem,
18
};
19
20
const MAX_URI: usize = 512;
21
const MAX_HEADER: usize = 1024;
22
23
/// Marks a response for gzip, which the cock ring asks for.
24
#[derive(Clone)]
25
pub struct Compress;
26
27
fn reject(request: &Request) -> Option<Problem> {
28
    if request.version() < Version::HTTP_11 {
29
        return Some(Problem::new(
30
            StatusCode::HTTP_VERSION_NOT_SUPPORTED,
31
            "pdf",
32
            "HTTP/1.0 is the version the original jelqing PDF was written for. The server has read it.",
33
        ));
34
    }
35
    if request.uri().to_string().len() > MAX_URI {
36
        return Some(Problem::new(
37
            StatusCode::URI_TOO_LONG,
38
            "uri-too-long",
39
            "The URI is longer than anything else here.",
40
        ));
41
    }
42
    if request.headers().values().any(|v| v.len() > MAX_HEADER) {
43
        return Some(Problem::new(
44
            StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE,
45
            "head-too-large",
46
            "Head too large.",
47
        ));
48
    }
49
    let expectation = request.headers().get(header::EXPECT)?;
50
    if expectation.as_bytes().eq_ignore_ascii_case(b"100-continue") {
51
        return None;
52
    }
53
    Some(Problem::new(
54
        StatusCode::EXPECTATION_FAILED,
55
        "expectations",
56
        format!(
57
            "Expect: {} cannot be met.",
58
            expectation.to_str().unwrap_or("that")
59
        ),
60
    ))
61
}
62
63
async fn compress(response: Response) -> Response {
64
    let (mut parts, body) = response.into_parts();
65
    let Ok(bytes) = axum::body::to_bytes(body, usize::MAX).await else {
66
        return Response::from_parts(parts, Body::empty());
67
    };
68
    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
69
    let gzipped = encoder.write_all(&bytes).and_then(|()| encoder.finish());
70
    let Ok(gzipped) = gzipped else {
71
        return Response::from_parts(parts, Body::from(bytes));
72
    };
73
    parts
74
        .headers
75
        .insert(header::CONTENT_ENCODING, HeaderValue::from_static("gzip"));
76
    parts.headers.remove(header::CONTENT_LENGTH);
77
    parts
78
        .headers
79
        .append(header::VARY, HeaderValue::from_static("Accept-Encoding"));
80
    Response::from_parts(parts, Body::from(gzipped))
81
}
82
83
async fn localise(response: Response, locale: Locale) -> Response {
84
    if locale == Locale::British {
85
        return response;
86
    }
87
    let (mut parts, body) = response.into_parts();
88
    for name in [header::VIA, header::VARY, X_OBSERVED_BY] {
89
        let localised: Vec<HeaderValue> = parts
90
            .headers
91
            .get_all(&name)
92
            .iter()
93
            .filter_map(|v| v.to_str().ok())
94
            .filter_map(|v| HeaderValue::from_str(&locale.localise(v)).ok())
95
            .collect();
96
        if !localised.is_empty() {
97
            parts.headers.remove(&name);
98
            for value in localised {
99
                parts.headers.append(name.clone(), value);
100
            }
101
        }
102
    }
103
    let json = parts
104
        .headers
105
        .get(header::CONTENT_TYPE)
106
        .and_then(|v| v.to_str().ok())
107
        .is_some_and(|ct| {
108
            ct.starts_with("application/json") || ct.starts_with("application/problem+json")
109
        });
110
    if !json || parts.status == StatusCode::PARTIAL_CONTENT {
111
        return Response::from_parts(parts, body);
112
    }
113
    let Ok(bytes) = axum::body::to_bytes(body, usize::MAX).await else {
114
        return Response::from_parts(parts, Body::empty());
115
    };
116
    let Ok(mut value) = serde_json::from_slice::<Value>(&bytes) else {
117
        return Response::from_parts(parts, Body::from(bytes));
118
    };
119
    locale.localise_json(&mut value);
120
    parts.headers.remove(header::CONTENT_LENGTH);
121
    Response::from_parts(parts, Body::from(value.to_string()))
122
}
123
124
pub async fn chrome(request: Request, next: Next) -> Response {
125
    let (gzip, locale) = {
126
        let header_text = |name| {
127
            request
128
                .headers()
129
                .get(name)
130
                .and_then(|v| v.to_str().ok())
131
                .map(str::to_owned)
132
        };
133
        let gzip = header_text(header::ACCEPT_ENCODING)
134
            .is_some_and(|e| e.to_ascii_lowercase().contains("gzip"));
135
        let locale = Locale::negotiate(header_text(header::ACCEPT_LANGUAGE).as_deref());
136
        (gzip, locale)
137
    };
138
    let response = match reject(&request) {
139
        Some(problem) => problem.into_response(),
140
        None => next.run(request).await,
141
    };
142
    let mut response = localise(response, locale).await;
143
    if gzip && response.extensions().get::<Compress>().is_some() {
144
        response = compress(response).await;
145
    }
146
    let headers = response.headers_mut();
147
    headers.insert(header::SERVER, HeaderValue::from_static("jelqd/0.6.9"));
148
    headers.insert(X_POWERED_BY, HeaderValue::from_static("blood"));
149
    headers.insert(
150
        header::CONTENT_LANGUAGE,
151
        HeaderValue::from_static(locale.tag()),
152
    );
153
    headers.append(header::VARY, HeaderValue::from_static("Accept-Language"));
154
    headers
155
        .entry(header::CACHE_CONTROL)
156
        .or_insert(HeaderValue::from_static("no-store, private"));
157
    response
158
}
159
160
/// A handler panic would otherwise drop the connection with no reply at all.
161
pub fn panicked(_: Box<dyn std::any::Any + Send + 'static>) -> Response {
162
    Problem::new(
163
        StatusCode::INTERNAL_SERVER_ERROR,
164
        "server-fault",
165
        "The server has had an internal event of its own. It is not discussing it.",
166
    )
167
    .into_response()
168
}