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