scufflecloud_video_api/
services.rs

1use 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    // https://github.com/timostamm/protobuf-ts/blob/main/MANUAL.md#grpc-web-transport
39    let allow_headers = [
40        CONTENT_TYPE,
41        HeaderName::from_static("x-grpc-web"),
42        HeaderName::from_static("grpc-timeout"),
43    ]
44    .into_iter();
45    // .chain(middleware::auth_headers());
46
47    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    // Internal authentication via mTLS
64    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() // allow external clients as well
77        .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        // REST
89        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        // gRPC
103        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}