Skip to main content

slim_config/grpc/
server.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4pub use crate::server::{AuthenticationConfig, KeepaliveServerParameters, ServerConfig};
5
6use std::convert::Infallible;
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10use std::{net::SocketAddr, str::FromStr};
11
12use display_error_chain::ErrorChainExt;
13use futures::FutureExt;
14use futures::Stream;
15use tokio::io::{AsyncRead, AsyncWrite};
16use tokio_util::sync::CancellationToken;
17use tonic::service::Routes;
18use tonic::transport::server::TcpIncoming;
19use tower_http::BoxError;
20use tracing::debug;
21
22#[cfg(target_family = "unix")]
23use {std::path::PathBuf, tokio::net::UnixListener, tokio_stream::wrappers::UnixListenerStream};
24
25use crate::auth::ServerAuthenticator;
26use crate::auth::jwt::Config as JwtAuthenticationConfig;
27use crate::auth::oidc::Config as OidcConfig;
28#[cfg(not(target_family = "windows"))]
29use crate::auth::spire::SpireConfig as SpireAuthConfig;
30use crate::errors::ConfigError;
31use crate::tls::common::RustlsConfigLoader;
32use crate::transport::TransportProtocol;
33
34/// Boxed future returned by [`ServerConfig::to_server_future`].
35pub type ServerFuture = Pin<Box<dyn Future<Output = Result<(), tonic::transport::Error>> + Send>>;
36
37fn routes_from_slice<S>(svc: &[S]) -> Routes
38where
39    S: tower_service::Service<
40            http::Request<tonic::body::Body>,
41            Response = http::Response<tonic::body::Body>,
42            Error = Infallible,
43        > + tonic::server::NamedService
44        + Clone
45        + Send
46        + Sync
47        + 'static,
48    S::Future: Send + 'static,
49{
50    let mut routes = Routes::new(svc[0].clone());
51    for s in svc.iter().skip(1) {
52        routes = routes.add_service(s.clone());
53    }
54    routes
55}
56
57impl ServerConfig {
58    /// Build the gRPC server future that drives the underlying tonic server.
59    ///
60    /// Returns [`ConfigError::GrpcServerUnsupportedTransport`] when invoked on
61    /// a config whose `transport != Grpc`.
62    pub async fn to_server_future<S>(&self, svc: &[S]) -> Result<ServerFuture, ConfigError>
63    where
64        S: tower_service::Service<
65                http::Request<tonic::body::Body>,
66                Response = http::Response<tonic::body::Body>,
67                Error = Infallible,
68            >
69            + tonic::server::NamedService
70            + Clone
71            + Send
72            + 'static
73            + Sync,
74        S::Future: Send + 'static,
75    {
76        if svc.is_empty() {
77            return Err(ConfigError::MissingServices);
78        }
79        self.to_server_future_with_routes(routes_from_slice(svc))
80            .await
81    }
82
83    /// `Routes`-based variant of [`Self::to_server_future`]. This is the
84    /// canonical entry point — the slice-based overload is kept as a thin
85    /// compat wrapper used by the gRPC-only `run_grpc_server` API.
86    pub async fn to_server_future_with_routes(
87        &self,
88        routes: Routes,
89    ) -> Result<ServerFuture, ConfigError> {
90        if self.resolved_transport() == TransportProtocol::Websocket {
91            return Err(ConfigError::GrpcServerUnsupportedTransport);
92        }
93
94        if self.endpoint.is_empty() {
95            return Err(ConfigError::MissingEndpoint);
96        }
97
98        #[cfg(target_family = "unix")]
99        if self.endpoint.starts_with("unix://") {
100            if !self.tls_setting.insecure {
101                // For local Unix domain sockets we currently require insecure=true
102                return Err(ConfigError::UnixSocketTlsUnsupported);
103            }
104
105            let socket_path = parse_unix_socket_path(self.endpoint.as_str())?;
106
107            // Best-effort cleanup of any stale socket file
108            let _ = std::fs::remove_file(&socket_path);
109
110            let listener = UnixListener::bind(&socket_path)?;
111            let incoming = UnixListenerStream::new(listener);
112
113            return self.serve_with_incoming(routes, incoming).await;
114        }
115
116        #[cfg(not(target_family = "unix"))]
117        if self.endpoint.starts_with("unix://") {
118            return Err(ConfigError::UnixSocketUnsupported);
119        }
120
121        let addr = SocketAddr::from_str(self.endpoint.as_str())?;
122
123        // Async TLS configuration load (may involve SPIFFE operations)
124        let tls_config = self.tls_setting.load_rustls_config().await?;
125        let incoming = TcpIncoming::bind(addr)?;
126
127        match tls_config {
128            Some(tls_config) => {
129                let incoming = tonic_tls::rustls::TlsIncoming::new(incoming, Arc::new(tls_config));
130                self.serve_with_incoming(routes, incoming).await
131            }
132            None => self.serve_with_incoming(routes, incoming).await,
133        }
134    }
135
136    /// Spawn the gRPC server and return a [`CancellationToken`] that can be
137    /// used to stop it. The server is also driven by the supplied `drain`
138    /// watch for cooperative shutdown.
139    ///
140    /// Generic over a slice of tonic services for backwards compatibility;
141    /// the unified entry point is [`Self::run_server`] (uses [`Routes`] via
142    /// the [`crate::ServerHandler`] trait).
143    pub async fn run_grpc_server<S>(
144        &self,
145        svc: &[S],
146        drain_rx: drain::Watch,
147    ) -> Result<CancellationToken, ConfigError>
148    where
149        S: tower_service::Service<
150                http::Request<tonic::body::Body>,
151                Response = http::Response<tonic::body::Body>,
152                Error = Infallible,
153            >
154            + tonic::server::NamedService
155            + Clone
156            + Send
157            + 'static
158            + Sync,
159        S::Future: Send + 'static,
160    {
161        if svc.is_empty() {
162            return Err(ConfigError::MissingServices);
163        }
164        self.run_grpc_server_with_routes(routes_from_slice(svc), drain_rx)
165            .await
166    }
167
168    /// `Routes`-based variant of [`Self::run_grpc_server`]. Used internally
169    /// by [`Self::run_server`].
170    pub async fn run_grpc_server_with_routes(
171        &self,
172        routes: Routes,
173        drain_rx: drain::Watch,
174    ) -> Result<CancellationToken, ConfigError> {
175        debug!(%self, "server configured: setting it up");
176        let server_future = self.to_server_future_with_routes(routes).await?;
177
178        // create a new cancellation token
179        let token = CancellationToken::new();
180        let token_clone = token.clone();
181
182        // spawn server acceptor in a new task
183        tokio::spawn(async move {
184            debug!("starting server main loop");
185            let shutdown = drain_rx.signaled();
186
187            tokio::select! {
188                res = server_future => {
189                    match res {
190                        Ok(_) => {
191                            debug!("server shutdown");
192                        }
193                        Err(e) => {
194                            tracing::error!(error = %e.chain(), "server error");
195                        }
196                    }
197                }
198                _ = shutdown => {
199                    debug!("shutting down server");
200                }
201                _ = token.cancelled() => {
202                    debug!("cancellation token triggered: shutting down server");
203                }
204            }
205        });
206
207        Ok(token_clone)
208    }
209
210    fn create_server_builder(&self) -> tonic::transport::Server {
211        let builder: tonic::transport::Server =
212            tonic::transport::Server::builder().accept_http1(false);
213
214        let builder = match self.max_concurrent_streams {
215            Some(max_concurrent_streams) => {
216                builder.concurrency_limit_per_connection(max_concurrent_streams as usize)
217            }
218            None => builder,
219        };
220
221        let builder = match self.max_frame_size {
222            Some(max_frame_size) => builder.max_frame_size(max_frame_size * 1024 * 1024),
223            None => builder,
224        };
225
226        let builder = match self.max_header_list_size {
227            Some(max_header_list_size) => builder.http2_max_header_list_size(max_header_list_size),
228            None => builder,
229        };
230
231        let builder = builder.http2_keepalive_interval(Some(self.keepalive.time.into()));
232        let builder = builder.http2_keepalive_timeout(Some(self.keepalive.timeout.into()));
233
234        builder.max_connection_age(self.keepalive.max_connection_age.into())
235    }
236
237    async fn serve_with_incoming<I, IO, IE>(
238        &self,
239        routes: Routes,
240        incoming: I,
241    ) -> Result<ServerFuture, ConfigError>
242    where
243        I: Stream<Item = Result<IO, IE>> + Send + 'static,
244        IO: AsyncRead + AsyncWrite + tonic::transport::server::Connected + Unpin + Send + 'static,
245        IE: Into<BoxError> + Send + 'static,
246    {
247        let mut builder = self.create_server_builder();
248
249        match &self.auth {
250            AuthenticationConfig::Basic(basic) => {
251                let auth_layer = basic.get_server_layer()?;
252                let router = builder.layer(auth_layer).add_routes(routes);
253                Ok(router.serve_with_incoming(incoming).boxed())
254            }
255            AuthenticationConfig::Jwt(jwt) => {
256                let auth_layer = <JwtAuthenticationConfig as ServerAuthenticator<
257                    http::Response<tonic::body::Body>,
258                >>::get_server_layer(jwt)?;
259
260                let router = builder.layer(auth_layer).add_routes(routes);
261                Ok(router.serve_with_incoming(incoming).boxed())
262            }
263            #[cfg(not(target_family = "windows"))]
264            AuthenticationConfig::Spire(spire) => {
265                let mut auth_layer = <SpireAuthConfig as ServerAuthenticator<
266                    http::Response<tonic::body::Body>,
267                >>::get_server_layer(spire)?;
268
269                auth_layer.initialize().await?;
270
271                let router = builder.layer(auth_layer).add_routes(routes);
272                Ok(router.serve_with_incoming(incoming).boxed())
273            }
274            AuthenticationConfig::Oidc(oidc) => {
275                // OidcVerifier::initialize is a no-op; JWKS warms on first verify.
276                let auth_layer = <OidcConfig as ServerAuthenticator<
277                    http::Response<tonic::body::Body>,
278                >>::get_server_layer(oidc)?;
279                let router = builder.layer(auth_layer).add_routes(routes);
280                Ok(router.serve_with_incoming(incoming).boxed())
281            }
282            AuthenticationConfig::None => {
283                let router = builder.add_routes(routes);
284                Ok(router.serve_with_incoming(incoming).boxed())
285            }
286        }
287    }
288}
289
290#[cfg(target_family = "unix")]
291fn parse_unix_socket_path(endpoint: &str) -> Result<PathBuf, ConfigError> {
292    let path = endpoint.strip_prefix("unix://").unwrap_or(endpoint);
293
294    let without_query = path.split_once('?').map(|(p, _)| p).unwrap_or(path);
295    let path_part = without_query
296        .split_once('#')
297        .map(|(p, _)| p)
298        .unwrap_or(without_query);
299
300    if path_part.is_empty() {
301        return Err(ConfigError::UnixSocketMissingPath);
302    }
303
304    Ok(PathBuf::from(path_part))
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use crate::testutils::{Empty, helloworld::greeter_server::GreeterServer};
311    use crate::tls::common::TlsSource;
312
313    static TEST_DATA_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/grpc");
314
315    #[tokio::test]
316    async fn test_to_incoming_server_config() {
317        let mut server_config = ServerConfig::default();
318        let empty_service = Arc::new(Empty::new());
319
320        // no endpoint - should return an error
321        let ret = server_config
322            .to_server_future(&[GreeterServer::from_arc(empty_service.clone())])
323            .await;
324        // Make sure the error is a ConfigError::MissingEndpoint
325        assert!(ret.is_err_and(|e| { e.to_string().contains("missing grpc endpoint") }));
326
327        // set the endpoint in the config. Now it should fail because of the invalid endpoint
328        server_config.endpoint = "0.0.0.0:123456".to_string();
329        let ret = server_config
330            .to_server_future(&[GreeterServer::from_arc(empty_service.clone())])
331            .await;
332        assert!(ret.is_err_and(|e| { matches!(e, ConfigError::EndpointParse(_)) }));
333
334        // set a valid endpoint. Should fail because of missing cert/key files for tls
335        server_config.endpoint = "0.0.0.0:12345".to_string();
336        let ret = server_config
337            .to_server_future(&[GreeterServer::from_arc(empty_service.clone())])
338            .await;
339        assert!(ret.is_err_and(|e| { matches!(e, ConfigError::TlsConfig(_)) }));
340
341        // set the tls setting to insecure. Now it should return a server future
342        server_config.tls_setting.insecure = true;
343        let ret = server_config
344            .to_server_future(&[GreeterServer::from_arc(empty_service.clone())])
345            .await;
346        assert!(ret.is_ok());
347
348        // drop it, as we have a server listening on the port now
349        drop(ret.unwrap());
350
351        // Set insecure to false and configure certificate/key via TlsSource::File
352        server_config.tls_setting.insecure = false;
353        server_config.tls_setting.config.source = TlsSource::File {
354            cert: format!("{}/server.crt", TEST_DATA_PATH),
355            key: format!("{}/server.key", TEST_DATA_PATH),
356        };
357        let ret = server_config
358            .to_server_future(&[GreeterServer::from_arc(empty_service.clone())])
359            .await;
360        assert!(ret.is_ok());
361    }
362
363    #[tokio::test]
364    async fn test_to_server_future_rejects_websocket_transport() {
365        let empty_service = Arc::new(Empty::new());
366        let server_config = ServerConfig::with_endpoint("ws://0.0.0.0:12345");
367        let ret = server_config
368            .to_server_future(&[GreeterServer::from_arc(empty_service)])
369            .await;
370        assert!(matches!(
371            ret,
372            Err(ConfigError::GrpcServerUnsupportedTransport)
373        ));
374    }
375}