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