atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
98432b6057aa79c009e5831b314585cd777f39c2
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/http/ui.rs

782069860236atagen2026-10-10 12:44 UTC1
use std::io::Read as _;
782069860236atagen2026-10-10 12:44 UTC2
782069860236atagen2026-10-10 12:44 UTC3
use axum::{
782069860236atagen2026-10-10 12:44 UTC4
    Json, Router,
782069860236atagen2026-10-10 12:44 UTC5
    body::{Body, Bytes},
782069860236atagen2026-10-10 12:44 UTC6
    extract::{Request, State},
782069860236atagen2026-10-10 12:44 UTC7
    http::{HeaderMap, HeaderValue, Method, StatusCode, header},
782069860236atagen2026-10-10 12:44 UTC8
    response::{Html, IntoResponse, Response},
782069860236atagen2026-10-10 12:44 UTC9
};
782069860236atagen2026-10-10 12:44 UTC10
use flate2::read::GzDecoder;
782069860236atagen2026-10-10 12:44 UTC11
use serde::Deserialize;
782069860236atagen2026-10-10 12:44 UTC12
use serde_json::json;
782069860236atagen2026-10-10 12:44 UTC13
use tower::ServiceExt;
782069860236atagen2026-10-10 12:44 UTC14
6619c458c23eatagen2026-10-14 08:08 UTC15
use super::resource::unsupported;
782069860236atagen2026-10-10 12:44 UTC16
use crate::problem::{Problem, reason};
782069860236atagen2026-10-10 12:44 UTC17
6619c458c23eatagen2026-10-14 08:08 UTC18
const BODY_LIMIT: usize = 64 * 1024;
782069860236atagen2026-10-10 12:44 UTC19
const RESPONSE_LIMIT: usize = 1 << 20;
782069860236atagen2026-10-10 12:44 UTC20
782069860236atagen2026-10-10 12:44 UTC21
#[derive(Deserialize)]
782069860236atagen2026-10-10 12:44 UTC22
struct Envelope {
782069860236atagen2026-10-10 12:44 UTC23
    method: String,
782069860236atagen2026-10-10 12:44 UTC24
    path: String,
782069860236atagen2026-10-10 12:44 UTC25
    #[serde(default)]
782069860236atagen2026-10-10 12:44 UTC26
    headers: Vec<(String, String)>,
782069860236atagen2026-10-10 12:44 UTC27
}
782069860236atagen2026-10-10 12:44 UTC28
782069860236atagen2026-10-10 12:44 UTC29
pub async fn page(method: Method) -> Response {
782069860236atagen2026-10-10 12:44 UTC30
    if method != Method::GET && method != Method::HEAD {
782069860236atagen2026-10-10 12:44 UTC31
        return unsupported(&method, "GET, HEAD, OPTIONS");
782069860236atagen2026-10-10 12:44 UTC32
    }
782069860236atagen2026-10-10 12:44 UTC33
    Html(include_str!("../ui.html")).into_response()
782069860236atagen2026-10-10 12:44 UTC34
}
782069860236atagen2026-10-10 12:44 UTC35
782069860236atagen2026-10-10 12:44 UTC36
fn bad_envelope(detail: &str) -> Response {
782069860236atagen2026-10-10 12:44 UTC37
    Problem::new(StatusCode::BAD_REQUEST, "bad-envelope", detail).into_response()
782069860236atagen2026-10-10 12:44 UTC38
}
782069860236atagen2026-10-10 12:44 UTC39
782069860236atagen2026-10-10 12:44 UTC40
fn decode(headers: &HeaderMap, bytes: Bytes) -> Vec<u8> {
782069860236atagen2026-10-10 12:44 UTC41
    let gzipped = headers.get(header::CONTENT_ENCODING) == Some(&HeaderValue::from_static("gzip"));
782069860236atagen2026-10-10 12:44 UTC42
    let mut decoded = Vec::new();
782069860236atagen2026-10-10 12:44 UTC43
    if gzipped
782069860236atagen2026-10-10 12:44 UTC44
        && GzDecoder::new(bytes.as_ref())
782069860236atagen2026-10-10 12:44 UTC45
            .read_to_end(&mut decoded)
782069860236atagen2026-10-10 12:44 UTC46
            .is_ok()
782069860236atagen2026-10-10 12:44 UTC47
    {
782069860236atagen2026-10-10 12:44 UTC48
        return decoded;
782069860236atagen2026-10-10 12:44 UTC49
    }
782069860236atagen2026-10-10 12:44 UTC50
    bytes.to_vec()
782069860236atagen2026-10-10 12:44 UTC51
}
782069860236atagen2026-10-10 12:44 UTC52
782069860236atagen2026-10-10 12:44 UTC53
// Browsers hide 3xx responses from fetch and refuse TRACE, Upgrade, and Expect, so the page
782069860236atagen2026-10-10 12:44 UTC54
// replays requests in-process to show the player exactly what the API said.
782069860236atagen2026-10-10 12:44 UTC55
pub async fn send(State(api): State<Router>, headers: HeaderMap, body: Bytes) -> Response {
8b8e83329a1datagen2026-10-12 12:01 UTC56
    if body.len() > BODY_LIMIT {
8b8e83329a1datagen2026-10-12 12:01 UTC57
        return bad_envelope("The envelope is larger than any request the server would take.");
8b8e83329a1datagen2026-10-12 12:01 UTC58
    }
782069860236atagen2026-10-10 12:44 UTC59
    let Ok(envelope) = serde_json::from_slice::<Envelope>(&body) else {
782069860236atagen2026-10-10 12:44 UTC60
        return bad_envelope("The envelope could not be parsed.");
782069860236atagen2026-10-10 12:44 UTC61
    };
782069860236atagen2026-10-10 12:44 UTC62
    if !envelope.path.starts_with('/') || envelope.path.starts_with("/ui") {
782069860236atagen2026-10-10 12:44 UTC63
        return bad_envelope("The path must be an API path.");
782069860236atagen2026-10-10 12:44 UTC64
    }
782069860236atagen2026-10-10 12:44 UTC65
    let Ok(method) = Method::from_bytes(envelope.method.as_bytes()) else {
782069860236atagen2026-10-10 12:44 UTC66
        return bad_envelope("That is not a method.");
782069860236atagen2026-10-10 12:44 UTC67
    };
782069860236atagen2026-10-10 12:44 UTC68
    let mut request = Request::builder().method(method).uri(&envelope.path);
782069860236atagen2026-10-10 12:44 UTC69
    for (name, value) in envelope.headers.iter().filter(|(_, v)| !v.is_empty()) {
782069860236atagen2026-10-10 12:44 UTC70
        request = request.header(name, value);
782069860236atagen2026-10-10 12:44 UTC71
    }
782069860236atagen2026-10-10 12:44 UTC72
    for cookie in headers.get_all(header::COOKIE) {
782069860236atagen2026-10-10 12:44 UTC73
        request = request.header(header::COOKIE, cookie);
782069860236atagen2026-10-10 12:44 UTC74
    }
6619c458c23eatagen2026-10-14 08:08 UTC75
    let Ok(request) = request.body(Body::empty()) else {
782069860236atagen2026-10-10 12:44 UTC76
        return bad_envelope("The request could not be built. A header may be malformed.");
782069860236atagen2026-10-10 12:44 UTC77
    };
782069860236atagen2026-10-10 12:44 UTC78
    let Ok(response) = api.oneshot(request).await;
782069860236atagen2026-10-10 12:44 UTC79
    let (parts, body) = response.into_parts();
782069860236atagen2026-10-10 12:44 UTC80
    let bytes = axum::body::to_bytes(body, RESPONSE_LIMIT)
782069860236atagen2026-10-10 12:44 UTC81
        .await
782069860236atagen2026-10-10 12:44 UTC82
        .unwrap_or_default();
782069860236atagen2026-10-10 12:44 UTC83
    let shown = decode(&parts.headers, bytes);
782069860236atagen2026-10-10 12:44 UTC84
    let listed: Vec<(String, String)> = parts
782069860236atagen2026-10-10 12:44 UTC85
        .headers
782069860236atagen2026-10-10 12:44 UTC86
        .iter()
782069860236atagen2026-10-10 12:44 UTC87
        .map(|(name, value)| {
782069860236atagen2026-10-10 12:44 UTC88
            (
782069860236atagen2026-10-10 12:44 UTC89
                name.to_string(),
782069860236atagen2026-10-10 12:44 UTC90
                value.to_str().unwrap_or("<binary>").to_owned(),
782069860236atagen2026-10-10 12:44 UTC91
            )
782069860236atagen2026-10-10 12:44 UTC92
        })
782069860236atagen2026-10-10 12:44 UTC93
        .collect();
782069860236atagen2026-10-10 12:44 UTC94
    let mut response = Json(json!({
782069860236atagen2026-10-10 12:44 UTC95
        "status": parts.status.as_u16(),
782069860236atagen2026-10-10 12:44 UTC96
        "reason": reason(parts.status),
782069860236atagen2026-10-10 12:44 UTC97
        "headers": listed,
782069860236atagen2026-10-10 12:44 UTC98
        "body": String::from_utf8_lossy(&shown),
782069860236atagen2026-10-10 12:44 UTC99
    }))
782069860236atagen2026-10-10 12:44 UTC100
    .into_response();
782069860236atagen2026-10-10 12:44 UTC101
    for cookie in parts.headers.get_all(header::SET_COOKIE) {
782069860236atagen2026-10-10 12:44 UTC102
        response
782069860236atagen2026-10-10 12:44 UTC103
            .headers_mut()
782069860236atagen2026-10-10 12:44 UTC104
            .append(header::SET_COOKIE, cookie.clone());
782069860236atagen2026-10-10 12:44 UTC105
    }
782069860236atagen2026-10-10 12:44 UTC106
    response
782069860236atagen2026-10-10 12:44 UTC107
}