atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
3f9091557ec3a198324147b05397e1bf5c50247a
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/middleware.rsRust180 lines6.0 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 compressible = response
65
        .headers()
66
        .get(header::CONTENT_TYPE)
67
        .and_then(|value| value.to_str().ok())
68
        .is_some_and(|type_| {
69
            type_.starts_with("application/json")
70
                || type_.starts_with("application/problem+json")
71
                || type_.starts_with("application/xml")
72
        });
73
    if !compressible {
74
        return response;
75
    }
76
    let (mut parts, body) = response.into_parts();
77
    let Ok(bytes) = axum::body::to_bytes(body, usize::MAX).await else {
78
        return Response::from_parts(parts, Body::empty());
79
    };
80
    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
81
    let gzipped = encoder.write_all(&bytes).and_then(|()| encoder.finish());
82
    let Ok(gzipped) = gzipped else {
83
        return Response::from_parts(parts, Body::from(bytes));
84
    };
85
    parts
86
        .headers
87
        .insert(header::CONTENT_ENCODING, HeaderValue::from_static("gzip"));
88
    parts.headers.remove(header::CONTENT_LENGTH);
89
    parts
90
        .headers
91
        .append(header::VARY, HeaderValue::from_static("Accept-Encoding"));
92
    Response::from_parts(parts, Body::from(gzipped))
93
}
94
95
async fn localise(response: Response, locale: Locale) -> Response {
96
    if locale == Locale::British {
97
        return response;
98
    }
99
    let (mut parts, body) = response.into_parts();
100
    for name in [header::VIA, header::VARY, X_OBSERVED_BY] {
101
        let localised: Vec<HeaderValue> = parts
102
            .headers
103
            .get_all(&name)
104
            .iter()
105
            .filter_map(|v| v.to_str().ok())
106
            .filter_map(|v| HeaderValue::from_str(&locale.localise(v)).ok())
107
            .collect();
108
        if !localised.is_empty() {
109
            parts.headers.remove(&name);
110
            for value in localised {
111
                parts.headers.append(name.clone(), value);
112
            }
113
        }
114
    }
115
    let json = parts
116
        .headers
117
        .get(header::CONTENT_TYPE)
118
        .and_then(|v| v.to_str().ok())
119
        .is_some_and(|ct| {
120
            ct.starts_with("application/json") || ct.starts_with("application/problem+json")
121
        });
122
    if !json || parts.status == StatusCode::PARTIAL_CONTENT {
123
        return Response::from_parts(parts, body);
124
    }
125
    let Ok(bytes) = axum::body::to_bytes(body, usize::MAX).await else {
126
        return Response::from_parts(parts, Body::empty());
127
    };
128
    let Ok(mut value) = serde_json::from_slice::<Value>(&bytes) else {
129
        return Response::from_parts(parts, Body::from(bytes));
130
    };
131
    locale.localise_json(&mut value);
132
    parts.headers.remove(header::CONTENT_LENGTH);
133
    Response::from_parts(parts, Body::from(value.to_string()))
134
}
135
136
pub async fn chrome(request: Request, next: Next) -> Response {
137
    let (gzip, locale) = {
138
        let header_text = |name| {
139
            request
140
                .headers()
141
                .get(name)
142
                .and_then(|v| v.to_str().ok())
143
                .map(str::to_owned)
144
        };
145
        let gzip = header_text(header::ACCEPT_ENCODING)
146
            .is_some_and(|e| e.to_ascii_lowercase().contains("gzip"));
147
        let locale = Locale::negotiate(header_text(header::ACCEPT_LANGUAGE).as_deref());
148
        (gzip, locale)
149
    };
150
    let response = match reject(&request) {
151
        Some(problem) => problem.into_response(),
152
        None => next.run(request).await,
153
    };
154
    let mut response = localise(response, locale).await;
155
    if gzip && response.extensions().get::<Compress>().is_some() {
156
        response = compress(response).await;
157
    }
158
    let headers = response.headers_mut();
159
    headers.insert(header::SERVER, HeaderValue::from_static("jelqd/0.6.9"));
160
    headers.insert(X_POWERED_BY, HeaderValue::from_static("blood"));
161
    headers.insert(
162
        header::CONTENT_LANGUAGE,
163
        HeaderValue::from_static(locale.tag()),
164
    );
165
    headers.append(header::VARY, HeaderValue::from_static("Accept-Language"));
166
    headers
167
        .entry(header::CACHE_CONTROL)
168
        .or_insert(HeaderValue::from_static("no-store, private"));
169
    response
170
}
171
172
/// A handler panic would otherwise drop the connection with no reply at all.
173
pub fn panicked(_: Box<dyn std::any::Any + Send + 'static>) -> Response {
174
    Problem::new(
175
        StatusCode::INTERNAL_SERVER_ERROR,
176
        "server-fault",
177
        "The server has had an internal event of its own. It is not discussing it.",
178
    )
179
    .into_response()
180
}