Skip to main content

foundry_wallets/wallet_browser/
router.rs

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