scufflecloud_video_api/
services.rs1use std::net::SocketAddr;
2use std::sync::Arc;
3
4use anyhow::Context;
5use axum::http::header::CONTENT_TYPE;
6use axum::http::{HeaderName, Method, StatusCode};
7use axum::{Extension, Json};
8use rustls::pki_types::pem::PemObject;
9use rustls::pki_types::{CertificateDer, PrivateKeyDer};
10use tinc::TincService;
11use tinc::openapi::Server;
12use tower_http::cors::{AllowHeaders, CorsLayer, ExposeHeaders};
13use tower_http::trace::TraceLayer;
14
15mod stream;
16
17#[derive(Debug)]
18pub struct VideoApiSvc<G> {
19 _phantom: std::marker::PhantomData<G>,
20}
21
22impl<G> Default for VideoApiSvc<G> {
23 fn default() -> Self {
24 Self {
25 _phantom: std::marker::PhantomData,
26 }
27 }
28}
29
30fn rest_cors_layer() -> CorsLayer {
31 CorsLayer::new()
32 .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
33 .allow_origin(tower_http::cors::Any)
34 .allow_headers(tower_http::cors::Any)
35}
36
37fn grpc_web_cors_layer() -> CorsLayer {
38 let allow_headers = [
40 CONTENT_TYPE,
41 HeaderName::from_static("x-grpc-web"),
42 HeaderName::from_static("grpc-timeout"),
43 ]
44 .into_iter();
45 let expose_headers = [
48 HeaderName::from_static("grpc-encoding"),
49 HeaderName::from_static("grpc-status"),
50 HeaderName::from_static("grpc-status-details-bin"),
51 HeaderName::from_static("grpc-message"),
52 ];
53
54 CorsLayer::new()
55 .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
56 .allow_headers(AllowHeaders::list(allow_headers))
57 .expose_headers(ExposeHeaders::list(expose_headers))
58 .allow_origin(tower_http::cors::Any)
59 .allow_headers(tower_http::cors::Any)
60}
61
62fn rustls_config<G: video_api_traits::MtlsInterface>(global: &Arc<G>) -> anyhow::Result<rustls::ServerConfig> {
63 let root_cert = CertificateDer::from_pem_slice(global.mtls_root_cert_pem()).context("failed to parse mTLS root cert")?;
65 let cert = CertificateDer::from_pem_slice(global.mtls_cert_pem()).context("failed to parse mTLS cert")?;
66 let private_key =
67 PrivateKeyDer::from_pem_slice(global.mtls_private_key_pem()).context("failed to parse mTLS private key")?;
68
69 let mut root_cert_store = rustls::RootCertStore::empty();
70 root_cert_store
71 .add(root_cert.clone())
72 .context("failed to add mTLS root cert to root cert store")?;
73 let cert_chain = vec![cert, root_cert];
74
75 let rustls_client_verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_cert_store))
76 .allow_unauthenticated() .build()
78 .context("failed to create client cert verifier")?;
79
80 rustls::ServerConfig::builder()
81 .with_client_cert_verifier(rustls_client_verifier)
82 .with_single_cert(cert_chain, private_key)
83 .context("failed to create rustls ServerConfig")
84}
85
86impl<G: video_api_traits::Global> scuffle_bootstrap::Service<G> for VideoApiSvc<G> {
87 async fn run(self, global: Arc<G>, ctx: scuffle_context::Context) -> anyhow::Result<()> {
88 let stream_svc_tinc =
90 pb::scufflecloud::video::api::v1::stream_service_tinc::StreamServiceTinc::new(VideoApiSvc::<G>::default());
91
92 let mut openapi_schema = stream_svc_tinc.openapi_schema();
93 openapi_schema.info.title = "Scuffle Cloud Video API".to_string();
94 openapi_schema.info.version = "v1".to_string();
95 openapi_schema.servers = Some(vec![Server::new("/v1")]);
96
97 let v1_rest_router = axum::Router::new()
98 .route("/openapi.json", axum::routing::get(Json(openapi_schema)))
99 .merge(stream_svc_tinc.into_router())
100 .layer(rest_cors_layer());
101
102 let stream_svc =
104 pb::scufflecloud::video::api::v1::stream_service_server::StreamServiceServer::new(VideoApiSvc::<G>::default());
105
106 let reflection_v1_svc = tonic_reflection::server::Builder::configure()
107 .register_encoded_file_descriptor_set(pb::ANNOTATIONS_PB)
108 .build_v1()?;
109 let reflection_v1alpha_svc = tonic_reflection::server::Builder::configure()
110 .register_encoded_file_descriptor_set(pb::ANNOTATIONS_PB)
111 .build_v1alpha()?;
112
113 let mut builder = tonic::service::Routes::builder();
114 builder.add_service(stream_svc);
115 builder.add_service(reflection_v1_svc);
116 builder.add_service(reflection_v1alpha_svc);
117
118 let grpc_router = builder.routes().prepare().into_axum_router();
119
120 let mut router = axum::Router::new()
121 .nest("/v1", v1_rest_router)
122 .merge(grpc_router)
123 .route_layer(axum::middleware::from_fn(crate::middleware::auth::<G>))
124 .layer(TraceLayer::new_for_http())
125 .layer(Extension(Arc::clone(&global)))
126 .layer(tonic_web::GrpcWebLayer::new())
127 .layer(grpc_web_cors_layer())
128 .fallback(StatusCode::NOT_FOUND);
129
130 if global.swagger_ui_enabled() {
131 router = router.merge(swagger_ui_dist::generate_routes(swagger_ui_dist::ApiDefinition {
132 uri_prefix: "/v1/docs",
133 api_definition: swagger_ui_dist::OpenApiSource::Uri("/v1/openapi.json"),
134 title: Some("Scuffle Cloud Video API v1 Docs"),
135 }));
136 }
137
138 scuffle_http::HttpServer::builder()
139 .tower_make_service_with_addr(router.into_make_service_with_connect_info::<SocketAddr>())
140 .bind(global.service_bind())
141 .rustls_config(rustls_config(&global)?)
142 .ctx(ctx)
143 .build()
144 .run()
145 .await?;
146
147 Ok(())
148 }
149}