o_sfu/runtime/http_server/rooms/
create.rs1use 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#[derive(Debug, Clone)]
63struct VerifiedRoomRequest {
64 issuer: String,
65 room_key: SecretSlice<u8>,
66 config: RoomConfig,
67 origin: RequestOrigin,
68}
69
70async 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}