1use std::{io, net::SocketAddr, sync::Arc};
2
3use axum::{
4 Router,
5 extract::{DefaultBodyLimit, MatchedPath, Path, Request, State},
6 http::{StatusCode, header},
7 middleware,
8 response::{IntoResponse, Response},
9 routing::{get, post},
10};
11use o_sfu_core::server::room::RoomManagerServeError;
12use o_sfu_protocol::wire::StreamType;
13use tokio::net::TcpListener;
14use tokio_util::sync::CancellationToken;
15use tracing::{Instrument, info};
16
17use crate::{
18 application::stream_catalog::counter_for_stream_type,
19 runtime::{
20 RuntimeState, diagnostics,
21 http_server::{
22 contract::{
23 IncomingBitRateStatsResponse, NoopResponse, RoomResponse, RoomStatsResponse,
24 UsersStatsResponse, route,
25 },
26 extractors::{
27 DiagnosticsServices, MetricsServices, OperatorAccess, OperatorAccessPolicy,
28 RoomServices, VerifiedDisconnectClaims, VerifiedRoomRequest,
29 },
30 },
31 metrics::{HttpRoute, RuntimeMetrics},
32 prometheus::{PROMETHEUS_CONTENT_TYPE, render_prometheus},
33 room::RuntimeRoomStatsSnapshot,
34 telemetry::{self, schema::event as telemetry_event},
35 websocket_server,
36 },
37};
38
39const MAX_DISCONNECT_BODY_BYTES: usize = 16 * 1024;
40
41pub(crate) async fn serve_http(
42 state: RuntimeState,
43 shutdown_token: CancellationToken,
44) -> io::Result<()> {
45 let listener = TcpListener::bind(state.config.http.bind_address).await?;
46 serve_http_on(listener, state, shutdown_token).await
47}
48
49pub(crate) async fn serve_http_on(
50 listener: TcpListener,
51 state: RuntimeState,
52 shutdown_token: CancellationToken,
53) -> io::Result<()> {
54 let local_address = listener.local_addr()?;
55 info!(
56 event = telemetry_event::HTTP_LISTENER_READY,
57 bind_address = %state.config.http.bind_address,
58 local_address = %local_address,
59 trust_proxy_headers = state.config.http.trust_proxy_headers,
60 "booted HTTP and WebSocket listener"
61 );
62 axum::serve(
63 listener,
64 app(state, local_address).into_make_service_with_connect_info::<SocketAddr>(),
65 )
66 .with_graceful_shutdown(shutdown_token.cancelled_owned())
67 .await
68}
69
70pub(crate) fn app(state: RuntimeState, listener_address: SocketAddr) -> Router {
72 let metrics = Arc::clone(&state.metrics);
73 let operator_policy = OperatorAccessPolicy::new(
74 state.config.diagnostics.auth_token.as_deref(),
75 listener_address,
76 );
77 Router::new()
78 .route(route::WEBSOCKET, get(websocket_server::upgrade))
79 .merge(http_router(metrics, operator_policy.clone()))
80 .merge(diagnostics_router(operator_policy))
81 .with_state(state)
82}
83
84fn http_router(
85 runtime_metrics: Arc<RuntimeMetrics>,
86 operator_policy: OperatorAccessPolicy,
87) -> Router<RuntimeState> {
88 let operator_layer =
89 middleware::from_extractor_with_state::<OperatorAccess, _>(operator_policy);
90 Router::new()
91 .route(route::v1::NOOP, get(noop))
92 .route(
93 route::v1::STATS,
94 get(stats).route_layer(operator_layer.clone()),
95 )
96 .route(route::v1::CHANNEL, get(room))
97 .route(
98 route::v1::DISCONNECT,
99 post(disconnect).layer(DefaultBodyLimit::max(MAX_DISCONNECT_BODY_BYTES)),
100 )
101 .route(route::METRICS, get(metrics).route_layer(operator_layer))
102 .route_layer(middleware::from_fn_with_state(
103 runtime_metrics,
104 track_http_request,
105 ))
106}
107
108async fn track_http_request(
109 State(metrics): State<Arc<RuntimeMetrics>>,
110 path: MatchedPath,
111 request: Request,
112 next: middleware::Next,
113) -> Response {
114 let route = match path.as_str() {
115 route::v1::NOOP => HttpRoute::Noop,
116 route::v1::STATS => HttpRoute::Stats,
117 route::v1::CHANNEL => HttpRoute::Room,
118 route::v1::DISCONNECT => HttpRoute::Disconnect,
119 route::METRICS => HttpRoute::Metrics,
120 _ => return next.run(request).await,
121 };
122 let _guard = metrics.track_http_request(route);
123 next.run(request).await
124}
125
126fn diagnostics_router(operator_policy: OperatorAccessPolicy) -> Router<RuntimeState> {
127 Router::new()
128 .route(route::diagnostics::SUMMARY, get(diagnostics_summary))
129 .route(route::diagnostics::ROOMS, get(diagnostics_rooms))
130 .route(route::diagnostics::WORKERS, get(diagnostics_workers))
131 .route(route::diagnostics::ROOM, get(diagnostics_room_detail))
132 .route(route::diagnostics::ROOM_USERS, get(diagnostics_room_users))
133 .route(route::diagnostics::ROOM_USER, get(diagnostics_user_detail))
134 .route(route::diagnostics::ROOM_GRAPH, get(diagnostics_room_graph))
135 .route(route::diagnostics::USER_GRAPH, get(diagnostics_user_graph))
136 .route_layer(middleware::from_extractor_with_state::<OperatorAccess, _>(
137 operator_policy,
138 ))
139}
140
141async fn noop() -> impl IntoResponse {
143 async { axum::Json(NoopResponse::ok()) }
144 .instrument(telemetry::http_request_span("noop"))
145 .await
146}
147
148async fn stats(State(services): State<RoomServices>) -> impl IntoResponse {
150 async {
151 axum::Json(
152 services
153 .room_manager
154 .stats_snapshots(&services.media_transport)
155 .await
156 .into_iter()
157 .map(http_room_stats)
158 .collect::<Vec<_>>(),
159 )
160 }
161 .instrument(telemetry::http_request_span("stats"))
162 .await
163}
164
165async fn metrics(State(services): State<MetricsServices>) -> impl IntoResponse {
167 async {
168 let room_gauges = services.room_manager.room_gauges().await;
169 (
170 [(header::CONTENT_TYPE, PROMETHEUS_CONTENT_TYPE)],
171 render_prometheus(&services.metrics, room_gauges),
172 )
173 }
174 .instrument(telemetry::http_request_span("metrics"))
175 .await
176}
177
178async fn room(State(services): State<RoomServices>, request: VerifiedRoomRequest) -> Response {
182 async {
183 let serve_result = services
184 .room_manager
185 .serve_room(
186 &request.issuer,
187 &request.room_key,
188 &request.config,
189 Some(request.origin.remote_address.as_str()),
190 )
191 .await;
192 match serve_result {
193 Ok(room) => {
194 services.metrics.record_http_room_success();
195 (
196 StatusCode::OK,
197 axum::Json(RoomResponse {
198 uuid: room.uuid().to_owned(),
199 url: request.origin.base_url,
200 }),
201 )
202 .into_response()
203 }
204 Err(RoomManagerServeError::ConflictingReservation) => {
205 services.metrics.record_http_room_conflict();
206 StatusCode::CONFLICT.into_response()
207 }
208 }
209 }
210 .instrument(telemetry::http_request_span("room"))
211 .await
212}
213
214async fn disconnect(
218 State(services): State<RoomServices>,
219 VerifiedDisconnectClaims(claims): VerifiedDisconnectClaims,
220) -> Response {
221 async {
222 for (room_id, user_ids) in &claims.user_ids_by_room {
223 services
224 .room_manager
225 .disconnect_users(room_id, user_ids, &services.media_transport)
226 .await;
227 }
228 services.metrics.record_http_disconnect_success();
229 StatusCode::OK.into_response()
230 }
231 .instrument(telemetry::http_request_span("disconnect"))
232 .await
233}
234
235async fn diagnostics_summary(State(services): State<DiagnosticsServices>) -> Response {
237 axum::Json(
238 diagnostics::summary_response(&services.room_manager, &services.media_transport).await,
239 )
240 .into_response()
241}
242
243async fn diagnostics_rooms(State(services): State<DiagnosticsServices>) -> Response {
245 axum::Json(diagnostics::rooms_response(&services.room_manager, &services.media_transport).await)
246 .into_response()
247}
248
249async fn diagnostics_workers(State(services): State<DiagnosticsServices>) -> Response {
251 axum::Json(
252 diagnostics::workers_response(&services.room_manager, &services.media_transport).await,
253 )
254 .into_response()
255}
256
257async fn diagnostics_room_detail(
259 State(services): State<DiagnosticsServices>,
260 Path(room_id): Path<String>,
261) -> Response {
262 let payload = diagnostics::room_detail_response(
263 &services.room_manager,
264 &services.media_transport,
265 &room_id,
266 )
267 .await;
268 diagnostics_optional_response(payload)
269}
270
271async fn diagnostics_room_users(
273 State(services): State<DiagnosticsServices>,
274 Path(room_id): Path<String>,
275) -> Response {
276 let payload = diagnostics::room_users_response(
277 &services.room_manager,
278 &services.media_transport,
279 &room_id,
280 )
281 .await;
282 diagnostics_optional_response(payload)
283}
284
285async fn diagnostics_room_graph(
287 State(services): State<DiagnosticsServices>,
288 Path(room_id): Path<String>,
289) -> Response {
290 let payload = diagnostics::room_detail_response(
291 &services.room_manager,
292 &services.media_transport,
293 &room_id,
294 )
295 .await
296 .map(|payload| diagnostics::build_graph(&payload));
297 diagnostics_optional_response(payload)
298}
299
300async fn diagnostics_user_graph(
302 State(services): State<DiagnosticsServices>,
303 Path((room_id, user_key)): Path<(String, String)>,
304) -> Response {
305 let payload = diagnostics::room_detail_response(
306 &services.room_manager,
307 &services.media_transport,
308 &room_id,
309 )
310 .await
311 .and_then(|payload| diagnostics::build_user_graph(&payload, &user_key));
312 diagnostics_optional_response(payload)
313}
314
315fn diagnostics_optional_response<T>(payload: Option<T>) -> Response
316where
317 axum::Json<T>: IntoResponse,
318{
319 payload.map_or_else(
320 || StatusCode::NOT_FOUND.into_response(),
321 |payload| axum::Json(payload).into_response(),
322 )
323}
324
325async fn diagnostics_user_detail(
327 State(services): State<DiagnosticsServices>,
328 Path((room_id, user_key)): Path<(String, String)>,
329) -> Response {
330 diagnostics_optional_response(
331 diagnostics::user_detail_response(
332 &services.room_manager,
333 &services.media_transport,
334 &room_id,
335 &user_key,
336 )
337 .await,
338 )
339}
340
341fn http_room_stats(snapshot: RuntimeRoomStatsSnapshot) -> RoomStatsResponse {
342 let incoming_bitrate = &snapshot.users_stats.incoming_bitrate;
343 let active_stream_counts = &snapshot.users_stats.active_stream_counts;
344 RoomStatsResponse {
345 create_date: snapshot.create_date,
346 uuid: snapshot.uuid,
347 remote_address: snapshot.remote_address,
348 users_stats: UsersStatsResponse {
349 incoming_bit_rate: IncomingBitRateStatsResponse {
350 total: incoming_bitrate.total,
351 audio: counter_for_stream_type(&incoming_bitrate.by_stream, StreamType::Audio),
352 camera: counter_for_stream_type(&incoming_bitrate.by_stream, StreamType::Camera),
353 screen: counter_for_stream_type(&incoming_bitrate.by_stream, StreamType::Screen),
354 },
355 count: snapshot.users_stats.count,
356 camera_count: counter_for_stream_type(active_stream_counts, StreamType::Camera),
357 screen_count: counter_for_stream_type(active_stream_counts, StreamType::Screen),
358 },
359 web_rtc_enabled: snapshot.web_rtc_enabled,
360 }
361}