Skip to main content

pingap_config/
common.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 super::{Error, Result};
16use bytesize::ByteSize;
17use http::{HeaderName, HeaderValue};
18use pingap_discovery::{DNS_DISCOVERY, is_static_discovery};
19use pingap_util::{is_pem, resolve_path};
20use regex::Regex;
21use rustls_pki_types::pem::PemObject;
22use serde::{Deserialize, Serialize, Serializer};
23use std::collections::HashSet;
24use std::fs::File;
25use std::hash::{DefaultHasher, Hash, Hasher};
26use std::io::{BufReader, Read};
27use std::net::{IpAddr, ToSocketAddrs};
28use std::path::Path;
29use std::time::Duration;
30use std::{collections::HashMap, str::FromStr};
31use strum::EnumString;
32use tempfile::tempfile_in;
33use toml::Table;
34use toml::{Value, map::Map};
35use url::Url;
36
37pub const CATEGORY_BASIC: &str = "basic";
38pub const CATEGORY_SERVER: &str = "server";
39pub const CATEGORY_LOCATION: &str = "location";
40pub const CATEGORY_UPSTREAM: &str = "upstream";
41pub const CATEGORY_PLUGIN: &str = "plugin";
42pub const CATEGORY_CERTIFICATE: &str = "certificate";
43pub const CATEGORY_STORAGE: &str = "storage";
44
45pub trait Validate {
46    fn validate(&self) -> Result<()>;
47}
48
49#[derive(PartialEq, Debug, Default, Clone, EnumString, strum::Display)]
50#[strum(serialize_all = "snake_case")]
51pub enum PluginCategory {
52    /// Statistics and metrics collection
53    #[default]
54    Stats,
55    /// Rate limiting and throttling
56    Limit,
57    /// Response compression (gzip, deflate, etc)
58    Compression,
59    /// Administrative interface and controls
60    Admin,
61    /// Static file serving and directory listing
62    Directory,
63    /// Mock/stub responses for testing
64    Mock,
65    /// Request ID generation and tracking
66    RequestId,
67    /// IP-based access control
68    IpRestriction,
69    /// API key authentication
70    KeyAuth,
71    /// HTTP Basic authentication
72    BasicAuth,
73    /// Combined authentication methods
74    CombinedAuth,
75    /// JSON Web Token (JWT) authentication
76    Jwt,
77    /// Response caching
78    Cache,
79    /// URL redirection rules
80    Redirect,
81    /// Health check endpoint
82    Ping,
83    /// Custom response header manipulation
84    ResponseHeaders,
85    /// Substring filter
86    SubFilter,
87    /// Referer-based access control
88    RefererRestriction,
89    /// User-Agent based access control
90    UaRestriction,
91    /// Cross-Site Request Forgery protection
92    Csrf,
93    /// Cross-Origin Resource Sharing
94    Cors,
95    /// Accept-Encoding header processing
96    AcceptEncoding,
97    /// Traffic splitting
98    TrafficSplitting,
99}
100impl Serialize for PluginCategory {
101    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102    where
103        S: Serializer,
104    {
105        serializer.serialize_str(self.to_string().as_ref())
106    }
107}
108
109impl<'de> Deserialize<'de> for PluginCategory {
110    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111    where
112        D: serde::Deserializer<'de>,
113    {
114        let value: String = serde::Deserialize::deserialize(deserializer)?;
115        PluginCategory::from_str(&value).map_err(|_| {
116            serde::de::Error::custom(format!(
117                "invalid plugin category: {value}"
118            ))
119        })
120    }
121}
122
123/// Configuration struct for TLS/SSL certificates
124#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
125pub struct CertificateConf {
126    /// Domain names this certificate is valid for (comma separated)
127    pub domains: Option<String>,
128    /// TLS certificate in PEM format or base64 encoded
129    pub tls_cert: Option<String>,
130    /// Private key in PEM format or base64 encoded
131    pub tls_key: Option<String>,
132    /// Whether this is the default certificate for the server
133    pub is_default: Option<bool>,
134    /// Whether this certificate is a Certificate Authority (CA)
135    pub is_ca: Option<bool>,
136    /// ACME configuration for automated certificate management
137    pub acme: Option<String>,
138    /// Whether to use DNS challenge for ACME certificate management
139    pub dns_challenge: Option<bool>,
140    /// DNS provider for ACME certificate management
141    pub dns_provider: Option<String>,
142    /// DNS service url for ACME certificate management
143    pub dns_service_url: Option<String>,
144    /// Buffer days for certificate renewal
145    pub buffer_days: Option<u16>,
146    /// Optional description/notes about this certificate
147    pub remark: Option<String>,
148}
149
150/// Validates a certificate in PEM format or base64 encoded
151fn validate_cert(value: &str) -> Result<()> {
152    // Convert from PEM/base64 to binary
153    let buf_list =
154        pingap_util::convert_pem(value).map_err(|e| Error::Invalid {
155            message: e.to_string(),
156        })?;
157    for buf in buf_list {
158        // Parse all certificates in the buffer
159        let certs = rustls_pki_types::CertificateDer::pem_slice_iter(&buf)
160            .collect::<std::result::Result<Vec<_>, _>>()
161            .map_err(|_| Error::Invalid {
162                message: "Failed to parse certificate".to_string(),
163            })?;
164
165        // Ensure at least one valid certificate was found
166        if certs.is_empty() {
167            return Err(Error::Invalid {
168                message: "No valid certificates found in input".to_string(),
169            });
170        }
171    }
172
173    Ok(())
174}
175
176// Generate hash key for certificate configuration
177// Add the content of the certificate and key files to the hash key
178impl Hashable for CertificateConf {
179    fn hash_key(&self) -> String {
180        let mut hasher = DefaultHasher::new();
181
182        // 1. Hash the struct's own fields first.
183        // This includes the paths themselves, so changes to paths affect the hash.
184        self.hash(&mut hasher);
185
186        // 2. Iterate through the optional certificate and key file paths.
187        for value in [&self.tls_cert, &self.tls_key].into_iter().flatten() {
188            if is_pem(value) {
189                continue;
190            }
191            let file_path = resolve_path(value);
192            let path = Path::new(&file_path);
193            if !path.is_file() {
194                continue;
195            }
196
197            match File::open(path) {
198                Ok(file) => {
199                    let mut reader = BufReader::new(file);
200                    let mut buffer = [0; 8192];
201
202                    loop {
203                        match reader.read(&mut buffer) {
204                            Ok(0) => break, // End of file reached successfully.
205                            Ok(bytes_read) => {
206                                // Hash the chunk that was read.
207                                hasher.write(&buffer[..bytes_read]);
208                            },
209                            Err(e) => {
210                                hasher.write(b"Error reading file content:");
211                                hasher.write(e.to_string().as_bytes());
212                                break;
213                            },
214                        }
215                    }
216                },
217                Err(e) => {
218                    hasher.write(b"Error opening file:");
219                    hasher.write(e.to_string().as_bytes());
220                },
221            }
222        }
223
224        format!("{:x}", hasher.finish())
225    }
226}
227
228impl Validate for CertificateConf {
229    /// Validates the certificate configuration:
230    /// - Validates private key can be parsed if present
231    /// - Validates certificate can be parsed if present  
232    /// - Validates certificate chain can be parsed if present
233    fn validate(&self) -> Result<()> {
234        // Validate private key
235        let tls_key = self.tls_key.clone().unwrap_or_default();
236        if !tls_key.is_empty() {
237            let buf_list = pingap_util::convert_pem(&tls_key).map_err(|e| {
238                Error::Invalid {
239                    message: e.to_string(),
240                }
241            })?;
242            let buf = &buf_list[0];
243            let _ = rustls_pki_types::PrivateKeyDer::from_pem_slice(buf)
244                .map_err(|_| Error::Invalid {
245                    message: "Failed to parse private key".to_string(),
246                })?;
247        }
248
249        // Validate main certificate
250        let tls_cert = self.tls_cert.clone().unwrap_or_default();
251        if !tls_cert.is_empty() {
252            validate_cert(&tls_cert)?;
253        }
254
255        Ok(())
256    }
257}
258
259/// Configuration for an upstream service that handles proxied requests
260#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
261pub struct UpstreamConf {
262    /// List of upstream server addresses in format "host:port" or "host:port weight"
263    pub addrs: Vec<String>,
264
265    /// Service discovery mechanism to use (e.g. "dns", "static")
266    pub discovery: Option<String>,
267
268    /// DNS server for DNS discovery
269    pub dns_server: Option<String>,
270
271    /// DNS domain for DNS discovery
272    pub dns_domain: Option<String>,
273
274    /// DNS search for DNS discovery
275    pub dns_search: Option<String>,
276
277    /// How frequently to update the upstream server list
278    #[serde(default)]
279    #[serde(with = "humantime_serde")]
280    pub update_frequency: Option<Duration>,
281
282    /// Load balancing algorithm (e.g. "round_robin", "hash:cookie")
283    pub algo: Option<String>,
284
285    /// Server Name Indication for TLS connections
286    pub sni: Option<String>,
287
288    /// Whether to verify upstream TLS certificates
289    pub verify_cert: Option<bool>,
290
291    /// Health check URL to verify upstream server status
292    pub health_check: Option<String>,
293
294    /// Whether to only use IPv4 addresses
295    pub ipv4_only: Option<bool>,
296
297    /// Enable request tracing
298    pub enable_tracer: Option<bool>,
299
300    /// Enable backend stats
301    pub enable_backend_stats: Option<bool>,
302
303    /// Failure status codes for backend stats
304    /// Format: "400,500,502,503,504"
305    pub backend_failure_status_code: Option<String>,
306
307    /// Maximum number of consecutive failures required to trip the circuit breaker
308    pub circuit_break_max_consecutive_failures: Option<u32>,
309
310    /// Maximum failure rate (percentage, 0 to 100) required to trip the circuit breaker
311    pub circuit_break_max_failure_percent: Option<u16>,
312
313    /// Minimum total number of requests (within the statistics window) required
314    /// before the failure rate is considered significant for circuit breaking.
315    /// Prevents tripping the breaker when traffic is very low.
316    pub circuit_break_min_requests_threshold: Option<u64>,
317
318    /// The number of consecutive successes required to reset the circuit breaker to closed.
319    pub circuit_break_half_open_consecutive_success_threshold: Option<u32>,
320
321    /// The duration of the open state of the circuit breaker.
322    #[serde(default)]
323    #[serde(with = "humantime_serde")]
324    pub circuit_break_open_duration: Option<Duration>,
325
326    /// Interval for backend stats, default is 60 seconds
327    #[serde(default)]
328    #[serde(with = "humantime_serde")]
329    pub backend_stats_interval: Option<Duration>,
330
331    /// Application Layer Protocol Negotiation for TLS
332    pub alpn: Option<String>,
333
334    /// Timeout for establishing new connections
335    #[serde(default)]
336    #[serde(with = "humantime_serde")]
337    pub connection_timeout: Option<Duration>,
338
339    /// Total timeout for the entire request/response cycle
340    #[serde(default)]
341    #[serde(with = "humantime_serde")]
342    pub total_connection_timeout: Option<Duration>,
343
344    /// Timeout for reading response data
345    #[serde(default)]
346    #[serde(with = "humantime_serde")]
347    pub read_timeout: Option<Duration>,
348
349    /// Timeout for idle connections in the pool
350    #[serde(default)]
351    #[serde(with = "humantime_serde")]
352    pub idle_timeout: Option<Duration>,
353
354    /// Timeout for writing request data
355    #[serde(default)]
356    #[serde(with = "humantime_serde")]
357    pub write_timeout: Option<Duration>,
358
359    /// TCP keepalive idle time
360    #[serde(default)]
361    #[serde(with = "humantime_serde")]
362    pub tcp_idle: Option<Duration>,
363
364    /// TCP keepalive probe interval
365    #[serde(default)]
366    #[serde(with = "humantime_serde")]
367    pub tcp_interval: Option<Duration>,
368
369    /// TCP keepalive user timeout
370    #[serde(default)]
371    #[serde(with = "humantime_serde")]
372    pub tcp_user_timeout: Option<Duration>,
373
374    /// Number of TCP keepalive probes before connection is dropped
375    pub tcp_probe_count: Option<usize>,
376
377    /// TCP receive buffer size
378    pub tcp_recv_buf: Option<ByteSize>,
379
380    /// Enable TCP Fast Open
381    pub tcp_fast_open: Option<bool>,
382
383    /// List of included configuration files
384    pub includes: Option<Vec<String>>,
385
386    /// Optional description/notes about this upstream
387    pub remark: Option<String>,
388}
389
390fn is_valid_upstream_ip(ip: IpAddr) -> bool {
391    match ip {
392        IpAddr::V4(ipv4) => {
393            !ipv4.is_unspecified()
394                && !ipv4.is_broadcast()
395                && !ipv4.is_multicast()
396                && !ipv4.is_link_local()
397        },
398        IpAddr::V6(ipv6) => {
399            !ipv6.is_unspecified()
400                && !ipv6.is_multicast()
401                && (ipv6.segments()[0] & 0xffc0) != 0xfe80
402        },
403    }
404}
405
406impl Validate for UpstreamConf {
407    /// Validates the upstream configuration:
408    /// 1. The address list can't be empty
409    /// 2. For static discovery, addresses must be valid socket addresses
410    /// 3. Health check URL must be valid if specified
411    /// 4. TCP probe count must not exceed maximum (16)
412    fn validate(&self) -> Result<()> {
413        // Validate address list
414        self.validate_addresses()?;
415
416        // Validate health check URL if specified
417        self.validate_health_check()?;
418
419        // Validate TCP probe count
420        self.validate_tcp_probe_count()?;
421
422        Ok(())
423    }
424}
425
426impl UpstreamConf {
427    /// Determines the appropriate service discovery mechanism:
428    /// - Returns configured discovery if set
429    /// - Returns DNS discovery if any address contains a hostname
430    /// - Returns empty string (static discovery) otherwise
431    pub fn guess_discovery(&self) -> String {
432        // Return explicitly configured discovery if set
433        if let Some(discovery) = &self.discovery {
434            return discovery.clone();
435        }
436
437        // Check if any address contains a hostname (non-IP)
438        let has_hostname = self.addrs.iter().any(|addr| {
439            // Extract host portion before port
440            let host =
441                addr.split_once(':').map_or(addr.as_str(), |(host, _)| host);
442
443            // If host can't be parsed as IP, it's a hostname
444            host.parse::<std::net::IpAddr>().is_err()
445        });
446
447        if has_hostname {
448            DNS_DISCOVERY.to_string()
449        } else {
450            String::new()
451        }
452    }
453
454    fn validate_addresses(&self) -> Result<()> {
455        if self.addrs.is_empty() {
456            return Err(Error::Invalid {
457                message: "upstream addrs is empty".to_string(),
458            });
459        }
460
461        // Only validate addresses for static discovery
462        if !is_static_discovery(&self.guess_discovery()) {
463            return Ok(());
464        }
465
466        for addr in &self.addrs {
467            let parts: Vec<_> = addr.split_whitespace().collect();
468            let host_port = parts[0].to_string();
469
470            let host = if host_port.starts_with('[') {
471                host_port
472                    .find(']')
473                    .map_or(host_port.as_str(), |i| &host_port[1..i])
474            } else {
475                host_port
476                    .split_once(':')
477                    .map_or(host_port.as_str(), |(h, _)| h)
478            };
479
480            if let Ok(ip) = host.parse::<IpAddr>()
481                && !is_valid_upstream_ip(ip)
482            {
483                return Err(Error::Invalid {
484                    message: format!(
485                        "upstream addr({host}) is an invalid IP \
486                             (unspecified, broadcast, multicast, or link-local)"
487                    ),
488                });
489            }
490
491            // Add default port 80 if not specified
492            let addr_to_check = if !host_port.contains(':') {
493                format!("{host_port}:80")
494            } else {
495                host_port
496            };
497
498            // Validate socket address
499            addr_to_check.to_socket_addrs().map_err(|e| Error::Io {
500                source: e,
501                file: addr_to_check,
502            })?;
503        }
504
505        Ok(())
506    }
507
508    fn validate_health_check(&self) -> Result<()> {
509        let health_check = match &self.health_check {
510            Some(url) if !url.is_empty() => url,
511            _ => return Ok(()),
512        };
513
514        Url::parse(health_check).map_err(|e| Error::UrlParse {
515            source: e,
516            url: health_check.to_string(),
517        })?;
518
519        Ok(())
520    }
521
522    fn validate_tcp_probe_count(&self) -> Result<()> {
523        const MAX_TCP_PROBE_COUNT: usize = 16;
524
525        if let Some(count) = self.tcp_probe_count
526            && count > MAX_TCP_PROBE_COUNT
527        {
528            return Err(Error::Invalid {
529                message: format!(
530                    "tcp probe count should be <= {MAX_TCP_PROBE_COUNT}"
531                ),
532            });
533        }
534
535        Ok(())
536    }
537}
538
539impl Validate for LocationConf {
540    fn validate(&self) -> Result<()> {
541        self.validate_with_upstream(None)?;
542        Ok(())
543    }
544}
545
546/// Configuration for a location/route that handles incoming requests
547#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
548pub struct LocationConf {
549    /// Name of the upstream service to proxy requests to
550    pub upstream: Option<String>,
551
552    /// URL path pattern to match requests against
553    /// Can start with:
554    /// - "=" for exact match
555    /// - "~" for regex match
556    /// - No prefix for prefix match
557    pub path: Option<String>,
558
559    /// Host/domain name to match requests against
560    pub host: Option<String>,
561
562    /// Headers to set on proxied requests (overwrites existing)
563    pub proxy_set_headers: Option<Vec<String>>,
564
565    /// Headers to add to proxied requests (appends to existing)
566    pub proxy_add_headers: Option<Vec<String>>,
567
568    /// URL rewrite rule in format "pattern replacement"
569    pub rewrite: Option<String>,
570
571    /// Manual weight for location matching priority
572    /// Higher weight = higher priority
573    pub weight: Option<u16>,
574
575    /// List of plugins to apply to requests matching this location
576    pub plugins: Option<Vec<String>>,
577
578    /// Maximum allowed size of request body
579    pub client_max_body_size: Option<ByteSize>,
580
581    /// Maximum number of concurrent requests being processed
582    pub max_processing: Option<i32>,
583
584    /// List of included configuration files
585    pub includes: Option<Vec<String>>,
586
587    /// Whether to enable gRPC-Web protocol support
588    pub grpc_web: Option<bool>,
589
590    /// Whether to enable reverse proxy headers
591    pub enable_reverse_proxy_headers: Option<bool>,
592
593    /// Maximum number of retries for failed connections
594    pub max_retries: Option<u8>,
595
596    /// Maximum window for retries
597    #[serde(default)]
598    #[serde(with = "humantime_serde")]
599    pub max_retry_window: Option<Duration>,
600
601    /// Optional description/notes about this location
602    pub remark: Option<String>,
603}
604
605impl LocationConf {
606    /// Validates the location configuration:
607    /// 1. Validates that headers are properly formatted as "name: value"
608    /// 2. Validates header names and values are valid HTTP headers
609    /// 3. Validates upstream exists if specified
610    /// 4. Validates rewrite pattern is valid regex if specified
611    fn validate_with_upstream(
612        &self,
613        upstream_names: Option<&[String]>,
614    ) -> Result<()> {
615        // Helper function to validate HTTP headers
616        let validate = |headers: &Option<Vec<String>>| -> Result<()> {
617            if let Some(headers) = headers {
618                for header in headers.iter() {
619                    // Split header into name and value parts
620                    let arr = header
621                        .split_once(':')
622                        .map(|(k, v)| (k.trim(), v.trim()));
623                    let Some((header_name, header_value)) = arr else {
624                        return Err(Error::Invalid {
625                            message: format!("header {header} is invalid"),
626                        });
627                    };
628
629                    // Validate header name is valid
630                    HeaderName::from_bytes(header_name.as_bytes()).map_err(|err| Error::Invalid {
631                        message: format!("header name({header_name}) is invalid, error: {err}"),
632                    })?;
633
634                    // Validate header value is valid
635                    HeaderValue::from_str(header_value).map_err(|err| Error::Invalid {
636                        message: format!("header value({header_value}) is invalid, error: {err}"),
637                    })?;
638                }
639            }
640            Ok(())
641        };
642
643        // Validate upstream exists if specified
644        if let Some(upstream_names) = upstream_names {
645            let upstream = self.upstream.clone().unwrap_or_default();
646            if !upstream.is_empty()
647                && !upstream.starts_with("$")
648                && !upstream_names.contains(&upstream)
649            {
650                return Err(Error::Invalid {
651                    message: format!("upstream({upstream}) is not found"),
652                });
653            }
654        }
655
656        // Validate headers
657        validate(&self.proxy_add_headers)?;
658        validate(&self.proxy_set_headers)?;
659
660        // Validate rewrite pattern is valid regex
661        if let Some(value) = &self.rewrite {
662            let arr: Vec<&str> = value.split(' ').collect();
663            let _ =
664                Regex::new(arr[0]).map_err(|e| Error::Regex { source: e })?;
665        }
666
667        Ok(())
668    }
669
670    /// Calculates the matching priority weight for this location
671    /// Higher weight = higher priority
672    /// Weight is based on:
673    /// - Path match type (exact=1024, prefix=512, regex=256)
674    /// - Path length (up to 64)
675    /// - Host presence (+128)
676    ///
677    /// Returns either the manual weight if set, or calculated weight
678    pub fn get_weight(&self) -> u16 {
679        // Return manual weight if set
680        if let Some(weight) = self.weight {
681            return weight;
682        }
683
684        let mut weight: u16 = 0;
685        let path = self.path.clone().unwrap_or("".to_string());
686
687        // Add weight based on path match type and length
688        if path.len() > 1 {
689            if path.starts_with('=') {
690                weight += 1024; // Exact match
691            } else if path.starts_with('~') {
692                weight += 256; // Regex match
693            } else {
694                weight += 512; // Prefix match
695            }
696            weight += path.len().min(64) as u16;
697        };
698        // Add weight if host is specified
699        if let Some(host) = &self.host {
700            let exist_regex = host.split(',').any(|item| item.starts_with("~"));
701            // exact host weight is 128
702            // regexp host weight is host length
703            if !exist_regex && !host.is_empty() {
704                weight += 128;
705            } else {
706                weight += host.len() as u16;
707            }
708        }
709
710        weight
711    }
712}
713
714/// Configuration for a server instance that handles incoming HTTP/HTTPS requests
715#[derive(Debug, Default, Deserialize, Clone, Serialize)]
716pub struct ServerConf {
717    /// Address to listen on in format "host:port" or multiple addresses separated by commas
718    pub addr: String,
719
720    /// Access log format string for request logging
721    pub access_log: Option<String>,
722
723    /// List of location names that this server handles
724    pub locations: Option<Vec<String>>,
725
726    /// Number of worker threads for this server instance
727    pub threads: Option<usize>,
728
729    /// OpenSSL cipher list string for TLS connections
730    pub tls_cipher_list: Option<String>,
731
732    /// TLS 1.3 ciphersuites string
733    pub tls_ciphersuites: Option<String>,
734
735    /// Minimum TLS version to accept (e.g. "TLSv1.2")
736    pub tls_min_version: Option<String>,
737
738    /// Maximum TLS version to use (e.g. "TLSv1.3")
739    pub tls_max_version: Option<String>,
740
741    /// Whether to use global certificates instead of per-server certs
742    pub global_certificates: Option<bool>,
743
744    /// Whether to enable HTTP/2 protocol support
745    pub enabled_h2: Option<bool>,
746
747    /// TCP keepalive idle timeout
748    #[serde(default)]
749    #[serde(with = "humantime_serde")]
750    pub tcp_idle: Option<Duration>,
751
752    /// TCP keepalive probe interval
753    #[serde(default)]
754    #[serde(with = "humantime_serde")]
755    pub tcp_interval: Option<Duration>,
756
757    /// TCP keepalive user timeout
758    #[serde(default)]
759    #[serde(with = "humantime_serde")]
760    pub tcp_user_timeout: Option<Duration>,
761
762    // downstream read timeout
763    #[serde(default)]
764    #[serde(with = "humantime_serde")]
765    pub downstream_read_timeout: Option<Duration>,
766
767    // downstream write timeout
768    #[serde(default)]
769    #[serde(with = "humantime_serde")]
770    pub downstream_write_timeout: Option<Duration>,
771
772    /// Number of TCP keepalive probes before connection is dropped
773    pub tcp_probe_count: Option<usize>,
774
775    /// TCP Fast Open queue length (0 to disable)
776    pub tcp_fastopen: Option<usize>,
777
778    /// Enable SO_REUSEPORT to allow multiple sockets to bind to the same address and port.
779    /// This is useful for load balancing across multiple worker processes.
780    /// See the [man page](https://man7.org/linux/man-pages/man7/socket.7.html) for more information.
781    pub reuse_port: Option<bool>,
782
783    /// Path to expose Prometheus metrics on
784    pub prometheus_metrics: Option<String>,
785
786    /// OpenTelemetry exporter configuration
787    pub otlp_exporter: Option<String>,
788
789    /// List of configuration files to include
790    pub includes: Option<Vec<String>>,
791
792    /// List of modules to enable for this server
793    pub modules: Option<Vec<String>>,
794
795    /// Whether to enable server-timing header
796    pub enable_server_timing: Option<bool>,
797
798    /// Optional description/notes about this server
799    pub remark: Option<String>,
800}
801
802impl Validate for ServerConf {
803    fn validate(&self) -> Result<()> {
804        self.validate_with_locations(&[])?;
805        Ok(())
806    }
807}
808
809impl ServerConf {
810    /// Validate the options of server config.
811    /// 1. Parse listen addr to socket addr.
812    /// 2. Check the locations are exists.
813    /// 3. Parse access log layout success.
814    fn validate_with_locations(&self, location_names: &[String]) -> Result<()> {
815        for addr in self.addr.split(',') {
816            let _ = addr.to_socket_addrs().map_err(|e| Error::Io {
817                source: e,
818                file: self.addr.clone(),
819            })?;
820        }
821        if !location_names.is_empty()
822            && let Some(locations) = &self.locations
823        {
824            for item in locations {
825                if !location_names.contains(item) {
826                    return Err(Error::Invalid {
827                        message: format!("location({item}) is not found"),
828                    });
829                }
830            }
831        }
832        let access_log = self.access_log.clone().unwrap_or_default();
833        if !access_log.is_empty() {
834            // TODO: validate access log format
835            // let logger = Parser::from(access_log.as_str());
836            // if logger.tags.is_empty() {
837            //     return Err(Error::Invalid {
838            //         message: "access log format is invalid".to_string(),
839            //     });
840            // }
841        }
842
843        Ok(())
844    }
845}
846
847/// Basic configuration options for the application
848#[derive(Debug, Default, Deserialize, Clone, Serialize)]
849pub struct BasicConf {
850    /// Application name
851    pub name: Option<String>,
852    /// Error page template
853    pub error_template: Option<String>,
854    /// Path to PID file (default: /run/pingap.pid)
855    pub pid_file: Option<String>,
856    /// Unix domain socket path for graceful upgrades(default: /tmp/pingap_upgrade.sock)
857    pub upgrade_sock: Option<String>,
858    /// User for daemon
859    pub user: Option<String>,
860    /// Group for daemon
861    pub group: Option<String>,
862    /// Number of worker threads(default: 1)
863    pub threads: Option<usize>,
864    /// Enable work stealing between worker threads(default: true)
865    pub work_stealing: Option<bool>,
866    /// Number of listener tasks to use per fd. This allows for parallel accepts.
867    pub listener_tasks_per_fd: Option<usize>,
868    /// Grace period before forcefully terminating during shutdown(default: 5m)
869    #[serde(default)]
870    #[serde(with = "humantime_serde")]
871    pub grace_period: Option<Duration>,
872    /// Maximum time to wait for graceful shutdown
873    #[serde(default)]
874    #[serde(with = "humantime_serde")]
875    pub graceful_shutdown_timeout: Option<Duration>,
876    /// Maximum number of idle connections to keep in upstream connection pool
877    pub upstream_keepalive_pool_size: Option<usize>,
878    /// Webhook URL for notifications
879    pub webhook: Option<String>,
880    /// Type of webhook (e.g. "wecom", "dingtalk")
881    pub webhook_type: Option<String>,
882    /// List of events to send webhook notifications for
883    pub webhook_notifications: Option<Vec<String>>,
884    /// Log level (debug, info, warn, error)
885    pub log_level: Option<String>,
886    /// Size of log buffer before flushing
887    pub log_buffered_size: Option<ByteSize>,
888    /// Whether to format logs as JSON
889    pub log_format_json: Option<bool>,
890    /// Sentry DSN for error reporting
891    pub sentry: Option<String>,
892    /// Pyroscope server URL for continuous profiling
893    pub pyroscope: Option<String>,
894    /// How often to check for configuration changes that require restart
895    #[serde(default)]
896    #[serde(with = "humantime_serde")]
897    pub auto_restart_check_interval: Option<Duration>,
898
899    // log compress algorithm: gzip, zstd
900    pub log_compress_algorithm: Option<String>,
901    /// Log compress level
902    pub log_compress_level: Option<u8>,
903    /// Log compress days ago
904    pub log_compress_days_ago: Option<u16>,
905    /// Log compress time point hour
906    pub log_compress_time_point_hour: Option<u8>,
907}
908
909impl Validate for BasicConf {
910    fn validate(&self) -> Result<()> {
911        Ok(())
912    }
913}
914
915impl BasicConf {
916    /// Returns the path to the PID file
917    /// - If pid_file is explicitly configured, uses that value
918    /// - Otherwise tries to use /run/pingap.pid or /var/run/pingap.pid if writable
919    /// - Falls back to /tmp/pingap.pid if neither system directories are writable
920    pub fn get_pid_file(&self) -> String {
921        if let Some(pid_file) = &self.pid_file {
922            return pid_file.clone();
923        }
924        for dir in ["/run", "/var/run"] {
925            if tempfile_in(dir).is_ok() {
926                return format!("{dir}/pingap.pid");
927            }
928        }
929        "/tmp/pingap.pid".to_string()
930    }
931}
932
933#[derive(Debug, Default, Deserialize, Clone, Serialize)]
934pub struct StorageConf {
935    pub category: String,
936    pub value: String,
937    pub secret: Option<String>,
938    pub remark: Option<String>,
939}
940
941impl Validate for StorageConf {
942    fn validate(&self) -> Result<()> {
943        Ok(())
944    }
945}
946
947pub trait Hashable: Hash {
948    fn hash_key(&self) -> String {
949        let mut hasher = DefaultHasher::new();
950        self.hash(&mut hasher);
951        format!("{:x}", hasher.finish())
952    }
953}
954impl Hashable for UpstreamConf {}
955impl Hashable for LocationConf {}
956
957#[derive(Deserialize, Debug, Serialize)]
958struct TomlConfig {
959    basic: Option<BasicConf>,
960    servers: Option<Map<String, Value>>,
961    upstreams: Option<Map<String, Value>>,
962    locations: Option<Map<String, Value>>,
963    plugins: Option<Map<String, Value>>,
964    certificates: Option<Map<String, Value>>,
965    storages: Option<Map<String, Value>>,
966}
967
968fn format_toml(value: &Value) -> String {
969    if let Some(value) = value.as_table() {
970        value.to_string()
971    } else {
972        "".to_string()
973    }
974}
975
976pub type PluginConf = Map<String, Value>;
977
978impl Validate for PluginConf {
979    fn validate(&self) -> Result<()> {
980        Ok(())
981    }
982}
983
984#[derive(Debug, Default, Clone, Deserialize, Serialize)]
985pub struct PingapConfig {
986    pub basic: BasicConf,
987    pub upstreams: HashMap<String, UpstreamConf>,
988    pub locations: HashMap<String, LocationConf>,
989    pub servers: HashMap<String, ServerConf>,
990    pub plugins: HashMap<String, PluginConf>,
991    pub certificates: HashMap<String, CertificateConf>,
992    pub storages: HashMap<String, StorageConf>,
993}
994
995impl PingapConfig {
996    // 需要一个辅助函数来避免重复
997    fn get_value_for_category<T: Serialize>(
998        &self,
999        map: &HashMap<String, T>,
1000        name: Option<&str>,
1001    ) -> Result<toml::Value> {
1002        match name {
1003            // 如果指定了名称,只序列化那一个项
1004            Some(name) => {
1005                if let Some(item) = map.get(name) {
1006                    let mut table = Map::new();
1007                    table.insert(
1008                        name.to_string(),
1009                        toml::Value::try_from(item)
1010                            .map_err(|e| Error::Ser { source: e })?,
1011                    );
1012                    Ok(toml::Value::Table(table))
1013                } else {
1014                    Ok(toml::Value::Table(Map::new())) // 未找到,返回空表
1015                }
1016            },
1017            // 否则,序列化整个类别
1018            None => Ok(toml::Value::try_from(map)
1019                .map_err(|e| Error::Ser { source: e })?),
1020        }
1021    }
1022    pub fn get_toml(
1023        &self,
1024        category: &str,
1025        name: Option<&str>,
1026    ) -> Result<(String, String)> {
1027        let (key, value_to_serialize) = match category {
1028            CATEGORY_SERVER => {
1029                ("servers", self.get_value_for_category(&self.servers, name)?)
1030            },
1031            CATEGORY_LOCATION => (
1032                "locations",
1033                self.get_value_for_category(&self.locations, name)?,
1034            ),
1035            CATEGORY_UPSTREAM => (
1036                "upstreams",
1037                self.get_value_for_category(&self.upstreams, name)?,
1038            ),
1039            CATEGORY_PLUGIN => {
1040                ("plugins", self.get_value_for_category(&self.plugins, name)?)
1041            },
1042            CATEGORY_CERTIFICATE => (
1043                "certificates",
1044                self.get_value_for_category(&self.certificates, name)?,
1045            ),
1046            CATEGORY_STORAGE => (
1047                "storages",
1048                self.get_value_for_category(&self.storages, name)?,
1049            ),
1050            _ => (
1051                CATEGORY_BASIC,
1052                toml::Value::try_from(&self.basic)
1053                    .map_err(|e| Error::Ser { source: e })?,
1054            ),
1055        };
1056
1057        let path = {
1058            let name = name.unwrap_or_default();
1059            if key == CATEGORY_BASIC || name.is_empty() {
1060                format!("/{key}.toml")
1061            } else {
1062                format!("/{key}/{name}.toml")
1063            }
1064        };
1065
1066        if let Some(table) = value_to_serialize.as_table()
1067            && table.is_empty()
1068        {
1069            return Ok((path, "".to_string()));
1070        }
1071
1072        let mut wrapper = Map::new();
1073        wrapper.insert(key.to_string(), value_to_serialize);
1074
1075        let toml_string = toml::to_string_pretty(&wrapper)
1076            .map_err(|e| Error::Ser { source: e })?;
1077
1078        Ok((path, toml_string))
1079    }
1080    pub fn get_storage_value(&self, name: &str) -> Result<String> {
1081        for (key, item) in self.storages.iter() {
1082            if key != name {
1083                continue;
1084            }
1085
1086            if let Some(key) = &item.secret {
1087                return pingap_util::aes_decrypt(key, &item.value).map_err(
1088                    |e| Error::Invalid {
1089                        message: e.to_string(),
1090                    },
1091                );
1092            }
1093            return Ok(item.value.clone());
1094        }
1095        Ok("".to_string())
1096    }
1097}
1098
1099fn convert_include_toml(
1100    data: &HashMap<String, String>,
1101    replace_includes: bool,
1102    mut value: Value,
1103) -> String {
1104    let Some(m) = value.as_table_mut() else {
1105        return "".to_string();
1106    };
1107    if !replace_includes {
1108        return m.to_string();
1109    }
1110    if let Some(includes) = m.remove("includes")
1111        && let Some(includes) = get_include_toml(data, includes)
1112        && let Ok(includes) = toml::from_str::<Table>(&includes)
1113    {
1114        for (key, value) in includes.iter() {
1115            m.insert(key.to_string(), value.clone());
1116        }
1117    }
1118    m.to_string()
1119}
1120
1121fn get_include_toml(
1122    data: &HashMap<String, String>,
1123    includes: Value,
1124) -> Option<String> {
1125    let values = includes.as_array()?;
1126    let arr: Vec<String> = values
1127        .iter()
1128        .map(|item| {
1129            let key = item.as_str().unwrap_or_default();
1130            if let Some(value) = data.get(key) {
1131                value.clone()
1132            } else {
1133                "".to_string()
1134            }
1135        })
1136        .collect();
1137    Some(arr.join("\n"))
1138}
1139
1140pub(crate) fn convert_pingap_config(
1141    data: &[u8],
1142    replace_include: bool,
1143) -> Result<PingapConfig, Error> {
1144    let data: TomlConfig = toml::from_str(
1145        std::string::String::from_utf8_lossy(data)
1146            .to_string()
1147            .as_str(),
1148    )
1149    .map_err(|e| Error::De { source: e })?;
1150
1151    let mut conf = PingapConfig {
1152        basic: data.basic.unwrap_or_default(),
1153        ..Default::default()
1154    };
1155    let mut includes = HashMap::new();
1156    for (name, value) in data.storages.unwrap_or_default() {
1157        let toml = format_toml(&value);
1158        let storage: StorageConf = toml::from_str(toml.as_str())
1159            .map_err(|e| Error::De { source: e })?;
1160        includes.insert(name.clone(), storage.value.clone());
1161        conf.storages.insert(name, storage);
1162    }
1163
1164    for (name, value) in data.upstreams.unwrap_or_default() {
1165        let toml = convert_include_toml(&includes, replace_include, value);
1166
1167        let upstream: UpstreamConf = toml::from_str(toml.as_str())
1168            .map_err(|e| Error::De { source: e })?;
1169        conf.upstreams.insert(name, upstream);
1170    }
1171    for (name, value) in data.locations.unwrap_or_default() {
1172        let toml = convert_include_toml(&includes, replace_include, value);
1173
1174        let location: LocationConf = toml::from_str(toml.as_str())
1175            .map_err(|e| Error::De { source: e })?;
1176        conf.locations.insert(name, location);
1177    }
1178    for (name, value) in data.servers.unwrap_or_default() {
1179        let toml = convert_include_toml(&includes, replace_include, value);
1180
1181        let server: ServerConf = toml::from_str(toml.as_str())
1182            .map_err(|e| Error::De { source: e })?;
1183        conf.servers.insert(name, server);
1184    }
1185    for (name, value) in data.plugins.unwrap_or_default() {
1186        let plugin: PluginConf = toml::from_str(format_toml(&value).as_str())
1187            .map_err(|e| Error::De { source: e })?;
1188        conf.plugins.insert(name, plugin);
1189    }
1190
1191    for (name, value) in data.certificates.unwrap_or_default() {
1192        let certificate: CertificateConf =
1193            toml::from_str(format_toml(&value).as_str())
1194                .map_err(|e| Error::De { source: e })?;
1195        conf.certificates.insert(name, certificate);
1196    }
1197
1198    Ok(conf)
1199}
1200
1201#[derive(Debug, Default, Clone, Deserialize, Serialize)]
1202struct Description {
1203    category: String,
1204    name: String,
1205    data: String,
1206}
1207
1208impl PingapConfig {
1209    pub fn new(data: &[u8], replace_includes: bool) -> Result<Self> {
1210        convert_pingap_config(data, replace_includes)
1211    }
1212    /// Validate the options of pinggap config.
1213    pub fn validate(&self) -> Result<()> {
1214        let mut upstream_names = vec![];
1215        for (name, upstream) in self.upstreams.iter() {
1216            upstream.validate()?;
1217            upstream_names.push(name.to_string());
1218        }
1219        let mut location_names = vec![];
1220        for (name, location) in self.locations.iter() {
1221            location.validate_with_upstream(Some(&upstream_names))?;
1222            location_names.push(name.to_string());
1223        }
1224        let mut listen_addr_list = vec![];
1225        for server in self.servers.values() {
1226            for addr in server.addr.split(',') {
1227                if listen_addr_list.contains(&addr.to_string()) {
1228                    return Err(Error::Invalid {
1229                        message: format!("{addr} is inused by other server"),
1230                    });
1231                }
1232                listen_addr_list.push(addr.to_string());
1233            }
1234            server.validate_with_locations(&location_names)?;
1235        }
1236        // TODO: validate plugins
1237        // for (name, plugin) in self.plugins.iter() {
1238        //     parse_plugins(vec![(name.to_string(), plugin.clone())]).map_err(
1239        //         |e| Error::Invalid {
1240        //             message: e.to_string(),
1241        //         },
1242        //     )?;
1243        // }
1244        for (_, certificate) in self.certificates.iter() {
1245            certificate.validate()?;
1246        }
1247        let ping_conf = toml::to_string_pretty(self)
1248            .map_err(|e| Error::Ser { source: e })?;
1249        convert_pingap_config(ping_conf.as_bytes(), true)?;
1250        Ok(())
1251    }
1252    /// Generate the content hash of config.
1253    pub fn hash(&self) -> Result<String> {
1254        let mut lines = vec![];
1255        for desc in self.descriptions() {
1256            lines.push(desc.category);
1257            lines.push(desc.name);
1258            lines.push(desc.data);
1259        }
1260        let hash = crc32fast::hash(lines.join("\n").as_bytes());
1261        Ok(format!("{hash:X}"))
1262    }
1263    /// Remove the config by name.
1264    pub fn remove(&mut self, category: &str, name: &str) -> Result<()> {
1265        match category {
1266            CATEGORY_UPSTREAM => {
1267                for (location_name, location) in self.locations.iter() {
1268                    if let Some(upstream) = &location.upstream
1269                        && upstream == name
1270                    {
1271                        return Err(Error::Invalid {
1272                            message: format!(
1273                                "upstream({name}) is in used by location({location_name})",
1274                            ),
1275                        });
1276                    }
1277                }
1278                self.upstreams.remove(name);
1279            },
1280            CATEGORY_LOCATION => {
1281                for (server_name, server) in self.servers.iter() {
1282                    if let Some(locations) = &server.locations
1283                        && locations.contains(&name.to_string())
1284                    {
1285                        return Err(Error::Invalid {
1286                            message: format!(
1287                                "location({name}) is in used by server({server_name})"
1288                            ),
1289                        });
1290                    }
1291                }
1292                self.locations.remove(name);
1293            },
1294            CATEGORY_SERVER => {
1295                self.servers.remove(name);
1296            },
1297            CATEGORY_PLUGIN => {
1298                for (location_name, location) in self.locations.iter() {
1299                    if let Some(plugins) = &location.plugins
1300                        && plugins.contains(&name.to_string())
1301                    {
1302                        return Err(Error::Invalid {
1303                            message: format!(
1304                                "proxy plugin({name}) is in used by location({location_name})"
1305                            ),
1306                        });
1307                    }
1308                }
1309                self.plugins.remove(name);
1310            },
1311            CATEGORY_CERTIFICATE => {
1312                self.certificates.remove(name);
1313            },
1314            _ => {},
1315        };
1316        Ok(())
1317    }
1318    fn descriptions(&self) -> Vec<Description> {
1319        let mut value = self.clone();
1320        let mut descriptions = vec![];
1321        for (name, data) in value.servers.iter() {
1322            descriptions.push(Description {
1323                category: CATEGORY_SERVER.to_string(),
1324                name: format!("server:{name}"),
1325                data: toml::to_string_pretty(data).unwrap_or_default(),
1326            });
1327        }
1328        for (name, data) in value.locations.iter() {
1329            descriptions.push(Description {
1330                category: CATEGORY_LOCATION.to_string(),
1331                name: format!("location:{name}"),
1332                data: toml::to_string_pretty(data).unwrap_or_default(),
1333            });
1334        }
1335        for (name, data) in value.upstreams.iter() {
1336            descriptions.push(Description {
1337                category: CATEGORY_UPSTREAM.to_string(),
1338                name: format!("upstream:{name}"),
1339                data: toml::to_string_pretty(data).unwrap_or_default(),
1340            });
1341        }
1342        for (name, data) in value.plugins.iter() {
1343            descriptions.push(Description {
1344                category: CATEGORY_PLUGIN.to_string(),
1345                name: format!("plugin:{name}"),
1346                data: toml::to_string_pretty(data).unwrap_or_default(),
1347            });
1348        }
1349        for (name, data) in value.certificates.iter() {
1350            let mut clone_data = data.clone();
1351            if let Some(cert) = &clone_data.tls_cert {
1352                clone_data.tls_cert = Some(format!(
1353                    "crc32:{:X}",
1354                    crc32fast::hash(cert.as_bytes())
1355                ));
1356            }
1357            if let Some(key) = &clone_data.tls_key {
1358                clone_data.tls_key = Some(format!(
1359                    "crc32:{:X}",
1360                    crc32fast::hash(key.as_bytes())
1361                ));
1362            }
1363            descriptions.push(Description {
1364                category: CATEGORY_CERTIFICATE.to_string(),
1365                name: format!("certificate:{name}"),
1366                data: toml::to_string_pretty(&clone_data).unwrap_or_default(),
1367            });
1368        }
1369        for (name, data) in value.storages.iter() {
1370            let mut clone_data = data.clone();
1371            if let Some(secret) = &clone_data.secret {
1372                clone_data.secret = Some(format!(
1373                    "crc32:{:X}",
1374                    crc32fast::hash(secret.as_bytes())
1375                ));
1376            }
1377            descriptions.push(Description {
1378                category: CATEGORY_STORAGE.to_string(),
1379                name: format!("storage:{name}"),
1380                data: toml::to_string_pretty(&clone_data).unwrap_or_default(),
1381            });
1382        }
1383        value.servers = HashMap::new();
1384        value.locations = HashMap::new();
1385        value.upstreams = HashMap::new();
1386        value.plugins = HashMap::new();
1387        value.certificates = HashMap::new();
1388        value.storages = HashMap::new();
1389        descriptions.push(Description {
1390            category: CATEGORY_BASIC.to_string(),
1391            name: CATEGORY_BASIC.to_string(),
1392            data: toml::to_string_pretty(&value).unwrap_or_default(),
1393        });
1394        descriptions.sort_by_key(|d| d.name.clone());
1395        descriptions
1396    }
1397    /// Get the different content of two config.
1398    pub fn diff(&self, other: &PingapConfig) -> (Vec<String>, Vec<String>) {
1399        // 1. 将描述列表转换为 HashMap,以便进行高效的键查找。
1400        let current_map: HashMap<_, _> = self
1401            .descriptions()
1402            .into_iter()
1403            .map(|d| (d.name.clone(), d))
1404            .collect();
1405        let new_map: HashMap<_, _> = other
1406            .descriptions()
1407            .into_iter()
1408            .map(|d| (d.name.clone(), d))
1409            .collect();
1410
1411        // 使用 HashSet 存储受影响的类别,以自动处理重复。
1412        let mut affected_categories = HashSet::new();
1413
1414        // 分别存储新增、删除和修改的项,以便最后格式化输出。
1415        let mut added_items = vec![];
1416        let mut removed_items = vec![];
1417        let mut modified_items = vec![];
1418
1419        // 2. 遍历当前配置,查找被删除或被修改的项。
1420        for (name, current_item) in &current_map {
1421            match new_map.get(name) {
1422                Some(new_item) => {
1423                    // 键存在于两个配置中,检查内容是否发生变化。
1424                    if current_item.data != new_item.data {
1425                        affected_categories
1426                            .insert(current_item.category.clone());
1427
1428                        // 使用 diff::lines 生成逐行差异
1429                        let mut item_diff_result = vec![];
1430                        for diff in
1431                            diff::lines(&current_item.data, &new_item.data)
1432                        {
1433                            match diff {
1434                                diff::Result::Left(l) => {
1435                                    item_diff_result.push(format!("- {l}"))
1436                                },
1437                                diff::Result::Right(r) => {
1438                                    item_diff_result.push(format!("+ {r}"))
1439                                },
1440                                _ => (),
1441                            }
1442                        }
1443
1444                        if !item_diff_result.is_empty() {
1445                            modified_items.push(format!("[MODIFIED] {name}"));
1446                            modified_items.extend(item_diff_result);
1447                            modified_items.push("".to_string()); // 添加空行分隔
1448                        }
1449                    }
1450                },
1451                None => {
1452                    // 在新配置中不存在,说明该项已被删除。
1453                    removed_items.push(format!("-- [REMOVED] {name}"));
1454                    affected_categories.insert(current_item.category.clone());
1455                },
1456            }
1457        }
1458
1459        // 3. 遍历新配置,查找新增的项。
1460        for (name, new_item) in &new_map {
1461            if !current_map.contains_key(name) {
1462                added_items.push(format!("++ [ADDED] {name}"));
1463                affected_categories.insert(new_item.category.clone());
1464            }
1465        }
1466
1467        // 4. 组合所有差异,生成最终的可读输出。
1468        let mut final_diff = Vec::new();
1469        if !added_items.is_empty() {
1470            final_diff.extend(added_items);
1471            final_diff.push("".to_string()); // 添加空行分隔
1472        }
1473        if !removed_items.is_empty() {
1474            final_diff.extend(removed_items);
1475            final_diff.push("".to_string());
1476        }
1477        if !modified_items.is_empty() {
1478            final_diff.extend(modified_items);
1479        }
1480
1481        // 将 HashSet 转换为 Vec 并返回结果
1482        (affected_categories.into_iter().collect(), final_diff)
1483    }
1484}
1485
1486#[cfg(test)]
1487mod tests {
1488    use super::{CertificateConf, Hashable, Validate, validate_cert};
1489    use super::{LocationConf, PluginCategory, ServerConf, UpstreamConf};
1490    use pingap_core::PluginStep;
1491    use pingap_util::base64_encode;
1492    use pretty_assertions::assert_eq;
1493    use serde::{Deserialize, Serialize};
1494    use std::str::FromStr;
1495
1496    #[test]
1497    fn test_plugin_step() {
1498        let step = PluginStep::from_str("early_request").unwrap();
1499        assert_eq!(step, PluginStep::EarlyRequest);
1500
1501        assert_eq!("early_request", step.to_string());
1502    }
1503
1504    #[test]
1505    fn test_validate_cert() {
1506        // spellchecker:off
1507        let pem = r#"-----BEGIN CERTIFICATE-----
1508MIIEljCCAv6gAwIBAgIQeYUdeFj3gpzhQes3aGaMZTANBgkqhkiG9w0BAQsFADCB
1509pTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMT0wOwYDVQQLDDR4aWVz
1510aHV6aG91QHhpZXNodXpob3VzLU1hY0Jvb2stQWlyLmxvY2FsICjosKLmoJHmtLIp
1511MUQwQgYDVQQDDDtta2NlcnQgeGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29r
1512LUFpci5sb2NhbCAo6LCi5qCR5rSyKTAeFw0yMzA5MjQxMzA1MjdaFw0yNTEyMjQx
1513MzA1MjdaMGgxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0
1514ZTE9MDsGA1UECww0eGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29rLUFpci5s
1515b2NhbCAo6LCi5qCR5rSyKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
1516ALuJ8lYEj9uf4iE9hguASq7re87Np+zJc2x/eqr1cR/SgXRStBsjxqI7i3xwMRqX
1517AuhAnM6ktlGuqidl7D9y6AN/UchqgX8AetslRJTpCcEDfL/q24zy0MqOS0FlYEgh
1518s4PIjWsSNoglBDeaIdUpN9cM/64IkAAtHndNt2p2vPfjrPeixLjese096SKEnZM/
1519xBdWF491hx06IyzjtWKqLm9OUmYZB9d/gDGnDsKpqClw8m95opKD4TBHAoE//WvI
1520m1mZnjNTNR27vVbmnc57d2Lx2Ib2eqJG5zMsP2hPBoqS8CKEwMRFLHAcclNkI67U
1521kcSEGaWgr15QGHJPN/FtjDsCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud
1522JQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFJo0y9bYUM/OuenDjsJ1RyHJfL3n
1523MDQGA1UdEQQtMCuCBm1lLmRldoIJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAA
1524AAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBgQAlQbow3+4UyQx+E+J0RwmHBltU6i+K
1525soFfza6FWRfAbTyv+4KEWl2mx51IfHhJHYZvsZqPqGWxm5UvBecskegDExFMNFVm
1526O5QixydQzHHY2krmBwmDZ6Ao88oW/qw4xmMUhzKAZbsqeQyE/uiUdyI4pfDcduLB
1527rol31g9OFsgwZrZr0d1ZiezeYEhemnSlh9xRZW3veKx9axgFttzCMmWdpGTCvnav
1528ZVc3rB+KBMjdCwsS37zmrNm9syCjW1O5a1qphwuMpqSnDHBgKWNpbsgqyZM0oyOc
15299Bkja+BV5wFO+4zH5WtestcrNMeoQ83a5lI0m42u/bUEJ/T/5BQBSFidNuvS7Ylw
1530IZpXa00xvlnm1BOHOfRI4Ehlfa5jmfcdnrGkQLGjiyygQtKcc7rOXGK+mSeyxwhs
1531sIARwslSQd4q0dbYTPKvvUHxTYiCv78vQBAsE15T2GGS80pAFDBW9vOf3upANvOf
1532EHjKf0Dweb4ppL4ddgeAKU5V0qn76K2fFaE=
1533-----END CERTIFICATE-----"#;
1534        // spellchecker:on
1535        let result = validate_cert(pem);
1536        assert_eq!(true, result.is_ok());
1537
1538        let value = base64_encode(pem);
1539        let result = validate_cert(&value);
1540        assert_eq!(true, result.is_ok());
1541    }
1542
1543    #[test]
1544    fn test_plugin_category_serde() {
1545        #[derive(Deserialize, Serialize)]
1546        struct TmpPluginCategory {
1547            category: PluginCategory,
1548        }
1549        let tmp = TmpPluginCategory {
1550            category: PluginCategory::RequestId,
1551        };
1552        let data = serde_json::to_string(&tmp).unwrap();
1553        assert_eq!(r#"{"category":"request_id"}"#, data);
1554
1555        let tmp: TmpPluginCategory = serde_json::from_str(&data).unwrap();
1556        assert_eq!(PluginCategory::RequestId, tmp.category);
1557    }
1558
1559    #[test]
1560    fn test_upstream_conf() {
1561        let mut conf = UpstreamConf::default();
1562
1563        let result = conf.validate();
1564        assert_eq!(true, result.is_err());
1565        assert_eq!(
1566            "Invalid error upstream addrs is empty",
1567            result.expect_err("").to_string()
1568        );
1569
1570        conf.addrs = vec!["127.0.0.1".to_string(), "github".to_string()];
1571        conf.discovery = Some("static".to_string());
1572        let result = conf.validate();
1573        assert_eq!(true, result.is_err());
1574        assert_eq!(
1575            true,
1576            result
1577                .expect_err("")
1578                .to_string()
1579                .contains("Io error failed to lookup address information")
1580        );
1581
1582        conf.addrs = vec!["127.0.0.1".to_string(), "github.com".to_string()];
1583        conf.health_check = Some("http:///".to_string());
1584        let result = conf.validate();
1585        assert_eq!(true, result.is_err());
1586        assert_eq!(
1587            "Url parse error empty host, http:///",
1588            result.expect_err("").to_string()
1589        );
1590
1591        conf.health_check = Some("http://github.com/".to_string());
1592        let result = conf.validate();
1593        assert_eq!(true, result.is_ok());
1594    }
1595
1596    #[test]
1597    fn test_upstream_invalid_ip() {
1598        let invalid_addrs = vec![
1599            "0.0.0.0:80",
1600            "255.255.255.255:80",
1601            "224.0.0.1:80",
1602            "169.254.1.1:80",
1603            "[::]:80",
1604            "[ff02::1]:80",
1605            "[fe80::1]:80",
1606        ];
1607        for addr in invalid_addrs {
1608            let conf = UpstreamConf {
1609                addrs: vec![addr.to_string()],
1610                discovery: Some("static".to_string()),
1611                ..Default::default()
1612            };
1613            let result = conf.validate();
1614            assert!(
1615                result.is_err(),
1616                "{addr} should be rejected as invalid upstream IP"
1617            );
1618            assert!(
1619                result.unwrap_err().to_string().contains("invalid IP"),
1620                "{addr} error should mention invalid IP"
1621            );
1622        }
1623
1624        let valid_addrs =
1625            vec!["127.0.0.1:80", "192.168.1.1:80", "10.0.0.1:8080"];
1626        for addr in valid_addrs {
1627            let conf = UpstreamConf {
1628                addrs: vec![addr.to_string()],
1629                discovery: Some("static".to_string()),
1630                ..Default::default()
1631            };
1632            let result = conf.validate();
1633            assert!(
1634                result.is_ok(),
1635                "{addr} should be accepted as valid upstream IP"
1636            );
1637        }
1638    }
1639
1640    #[test]
1641    fn test_location_conf() {
1642        let mut conf = LocationConf::default();
1643        let upstream_names = vec!["upstream1".to_string()];
1644
1645        conf.upstream = Some("upstream2".to_string());
1646        let result = conf.validate_with_upstream(Some(&upstream_names));
1647        assert_eq!(true, result.is_err());
1648        assert_eq!(
1649            "Invalid error upstream(upstream2) is not found",
1650            result.expect_err("").to_string()
1651        );
1652
1653        conf.upstream = Some("upstream1".to_string());
1654        conf.proxy_set_headers = Some(vec!["X-Request-Id".to_string()]);
1655        let result = conf.validate_with_upstream(Some(&upstream_names));
1656        assert_eq!(true, result.is_err());
1657        assert_eq!(
1658            "Invalid error header X-Request-Id is invalid",
1659            result.expect_err("").to_string()
1660        );
1661
1662        conf.proxy_set_headers = Some(vec!["请求:响应".to_string()]);
1663        let result = conf.validate_with_upstream(Some(&upstream_names));
1664        assert_eq!(true, result.is_err());
1665        assert_eq!(
1666            "Invalid error header name(请求) is invalid, error: invalid HTTP header name",
1667            result.expect_err("").to_string()
1668        );
1669
1670        conf.proxy_set_headers = Some(vec!["X-Request-Id: abcd".to_string()]);
1671        let result = conf.validate_with_upstream(Some(&upstream_names));
1672        assert_eq!(true, result.is_ok());
1673
1674        conf.rewrite = Some(r"foo(bar".to_string());
1675        let result = conf.validate_with_upstream(Some(&upstream_names));
1676        assert_eq!(true, result.is_err());
1677        assert_eq!(
1678            true,
1679            result
1680                .expect_err("")
1681                .to_string()
1682                .starts_with("Regex error regex parse error")
1683        );
1684
1685        conf.rewrite = Some(r"^/api /".to_string());
1686        let result = conf.validate_with_upstream(Some(&upstream_names));
1687        assert_eq!(true, result.is_ok());
1688    }
1689
1690    #[test]
1691    fn test_location_get_wegiht() {
1692        let mut conf = LocationConf {
1693            weight: Some(2048),
1694            ..Default::default()
1695        };
1696
1697        assert_eq!(2048, conf.get_weight());
1698
1699        conf.weight = None;
1700        conf.path = Some("=/api".to_string());
1701        assert_eq!(1029, conf.get_weight());
1702
1703        conf.path = Some("~/api".to_string());
1704        assert_eq!(261, conf.get_weight());
1705
1706        conf.path = Some("/api".to_string());
1707        assert_eq!(516, conf.get_weight());
1708
1709        conf.path = None;
1710        conf.host = Some("github.com".to_string());
1711        assert_eq!(128, conf.get_weight());
1712
1713        conf.host = Some("~github.com".to_string());
1714        assert_eq!(11, conf.get_weight());
1715
1716        conf.host = Some("".to_string());
1717        assert_eq!(0, conf.get_weight());
1718    }
1719
1720    #[test]
1721    fn test_server_conf() {
1722        let mut conf = ServerConf::default();
1723        let location_names = vec!["lo".to_string()];
1724
1725        let result = conf.validate_with_locations(&location_names);
1726        assert_eq!(true, result.is_err());
1727        assert_eq!(
1728            "Io error invalid socket address, ",
1729            result.expect_err("").to_string()
1730        );
1731
1732        conf.addr = "127.0.0.1:3001".to_string();
1733        conf.locations = Some(vec!["lo1".to_string()]);
1734        let result = conf.validate_with_locations(&location_names);
1735        assert_eq!(true, result.is_err());
1736        assert_eq!(
1737            "Invalid error location(lo1) is not found",
1738            result.expect_err("").to_string()
1739        );
1740
1741        conf.locations = Some(vec!["lo".to_string()]);
1742        let result = conf.validate_with_locations(&location_names);
1743        assert_eq!(true, result.is_ok());
1744    }
1745
1746    #[test]
1747    fn test_certificate_conf() {
1748        // spellchecker:off
1749        let pem = r#"-----BEGIN CERTIFICATE-----
1750MIIEljCCAv6gAwIBAgIQeYUdeFj3gpzhQes3aGaMZTANBgkqhkiG9w0BAQsFADCB
1751pTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMT0wOwYDVQQLDDR4aWVz
1752aHV6aG91QHhpZXNodXpob3VzLU1hY0Jvb2stQWlyLmxvY2FsICjosKLmoJHmtLIp
1753MUQwQgYDVQQDDDtta2NlcnQgeGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29r
1754LUFpci5sb2NhbCAo6LCi5qCR5rSyKTAeFw0yMzA5MjQxMzA1MjdaFw0yNTEyMjQx
1755MzA1MjdaMGgxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0
1756ZTE9MDsGA1UECww0eGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29rLUFpci5s
1757b2NhbCAo6LCi5qCR5rSyKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
1758ALuJ8lYEj9uf4iE9hguASq7re87Np+zJc2x/eqr1cR/SgXRStBsjxqI7i3xwMRqX
1759AuhAnM6ktlGuqidl7D9y6AN/UchqgX8AetslRJTpCcEDfL/q24zy0MqOS0FlYEgh
1760s4PIjWsSNoglBDeaIdUpN9cM/64IkAAtHndNt2p2vPfjrPeixLjese096SKEnZM/
1761xBdWF491hx06IyzjtWKqLm9OUmYZB9d/gDGnDsKpqClw8m95opKD4TBHAoE//WvI
1762m1mZnjNTNR27vVbmnc57d2Lx2Ib2eqJG5zMsP2hPBoqS8CKEwMRFLHAcclNkI67U
1763kcSEGaWgr15QGHJPN/FtjDsCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud
1764JQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFJo0y9bYUM/OuenDjsJ1RyHJfL3n
1765MDQGA1UdEQQtMCuCBm1lLmRldoIJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAA
1766AAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBgQAlQbow3+4UyQx+E+J0RwmHBltU6i+K
1767soFfza6FWRfAbTyv+4KEWl2mx51IfHhJHYZvsZqPqGWxm5UvBecskegDExFMNFVm
1768O5QixydQzHHY2krmBwmDZ6Ao88oW/qw4xmMUhzKAZbsqeQyE/uiUdyI4pfDcduLB
1769rol31g9OFsgwZrZr0d1ZiezeYEhemnSlh9xRZW3veKx9axgFttzCMmWdpGTCvnav
1770ZVc3rB+KBMjdCwsS37zmrNm9syCjW1O5a1qphwuMpqSnDHBgKWNpbsgqyZM0oyOc
17719Bkja+BV5wFO+4zH5WtestcrNMeoQ83a5lI0m42u/bUEJ/T/5BQBSFidNuvS7Ylw
1772IZpXa00xvlnm1BOHOfRI4Ehlfa5jmfcdnrGkQLGjiyygQtKcc7rOXGK+mSeyxwhs
1773sIARwslSQd4q0dbYTPKvvUHxTYiCv78vQBAsE15T2GGS80pAFDBW9vOf3upANvOf
1774EHjKf0Dweb4ppL4ddgeAKU5V0qn76K2fFaE=
1775-----END CERTIFICATE-----"#;
1776        let key = r#"-----BEGIN PRIVATE KEY-----
1777MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7ifJWBI/bn+Ih
1778PYYLgEqu63vOzafsyXNsf3qq9XEf0oF0UrQbI8aiO4t8cDEalwLoQJzOpLZRrqon
1779Zew/cugDf1HIaoF/AHrbJUSU6QnBA3y/6tuM8tDKjktBZWBIIbODyI1rEjaIJQQ3
1780miHVKTfXDP+uCJAALR53Tbdqdrz346z3osS43rHtPekihJ2TP8QXVhePdYcdOiMs
178147Viqi5vTlJmGQfXf4Axpw7CqagpcPJveaKSg+EwRwKBP/1ryJtZmZ4zUzUdu71W
17825p3Oe3di8diG9nqiRuczLD9oTwaKkvAihMDERSxwHHJTZCOu1JHEhBmloK9eUBhy
1783TzfxbYw7AgMBAAECggEALjed0FMJfO+XE+gMm9L/FMKV3W5TXwh6eJemDHG2ckg3
1784fQpQtouHjT2tb3par5ndro0V19tBzzmDV3hH048m3I3JAuI0ja75l/5EO4p+y+Fn
1785IgjoGIFSsUiGBVTNeJlNm0GWkHeJlt3Af09t3RFuYIIklKgpjNGRu4ccl5ExmslF
1786WHv7/1dwzeJCi8iOY2gJZz6N7qHD95VkgVyDj/EtLltONAtIGVdorgq70CYmtwSM
17879XgXszqOTtSJxle+UBmeQTL4ZkUR0W+h6JSpcTn0P9c3fiNDrHSKFZbbpAhO/wHd
1788Ab4IK8IksVyg+tem3m5W9QiXn3WbgcvjJTi83Y3syQKBgQD5IsaSbqwEG3ruttQe
1789yfMeq9NUGVfmj7qkj2JiF4niqXwTpvoaSq/5gM/p7lAtSMzhCKtlekP8VLuwx8ih
1790n4hJAr8pGfyu/9IUghXsvP2DXsCKyypbhzY/F2m4WNIjtyLmed62Nt1PwWWUlo9Q
1791igHI6pieT45vJTBICsRyqC/a/wKBgQDAtLXUsCABQDTPHdy/M/dHZA/QQ/xU8NOs
1792ul5UMJCkSfFNk7b2etQG/iLlMSNup3bY3OPvaCGwwEy/gZ31tTSymgooXQMFxJ7G
17931S/DF45yKD6xJEmAUhwz/Hzor1cM95g78UpZFCEVMnEmkBNb9pmrXRLDuWb0vLE6
1794B6YgiEP6xQKBgBOXuooVjg2co6RWWIQ7WZVV6f65J4KIVyNN62zPcRaUQZ/CB/U9
1795Xm1+xdsd1Mxa51HjPqdyYBpeB4y1iX+8bhlfz+zJkGeq0riuKk895aoJL5c6txAP
1796qCJ6EuReh9grNOFvQCaQVgNJsFVpKcgpsk48tNfuZcMz54Ii5qQlue29AoGAA2Sr
1797Nv2K8rqws1zxQCSoHAe1B5PK46wB7i6x7oWUZnAu4ZDSTfDHvv/GmYaN+yrTuunY
17980aRhw3z/XPfpUiRIs0RnHWLV5MobiaDDYIoPpg7zW6cp7CqF+JxfjrFXtRC/C38q
1799MftawcbLm0Q6MwpallvjMrMXDwQrkrwDvtrnZ4kCgYEA0oSvmSK5ADD0nqYFdaro
1800K+hM90AVD1xmU7mxy3EDPwzjK1wZTj7u0fvcAtZJztIfL+lmVpkvK8KDLQ9wCWE7
1801SGToOzVHYX7VazxioA9nhNne9kaixvnIUg3iowAz07J7o6EU8tfYsnHxsvjlIkBU
1802ai02RHnemmqJaNepfmCdyec=
1803-----END PRIVATE KEY-----"#;
1804        // spellchecker:on
1805        let conf = CertificateConf {
1806            tls_cert: Some(pem.to_string()),
1807            tls_key: Some(key.to_string()),
1808            ..Default::default()
1809        };
1810        let result = conf.validate();
1811        assert_eq!(true, result.is_ok());
1812
1813        // spellchecker:off
1814        assert_eq!("15ba921aee80abc3", conf.hash_key());
1815        // spellchecker:on
1816    }
1817}