foundry_wallets/wallet_browser/
router.rs

1use std::sync::Arc;
2
3use axum::{
4    Router,
5    extract::{Request, State},
6    http::{HeaderValue, Method, StatusCode, header},
7    middleware::{self, Next},
8    response::Response,
9    routing::{get, post},
10};
11use tower::ServiceBuilder;
12use tower_http::{cors::CorsLayer, set_header::SetResponseHeaderLayer};
13
14use crate::wallet_browser::{handlers, state::BrowserWalletState};
15
16pub async fn build_router(state: Arc<BrowserWalletState>, port: u16) -> Router {
17    let api = Router::new()
18        .route("/transaction/request", get(handlers::get_next_transaction_request))
19        .route("/transaction/response", post(handlers::post_transaction_response))
20        .route("/signing/request", get(handlers::get_next_signing_request))
21        .route("/signing/response", post(handlers::post_signing_response))
22        .route("/connection", get(handlers::get_connection_info))
23        .route("/connection", post(handlers::post_connection_update))
24        .route_layer(middleware::from_fn_with_state(state.clone(), require_session_token))
25        .with_state(state.clone());
26
27    let mut origins = vec![format!("http://127.0.0.1:{port}").parse().unwrap()];
28
29    // Allow default port of 5173 in development mode.
30    if state.is_development() {
31        origins.push("https://localhost:5173".to_string().parse().unwrap());
32    }
33
34    let security_headers = ServiceBuilder::new()
35        .layer(SetResponseHeaderLayer::if_not_present(
36            header::CONTENT_SECURITY_POLICY,
37            HeaderValue::from_static(concat!(
38                "default-src 'none'; ",
39                "object-src 'none'; ",
40                "base-uri 'none'; ",
41                "frame-ancestors 'none'; ",
42                "img-src 'self'; ",
43                "font-src 'none'; ",
44                "connect-src 'self' https: http: wss: ws:;",
45                "style-src 'self'; ",
46                "script-src 'self'; ",
47                "form-action 'none'; ",
48                "worker-src 'none'; ",
49                "frame-src https://id.porto.sh;"
50            )),
51        ))
52        .layer(SetResponseHeaderLayer::if_not_present(
53            header::REFERRER_POLICY,
54            HeaderValue::from_static("no-referrer"),
55        ))
56        .layer(SetResponseHeaderLayer::if_not_present(
57            header::X_CONTENT_TYPE_OPTIONS,
58            HeaderValue::from_static("nosniff"),
59        ))
60        .layer(
61            CorsLayer::new()
62                .allow_origin(origins)
63                .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
64                .allow_headers([header::CONTENT_TYPE])
65                .allow_credentials(false),
66        );
67
68    Router::new()
69        .route("/", get(handlers::serve_index))
70        .route("/styles.css", get(handlers::serve_css))
71        .route("/main.js", get(handlers::serve_js))
72        .route("/banner.png", get(handlers::serve_banner_png))
73        .route("/logo.png", get(handlers::serve_logo_png))
74        .nest("/api", api)
75        .layer(security_headers)
76        .with_state(state)
77}
78
79async fn require_session_token(
80    State(state): State<Arc<BrowserWalletState>>,
81    req: Request,
82    next: Next,
83) -> Result<Response, StatusCode> {
84    if req.method() == Method::OPTIONS {
85        return Ok(next.run(req).await);
86    }
87
88    // In development mode, skip session token check.
89    if state.is_development() {
90        return Ok(next.run(req).await);
91    }
92
93    let expected = state.session_token();
94    let provided = req
95        .headers()
96        .get("X-Session-Token")
97        .and_then(|v| v.to_str().ok())
98        .ok_or(StatusCode::FORBIDDEN)?;
99
100    if provided != expected {
101        return Err(StatusCode::FORBIDDEN);
102    }
103
104    Ok(next.run(req).await)
105}