atagen

jelq

public

an http based roguelike rpg about jelqing

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