Skip to main content

mina_node_native/http_server/routes/
webrtc.rs

1//! WebRTC signaling endpoints (feature-gated).
2//!
3//! - `GET /mina/webrtc/signal/{offer}` - Handle WebRTC signaling (GET with base58 encoded offer)
4//! - `POST /mina/webrtc/signal` - Handle WebRTC signaling (POST with JSON offer)
5
6use axum::{
7    extract::{Path, State},
8    http::StatusCode,
9    Json,
10};
11use utoipa_axum::{router::OpenApiRouter, routes};
12
13use mina_node::{
14    p2p::{
15        connection::{
16            incoming::{IncomingSignalingMethod, P2pConnectionIncomingInitOpts},
17            P2pConnectionResponse,
18        },
19        webrtc, PeerId,
20    },
21    rpc::RpcRequest,
22};
23use mina_node_common::rpc::RpcP2pConnectionIncomingResponse;
24
25use crate::http_server::{types::AssumeJson, AppState};
26
27/// WebRTC routes
28pub fn routes() -> OpenApiRouter<AppState> {
29    OpenApiRouter::new()
30        .routes(routes!(signal_get))
31        .routes(routes!(signal_post))
32}
33
34/// WebRTC signaling (base58 offer in path)
35#[utoipa::path(
36    get,
37    path = "/mina/webrtc/signal/{offer}",
38    tag = "webrtc",
39    params(
40        ("offer" = String, Path, description = "Base58 encoded WebRTC offer")
41    ),
42    responses(
43        (status = 200, description = "Connection accepted or rejected"),
44        (status = 400, description = "Bad offer or decryption failed"),
45        (status = 500, description = "Internal error")
46    )
47)]
48async fn signal_get(
49    State(state): State<AppState>,
50    Path(offer): Path<String>,
51) -> (StatusCode, Json<P2pConnectionResponse>) {
52    // TODO(axum-migration): Returns 400 for both bad base58 AND bad JSON schema inside.
53    // This matches warp behavior but differs from signal_post which returns 422 for
54    // bad JSON schema. Could split: 400 for bad base58, 422 for bad JSON schema.
55    let decode_result = bs58::decode(&offer)
56        .into_vec()
57        .ok()
58        .and_then(|json| serde_json::from_slice(&json).ok());
59
60    match decode_result {
61        None => (
62            StatusCode::BAD_REQUEST,
63            Json(P2pConnectionResponse::SignalDecryptionFailed),
64        ),
65        Some(offer) => handle_offer(state, offer).await,
66    }
67}
68
69/// WebRTC signaling (JSON offer in body)
70#[utoipa::path(
71    post,
72    path = "/mina/webrtc/signal",
73    tag = "webrtc",
74    responses(
75        (status = 200, description = "Connection accepted or rejected"),
76        (status = 400, description = "Bad offer"),
77        (status = 415, description = "Unsupported Content-Type"),
78        (status = 422, description = "Malformed JSON"),
79        (status = 500, description = "Internal error")
80    )
81)]
82async fn signal_post(
83    State(state): State<AppState>,
84    AssumeJson(offer): AssumeJson<Box<webrtc::Offer>>,
85) -> (StatusCode, Json<P2pConnectionResponse>) {
86    // TODO(axum-migration): Malformed JSON returns 422 (axum default) vs warp's 400.
87    // Both are framework defaults, not explicit choices. 422 is arguably more correct
88    // (valid JSON, wrong schema = "unprocessable entity"). Noted for awareness.
89    handle_offer(state, offer).await
90}
91
92/// Shared handler for processing WebRTC offers.
93async fn handle_offer(
94    state: AppState,
95    offer: Box<webrtc::Offer>,
96) -> (StatusCode, Json<P2pConnectionResponse>) {
97    let mut rx = state
98        .rpc_sender()
99        .multishot_request(
100            2,
101            RpcRequest::P2pConnectionIncoming(P2pConnectionIncomingInitOpts {
102                peer_id: PeerId::from_public_key(offer.identity_pub_key.clone()),
103                signaling: IncomingSignalingMethod::Http,
104                offer,
105            }),
106        )
107        .await;
108
109    match rx.recv().await {
110        Some(RpcP2pConnectionIncomingResponse::Answer(answer)) => {
111            let status = match &answer {
112                P2pConnectionResponse::Accepted(_) => StatusCode::OK,
113                P2pConnectionResponse::Rejected(reason) => {
114                    if reason.is_bad() {
115                        StatusCode::BAD_REQUEST
116                    } else {
117                        StatusCode::OK
118                    }
119                }
120                P2pConnectionResponse::SignalDecryptionFailed => StatusCode::BAD_REQUEST,
121                P2pConnectionResponse::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
122            };
123            (status, Json(answer))
124        }
125        _ => (
126            StatusCode::INTERNAL_SERVER_ERROR,
127            Json(P2pConnectionResponse::InternalError),
128        ),
129    }
130}