Skip to main content

wire/server/
bundle.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Serving the client bundle over HTTP: headers on, policy out. Which
7//! bytes answer a path is `wire::assets`' decision.
8
9use std::sync::Arc;
10
11use axum::Router;
12use axum::extract::State;
13use axum::http::{StatusCode, header};
14use axum::response::{IntoResponse, Response};
15use axum::routing::get;
16
17use crate::assets::{AssetReply, ClientAssets};
18
19use super::config::refusal_text;
20
21/// The two routes that answer the client bundle: `/` and every path
22/// below it that no other route of the same table claims.
23///
24/// The city's own port and the remote listener (`bin::outside`) serve
25/// the page through this one table, so a device that opens the remote
26/// address gets the same bytes, under the same headers, as a browser on
27/// this machine (`crates/wire/Spec.lean` ยง8-2). The routes take no
28/// pairing token: a browser has to load the page before it has
29/// anywhere to type the code.
30pub fn bundle_routes<S>(client: Arc<ClientAssets>) -> Router<S>
31where
32    S: Clone + Send + Sync + 'static,
33{
34    Router::new()
35        .route("/", get(serve_index))
36        .route("/{*asset}", get(serve_asset))
37        .with_state(client)
38}
39
40async fn serve_index(State(client): State<Arc<ClientAssets>>) -> Response {
41    asset_response(client.lookup("index.html"))
42}
43
44async fn serve_asset(
45    State(client): State<Arc<ClientAssets>>,
46    axum::extract::Path(asset): axum::extract::Path<String>,
47) -> Response {
48    asset_response(client.lookup(&asset))
49}
50
51/// The shell around [`ClientAssets::lookup`]: headers on, policy out.
52fn asset_response(reply: AssetReply) -> Response {
53    match reply {
54        AssetReply::Found {
55            bytes,
56            content_type,
57            gzipped: true,
58        } => (
59            StatusCode::OK,
60            [
61                (header::CONTENT_TYPE, content_type),
62                (header::CONTENT_ENCODING, "gzip"),
63            ],
64            bytes,
65        )
66            .into_response(),
67        AssetReply::Found {
68            bytes,
69            content_type,
70            gzipped: false,
71        } => (
72            StatusCode::OK,
73            [(header::CONTENT_TYPE, content_type)],
74            bytes,
75        )
76            .into_response(),
77        AssetReply::Miss(err) => (StatusCode::NOT_FOUND, refusal_text(&err)).into_response(),
78    }
79}