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