Skip to main content

slim_config/
server.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Transport-agnostic server configuration.
5//!
6//! [`ServerConfig`] carries connection-agnostic settings (endpoint, TLS,
7//! keepalive, auth, metadata, frame/header limits). Transport-specific server
8//! plumbing lives in:
9//!
10//! * `crate::grpc::server` — tonic / gRPC server (`to_server_future`,
11//!   `run_server`)
12//! * `crate::websocket::server` — WebSocket server (`run_websocket_server`)
13//!
14//! The two server-side public entry points stay asymmetric on purpose: gRPC
15//! takes typed `NamedService` impls; WebSocket takes a connection-accepted
16//! callback. The polymorphic dispatch happens in the datapath
17//! (`message_processing`) based on `config.transport`.
18
19use duration_string::DurationString;
20use schemars::JsonSchema;
21use serde::{Deserialize, Serialize};
22use std::sync::Arc;
23use std::time::Duration;
24use tokio_util::sync::CancellationToken;
25
26use crate::server_handler::ServerHandler;
27
28use crate::auth::basic::Config as BasicAuthenticationConfig;
29use crate::auth::jwt::Config as JwtAuthenticationConfig;
30use crate::auth::oidc::Config as OidcAuthConfig;
31#[cfg(not(target_family = "windows"))]
32use crate::auth::spire::SpireConfig as SpireAuthConfig;
33use crate::component::configuration::Configuration;
34use crate::errors::ConfigError;
35use crate::tls::server::TlsServerConfig as TLSSetting;
36use crate::transport::{TransportProtocol, validate_endpoint_scheme};
37use slim_auth::metadata::MetadataMap;
38
39#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, JsonSchema)]
40pub struct KeepaliveServerParameters {
41    /// max_connection_idle sets the time after which an idle connection is closed.
42    #[serde(default = "default_max_connection_idle")]
43    #[schemars(with = "String")]
44    pub max_connection_idle: DurationString,
45
46    /// max_connection_age sets the maximum amount of time a connection may exist before it will be closed.
47    #[serde(default = "default_max_connection_age")]
48    #[schemars(with = "String")]
49    pub max_connection_age: DurationString,
50
51    /// max_connection_age_grace is an additional time given after MaxConnectionAge before closing the connection.
52    #[serde(default = "default_max_connection_age_grace")]
53    #[schemars(with = "String")]
54    pub max_connection_age_grace: DurationString,
55
56    /// Time sets the frequency of the keepalive ping.
57    #[serde(default = "default_time")]
58    #[schemars(with = "String")]
59    pub time: DurationString,
60
61    /// Timeout sets the amount of time the server waits for a keepalive ping ack.
62    #[serde(default = "default_timeout")]
63    #[schemars(with = "String")]
64    pub timeout: DurationString,
65}
66
67/// Enum holding one configuration for the server.
68#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, JsonSchema)]
69#[serde(rename_all = "snake_case", tag = "type")]
70pub enum AuthenticationConfig {
71    /// Basic authentication configuration.
72    Basic(BasicAuthenticationConfig),
73    /// JWT authentication configuration.
74    Jwt(JwtAuthenticationConfig),
75    /// OIDC authentication configuration (JWT validation + optional group claim check).
76    Oidc(OidcAuthConfig),
77    /// SPIRE/SPIFFE authentication configuration.
78    #[cfg(not(target_family = "windows"))]
79    Spire(SpireAuthConfig),
80    /// None
81    #[default]
82    None,
83}
84
85#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, JsonSchema)]
86pub struct ServerConfig {
87    /// Endpoint is the address to listen on.
88    ///
89    /// The transport protocol is inferred from the endpoint scheme:
90    /// * `ws://`, `wss://`  → WebSocket (TLS when `wss`)
91    /// * `http://`, `https://`, `unix://`, bare `host:port` → gRPC
92    pub endpoint: String,
93
94    /// Configures the protocol to use TLS.
95    #[serde(default, rename = "tls")]
96    pub tls_setting: TLSSetting,
97
98    /// Use HTTP 2 only.
99    #[serde(default = "default_http2_only")]
100    pub http2_only: bool,
101
102    /// Maximum size (in MiB) of messages accepted by the server.
103    pub max_frame_size: Option<u32>,
104
105    /// MaxConcurrentStreams sets the limit on the number of concurrent streams to each ServerTransport.
106    pub max_concurrent_streams: Option<u32>,
107
108    /// Max header list size
109    pub max_header_list_size: Option<u32>,
110
111    /// ReadBufferSize for gRPC server.
112    // TODO(msardara): not implemented yet
113    pub read_buffer_size: Option<usize>,
114
115    /// WriteBufferSize for gRPC server.
116    // TODO(msardara): not implemented yet
117    pub write_buffer_size: Option<usize>,
118
119    /// Keepalive anchor for all the settings related to keepalive.
120    #[serde(default)]
121    pub keepalive: KeepaliveServerParameters,
122
123    /// Auth for this receiver.
124    #[serde(default)]
125    pub auth: AuthenticationConfig,
126
127    /// Arbitrary user-provided metadata.
128    pub metadata: Option<MetadataMap>,
129
130    /// Flag to enforce header integrity validation
131    /// By default it is `true`
132    #[serde(default = "default_require_header_mac")]
133    pub require_header_mac: bool,
134
135    /// Timeout (in seconds) for link negotiation to complete.
136    /// By default it is 5 seconds.
137    #[serde(
138        default = "default_negotiation_timeout_secs",
139        alias = "link_hmac_timeout_secs"
140    )]
141    pub negotiation_timeout_secs: u64,
142
143    /// Polling interval (in milliseconds) to wait between HMAC existence checks.
144    /// By default it is 5 milliseconds.
145    #[serde(default = "default_link_hmac_poll_interval_ms")]
146    pub link_hmac_poll_interval_ms: u64,
147}
148
149/// Default values for KeepaliveServerParameters
150impl Default for KeepaliveServerParameters {
151    fn default() -> Self {
152        Self {
153            max_connection_idle: default_max_connection_idle(),
154            max_connection_age: default_max_connection_age(),
155            max_connection_age_grace: default_max_connection_age_grace(),
156            time: default_time(),
157            timeout: default_timeout(),
158        }
159    }
160}
161
162fn default_max_connection_idle() -> DurationString {
163    Duration::from_secs(3600).into()
164}
165
166fn default_max_connection_age() -> DurationString {
167    Duration::from_secs(2 * 3600).into()
168}
169
170fn default_max_connection_age_grace() -> DurationString {
171    Duration::from_secs(5 * 60).into()
172}
173
174fn default_time() -> DurationString {
175    Duration::from_secs(2 * 60).into()
176}
177
178fn default_timeout() -> DurationString {
179    Duration::from_secs(20).into()
180}
181
182/// Default values for ServerConfig
183impl Default for ServerConfig {
184    fn default() -> Self {
185        Self {
186            endpoint: String::new(),
187            tls_setting: TLSSetting::default(),
188            http2_only: default_http2_only(),
189            max_frame_size: Some(4),
190            max_concurrent_streams: Some(100),
191            max_header_list_size: None,
192            read_buffer_size: Some(1024 * 1024),
193            write_buffer_size: Some(1024 * 1024),
194            keepalive: KeepaliveServerParameters::default(),
195            auth: AuthenticationConfig::default(),
196            metadata: None,
197            require_header_mac: true,
198            negotiation_timeout_secs: default_negotiation_timeout_secs(),
199            link_hmac_poll_interval_ms: default_link_hmac_poll_interval_ms(),
200        }
201    }
202}
203
204fn default_http2_only() -> bool {
205    true
206}
207
208fn default_require_header_mac() -> bool {
209    true
210}
211
212fn default_negotiation_timeout_secs() -> u64 {
213    5
214}
215
216fn default_link_hmac_poll_interval_ms() -> u64 {
217    5
218}
219
220/// Display implementation for ServerConfig
221impl std::fmt::Display for ServerConfig {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        write!(
224            f,
225            "ServerConfig {{ endpoint: {}, transport: {:?}, tls_setting: {}, http2_only: {}, max_frame_size: {:?}, max_concurrent_streams: {:?}, max_header_list_size: {:?}, read_buffer_size: {:?}, write_buffer_size: {:?}, keepalive: {:?}, auth: {:?}, metadata: {:?}, require_header_mac: {}, negotiation_timeout_secs: {}, link_hmac_poll_interval_ms: {} }}",
226            self.endpoint,
227            self.resolved_transport(),
228            self.tls_setting,
229            self.http2_only,
230            self.max_frame_size,
231            self.max_concurrent_streams,
232            self.max_header_list_size,
233            self.read_buffer_size,
234            self.write_buffer_size,
235            self.keepalive,
236            self.auth,
237            self.metadata,
238            self.require_header_mac,
239            self.negotiation_timeout_secs,
240            self.link_hmac_poll_interval_ms,
241        )
242    }
243}
244
245impl Configuration for ServerConfig {
246    type Error = ConfigError;
247
248    fn validate(&self) -> Result<(), Self::Error> {
249        self.tls_setting.validate()?;
250        validate_endpoint_scheme(&self.endpoint)?;
251        Ok(())
252    }
253}
254
255impl ServerConfig {
256    pub fn with_endpoint(endpoint: &str) -> Self {
257        Self {
258            endpoint: endpoint.to_string(),
259            ..Default::default()
260        }
261    }
262
263    pub fn with_tls_settings(self, tls_setting: TLSSetting) -> Self {
264        Self {
265            tls_setting,
266            ..self
267        }
268    }
269
270    pub fn with_http2_only(self, http2_only: bool) -> Self {
271        Self { http2_only, ..self }
272    }
273
274    pub fn with_max_frame_size(self, max_frame_size: Option<u32>) -> Self {
275        Self {
276            max_frame_size,
277            ..self
278        }
279    }
280
281    pub fn with_max_concurrent_streams(self, max_concurrent_streams: Option<u32>) -> Self {
282        Self {
283            max_concurrent_streams,
284            ..self
285        }
286    }
287
288    pub fn with_max_header_list_size(self, max_header_list_size: Option<u32>) -> Self {
289        Self {
290            max_header_list_size,
291            ..self
292        }
293    }
294
295    pub fn with_read_buffer_size(self, read_buffer_size: Option<usize>) -> Self {
296        Self {
297            read_buffer_size,
298            ..self
299        }
300    }
301
302    pub fn with_write_buffer_size(self, write_buffer_size: Option<usize>) -> Self {
303        Self {
304            write_buffer_size,
305            ..self
306        }
307    }
308
309    pub fn with_keepalive(self, keepalive: KeepaliveServerParameters) -> Self {
310        Self { keepalive, ..self }
311    }
312
313    pub fn with_auth(self, auth: AuthenticationConfig) -> Self {
314        Self { auth, ..self }
315    }
316
317    /// Transport-agnostic server entry point. Dispatches on `self.transport`
318    /// and calls into the matching transport-specific run helper, requesting
319    /// the appropriate adapter from `handler`. Returns
320    /// [`ConfigError::HandlerMissingGrpcSupport`] /
321    /// [`ConfigError::HandlerMissingWebSocketSupport`] when the handler does
322    /// not implement the method for the configured transport.
323    pub async fn run_server<H: ServerHandler>(
324        &self,
325        watch: drain::Watch,
326        handler: Arc<H>,
327    ) -> Result<CancellationToken, ConfigError> {
328        match self.resolved_transport() {
329            TransportProtocol::Grpc => {
330                let routes = handler
331                    .grpc_routes()
332                    .ok_or(ConfigError::HandlerMissingGrpcSupport)?;
333                self.run_grpc_server_with_routes(routes, watch).await
334            }
335            TransportProtocol::Websocket => {
336                let on_accepted = handler
337                    .on_websocket_accepted()
338                    .ok_or(ConfigError::HandlerMissingWebSocketSupport)?;
339                self.run_websocket_server(watch, on_accepted).await
340            }
341        }
342    }
343
344    /// Resolve the transport protocol for this configuration by inspecting
345    /// the endpoint URI scheme. See [`TransportProtocol::from_endpoint`].
346    pub fn resolved_transport(&self) -> TransportProtocol {
347        TransportProtocol::from_endpoint(&self.endpoint)
348    }
349}
350
351#[cfg(test)]
352mod metadata_tests {
353    use super::*;
354
355    #[test]
356    fn server_config_with_metadata_roundtrip_yaml() {
357        let mut md = MetadataMap::default();
358        md.insert("role", "ingress");
359        md.insert("replicas", 3u64);
360        let mut nested = MetadataMap::default();
361        nested.insert("inner", "v");
362        md.insert("nested", nested);
363
364        let cfg = ServerConfig {
365            endpoint: "127.0.0.1:50051".to_string(),
366            metadata: Some(md.clone()),
367            ..Default::default()
368        };
369
370        let s = serde_yaml::to_string(&cfg).expect("serialize");
371        let deser: ServerConfig = serde_yaml::from_str(&s).expect("deserialize");
372        assert_eq!(deser.metadata, Some(md));
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn test_validate_rejects_unknown_scheme() {
382        let cfg = ServerConfig::with_endpoint("ftp://0.0.0.0:46357");
383        assert!(matches!(
384            cfg.validate(),
385            Err(ConfigError::InvalidEndpointScheme)
386        ));
387    }
388
389    #[test]
390    fn test_validate_accepts_supported_schemes() {
391        for endpoint in [
392            "0.0.0.0:46357",
393            "http://0.0.0.0:46357",
394            "https://0.0.0.0:46357",
395            "ws://0.0.0.0:46357",
396            "wss://0.0.0.0:46357",
397            "unix:///tmp/slim.sock",
398        ] {
399            let cfg = ServerConfig::with_endpoint(endpoint);
400            cfg.validate()
401                .unwrap_or_else(|e| panic!("endpoint {endpoint} rejected: {e}"));
402        }
403    }
404
405    #[test]
406    fn test_default_keepalive_server_parameters() {
407        let keepalive = KeepaliveServerParameters::default();
408        assert_eq!(keepalive.max_connection_idle, default_max_connection_idle());
409        assert_eq!(keepalive.max_connection_age, default_max_connection_age());
410        assert_eq!(
411            keepalive.max_connection_age_grace,
412            default_max_connection_age_grace()
413        );
414        assert_eq!(keepalive.time, default_time());
415        assert_eq!(keepalive.timeout, default_timeout());
416    }
417
418    #[test]
419    fn test_default_server_config() {
420        let server_config = ServerConfig::default();
421        assert_eq!(server_config.endpoint, String::new());
422        assert_eq!(server_config.resolved_transport(), TransportProtocol::Grpc);
423        assert_eq!(server_config.tls_setting, TLSSetting::default());
424        assert_eq!(server_config.http2_only, default_http2_only());
425        assert_eq!(server_config.max_frame_size, Some(4));
426        assert_eq!(server_config.max_concurrent_streams, Some(100));
427        assert_eq!(server_config.max_header_list_size, None);
428        assert_eq!(server_config.read_buffer_size, Some(1024 * 1024));
429        assert_eq!(server_config.write_buffer_size, Some(1024 * 1024));
430        assert_eq!(
431            server_config.keepalive,
432            KeepaliveServerParameters::default()
433        );
434        assert_eq!(server_config.auth, AuthenticationConfig::None);
435    }
436
437    #[test]
438    fn test_keepalive_server_parameters_valid_durations_deserialize() {
439        let json = r#"{
440            "endpoint": "0.0.0.0:12345",
441            "keepalive": {
442                "max_connection_idle": "30m",
443                "max_connection_age": "1h30m",
444                "max_connection_age_grace": "15s",
445                "time": "5s",
446                "timeout": "2s"
447            }
448        }"#;
449
450        let cfg: ServerConfig = serde_json::from_str(json).expect("deserialization should succeed");
451        assert_eq!(
452            cfg.keepalive.max_connection_idle,
453            Duration::from_secs(30 * 60)
454        );
455        assert_eq!(
456            cfg.keepalive.max_connection_age,
457            Duration::from_secs(90 * 60)
458        );
459        assert_eq!(
460            cfg.keepalive.max_connection_age_grace,
461            Duration::from_secs(15)
462        );
463        assert_eq!(cfg.keepalive.time, Duration::from_secs(5));
464        assert_eq!(cfg.keepalive.timeout, Duration::from_secs(2));
465    }
466
467    #[test]
468    fn test_invalid_keepalive_duration_strings_fail_deserialize() {
469        let invalid_json_cases = [
470            r#"{ "keepalive": { "time": "zz" } }"#,
471            r#"{ "keepalive": { "timeout": "-5s" } }"#,
472            r#"{ "keepalive": { "max_connection_age": "10x" } }"#,
473        ];
474        for js in invalid_json_cases {
475            let res: Result<ServerConfig, _> = serde_json::from_str(js);
476            assert!(res.is_err(), "expected error for json: {}", js);
477        }
478    }
479
480    #[test]
481    fn test_server_config_keepalive_roundtrip_duration_serialization() {
482        let keepalive = KeepaliveServerParameters {
483            max_connection_idle: Duration::from_secs(10).into(),
484            max_connection_age: Duration::from_secs(20).into(),
485            max_connection_age_grace: Duration::from_secs(30).into(),
486            time: Duration::from_secs(3).into(),
487            timeout: Duration::from_secs(1).into(),
488        };
489
490        let cfg = ServerConfig::with_endpoint("127.0.0.1:50000").with_keepalive(keepalive.clone());
491        let serialized = serde_json::to_string(&cfg).expect("serialize");
492        let deserialized: ServerConfig = serde_json::from_str(&serialized).expect("deserialize");
493
494        assert_eq!(
495            deserialized.keepalive.max_connection_idle,
496            Duration::from_secs(10)
497        );
498        assert_eq!(
499            deserialized.keepalive.max_connection_age,
500            Duration::from_secs(20)
501        );
502        assert_eq!(
503            deserialized.keepalive.max_connection_age_grace,
504            Duration::from_secs(30)
505        );
506        assert_eq!(deserialized.keepalive.time, Duration::from_secs(3));
507        assert_eq!(deserialized.keepalive.timeout, Duration::from_secs(1));
508    }
509}