an http based roguelike rpg about jelqing
| 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 | } |