atagen

jelq

public

an http based roguelike rpg about jelqing

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