1use super::{Error, Result};
16use 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 #[default]
50 Stats,
51 Limit,
53 Compression,
55 Admin,
57 Directory,
59 Mock,
61 RequestId,
63 IpRestriction,
65 KeyAuth,
67 BasicAuth,
69 CombinedAuth,
71 Jwt,
73 Cache,
75 Redirect,
77 Ping,
79 ResponseHeaders,
81 SubFilter,
83 RefererRestriction,
85 UaRestriction,
87 Csrf,
89 Cors,
91 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#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
119pub struct CertificateConf {
120 pub domains: Option<String>,
122 pub tls_cert: Option<String>,
124 pub tls_key: Option<String>,
126 pub is_default: Option<bool>,
128 pub is_ca: Option<bool>,
130 pub acme: Option<String>,
132 pub dns_challenge: Option<bool>,
134 pub buffer_days: Option<u16>,
136 pub remark: Option<String>,
138}
139
140fn validate_cert(value: &str) -> Result<()> {
142 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 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 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 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 pub fn validate(&self) -> Result<()> {
181 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 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#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
209pub struct UpstreamConf {
210 pub addrs: Vec<String>,
212
213 pub discovery: Option<String>,
215
216 pub dns_server: Option<String>,
218
219 pub dns_domain: Option<String>,
221
222 pub dns_search: Option<String>,
224
225 #[serde(default)]
227 #[serde(with = "humantime_serde")]
228 pub update_frequency: Option<Duration>,
229
230 pub algo: Option<String>,
232
233 pub sni: Option<String>,
235
236 pub verify_cert: Option<bool>,
238
239 pub health_check: Option<String>,
241
242 pub ipv4_only: Option<bool>,
244
245 pub enable_tracer: Option<bool>,
247
248 pub alpn: Option<String>,
250
251 #[serde(default)]
253 #[serde(with = "humantime_serde")]
254 pub connection_timeout: Option<Duration>,
255
256 #[serde(default)]
258 #[serde(with = "humantime_serde")]
259 pub total_connection_timeout: Option<Duration>,
260
261 #[serde(default)]
263 #[serde(with = "humantime_serde")]
264 pub read_timeout: Option<Duration>,
265
266 #[serde(default)]
268 #[serde(with = "humantime_serde")]
269 pub idle_timeout: Option<Duration>,
270
271 #[serde(default)]
273 #[serde(with = "humantime_serde")]
274 pub write_timeout: Option<Duration>,
275
276 #[serde(default)]
278 #[serde(with = "humantime_serde")]
279 pub tcp_idle: Option<Duration>,
280
281 #[serde(default)]
283 #[serde(with = "humantime_serde")]
284 pub tcp_interval: Option<Duration>,
285
286 #[serde(default)]
288 #[serde(with = "humantime_serde")]
289 pub tcp_user_timeout: Option<Duration>,
290
291 pub tcp_probe_count: Option<usize>,
293
294 pub tcp_recv_buf: Option<ByteSize>,
296
297 pub tcp_fast_open: Option<bool>,
299
300 pub includes: Option<Vec<String>>,
302
303 pub remark: Option<String>,
305}
306
307impl UpstreamConf {
308 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 pub fn guess_discovery(&self) -> String {
321 if let Some(discovery) = &self.discovery {
323 return discovery.clone();
324 }
325
326 let has_hostname = self.addrs.iter().any(|addr| {
328 let host =
330 addr.split_once(':').map_or(addr.as_str(), |(host, _)| host);
331
332 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 pub fn validate(&self, name: &str) -> Result<()> {
349 self.validate_addresses(name)?;
351
352 self.validate_health_check()?;
354
355 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 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 let addr_to_check = if !host_port.contains(':') {
379 format!("{host_port}:80")
380 } else {
381 host_port
382 };
383
384 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#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
427pub struct LocationConf {
428 pub upstream: Option<String>,
430
431 pub path: Option<String>,
437
438 pub host: Option<String>,
440
441 pub proxy_set_headers: Option<Vec<String>>,
443
444 pub proxy_add_headers: Option<Vec<String>>,
446
447 pub rewrite: Option<String>,
449
450 pub weight: Option<u16>,
453
454 pub plugins: Option<Vec<String>>,
456
457 pub client_max_body_size: Option<ByteSize>,
459
460 pub max_processing: Option<i32>,
462
463 pub includes: Option<Vec<String>>,
465
466 pub grpc_web: Option<bool>,
468
469 pub enable_reverse_proxy_headers: Option<bool>,
471
472 pub remark: Option<String>,
474}
475
476impl LocationConf {
477 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 fn validate(&self, name: &str, upstream_names: &[String]) -> Result<()> {
491 let validate = |headers: &Option<Vec<String>>| -> Result<()> {
493 if let Some(headers) = headers {
494 for header in headers.iter() {
495 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 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 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 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(&self.proxy_add_headers)?;
537 validate(&self.proxy_set_headers)?;
538
539 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 pub fn get_weight(&self) -> u16 {
558 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 if path.len() > 1 {
568 if path.starts_with('=') {
569 weight += 1024; } else if path.starts_with('~') {
571 weight += 256; } else {
573 weight += 512; }
575 weight += path.len().min(64) as u16;
576 };
577 if let Some(host) = &self.host {
579 let exist_regex = host.split(',').any(|item| item.starts_with("~"));
580 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#[derive(Debug, Default, Deserialize, Clone, Serialize)]
595pub struct ServerConf {
596 pub addr: String,
598
599 pub access_log: Option<String>,
601
602 pub locations: Option<Vec<String>>,
604
605 pub threads: Option<usize>,
607
608 pub tls_cipher_list: Option<String>,
610
611 pub tls_ciphersuites: Option<String>,
613
614 pub tls_min_version: Option<String>,
616
617 pub tls_max_version: Option<String>,
619
620 pub global_certificates: Option<bool>,
622
623 pub enabled_h2: Option<bool>,
625
626 #[serde(default)]
628 #[serde(with = "humantime_serde")]
629 pub tcp_idle: Option<Duration>,
630
631 #[serde(default)]
633 #[serde(with = "humantime_serde")]
634 pub tcp_interval: Option<Duration>,
635
636 #[serde(default)]
638 #[serde(with = "humantime_serde")]
639 pub tcp_user_timeout: Option<Duration>,
640
641 #[serde(default)]
643 #[serde(with = "humantime_serde")]
644 pub downstream_read_timeout: Option<Duration>,
645
646 #[serde(default)]
648 #[serde(with = "humantime_serde")]
649 pub downstream_write_timeout: Option<Duration>,
650
651 pub tcp_probe_count: Option<usize>,
653
654 pub tcp_fastopen: Option<usize>,
656
657 pub reuse_port: Option<bool>,
661
662 pub prometheus_metrics: Option<String>,
664
665 pub otlp_exporter: Option<String>,
667
668 pub includes: Option<Vec<String>>,
670
671 pub modules: Option<Vec<String>>,
673
674 pub enable_server_timing: Option<bool>,
676
677 pub remark: Option<String>,
679}
680
681impl ServerConf {
682 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 }
714
715 Ok(())
716 }
717}
718
719#[derive(Debug, Default, Deserialize, Clone, Serialize)]
721pub struct BasicConf {
722 pub name: Option<String>,
724 pub error_template: Option<String>,
726 pub pid_file: Option<String>,
728 pub upgrade_sock: Option<String>,
730 pub user: Option<String>,
732 pub group: Option<String>,
734 pub threads: Option<usize>,
736 pub work_stealing: Option<bool>,
738 pub listener_tasks_per_fd: Option<usize>,
740 #[serde(default)]
742 #[serde(with = "humantime_serde")]
743 pub grace_period: Option<Duration>,
744 #[serde(default)]
746 #[serde(with = "humantime_serde")]
747 pub graceful_shutdown_timeout: Option<Duration>,
748 pub upstream_keepalive_pool_size: Option<usize>,
750 pub webhook: Option<String>,
752 pub webhook_type: Option<String>,
754 pub webhook_notifications: Option<Vec<String>>,
756 pub log_level: Option<String>,
758 pub log_buffered_size: Option<ByteSize>,
760 pub log_format_json: Option<bool>,
762 pub sentry: Option<String>,
764 pub pyroscope: Option<String>,
766 #[serde(default)]
768 #[serde(with = "humantime_serde")]
769 pub auto_restart_check_interval: Option<Duration>,
770 pub cache_directory: Option<String>,
772 pub cache_max_size: Option<ByteSize>,
774}
775
776impl BasicConf {
777 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 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 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 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 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 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 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 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()));
1303pub fn set_current_config(value: &PingapConf) {
1305 CURRENT_CONFIG.store(Arc::new(value.clone()));
1306}
1307
1308pub fn get_current_config() -> Arc<PingapConf> {
1310 CURRENT_CONFIG.load().clone()
1311}
1312
1313pub 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 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 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 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 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}