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::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
390impl Validate for UpstreamConf {
391    /// Validates the upstream configuration:
392    /// 1. The address list can't be empty
393    /// 2. For static discovery, addresses must be valid socket addresses
394    /// 3. Health check URL must be valid if specified
395    /// 4. TCP probe count must not exceed maximum (16)
396    fn validate(&self) -> Result<()> {
397        // Validate address list
398        self.validate_addresses()?;
399
400        // Validate health check URL if specified
401        self.validate_health_check()?;
402
403        // Validate TCP probe count
404        self.validate_tcp_probe_count()?;
405
406        Ok(())
407    }
408}
409
410impl UpstreamConf {
411    /// Determines the appropriate service discovery mechanism:
412    /// - Returns configured discovery if set
413    /// - Returns DNS discovery if any address contains a hostname
414    /// - Returns empty string (static discovery) otherwise
415    pub fn guess_discovery(&self) -> String {
416        // Return explicitly configured discovery if set
417        if let Some(discovery) = &self.discovery {
418            return discovery.clone();
419        }
420
421        // Check if any address contains a hostname (non-IP)
422        let has_hostname = self.addrs.iter().any(|addr| {
423            // Extract host portion before port
424            let host =
425                addr.split_once(':').map_or(addr.as_str(), |(host, _)| host);
426
427            // If host can't be parsed as IP, it's a hostname
428            host.parse::<std::net::IpAddr>().is_err()
429        });
430
431        if has_hostname {
432            DNS_DISCOVERY.to_string()
433        } else {
434            String::new()
435        }
436    }
437
438    fn validate_addresses(&self) -> Result<()> {
439        if self.addrs.is_empty() {
440            return Err(Error::Invalid {
441                message: "upstream addrs is empty".to_string(),
442            });
443        }
444
445        // Only validate addresses for static discovery
446        if !is_static_discovery(&self.guess_discovery()) {
447            return Ok(());
448        }
449
450        for addr in &self.addrs {
451            let parts: Vec<_> = addr.split_whitespace().collect();
452            let host_port = parts[0].to_string();
453
454            // Add default port 80 if not specified
455            let addr_to_check = if !host_port.contains(':') {
456                format!("{host_port}:80")
457            } else {
458                host_port
459            };
460
461            // Validate socket address
462            addr_to_check.to_socket_addrs().map_err(|e| Error::Io {
463                source: e,
464                file: addr_to_check,
465            })?;
466        }
467
468        Ok(())
469    }
470
471    fn validate_health_check(&self) -> Result<()> {
472        let health_check = match &self.health_check {
473            Some(url) if !url.is_empty() => url,
474            _ => return Ok(()),
475        };
476
477        Url::parse(health_check).map_err(|e| Error::UrlParse {
478            source: e,
479            url: health_check.to_string(),
480        })?;
481
482        Ok(())
483    }
484
485    fn validate_tcp_probe_count(&self) -> Result<()> {
486        const MAX_TCP_PROBE_COUNT: usize = 16;
487
488        if let Some(count) = self.tcp_probe_count {
489            if count > MAX_TCP_PROBE_COUNT {
490                return Err(Error::Invalid {
491                    message: format!(
492                        "tcp probe count should be <= {MAX_TCP_PROBE_COUNT}"
493                    ),
494                });
495            }
496        }
497
498        Ok(())
499    }
500}
501
502impl Validate for LocationConf {
503    fn validate(&self) -> Result<()> {
504        self.validate_with_upstream(None)?;
505        Ok(())
506    }
507}
508
509/// Configuration for a location/route that handles incoming requests
510#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
511pub struct LocationConf {
512    /// Name of the upstream service to proxy requests to
513    pub upstream: Option<String>,
514
515    /// URL path pattern to match requests against
516    /// Can start with:
517    /// - "=" for exact match
518    /// - "~" for regex match
519    /// - No prefix for prefix match
520    pub path: Option<String>,
521
522    /// Host/domain name to match requests against
523    pub host: Option<String>,
524
525    /// Headers to set on proxied requests (overwrites existing)
526    pub proxy_set_headers: Option<Vec<String>>,
527
528    /// Headers to add to proxied requests (appends to existing)
529    pub proxy_add_headers: Option<Vec<String>>,
530
531    /// URL rewrite rule in format "pattern replacement"
532    pub rewrite: Option<String>,
533
534    /// Manual weight for location matching priority
535    /// Higher weight = higher priority
536    pub weight: Option<u16>,
537
538    /// List of plugins to apply to requests matching this location
539    pub plugins: Option<Vec<String>>,
540
541    /// Maximum allowed size of request body
542    pub client_max_body_size: Option<ByteSize>,
543
544    /// Maximum number of concurrent requests being processed
545    pub max_processing: Option<i32>,
546
547    /// List of included configuration files
548    pub includes: Option<Vec<String>>,
549
550    /// Whether to enable gRPC-Web protocol support
551    pub grpc_web: Option<bool>,
552
553    /// Whether to enable reverse proxy headers
554    pub enable_reverse_proxy_headers: Option<bool>,
555
556    /// Maximum number of retries for failed connections
557    pub max_retries: Option<u8>,
558
559    /// Maximum window for retries
560    #[serde(default)]
561    #[serde(with = "humantime_serde")]
562    pub max_retry_window: Option<Duration>,
563
564    /// Optional description/notes about this location
565    pub remark: Option<String>,
566}
567
568impl LocationConf {
569    /// Validates the location configuration:
570    /// 1. Validates that headers are properly formatted as "name: value"
571    /// 2. Validates header names and values are valid HTTP headers
572    /// 3. Validates upstream exists if specified
573    /// 4. Validates rewrite pattern is valid regex if specified
574    fn validate_with_upstream(
575        &self,
576        upstream_names: Option<&[String]>,
577    ) -> Result<()> {
578        // Helper function to validate HTTP headers
579        let validate = |headers: &Option<Vec<String>>| -> Result<()> {
580            if let Some(headers) = headers {
581                for header in headers.iter() {
582                    // Split header into name and value parts
583                    let arr = header
584                        .split_once(':')
585                        .map(|(k, v)| (k.trim(), v.trim()));
586                    let Some((header_name, header_value)) = arr else {
587                        return Err(Error::Invalid {
588                            message: format!("header {header} is invalid"),
589                        });
590                    };
591
592                    // Validate header name is valid
593                    HeaderName::from_bytes(header_name.as_bytes()).map_err(|err| Error::Invalid {
594                        message: format!("header name({header_name}) is invalid, error: {err}"),
595                    })?;
596
597                    // Validate header value is valid
598                    HeaderValue::from_str(header_value).map_err(|err| Error::Invalid {
599                        message: format!("header value({header_value}) is invalid, error: {err}"),
600                    })?;
601                }
602            }
603            Ok(())
604        };
605
606        // Validate upstream exists if specified
607        if let Some(upstream_names) = upstream_names {
608            let upstream = self.upstream.clone().unwrap_or_default();
609            if !upstream.is_empty()
610                && !upstream.starts_with("$")
611                && !upstream_names.contains(&upstream)
612            {
613                return Err(Error::Invalid {
614                    message: format!("upstream({upstream}) is not found"),
615                });
616            }
617        }
618
619        // Validate headers
620        validate(&self.proxy_add_headers)?;
621        validate(&self.proxy_set_headers)?;
622
623        // Validate rewrite pattern is valid regex
624        if let Some(value) = &self.rewrite {
625            let arr: Vec<&str> = value.split(' ').collect();
626            let _ =
627                Regex::new(arr[0]).map_err(|e| Error::Regex { source: e })?;
628        }
629
630        Ok(())
631    }
632
633    /// Calculates the matching priority weight for this location
634    /// Higher weight = higher priority
635    /// Weight is based on:
636    /// - Path match type (exact=1024, prefix=512, regex=256)
637    /// - Path length (up to 64)
638    /// - Host presence (+128)
639    ///
640    /// Returns either the manual weight if set, or calculated weight
641    pub fn get_weight(&self) -> u16 {
642        // Return manual weight if set
643        if let Some(weight) = self.weight {
644            return weight;
645        }
646
647        let mut weight: u16 = 0;
648        let path = self.path.clone().unwrap_or("".to_string());
649
650        // Add weight based on path match type and length
651        if path.len() > 1 {
652            if path.starts_with('=') {
653                weight += 1024; // Exact match
654            } else if path.starts_with('~') {
655                weight += 256; // Regex match
656            } else {
657                weight += 512; // Prefix match
658            }
659            weight += path.len().min(64) as u16;
660        };
661        // Add weight if host is specified
662        if let Some(host) = &self.host {
663            let exist_regex = host.split(',').any(|item| item.starts_with("~"));
664            // exact host weight is 128
665            // regexp host weight is host length
666            if !exist_regex && !host.is_empty() {
667                weight += 128;
668            } else {
669                weight += host.len() as u16;
670            }
671        }
672
673        weight
674    }
675}
676
677/// Configuration for a server instance that handles incoming HTTP/HTTPS requests
678#[derive(Debug, Default, Deserialize, Clone, Serialize)]
679pub struct ServerConf {
680    /// Address to listen on in format "host:port" or multiple addresses separated by commas
681    pub addr: String,
682
683    /// Access log format string for request logging
684    pub access_log: Option<String>,
685
686    /// List of location names that this server handles
687    pub locations: Option<Vec<String>>,
688
689    /// Number of worker threads for this server instance
690    pub threads: Option<usize>,
691
692    /// OpenSSL cipher list string for TLS connections
693    pub tls_cipher_list: Option<String>,
694
695    /// TLS 1.3 ciphersuites string
696    pub tls_ciphersuites: Option<String>,
697
698    /// Minimum TLS version to accept (e.g. "TLSv1.2")
699    pub tls_min_version: Option<String>,
700
701    /// Maximum TLS version to use (e.g. "TLSv1.3")
702    pub tls_max_version: Option<String>,
703
704    /// Whether to use global certificates instead of per-server certs
705    pub global_certificates: Option<bool>,
706
707    /// Whether to enable HTTP/2 protocol support
708    pub enabled_h2: Option<bool>,
709
710    /// TCP keepalive idle timeout
711    #[serde(default)]
712    #[serde(with = "humantime_serde")]
713    pub tcp_idle: Option<Duration>,
714
715    /// TCP keepalive probe interval
716    #[serde(default)]
717    #[serde(with = "humantime_serde")]
718    pub tcp_interval: Option<Duration>,
719
720    /// TCP keepalive user timeout
721    #[serde(default)]
722    #[serde(with = "humantime_serde")]
723    pub tcp_user_timeout: Option<Duration>,
724
725    // downstream read timeout
726    #[serde(default)]
727    #[serde(with = "humantime_serde")]
728    pub downstream_read_timeout: Option<Duration>,
729
730    // downstream write timeout
731    #[serde(default)]
732    #[serde(with = "humantime_serde")]
733    pub downstream_write_timeout: Option<Duration>,
734
735    /// Number of TCP keepalive probes before connection is dropped
736    pub tcp_probe_count: Option<usize>,
737
738    /// TCP Fast Open queue length (0 to disable)
739    pub tcp_fastopen: Option<usize>,
740
741    /// Enable SO_REUSEPORT to allow multiple sockets to bind to the same address and port.
742    /// This is useful for load balancing across multiple worker processes.
743    /// See the [man page](https://man7.org/linux/man-pages/man7/socket.7.html) for more information.
744    pub reuse_port: Option<bool>,
745
746    /// Path to expose Prometheus metrics on
747    pub prometheus_metrics: Option<String>,
748
749    /// OpenTelemetry exporter configuration
750    pub otlp_exporter: Option<String>,
751
752    /// List of configuration files to include
753    pub includes: Option<Vec<String>>,
754
755    /// List of modules to enable for this server
756    pub modules: Option<Vec<String>>,
757
758    /// Whether to enable server-timing header
759    pub enable_server_timing: Option<bool>,
760
761    /// Optional description/notes about this server
762    pub remark: Option<String>,
763}
764
765impl Validate for ServerConf {
766    fn validate(&self) -> Result<()> {
767        self.validate_with_locations(&[])?;
768        Ok(())
769    }
770}
771
772impl ServerConf {
773    /// Validate the options of server config.
774    /// 1. Parse listen addr to socket addr.
775    /// 2. Check the locations are exists.
776    /// 3. Parse access log layout success.
777    fn validate_with_locations(&self, location_names: &[String]) -> Result<()> {
778        for addr in self.addr.split(',') {
779            let _ = addr.to_socket_addrs().map_err(|e| Error::Io {
780                source: e,
781                file: self.addr.clone(),
782            })?;
783        }
784        if !location_names.is_empty() {
785            if let Some(locations) = &self.locations {
786                for item in locations {
787                    if !location_names.contains(item) {
788                        return Err(Error::Invalid {
789                            message: format!("location({item}) is not found"),
790                        });
791                    }
792                }
793            }
794        }
795        let access_log = self.access_log.clone().unwrap_or_default();
796        if !access_log.is_empty() {
797            // TODO: validate access log format
798            // let logger = Parser::from(access_log.as_str());
799            // if logger.tags.is_empty() {
800            //     return Err(Error::Invalid {
801            //         message: "access log format is invalid".to_string(),
802            //     });
803            // }
804        }
805
806        Ok(())
807    }
808}
809
810/// Basic configuration options for the application
811#[derive(Debug, Default, Deserialize, Clone, Serialize)]
812pub struct BasicConf {
813    /// Application name
814    pub name: Option<String>,
815    /// Error page template
816    pub error_template: Option<String>,
817    /// Path to PID file (default: /run/pingap.pid)
818    pub pid_file: Option<String>,
819    /// Unix domain socket path for graceful upgrades(default: /tmp/pingap_upgrade.sock)
820    pub upgrade_sock: Option<String>,
821    /// User for daemon
822    pub user: Option<String>,
823    /// Group for daemon
824    pub group: Option<String>,
825    /// Number of worker threads(default: 1)
826    pub threads: Option<usize>,
827    /// Enable work stealing between worker threads(default: true)
828    pub work_stealing: Option<bool>,
829    /// Number of listener tasks to use per fd. This allows for parallel accepts.
830    pub listener_tasks_per_fd: Option<usize>,
831    /// Grace period before forcefully terminating during shutdown(default: 5m)
832    #[serde(default)]
833    #[serde(with = "humantime_serde")]
834    pub grace_period: Option<Duration>,
835    /// Maximum time to wait for graceful shutdown
836    #[serde(default)]
837    #[serde(with = "humantime_serde")]
838    pub graceful_shutdown_timeout: Option<Duration>,
839    /// Maximum number of idle connections to keep in upstream connection pool
840    pub upstream_keepalive_pool_size: Option<usize>,
841    /// Webhook URL for notifications
842    pub webhook: Option<String>,
843    /// Type of webhook (e.g. "wecom", "dingtalk")
844    pub webhook_type: Option<String>,
845    /// List of events to send webhook notifications for
846    pub webhook_notifications: Option<Vec<String>>,
847    /// Log level (debug, info, warn, error)
848    pub log_level: Option<String>,
849    /// Size of log buffer before flushing
850    pub log_buffered_size: Option<ByteSize>,
851    /// Whether to format logs as JSON
852    pub log_format_json: Option<bool>,
853    /// Sentry DSN for error reporting
854    pub sentry: Option<String>,
855    /// Pyroscope server URL for continuous profiling
856    pub pyroscope: Option<String>,
857    /// How often to check for configuration changes that require restart
858    #[serde(default)]
859    #[serde(with = "humantime_serde")]
860    pub auto_restart_check_interval: Option<Duration>,
861
862    // log compress algorithm: gzip, zstd
863    pub log_compress_algorithm: Option<String>,
864    /// Log compress level
865    pub log_compress_level: Option<u8>,
866    /// Log compress days ago
867    pub log_compress_days_ago: Option<u16>,
868    /// Log compress time point hour
869    pub log_compress_time_point_hour: Option<u8>,
870}
871
872impl Validate for BasicConf {
873    fn validate(&self) -> Result<()> {
874        Ok(())
875    }
876}
877
878impl BasicConf {
879    /// Returns the path to the PID file
880    /// - If pid_file is explicitly configured, uses that value
881    /// - Otherwise tries to use /run/pingap.pid or /var/run/pingap.pid if writable
882    /// - Falls back to /tmp/pingap.pid if neither system directories are writable
883    pub fn get_pid_file(&self) -> String {
884        if let Some(pid_file) = &self.pid_file {
885            return pid_file.clone();
886        }
887        for dir in ["/run", "/var/run"] {
888            if tempfile_in(dir).is_ok() {
889                return format!("{dir}/pingap.pid");
890            }
891        }
892        "/tmp/pingap.pid".to_string()
893    }
894}
895
896#[derive(Debug, Default, Deserialize, Clone, Serialize)]
897pub struct StorageConf {
898    pub category: String,
899    pub value: String,
900    pub secret: Option<String>,
901    pub remark: Option<String>,
902}
903
904impl Validate for StorageConf {
905    fn validate(&self) -> Result<()> {
906        Ok(())
907    }
908}
909
910pub trait Hashable: Hash {
911    fn hash_key(&self) -> String {
912        let mut hasher = DefaultHasher::new();
913        self.hash(&mut hasher);
914        format!("{:x}", hasher.finish())
915    }
916}
917impl Hashable for UpstreamConf {}
918impl Hashable for LocationConf {}
919
920#[derive(Deserialize, Debug, Serialize)]
921struct TomlConfig {
922    basic: Option<BasicConf>,
923    servers: Option<Map<String, Value>>,
924    upstreams: Option<Map<String, Value>>,
925    locations: Option<Map<String, Value>>,
926    plugins: Option<Map<String, Value>>,
927    certificates: Option<Map<String, Value>>,
928    storages: Option<Map<String, Value>>,
929}
930
931fn format_toml(value: &Value) -> String {
932    if let Some(value) = value.as_table() {
933        value.to_string()
934    } else {
935        "".to_string()
936    }
937}
938
939pub type PluginConf = Map<String, Value>;
940
941impl Validate for PluginConf {
942    fn validate(&self) -> Result<()> {
943        Ok(())
944    }
945}
946
947#[derive(Debug, Default, Clone, Deserialize, Serialize)]
948pub struct PingapConfig {
949    pub basic: BasicConf,
950    pub upstreams: HashMap<String, UpstreamConf>,
951    pub locations: HashMap<String, LocationConf>,
952    pub servers: HashMap<String, ServerConf>,
953    pub plugins: HashMap<String, PluginConf>,
954    pub certificates: HashMap<String, CertificateConf>,
955    pub storages: HashMap<String, StorageConf>,
956}
957
958impl PingapConfig {
959    // 需要一个辅助函数来避免重复
960    fn get_value_for_category<T: Serialize>(
961        &self,
962        map: &HashMap<String, T>,
963        name: Option<&str>,
964    ) -> Result<toml::Value> {
965        match name {
966            // 如果指定了名称,只序列化那一个项
967            Some(name) => {
968                if let Some(item) = map.get(name) {
969                    let mut table = Map::new();
970                    table.insert(
971                        name.to_string(),
972                        toml::Value::try_from(item)
973                            .map_err(|e| Error::Ser { source: e })?,
974                    );
975                    Ok(toml::Value::Table(table))
976                } else {
977                    Ok(toml::Value::Table(Map::new())) // 未找到,返回空表
978                }
979            },
980            // 否则,序列化整个类别
981            None => Ok(toml::Value::try_from(map)
982                .map_err(|e| Error::Ser { source: e })?),
983        }
984    }
985    pub fn get_toml(
986        &self,
987        category: &str,
988        name: Option<&str>,
989    ) -> Result<(String, String)> {
990        let (key, value_to_serialize) = match category {
991            CATEGORY_SERVER => {
992                ("servers", self.get_value_for_category(&self.servers, name)?)
993            },
994            CATEGORY_LOCATION => (
995                "locations",
996                self.get_value_for_category(&self.locations, name)?,
997            ),
998            CATEGORY_UPSTREAM => (
999                "upstreams",
1000                self.get_value_for_category(&self.upstreams, name)?,
1001            ),
1002            CATEGORY_PLUGIN => {
1003                ("plugins", self.get_value_for_category(&self.plugins, name)?)
1004            },
1005            CATEGORY_CERTIFICATE => (
1006                "certificates",
1007                self.get_value_for_category(&self.certificates, name)?,
1008            ),
1009            CATEGORY_STORAGE => (
1010                "storages",
1011                self.get_value_for_category(&self.storages, name)?,
1012            ),
1013            _ => (
1014                CATEGORY_BASIC,
1015                toml::Value::try_from(&self.basic)
1016                    .map_err(|e| Error::Ser { source: e })?,
1017            ),
1018        };
1019
1020        let path = {
1021            let name = name.unwrap_or_default();
1022            if key == CATEGORY_BASIC || name.is_empty() {
1023                format!("/{key}.toml")
1024            } else {
1025                format!("/{key}/{name}.toml")
1026            }
1027        };
1028
1029        if let Some(table) = value_to_serialize.as_table() {
1030            if table.is_empty() {
1031                return Ok((path, "".to_string()));
1032            }
1033        }
1034
1035        let mut wrapper = Map::new();
1036        wrapper.insert(key.to_string(), value_to_serialize);
1037
1038        let toml_string = toml::to_string_pretty(&wrapper)
1039            .map_err(|e| Error::Ser { source: e })?;
1040
1041        Ok((path, toml_string))
1042    }
1043    pub fn get_storage_value(&self, name: &str) -> Result<String> {
1044        for (key, item) in self.storages.iter() {
1045            if key != name {
1046                continue;
1047            }
1048
1049            if let Some(key) = &item.secret {
1050                return pingap_util::aes_decrypt(key, &item.value).map_err(
1051                    |e| Error::Invalid {
1052                        message: e.to_string(),
1053                    },
1054                );
1055            }
1056            return Ok(item.value.clone());
1057        }
1058        Ok("".to_string())
1059    }
1060}
1061
1062fn convert_include_toml(
1063    data: &HashMap<String, String>,
1064    replace_includes: bool,
1065    mut value: Value,
1066) -> String {
1067    let Some(m) = value.as_table_mut() else {
1068        return "".to_string();
1069    };
1070    if !replace_includes {
1071        return m.to_string();
1072    }
1073    if let Some(includes) = m.remove("includes") {
1074        if let Some(includes) = get_include_toml(data, includes) {
1075            if let Ok(includes) = toml::from_str::<Table>(&includes) {
1076                for (key, value) in includes.iter() {
1077                    m.insert(key.to_string(), value.clone());
1078                }
1079            }
1080        }
1081    }
1082    m.to_string()
1083}
1084
1085fn get_include_toml(
1086    data: &HashMap<String, String>,
1087    includes: Value,
1088) -> Option<String> {
1089    let values = includes.as_array()?;
1090    let arr: Vec<String> = values
1091        .iter()
1092        .map(|item| {
1093            let key = item.as_str().unwrap_or_default();
1094            if let Some(value) = data.get(key) {
1095                value.clone()
1096            } else {
1097                "".to_string()
1098            }
1099        })
1100        .collect();
1101    Some(arr.join("\n"))
1102}
1103
1104pub(crate) fn convert_pingap_config(
1105    data: &[u8],
1106    replace_include: bool,
1107) -> Result<PingapConfig, Error> {
1108    let data: TomlConfig = toml::from_str(
1109        std::string::String::from_utf8_lossy(data)
1110            .to_string()
1111            .as_str(),
1112    )
1113    .map_err(|e| Error::De { source: e })?;
1114
1115    let mut conf = PingapConfig {
1116        basic: data.basic.unwrap_or_default(),
1117        ..Default::default()
1118    };
1119    let mut includes = HashMap::new();
1120    for (name, value) in data.storages.unwrap_or_default() {
1121        let toml = format_toml(&value);
1122        let storage: StorageConf = toml::from_str(toml.as_str())
1123            .map_err(|e| Error::De { source: e })?;
1124        includes.insert(name.clone(), storage.value.clone());
1125        conf.storages.insert(name, storage);
1126    }
1127
1128    for (name, value) in data.upstreams.unwrap_or_default() {
1129        let toml = convert_include_toml(&includes, replace_include, value);
1130
1131        let upstream: UpstreamConf = toml::from_str(toml.as_str())
1132            .map_err(|e| Error::De { source: e })?;
1133        conf.upstreams.insert(name, upstream);
1134    }
1135    for (name, value) in data.locations.unwrap_or_default() {
1136        let toml = convert_include_toml(&includes, replace_include, value);
1137
1138        let location: LocationConf = toml::from_str(toml.as_str())
1139            .map_err(|e| Error::De { source: e })?;
1140        conf.locations.insert(name, location);
1141    }
1142    for (name, value) in data.servers.unwrap_or_default() {
1143        let toml = convert_include_toml(&includes, replace_include, value);
1144
1145        let server: ServerConf = toml::from_str(toml.as_str())
1146            .map_err(|e| Error::De { source: e })?;
1147        conf.servers.insert(name, server);
1148    }
1149    for (name, value) in data.plugins.unwrap_or_default() {
1150        let plugin: PluginConf = toml::from_str(format_toml(&value).as_str())
1151            .map_err(|e| Error::De { source: e })?;
1152        conf.plugins.insert(name, plugin);
1153    }
1154
1155    for (name, value) in data.certificates.unwrap_or_default() {
1156        let certificate: CertificateConf =
1157            toml::from_str(format_toml(&value).as_str())
1158                .map_err(|e| Error::De { source: e })?;
1159        conf.certificates.insert(name, certificate);
1160    }
1161
1162    Ok(conf)
1163}
1164
1165#[derive(Debug, Default, Clone, Deserialize, Serialize)]
1166struct Description {
1167    category: String,
1168    name: String,
1169    data: String,
1170}
1171
1172impl PingapConfig {
1173    pub fn new(data: &[u8], replace_includes: bool) -> Result<Self> {
1174        convert_pingap_config(data, replace_includes)
1175    }
1176    /// Validate the options of pinggap config.
1177    pub fn validate(&self) -> Result<()> {
1178        let mut upstream_names = vec![];
1179        for (name, upstream) in self.upstreams.iter() {
1180            upstream.validate()?;
1181            upstream_names.push(name.to_string());
1182        }
1183        let mut location_names = vec![];
1184        for (name, location) in self.locations.iter() {
1185            location.validate_with_upstream(Some(&upstream_names))?;
1186            location_names.push(name.to_string());
1187        }
1188        let mut listen_addr_list = vec![];
1189        for server in self.servers.values() {
1190            for addr in server.addr.split(',') {
1191                if listen_addr_list.contains(&addr.to_string()) {
1192                    return Err(Error::Invalid {
1193                        message: format!("{addr} is inused by other server"),
1194                    });
1195                }
1196                listen_addr_list.push(addr.to_string());
1197            }
1198            server.validate_with_locations(&location_names)?;
1199        }
1200        // TODO: validate plugins
1201        // for (name, plugin) in self.plugins.iter() {
1202        //     parse_plugins(vec![(name.to_string(), plugin.clone())]).map_err(
1203        //         |e| Error::Invalid {
1204        //             message: e.to_string(),
1205        //         },
1206        //     )?;
1207        // }
1208        for (_, certificate) in self.certificates.iter() {
1209            certificate.validate()?;
1210        }
1211        let ping_conf = toml::to_string_pretty(self)
1212            .map_err(|e| Error::Ser { source: e })?;
1213        convert_pingap_config(ping_conf.as_bytes(), true)?;
1214        Ok(())
1215    }
1216    /// Generate the content hash of config.
1217    pub fn hash(&self) -> Result<String> {
1218        let mut lines = vec![];
1219        for desc in self.descriptions() {
1220            lines.push(desc.category);
1221            lines.push(desc.name);
1222            lines.push(desc.data);
1223        }
1224        let hash = crc32fast::hash(lines.join("\n").as_bytes());
1225        Ok(format!("{hash:X}"))
1226    }
1227    /// Remove the config by name.
1228    pub fn remove(&mut self, category: &str, name: &str) -> Result<()> {
1229        match category {
1230            CATEGORY_UPSTREAM => {
1231                for (location_name, location) in self.locations.iter() {
1232                    if let Some(upstream) = &location.upstream {
1233                        if upstream == name {
1234                            return Err(Error::Invalid {
1235                                message: format!(
1236                                    "upstream({name}) is in used by location({location_name})",
1237                                ),
1238                            });
1239                        }
1240                    }
1241                }
1242                self.upstreams.remove(name);
1243            },
1244            CATEGORY_LOCATION => {
1245                for (server_name, server) in self.servers.iter() {
1246                    if let Some(locations) = &server.locations {
1247                        if locations.contains(&name.to_string()) {
1248                            return Err(Error::Invalid {
1249                                message: format!(
1250                                    "location({name}) is in used by server({server_name})"
1251                                ),
1252                            });
1253                        }
1254                    }
1255                }
1256                self.locations.remove(name);
1257            },
1258            CATEGORY_SERVER => {
1259                self.servers.remove(name);
1260            },
1261            CATEGORY_PLUGIN => {
1262                for (location_name, location) in self.locations.iter() {
1263                    if let Some(plugins) = &location.plugins {
1264                        if plugins.contains(&name.to_string()) {
1265                            return Err(Error::Invalid {
1266                                message: format!(
1267                                    "proxy plugin({name}) is in used by location({location_name})"
1268                                ),
1269                            });
1270                        }
1271                    }
1272                }
1273                self.plugins.remove(name);
1274            },
1275            CATEGORY_CERTIFICATE => {
1276                self.certificates.remove(name);
1277            },
1278            _ => {},
1279        };
1280        Ok(())
1281    }
1282    fn descriptions(&self) -> Vec<Description> {
1283        let mut value = self.clone();
1284        let mut descriptions = vec![];
1285        for (name, data) in value.servers.iter() {
1286            descriptions.push(Description {
1287                category: CATEGORY_SERVER.to_string(),
1288                name: format!("server:{name}"),
1289                data: toml::to_string_pretty(data).unwrap_or_default(),
1290            });
1291        }
1292        for (name, data) in value.locations.iter() {
1293            descriptions.push(Description {
1294                category: CATEGORY_LOCATION.to_string(),
1295                name: format!("location:{name}"),
1296                data: toml::to_string_pretty(data).unwrap_or_default(),
1297            });
1298        }
1299        for (name, data) in value.upstreams.iter() {
1300            descriptions.push(Description {
1301                category: CATEGORY_UPSTREAM.to_string(),
1302                name: format!("upstream:{name}"),
1303                data: toml::to_string_pretty(data).unwrap_or_default(),
1304            });
1305        }
1306        for (name, data) in value.plugins.iter() {
1307            descriptions.push(Description {
1308                category: CATEGORY_PLUGIN.to_string(),
1309                name: format!("plugin:{name}"),
1310                data: toml::to_string_pretty(data).unwrap_or_default(),
1311            });
1312        }
1313        for (name, data) in value.certificates.iter() {
1314            let mut clone_data = data.clone();
1315            if let Some(cert) = &clone_data.tls_cert {
1316                clone_data.tls_cert = Some(format!(
1317                    "crc32:{:X}",
1318                    crc32fast::hash(cert.as_bytes())
1319                ));
1320            }
1321            if let Some(key) = &clone_data.tls_key {
1322                clone_data.tls_key = Some(format!(
1323                    "crc32:{:X}",
1324                    crc32fast::hash(key.as_bytes())
1325                ));
1326            }
1327            descriptions.push(Description {
1328                category: CATEGORY_CERTIFICATE.to_string(),
1329                name: format!("certificate:{name}"),
1330                data: toml::to_string_pretty(&clone_data).unwrap_or_default(),
1331            });
1332        }
1333        for (name, data) in value.storages.iter() {
1334            let mut clone_data = data.clone();
1335            if let Some(secret) = &clone_data.secret {
1336                clone_data.secret = Some(format!(
1337                    "crc32:{:X}",
1338                    crc32fast::hash(secret.as_bytes())
1339                ));
1340            }
1341            descriptions.push(Description {
1342                category: CATEGORY_STORAGE.to_string(),
1343                name: format!("storage:{name}"),
1344                data: toml::to_string_pretty(&clone_data).unwrap_or_default(),
1345            });
1346        }
1347        value.servers = HashMap::new();
1348        value.locations = HashMap::new();
1349        value.upstreams = HashMap::new();
1350        value.plugins = HashMap::new();
1351        value.certificates = HashMap::new();
1352        value.storages = HashMap::new();
1353        descriptions.push(Description {
1354            category: CATEGORY_BASIC.to_string(),
1355            name: CATEGORY_BASIC.to_string(),
1356            data: toml::to_string_pretty(&value).unwrap_or_default(),
1357        });
1358        descriptions.sort_by_key(|d| d.name.clone());
1359        descriptions
1360    }
1361    /// Get the different content of two config.
1362    pub fn diff(&self, other: &PingapConfig) -> (Vec<String>, Vec<String>) {
1363        // 1. 将描述列表转换为 HashMap,以便进行高效的键查找。
1364        let current_map: HashMap<_, _> = self
1365            .descriptions()
1366            .into_iter()
1367            .map(|d| (d.name.clone(), d))
1368            .collect();
1369        let new_map: HashMap<_, _> = other
1370            .descriptions()
1371            .into_iter()
1372            .map(|d| (d.name.clone(), d))
1373            .collect();
1374
1375        // 使用 HashSet 存储受影响的类别,以自动处理重复。
1376        let mut affected_categories = HashSet::new();
1377
1378        // 分别存储新增、删除和修改的项,以便最后格式化输出。
1379        let mut added_items = vec![];
1380        let mut removed_items = vec![];
1381        let mut modified_items = vec![];
1382
1383        // 2. 遍历当前配置,查找被删除或被修改的项。
1384        for (name, current_item) in &current_map {
1385            match new_map.get(name) {
1386                Some(new_item) => {
1387                    // 键存在于两个配置中,检查内容是否发生变化。
1388                    if current_item.data != new_item.data {
1389                        affected_categories
1390                            .insert(current_item.category.clone());
1391
1392                        // 使用 diff::lines 生成逐行差异
1393                        let mut item_diff_result = vec![];
1394                        for diff in
1395                            diff::lines(&current_item.data, &new_item.data)
1396                        {
1397                            match diff {
1398                                diff::Result::Left(l) => {
1399                                    item_diff_result.push(format!("- {l}"))
1400                                },
1401                                diff::Result::Right(r) => {
1402                                    item_diff_result.push(format!("+ {r}"))
1403                                },
1404                                _ => (),
1405                            }
1406                        }
1407
1408                        if !item_diff_result.is_empty() {
1409                            modified_items.push(format!("[MODIFIED] {name}"));
1410                            modified_items.extend(item_diff_result);
1411                            modified_items.push("".to_string()); // 添加空行分隔
1412                        }
1413                    }
1414                },
1415                None => {
1416                    // 在新配置中不存在,说明该项已被删除。
1417                    removed_items.push(format!("-- [REMOVED] {name}"));
1418                    affected_categories.insert(current_item.category.clone());
1419                },
1420            }
1421        }
1422
1423        // 3. 遍历新配置,查找新增的项。
1424        for (name, new_item) in &new_map {
1425            if !current_map.contains_key(name) {
1426                added_items.push(format!("++ [ADDED] {name}"));
1427                affected_categories.insert(new_item.category.clone());
1428            }
1429        }
1430
1431        // 4. 组合所有差异,生成最终的可读输出。
1432        let mut final_diff = Vec::new();
1433        if !added_items.is_empty() {
1434            final_diff.extend(added_items);
1435            final_diff.push("".to_string()); // 添加空行分隔
1436        }
1437        if !removed_items.is_empty() {
1438            final_diff.extend(removed_items);
1439            final_diff.push("".to_string());
1440        }
1441        if !modified_items.is_empty() {
1442            final_diff.extend(modified_items);
1443        }
1444
1445        // 将 HashSet 转换为 Vec 并返回结果
1446        (affected_categories.into_iter().collect(), final_diff)
1447    }
1448}
1449
1450#[cfg(test)]
1451mod tests {
1452    use super::{CertificateConf, Hashable, Validate, validate_cert};
1453    use super::{LocationConf, PluginCategory, ServerConf, UpstreamConf};
1454    use pingap_core::PluginStep;
1455    use pingap_util::base64_encode;
1456    use pretty_assertions::assert_eq;
1457    use serde::{Deserialize, Serialize};
1458    use std::str::FromStr;
1459
1460    #[test]
1461    fn test_plugin_step() {
1462        let step = PluginStep::from_str("early_request").unwrap();
1463        assert_eq!(step, PluginStep::EarlyRequest);
1464
1465        assert_eq!("early_request", step.to_string());
1466    }
1467
1468    #[test]
1469    fn test_validate_cert() {
1470        // spellchecker:off
1471        let pem = r#"-----BEGIN CERTIFICATE-----
1472MIIEljCCAv6gAwIBAgIQeYUdeFj3gpzhQes3aGaMZTANBgkqhkiG9w0BAQsFADCB
1473pTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMT0wOwYDVQQLDDR4aWVz
1474aHV6aG91QHhpZXNodXpob3VzLU1hY0Jvb2stQWlyLmxvY2FsICjosKLmoJHmtLIp
1475MUQwQgYDVQQDDDtta2NlcnQgeGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29r
1476LUFpci5sb2NhbCAo6LCi5qCR5rSyKTAeFw0yMzA5MjQxMzA1MjdaFw0yNTEyMjQx
1477MzA1MjdaMGgxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0
1478ZTE9MDsGA1UECww0eGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29rLUFpci5s
1479b2NhbCAo6LCi5qCR5rSyKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
1480ALuJ8lYEj9uf4iE9hguASq7re87Np+zJc2x/eqr1cR/SgXRStBsjxqI7i3xwMRqX
1481AuhAnM6ktlGuqidl7D9y6AN/UchqgX8AetslRJTpCcEDfL/q24zy0MqOS0FlYEgh
1482s4PIjWsSNoglBDeaIdUpN9cM/64IkAAtHndNt2p2vPfjrPeixLjese096SKEnZM/
1483xBdWF491hx06IyzjtWKqLm9OUmYZB9d/gDGnDsKpqClw8m95opKD4TBHAoE//WvI
1484m1mZnjNTNR27vVbmnc57d2Lx2Ib2eqJG5zMsP2hPBoqS8CKEwMRFLHAcclNkI67U
1485kcSEGaWgr15QGHJPN/FtjDsCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud
1486JQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFJo0y9bYUM/OuenDjsJ1RyHJfL3n
1487MDQGA1UdEQQtMCuCBm1lLmRldoIJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAA
1488AAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBgQAlQbow3+4UyQx+E+J0RwmHBltU6i+K
1489soFfza6FWRfAbTyv+4KEWl2mx51IfHhJHYZvsZqPqGWxm5UvBecskegDExFMNFVm
1490O5QixydQzHHY2krmBwmDZ6Ao88oW/qw4xmMUhzKAZbsqeQyE/uiUdyI4pfDcduLB
1491rol31g9OFsgwZrZr0d1ZiezeYEhemnSlh9xRZW3veKx9axgFttzCMmWdpGTCvnav
1492ZVc3rB+KBMjdCwsS37zmrNm9syCjW1O5a1qphwuMpqSnDHBgKWNpbsgqyZM0oyOc
14939Bkja+BV5wFO+4zH5WtestcrNMeoQ83a5lI0m42u/bUEJ/T/5BQBSFidNuvS7Ylw
1494IZpXa00xvlnm1BOHOfRI4Ehlfa5jmfcdnrGkQLGjiyygQtKcc7rOXGK+mSeyxwhs
1495sIARwslSQd4q0dbYTPKvvUHxTYiCv78vQBAsE15T2GGS80pAFDBW9vOf3upANvOf
1496EHjKf0Dweb4ppL4ddgeAKU5V0qn76K2fFaE=
1497-----END CERTIFICATE-----"#;
1498        // spellchecker:on
1499        let result = validate_cert(pem);
1500        assert_eq!(true, result.is_ok());
1501
1502        let value = base64_encode(pem);
1503        let result = validate_cert(&value);
1504        assert_eq!(true, result.is_ok());
1505    }
1506
1507    #[test]
1508    fn test_plugin_category_serde() {
1509        #[derive(Deserialize, Serialize)]
1510        struct TmpPluginCategory {
1511            category: PluginCategory,
1512        }
1513        let tmp = TmpPluginCategory {
1514            category: PluginCategory::RequestId,
1515        };
1516        let data = serde_json::to_string(&tmp).unwrap();
1517        assert_eq!(r#"{"category":"request_id"}"#, data);
1518
1519        let tmp: TmpPluginCategory = serde_json::from_str(&data).unwrap();
1520        assert_eq!(PluginCategory::RequestId, tmp.category);
1521    }
1522
1523    #[test]
1524    fn test_upstream_conf() {
1525        let mut conf = UpstreamConf::default();
1526
1527        let result = conf.validate();
1528        assert_eq!(true, result.is_err());
1529        assert_eq!(
1530            "Invalid error upstream addrs is empty",
1531            result.expect_err("").to_string()
1532        );
1533
1534        conf.addrs = vec!["127.0.0.1".to_string(), "github".to_string()];
1535        conf.discovery = Some("static".to_string());
1536        let result = conf.validate();
1537        assert_eq!(true, result.is_err());
1538        assert_eq!(
1539            true,
1540            result
1541                .expect_err("")
1542                .to_string()
1543                .contains("Io error failed to lookup address information")
1544        );
1545
1546        conf.addrs = vec!["127.0.0.1".to_string(), "github.com".to_string()];
1547        conf.health_check = Some("http:///".to_string());
1548        let result = conf.validate();
1549        assert_eq!(true, result.is_err());
1550        assert_eq!(
1551            "Url parse error empty host, http:///",
1552            result.expect_err("").to_string()
1553        );
1554
1555        conf.health_check = Some("http://github.com/".to_string());
1556        let result = conf.validate();
1557        assert_eq!(true, result.is_ok());
1558    }
1559
1560    #[test]
1561    fn test_location_conf() {
1562        let mut conf = LocationConf::default();
1563        let upstream_names = vec!["upstream1".to_string()];
1564
1565        conf.upstream = Some("upstream2".to_string());
1566        let result = conf.validate_with_upstream(Some(&upstream_names));
1567        assert_eq!(true, result.is_err());
1568        assert_eq!(
1569            "Invalid error upstream(upstream2) is not found",
1570            result.expect_err("").to_string()
1571        );
1572
1573        conf.upstream = Some("upstream1".to_string());
1574        conf.proxy_set_headers = Some(vec!["X-Request-Id".to_string()]);
1575        let result = conf.validate_with_upstream(Some(&upstream_names));
1576        assert_eq!(true, result.is_err());
1577        assert_eq!(
1578            "Invalid error header X-Request-Id is invalid",
1579            result.expect_err("").to_string()
1580        );
1581
1582        conf.proxy_set_headers = Some(vec!["请求:响应".to_string()]);
1583        let result = conf.validate_with_upstream(Some(&upstream_names));
1584        assert_eq!(true, result.is_err());
1585        assert_eq!(
1586            "Invalid error header name(请求) is invalid, error: invalid HTTP header name",
1587            result.expect_err("").to_string()
1588        );
1589
1590        conf.proxy_set_headers = Some(vec!["X-Request-Id: abcd".to_string()]);
1591        let result = conf.validate_with_upstream(Some(&upstream_names));
1592        assert_eq!(true, result.is_ok());
1593
1594        conf.rewrite = Some(r"foo(bar".to_string());
1595        let result = conf.validate_with_upstream(Some(&upstream_names));
1596        assert_eq!(true, result.is_err());
1597        assert_eq!(
1598            true,
1599            result
1600                .expect_err("")
1601                .to_string()
1602                .starts_with("Regex error regex parse error")
1603        );
1604
1605        conf.rewrite = Some(r"^/api /".to_string());
1606        let result = conf.validate_with_upstream(Some(&upstream_names));
1607        assert_eq!(true, result.is_ok());
1608    }
1609
1610    #[test]
1611    fn test_location_get_wegiht() {
1612        let mut conf = LocationConf {
1613            weight: Some(2048),
1614            ..Default::default()
1615        };
1616
1617        assert_eq!(2048, conf.get_weight());
1618
1619        conf.weight = None;
1620        conf.path = Some("=/api".to_string());
1621        assert_eq!(1029, conf.get_weight());
1622
1623        conf.path = Some("~/api".to_string());
1624        assert_eq!(261, conf.get_weight());
1625
1626        conf.path = Some("/api".to_string());
1627        assert_eq!(516, conf.get_weight());
1628
1629        conf.path = None;
1630        conf.host = Some("github.com".to_string());
1631        assert_eq!(128, conf.get_weight());
1632
1633        conf.host = Some("~github.com".to_string());
1634        assert_eq!(11, conf.get_weight());
1635
1636        conf.host = Some("".to_string());
1637        assert_eq!(0, conf.get_weight());
1638    }
1639
1640    #[test]
1641    fn test_server_conf() {
1642        let mut conf = ServerConf::default();
1643        let location_names = vec!["lo".to_string()];
1644
1645        let result = conf.validate_with_locations(&location_names);
1646        assert_eq!(true, result.is_err());
1647        assert_eq!(
1648            "Io error invalid socket address, ",
1649            result.expect_err("").to_string()
1650        );
1651
1652        conf.addr = "127.0.0.1:3001".to_string();
1653        conf.locations = Some(vec!["lo1".to_string()]);
1654        let result = conf.validate_with_locations(&location_names);
1655        assert_eq!(true, result.is_err());
1656        assert_eq!(
1657            "Invalid error location(lo1) is not found",
1658            result.expect_err("").to_string()
1659        );
1660
1661        conf.locations = Some(vec!["lo".to_string()]);
1662        let result = conf.validate_with_locations(&location_names);
1663        assert_eq!(true, result.is_ok());
1664    }
1665
1666    #[test]
1667    fn test_certificate_conf() {
1668        // spellchecker:off
1669        let pem = r#"-----BEGIN CERTIFICATE-----
1670MIIEljCCAv6gAwIBAgIQeYUdeFj3gpzhQes3aGaMZTANBgkqhkiG9w0BAQsFADCB
1671pTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMT0wOwYDVQQLDDR4aWVz
1672aHV6aG91QHhpZXNodXpob3VzLU1hY0Jvb2stQWlyLmxvY2FsICjosKLmoJHmtLIp
1673MUQwQgYDVQQDDDtta2NlcnQgeGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29r
1674LUFpci5sb2NhbCAo6LCi5qCR5rSyKTAeFw0yMzA5MjQxMzA1MjdaFw0yNTEyMjQx
1675MzA1MjdaMGgxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0
1676ZTE9MDsGA1UECww0eGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29rLUFpci5s
1677b2NhbCAo6LCi5qCR5rSyKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
1678ALuJ8lYEj9uf4iE9hguASq7re87Np+zJc2x/eqr1cR/SgXRStBsjxqI7i3xwMRqX
1679AuhAnM6ktlGuqidl7D9y6AN/UchqgX8AetslRJTpCcEDfL/q24zy0MqOS0FlYEgh
1680s4PIjWsSNoglBDeaIdUpN9cM/64IkAAtHndNt2p2vPfjrPeixLjese096SKEnZM/
1681xBdWF491hx06IyzjtWKqLm9OUmYZB9d/gDGnDsKpqClw8m95opKD4TBHAoE//WvI
1682m1mZnjNTNR27vVbmnc57d2Lx2Ib2eqJG5zMsP2hPBoqS8CKEwMRFLHAcclNkI67U
1683kcSEGaWgr15QGHJPN/FtjDsCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud
1684JQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFJo0y9bYUM/OuenDjsJ1RyHJfL3n
1685MDQGA1UdEQQtMCuCBm1lLmRldoIJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAA
1686AAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBgQAlQbow3+4UyQx+E+J0RwmHBltU6i+K
1687soFfza6FWRfAbTyv+4KEWl2mx51IfHhJHYZvsZqPqGWxm5UvBecskegDExFMNFVm
1688O5QixydQzHHY2krmBwmDZ6Ao88oW/qw4xmMUhzKAZbsqeQyE/uiUdyI4pfDcduLB
1689rol31g9OFsgwZrZr0d1ZiezeYEhemnSlh9xRZW3veKx9axgFttzCMmWdpGTCvnav
1690ZVc3rB+KBMjdCwsS37zmrNm9syCjW1O5a1qphwuMpqSnDHBgKWNpbsgqyZM0oyOc
16919Bkja+BV5wFO+4zH5WtestcrNMeoQ83a5lI0m42u/bUEJ/T/5BQBSFidNuvS7Ylw
1692IZpXa00xvlnm1BOHOfRI4Ehlfa5jmfcdnrGkQLGjiyygQtKcc7rOXGK+mSeyxwhs
1693sIARwslSQd4q0dbYTPKvvUHxTYiCv78vQBAsE15T2GGS80pAFDBW9vOf3upANvOf
1694EHjKf0Dweb4ppL4ddgeAKU5V0qn76K2fFaE=
1695-----END CERTIFICATE-----"#;
1696        let key = r#"-----BEGIN PRIVATE KEY-----
1697MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7ifJWBI/bn+Ih
1698PYYLgEqu63vOzafsyXNsf3qq9XEf0oF0UrQbI8aiO4t8cDEalwLoQJzOpLZRrqon
1699Zew/cugDf1HIaoF/AHrbJUSU6QnBA3y/6tuM8tDKjktBZWBIIbODyI1rEjaIJQQ3
1700miHVKTfXDP+uCJAALR53Tbdqdrz346z3osS43rHtPekihJ2TP8QXVhePdYcdOiMs
170147Viqi5vTlJmGQfXf4Axpw7CqagpcPJveaKSg+EwRwKBP/1ryJtZmZ4zUzUdu71W
17025p3Oe3di8diG9nqiRuczLD9oTwaKkvAihMDERSxwHHJTZCOu1JHEhBmloK9eUBhy
1703TzfxbYw7AgMBAAECggEALjed0FMJfO+XE+gMm9L/FMKV3W5TXwh6eJemDHG2ckg3
1704fQpQtouHjT2tb3par5ndro0V19tBzzmDV3hH048m3I3JAuI0ja75l/5EO4p+y+Fn
1705IgjoGIFSsUiGBVTNeJlNm0GWkHeJlt3Af09t3RFuYIIklKgpjNGRu4ccl5ExmslF
1706WHv7/1dwzeJCi8iOY2gJZz6N7qHD95VkgVyDj/EtLltONAtIGVdorgq70CYmtwSM
17079XgXszqOTtSJxle+UBmeQTL4ZkUR0W+h6JSpcTn0P9c3fiNDrHSKFZbbpAhO/wHd
1708Ab4IK8IksVyg+tem3m5W9QiXn3WbgcvjJTi83Y3syQKBgQD5IsaSbqwEG3ruttQe
1709yfMeq9NUGVfmj7qkj2JiF4niqXwTpvoaSq/5gM/p7lAtSMzhCKtlekP8VLuwx8ih
1710n4hJAr8pGfyu/9IUghXsvP2DXsCKyypbhzY/F2m4WNIjtyLmed62Nt1PwWWUlo9Q
1711igHI6pieT45vJTBICsRyqC/a/wKBgQDAtLXUsCABQDTPHdy/M/dHZA/QQ/xU8NOs
1712ul5UMJCkSfFNk7b2etQG/iLlMSNup3bY3OPvaCGwwEy/gZ31tTSymgooXQMFxJ7G
17131S/DF45yKD6xJEmAUhwz/Hzor1cM95g78UpZFCEVMnEmkBNb9pmrXRLDuWb0vLE6
1714B6YgiEP6xQKBgBOXuooVjg2co6RWWIQ7WZVV6f65J4KIVyNN62zPcRaUQZ/CB/U9
1715Xm1+xdsd1Mxa51HjPqdyYBpeB4y1iX+8bhlfz+zJkGeq0riuKk895aoJL5c6txAP
1716qCJ6EuReh9grNOFvQCaQVgNJsFVpKcgpsk48tNfuZcMz54Ii5qQlue29AoGAA2Sr
1717Nv2K8rqws1zxQCSoHAe1B5PK46wB7i6x7oWUZnAu4ZDSTfDHvv/GmYaN+yrTuunY
17180aRhw3z/XPfpUiRIs0RnHWLV5MobiaDDYIoPpg7zW6cp7CqF+JxfjrFXtRC/C38q
1719MftawcbLm0Q6MwpallvjMrMXDwQrkrwDvtrnZ4kCgYEA0oSvmSK5ADD0nqYFdaro
1720K+hM90AVD1xmU7mxy3EDPwzjK1wZTj7u0fvcAtZJztIfL+lmVpkvK8KDLQ9wCWE7
1721SGToOzVHYX7VazxioA9nhNne9kaixvnIUg3iowAz07J7o6EU8tfYsnHxsvjlIkBU
1722ai02RHnemmqJaNepfmCdyec=
1723-----END PRIVATE KEY-----"#;
1724        // spellchecker:on
1725        let conf = CertificateConf {
1726            tls_cert: Some(pem.to_string()),
1727            tls_key: Some(key.to_string()),
1728            ..Default::default()
1729        };
1730        let result = conf.validate();
1731        assert_eq!(true, result.is_ok());
1732
1733        // spellchecker:off
1734        assert_eq!("15ba921aee80abc3", conf.hash_key());
1735        // spellchecker:on
1736    }
1737}