Skip to main content

pingap_proxy/
server_conf.rs

1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use pingap_config::PingapConfig;
16use pingora::protocols::l4::ext::TcpKeepalive;
17use std::fmt;
18use std::time::Duration;
19
20static ERROR_TEMPLATE: &str = include_str!("error.html");
21
22// ServerConf struct represents the configuration for a server instance
23#[derive(Debug, Default)]
24pub struct ServerConf {
25    // Whether this server instance handles admin-related functionality
26    pub admin: bool,
27
28    // Unique identifier for this server instance
29    pub name: String,
30
31    // Comma-separated list of IP:port combinations where the server will listen
32    // Example: "127.0.0.1:3000,127.0.0.1:3001"
33    pub addr: String,
34
35    // Access log configuration string. Supports formats like "combined", "tiny", etc.
36    // None means access logging is disabled
37    pub access_log: Option<String>,
38
39    // List of location route identifiers that this server will handle
40    pub locations: Vec<String>,
41
42    // OpenSSL cipher list string for TLS versions below 1.3
43    // Example: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"
44    pub tls_cipher_list: Option<String>,
45
46    // TLS 1.3 specific cipher suite configuration
47    // Example: "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"
48    pub tls_ciphersuites: Option<String>,
49
50    // Minimum TLS version the server will accept
51    // Common values: "TLSv1.2", "TLSv1.3"
52    pub tls_min_version: Option<String>,
53
54    // Maximum TLS version the server will support
55    // Common values: "TLSv1.2", "TLSv1.3"
56    pub tls_max_version: Option<String>,
57
58    // Number of worker threads for handling connections
59    // None means use system default
60    pub threads: Option<usize>,
61
62    // HTML template used for displaying error pages to clients
63    // Will use default template if not specified
64    pub error_template: String,
65
66    // TCP keepalive configuration for maintaining persistent connections
67    // Includes idle time, probe count, and interval settings
68    pub tcp_keepalive: Option<TcpKeepalive>,
69
70    // TCP Fast Open queue size for improved connection establishment
71    // None means TCP Fast Open is disabled
72    pub tcp_fastopen: Option<usize>,
73
74    /// Enable SO_REUSEPORT to allow multiple sockets to bind to the same address and port.
75    /// This is useful for load balancing across multiple worker processes.
76    /// See the [man page](https://man7.org/linux/man-pages/man7/socket.7.html) for more information.
77    pub reuse_port: Option<bool>,
78
79    // Whether to use globally configured TLS certificates
80    // False means the server is using http protocol
81    pub global_certificates: bool,
82
83    // Whether HTTP/2 protocol support is enabled for this server
84    // The http protocol is using h2c
85    pub enabled_h2: bool,
86
87    // Endpoint path for exposing Prometheus metrics
88    // None means metrics collection is disabled
89    pub prometheus_metrics: Option<String>,
90
91    // OpenTelemetry exporter configuration string
92    // Used for distributed tracing support
93    pub otlp_exporter: Option<String>,
94
95    // List of enabled module names for this server instance
96    // Allows for dynamic functionality extension
97    pub modules: Option<Vec<String>>,
98
99    // Whether to enable server-timing header
100    pub enable_server_timing: bool,
101
102    // downstream read timeout
103    pub downstream_read_timeout: Option<Duration>,
104
105    // downstream write timeout
106    pub downstream_write_timeout: Option<Duration>,
107}
108
109impl fmt::Display for ServerConf {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        // Helper closure to format optional values consistently
112        let format_opt_usize = |opt: &Option<usize>| {
113            opt.map_or("default".to_string(), |v| v.to_string())
114        };
115        let format_opt_duration = |opt: &Option<Duration>| {
116            opt.map_or("default".to_string(), |v| format!("{v:?}"))
117        };
118
119        // Main Server Identity
120        writeln!(f, "Server Configuration: '{}'", self.name)?;
121        writeln!(f, "  Listen Addresses: {}", self.addr)?;
122
123        // --- General ---
124        writeln!(f, "  - General Settings:")?;
125        writeln!(f, "    Admin Role: {}", self.admin)?;
126        writeln!(
127            f,
128            "    Access Log: {}",
129            self.access_log.as_deref().unwrap_or("disabled")
130        )?;
131        if !self.locations.is_empty() {
132            writeln!(f, "    Locations: {}", self.locations.join(", "))?;
133        }
134        writeln!(f, "    Worker Threads: {}", format_opt_usize(&self.threads))?;
135
136        // --- Protocols & Timeouts ---
137        writeln!(f, "  - Protocols & Timeouts:")?;
138        writeln!(f, "    HTTP/2 Enabled: {}", self.enabled_h2)?;
139        writeln!(
140            f,
141            "    Downstream Read Timeout: {}",
142            format_opt_duration(&self.downstream_read_timeout)
143        )?;
144        writeln!(
145            f,
146            "    Downstream Write Timeout: {}",
147            format_opt_duration(&self.downstream_write_timeout)
148        )?;
149
150        // --- TLS ---
151        writeln!(f, "  - TLS Settings:")?;
152        writeln!(f, "    Global Certificates: {}", self.global_certificates)?;
153        writeln!(
154            f,
155            "    Min Version: {}",
156            self.tls_min_version.clone().unwrap_or_default()
157        )?;
158        writeln!(
159            f,
160            "    Max Version: {}",
161            self.tls_max_version.clone().unwrap_or_default()
162        )?;
163        writeln!(
164            f,
165            "    Cipher List (TLS <1.3): {}",
166            self.tls_cipher_list.clone().unwrap_or_default()
167        )?;
168        writeln!(
169            f,
170            "    Ciphersuites (TLS 1.3): {}",
171            self.tls_ciphersuites.clone().unwrap_or_default()
172        )?;
173
174        // --- TCP ---
175        writeln!(f, "  - TCP Settings:")?;
176        if let Some(keepalive) = &self.tcp_keepalive {
177            // Assuming TcpKeepalive has a readable Debug format
178            writeln!(
179                f,
180                "    Keepalive: idle={:?}, interval={:?}, count={:?}",
181                keepalive.idle, keepalive.interval, keepalive.count
182            )?;
183        } else {
184            writeln!(f, "    Keepalive: disabled")?;
185        }
186        writeln!(f, "    Fast Open: {}", format_opt_usize(&self.tcp_fastopen))?;
187        writeln!(f, "    Reuse Port: {}", self.reuse_port.unwrap_or(false))?;
188
189        // --- Observability ---
190        writeln!(f, "  - Observability:")?;
191        writeln!(
192            f,
193            "    Prometheus Endpoint: {}",
194            self.prometheus_metrics.as_deref().unwrap_or("disabled")
195        )?;
196        writeln!(
197            f,
198            "    OTLP Exporter: {}",
199            self.otlp_exporter.as_deref().unwrap_or("disabled")
200        )?;
201        writeln!(f, "    Server-Timing Header: {}", self.enable_server_timing)?;
202
203        // --- Extensibility ---
204        if let Some(modules) = &self.modules
205            && !modules.is_empty()
206        {
207            writeln!(f, "  - Enabled Modules:")?;
208            writeln!(f, "    {}", modules.join(", "))?;
209        }
210
211        Ok(())
212    }
213}
214// Conversion implementation from PingapConfig to Vec<ServerConf>
215pub fn parse_from_conf(conf: PingapConfig) -> Vec<ServerConf> {
216    let mut upstreams = vec![];
217    for (name, item) in conf.upstreams {
218        upstreams.push((name, item));
219    }
220    let mut locations = vec![];
221    for (name, item) in conf.locations {
222        locations.push((name, item));
223    }
224    // Sort locations by weight in descending order for priority routing
225    locations.sort_by_key(|b| std::cmp::Reverse(b.1.get_weight()));
226    let mut servers = vec![];
227    for (name, item) in conf.servers {
228        // Set up error template, using default if none specified
229        let mut error_template =
230            conf.basic.error_template.clone().unwrap_or_default();
231        if error_template.is_empty() {
232            error_template = ERROR_TEMPLATE.to_string();
233        }
234
235        // Configure TCP keepalive only if all required parameters are present
236        let tcp_keepalive = if (item.tcp_idle.is_some()
237            && item.tcp_probe_count.is_some()
238            && item.tcp_interval.is_some())
239            || item.tcp_user_timeout.is_some()
240        {
241            Some(TcpKeepalive {
242                idle: item.tcp_idle.unwrap_or_default(),
243                count: item.tcp_probe_count.unwrap_or_default(),
244                interval: item.tcp_interval.unwrap_or_default(),
245                #[cfg(target_os = "linux")]
246                user_timeout: item.tcp_user_timeout.unwrap_or_default(),
247            })
248        } else {
249            None
250        };
251
252        // Create server configuration with all settings
253        servers.push(ServerConf {
254            name,
255            admin: false,
256            tls_cipher_list: item.tls_cipher_list.clone(),
257            tls_ciphersuites: item.tls_ciphersuites.clone(),
258            tls_min_version: item.tls_min_version.clone(),
259            tls_max_version: item.tls_max_version.clone(),
260            addr: item.addr,
261            access_log: item.access_log,
262            locations: item.locations.unwrap_or_default(),
263            threads: item.threads,
264            global_certificates: item.global_certificates.unwrap_or_default(),
265            enabled_h2: item.enabled_h2.unwrap_or_default(),
266            tcp_keepalive,
267            tcp_fastopen: item.tcp_fastopen,
268            reuse_port: item.reuse_port,
269            prometheus_metrics: item.prometheus_metrics,
270            otlp_exporter: item.otlp_exporter.clone(),
271            modules: item.modules.clone(),
272            enable_server_timing: item.enable_server_timing.unwrap_or_default(),
273            error_template,
274            downstream_read_timeout: item.downstream_read_timeout,
275            downstream_write_timeout: item.downstream_write_timeout,
276        });
277    }
278
279    servers
280}
281
282#[cfg(test)]
283mod tests {
284    use super::ServerConf;
285    use pingora::protocols::l4::ext::TcpKeepalive;
286    use pretty_assertions::assert_eq;
287    use std::time::Duration;
288
289    #[test]
290    fn test_server_conf() {
291        let conf = ServerConf {
292            name: "pingap".to_string(),
293            addr: "127.0.0.1:3000,127.0.0.1:3001".to_string(),
294            access_log: Some("combined".to_string()),
295
296            locations: vec!["charts-location".to_string()],
297            threads: Some(4),
298            error_template: "<html></html>".to_string(),
299            tcp_keepalive: Some(TcpKeepalive {
300                idle: Duration::from_secs(10),
301                interval: Duration::from_secs(5),
302                count: 10,
303                #[cfg(target_os = "linux")]
304                user_timeout: Duration::from_secs(0),
305            }),
306            tcp_fastopen: Some(10),
307            enabled_h2: true,
308            ..Default::default()
309        };
310
311        #[cfg(target_os = "linux")]
312        assert_eq!(
313            r#"Server Configuration: 'pingap'
314  Listen Addresses: 127.0.0.1:3000,127.0.0.1:3001
315  - General Settings:
316    Admin Role: false
317    Access Log: combined
318    Locations: charts-location
319    Worker Threads: 4
320  - Protocols & Timeouts:
321    HTTP/2 Enabled: true
322    Downstream Read Timeout: default
323    Downstream Write Timeout: default
324  - TLS Settings:
325    Global Certificates: false
326    Min Version: 
327    Max Version: 
328    Cipher List (TLS <1.3): 
329    Ciphersuites (TLS 1.3): 
330  - TCP Settings:
331    Keepalive: idle=10s, interval=5s, count=10
332    Fast Open: 10
333    Reuse Port: false
334  - Observability:
335    Prometheus Endpoint: disabled
336    OTLP Exporter: disabled
337    Server-Timing Header: false
338"#,
339            conf.to_string()
340        );
341        #[cfg(not(target_os = "linux"))]
342        assert_eq!(
343            r#"Server Configuration: 'pingap'
344  Listen Addresses: 127.0.0.1:3000,127.0.0.1:3001
345  - General Settings:
346    Admin Role: false
347    Access Log: combined
348    Locations: charts-location
349    Worker Threads: 4
350  - Protocols & Timeouts:
351    HTTP/2 Enabled: true
352    Downstream Read Timeout: default
353    Downstream Write Timeout: default
354  - TLS Settings:
355    Global Certificates: false
356    Min Version: 
357    Max Version: 
358    Cipher List (TLS <1.3): 
359    Ciphersuites (TLS 1.3): 
360  - TCP Settings:
361    Keepalive: idle=10s, interval=5s, count=10
362    Fast Open: 10
363    Reuse Port: false
364  - Observability:
365    Prometheus Endpoint: disabled
366    OTLP Exporter: disabled
367    Server-Timing Header: false
368"#,
369            conf.to_string()
370        );
371    }
372}