atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
bad9f5b8202f43735ec38acb2070d214d84302e6
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/ui.rsRust107 lines3.5 KB
1
use std::io::Read as _;
2
3
use axum::{
4
    Json, Router,
5
    body::{Body, Bytes},
6
    extract::{Request, State},
7
    http::{HeaderMap, HeaderValue, Method, StatusCode, header},
8
    response::{Html, IntoResponse, Response},
9
};
10
use flate2::read::GzDecoder;
11
use serde::Deserialize;
12
use serde_json::json;
13
use tower::ServiceExt;
14
15
use super::resource::unsupported;
16
use crate::problem::{Problem, reason};
17
18
const BODY_LIMIT: usize = 64 * 1024;
19
const RESPONSE_LIMIT: usize = 1 << 20;
20
21
#[derive(Deserialize)]
22
struct Envelope {
23
    method: String,
24
    path: String,
25
    #[serde(default)]
26
    headers: Vec<(String, String)>,
27
}
28
29
pub async fn page(method: Method) -> Response {
30
    if method != Method::GET && method != Method::HEAD {
31
        return unsupported(&method, "GET, HEAD, OPTIONS");
32
    }
33
    Html(include_str!("../ui.html")).into_response()
34
}
35
36
fn bad_envelope(detail: &str) -> Response {
37
    Problem::new(StatusCode::BAD_REQUEST, "bad-envelope", detail).into_response()
38
}
39
40
fn decode(headers: &HeaderMap, bytes: Bytes) -> Vec<u8> {
41
    let gzipped = headers.get(header::CONTENT_ENCODING) == Some(&HeaderValue::from_static("gzip"));
42
    let mut decoded = Vec::new();
43
    if gzipped
44
        && GzDecoder::new(bytes.as_ref())
45
            .read_to_end(&mut decoded)
46
            .is_ok()
47
    {
48
        return decoded;
49
    }
50
    bytes.to_vec()
51
}
52
53
// Browsers hide 3xx responses from fetch and refuse TRACE, Upgrade, and Expect, so the page
54
// replays requests in-process to show the player exactly what the API said.
55
pub async fn send(State(api): State<Router>, headers: HeaderMap, body: Bytes) -> Response {
56
    if body.len() > BODY_LIMIT {
57
        return bad_envelope("The envelope is larger than any request the server would take.");
58
    }
59
    let Ok(envelope) = serde_json::from_slice::<Envelope>(&body) else {
60
        return bad_envelope("The envelope could not be parsed.");
61
    };
62
    if !envelope.path.starts_with('/') || envelope.path.starts_with("/ui") {
63
        return bad_envelope("The path must be an API path.");
64
    }
65
    let Ok(method) = Method::from_bytes(envelope.method.as_bytes()) else {
66
        return bad_envelope("That is not a method.");
67
    };
68
    let mut request = Request::builder().method(method).uri(&envelope.path);
69
    for (name, value) in envelope.headers.iter().filter(|(_, v)| !v.is_empty()) {
70
        request = request.header(name, value);
71
    }
72
    for cookie in headers.get_all(header::COOKIE) {
73
        request = request.header(header::COOKIE, cookie);
74
    }
75
    let Ok(request) = request.body(Body::empty()) else {
76
        return bad_envelope("The request could not be built. A header may be malformed.");
77
    };
78
    let Ok(response) = api.oneshot(request).await;
79
    let (parts, body) = response.into_parts();
80
    let bytes = axum::body::to_bytes(body, RESPONSE_LIMIT)
81
        .await
82
        .unwrap_or_default();
83
    let shown = decode(&parts.headers, bytes);
84
    let listed: Vec<(String, String)> = parts
85
        .headers
86
        .iter()
87
        .map(|(name, value)| {
88
            (
89
                name.to_string(),
90
                value.to_str().unwrap_or("<binary>").to_owned(),
91
            )
92
        })
93
        .collect();
94
    let mut response = Json(json!({
95
        "status": parts.status.as_u16(),
96
        "reason": reason(parts.status),
97
        "headers": listed,
98
        "body": String::from_utf8_lossy(&shown),
99
    }))
100
    .into_response();
101
    for cookie in parts.headers.get_all(header::SET_COOKIE) {
102
        response
103
            .headers_mut()
104
            .append(header::SET_COOKIE, cookie.clone());
105
    }
106
    response
107
}