foundry_wallets/wallet_browser/
router.rs1use 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 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 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}