Skip to main content

o_sfu/runtime/http_server/rooms/
create.rs

1//! Room creation with verified credentials and proxy-aware request configuration.
2
3use std::sync::Arc;
4
5use axum::{
6    extract::{FromRef, FromRequestParts, Query, State},
7    http::{StatusCode, request::Parts},
8    response::{IntoResponse, Response},
9    routing::{MethodRouter, get},
10};
11use o_sfu_core::server::room::RoomManagerServeError;
12use o_sfu_rfc::jwt::RegisteredJwtClaims;
13use secrecy::{ExposeSecret, SecretSlice, SecretString};
14use tracing::Instrument;
15
16use super::super::{
17    access::room_authorization_token,
18    contract::{CreateRoomQuery, RoomResponse},
19};
20use crate::runtime::{
21    RuntimeMetrics, RuntimeState,
22    auth::{self, HttpRoomClaims, derive_key_from_seed},
23    request_origin::RequestOrigin,
24    room::{RoomConfig, RoomManager},
25    telemetry,
26};
27
28pub(super) fn route() -> MethodRouter<RuntimeState> {
29    get(create)
30}
31
32#[derive(Debug, Clone)]
33struct Services {
34    room_manager: Arc<RoomManager>,
35    metrics: Arc<RuntimeMetrics>,
36}
37
38impl FromRef<RuntimeState> for Services {
39    fn from_ref(state: &RuntimeState) -> Self {
40        Self {
41            room_manager: Arc::clone(&state.room_manager),
42            metrics: Arc::clone(&state.metrics),
43        }
44    }
45}
46
47/// Room credentials verified with the global auth key and a present issuer.
48///
49/// Accepts the `Bearer` and legacy `jwt` authorization schemes.
50/// `keySeed` takes precedence over `key` and must be nonempty and decodable.
51/// Query configuration and the proxy-aware [`RequestOrigin`] accompany the
52/// resolved room key. Extraction does not create or reserve a room.
53///
54/// # Errors
55///
56/// Extraction returns a [`StatusCode`] and records its room rejection counter:
57/// `400 Bad Request` for invalid query parameters, absent key claims or failed
58/// seed resolution or room key validation, `401 Unauthorized` for missing or
59/// unsupported authorization headers or failed JWT verification and
60/// `403 Forbidden` for a missing issuer.
61/// Query rejection takes precedence over credential rejection.
62#[derive(Debug, Clone)]
63struct VerifiedRoomRequest {
64    issuer: String,
65    room_key: SecretSlice<u8>,
66    config: RoomConfig,
67    origin: RequestOrigin,
68}
69
70/// Room creation endpoint used by Odoo to bind a channel key to an SFU room.
71///
72/// [`VerifiedRoomRequest`] owns JWT verification and request-origin projection.
73async fn create(State(services): State<Services>, request: VerifiedRoomRequest) -> Response {
74    async {
75        let remote_address = request
76            .origin
77            .remote_address
78            .map(|address| address.to_string());
79        let serve_result = services
80            .room_manager
81            .serve_room(
82                &request.issuer,
83                request.room_key,
84                &request.config,
85                remote_address.as_deref(),
86            )
87            .await;
88        match serve_result {
89            Ok(room) => {
90                services.metrics.record_http_room_success();
91                (
92                    StatusCode::OK,
93                    axum::Json(RoomResponse {
94                        uuid: room.uuid().to_owned(),
95                        url: request.origin.base_url,
96                    }),
97                )
98                    .into_response()
99            }
100            Err(RoomManagerServeError::ConflictingReservation) => {
101                services.metrics.record_http_room_conflict();
102                StatusCode::CONFLICT.into_response()
103            }
104        }
105    }
106    .instrument(telemetry::http_request_span("room"))
107    .await
108}
109
110impl FromRequestParts<RuntimeState> for VerifiedRoomRequest {
111    type Rejection = StatusCode;
112
113    async fn from_request_parts(
114        parts: &mut Parts,
115        state: &RuntimeState,
116    ) -> Result<Self, Self::Rejection> {
117        let origin = match RequestOrigin::from_request_parts(parts, state).await {
118            Ok(origin) => origin,
119            Err(error) => match error {},
120        };
121        let Query(query) = Query::<CreateRoomQuery>::from_request_parts(parts, state)
122            .await
123            .map_err(|_error| record_rejection(state, StatusCode::BAD_REQUEST))?;
124        let Some(token) = room_authorization_token(&parts.headers) else {
125            return Err(record_rejection(state, StatusCode::UNAUTHORIZED));
126        };
127        let token = SecretString::from(token);
128        let HttpRoomClaims {
129            registered: RegisteredJwtClaims { iss, .. },
130            key,
131            key_seed,
132        } = auth::verify_claims::<HttpRoomClaims>(&token, &state.config.auth.key)
133            .map_err(|_error| record_rejection(state, StatusCode::UNAUTHORIZED))?;
134        let Some(issuer) = iss else {
135            return Err(record_rejection(state, StatusCode::FORBIDDEN));
136        };
137        let room_key = match (key, key_seed) {
138            (None, None) => {
139                return Err(record_rejection(state, StatusCode::BAD_REQUEST));
140            }
141            (Some(key), None) => auth::decode_signing_key(&key)
142                .map_err(|_error| record_rejection(state, StatusCode::BAD_REQUEST))?,
143            (_, Some(seed)) if seed.expose_secret().is_empty() => {
144                return Err(record_rejection(state, StatusCode::BAD_REQUEST));
145            }
146            (_, Some(seed)) => derive_key_from_seed(&state.config.auth.key, &seed)
147                .map_err(|_error| record_rejection(state, StatusCode::BAD_REQUEST))?,
148        };
149        Ok(Self {
150            issuer,
151            room_key,
152            config: RoomConfig {
153                web_rtc_enabled: query.web_rtc_enabled(),
154                recording_address: query.recording_address,
155            },
156            origin,
157        })
158    }
159}
160
161fn record_rejection(state: &RuntimeState, status: StatusCode) -> StatusCode {
162    match status {
163        StatusCode::UNAUTHORIZED => state.metrics.record_http_room_unauthorized(),
164        StatusCode::FORBIDDEN => state.metrics.record_http_room_forbidden(),
165        StatusCode::BAD_REQUEST => state.metrics.record_http_room_bad_request(),
166        _ => {}
167    }
168    status
169}