atagen

jelq

public

an http based roguelike rpg about jelqing

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