1use super::{Error, Result};
16use bytesize::ByteSize;
17use http::{HeaderName, HeaderValue};
18use pingap_discovery::{DNS_DISCOVERY, is_static_discovery};
19use pingap_util::{is_pem, resolve_path};
20use regex::Regex;
21use rustls_pki_types::pem::PemObject;
22use serde::{Deserialize, Serialize, Serializer};
23use std::collections::HashSet;
24use std::fs::File;
25use std::hash::{DefaultHasher, Hash, Hasher};
26use std::io::{BufReader, Read};
27use std::net::ToSocketAddrs;
28use std::path::Path;
29use std::time::Duration;
30use std::{collections::HashMap, str::FromStr};
31use strum::EnumString;
32use tempfile::tempfile_in;
33use toml::Table;
34use toml::{Value, map::Map};
35use url::Url;
36
37pub const CATEGORY_BASIC: &str = "basic";
38pub const CATEGORY_SERVER: &str = "server";
39pub const CATEGORY_LOCATION: &str = "location";
40pub const CATEGORY_UPSTREAM: &str = "upstream";
41pub const CATEGORY_PLUGIN: &str = "plugin";
42pub const CATEGORY_CERTIFICATE: &str = "certificate";
43pub const CATEGORY_STORAGE: &str = "storage";
44
45pub trait Validate {
46 fn validate(&self) -> Result<()>;
47}
48
49#[derive(PartialEq, Debug, Default, Clone, EnumString, strum::Display)]
50#[strum(serialize_all = "snake_case")]
51pub enum PluginCategory {
52 #[default]
54 Stats,
55 Limit,
57 Compression,
59 Admin,
61 Directory,
63 Mock,
65 RequestId,
67 IpRestriction,
69 KeyAuth,
71 BasicAuth,
73 CombinedAuth,
75 Jwt,
77 Cache,
79 Redirect,
81 Ping,
83 ResponseHeaders,
85 SubFilter,
87 RefererRestriction,
89 UaRestriction,
91 Csrf,
93 Cors,
95 AcceptEncoding,
97 TrafficSplitting,
99}
100impl Serialize for PluginCategory {
101 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102 where
103 S: Serializer,
104 {
105 serializer.serialize_str(self.to_string().as_ref())
106 }
107}
108
109impl<'de> Deserialize<'de> for PluginCategory {
110 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111 where
112 D: serde::Deserializer<'de>,
113 {
114 let value: String = serde::Deserialize::deserialize(deserializer)?;
115 PluginCategory::from_str(&value).map_err(|_| {
116 serde::de::Error::custom(format!(
117 "invalid plugin category: {value}"
118 ))
119 })
120 }
121}
122
123#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
125pub struct CertificateConf {
126 pub domains: Option<String>,
128 pub tls_cert: Option<String>,
130 pub tls_key: Option<String>,
132 pub is_default: Option<bool>,
134 pub is_ca: Option<bool>,
136 pub acme: Option<String>,
138 pub dns_challenge: Option<bool>,
140 pub dns_provider: Option<String>,
142 pub dns_service_url: Option<String>,
144 pub buffer_days: Option<u16>,
146 pub remark: Option<String>,
148}
149
150fn validate_cert(value: &str) -> Result<()> {
152 let buf_list =
154 pingap_util::convert_pem(value).map_err(|e| Error::Invalid {
155 message: e.to_string(),
156 })?;
157 for buf in buf_list {
158 let certs = rustls_pki_types::CertificateDer::pem_slice_iter(&buf)
160 .collect::<std::result::Result<Vec<_>, _>>()
161 .map_err(|_| Error::Invalid {
162 message: "Failed to parse certificate".to_string(),
163 })?;
164
165 if certs.is_empty() {
167 return Err(Error::Invalid {
168 message: "No valid certificates found in input".to_string(),
169 });
170 }
171 }
172
173 Ok(())
174}
175
176impl Hashable for CertificateConf {
179 fn hash_key(&self) -> String {
180 let mut hasher = DefaultHasher::new();
181
182 self.hash(&mut hasher);
185
186 for value in [&self.tls_cert, &self.tls_key].into_iter().flatten() {
188 if is_pem(value) {
189 continue;
190 }
191 let file_path = resolve_path(value);
192 let path = Path::new(&file_path);
193 if !path.is_file() {
194 continue;
195 }
196
197 match File::open(path) {
198 Ok(file) => {
199 let mut reader = BufReader::new(file);
200 let mut buffer = [0; 8192];
201
202 loop {
203 match reader.read(&mut buffer) {
204 Ok(0) => break, Ok(bytes_read) => {
206 hasher.write(&buffer[..bytes_read]);
208 },
209 Err(e) => {
210 hasher.write(b"Error reading file content:");
211 hasher.write(e.to_string().as_bytes());
212 break;
213 },
214 }
215 }
216 },
217 Err(e) => {
218 hasher.write(b"Error opening file:");
219 hasher.write(e.to_string().as_bytes());
220 },
221 }
222 }
223
224 format!("{:x}", hasher.finish())
225 }
226}
227
228impl Validate for CertificateConf {
229 fn validate(&self) -> Result<()> {
234 let tls_key = self.tls_key.clone().unwrap_or_default();
236 if !tls_key.is_empty() {
237 let buf_list = pingap_util::convert_pem(&tls_key).map_err(|e| {
238 Error::Invalid {
239 message: e.to_string(),
240 }
241 })?;
242 let buf = &buf_list[0];
243 let _ = rustls_pki_types::PrivateKeyDer::from_pem_slice(buf)
244 .map_err(|_| Error::Invalid {
245 message: "Failed to parse private key".to_string(),
246 })?;
247 }
248
249 let tls_cert = self.tls_cert.clone().unwrap_or_default();
251 if !tls_cert.is_empty() {
252 validate_cert(&tls_cert)?;
253 }
254
255 Ok(())
256 }
257}
258
259#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
261pub struct UpstreamConf {
262 pub addrs: Vec<String>,
264
265 pub discovery: Option<String>,
267
268 pub dns_server: Option<String>,
270
271 pub dns_domain: Option<String>,
273
274 pub dns_search: Option<String>,
276
277 #[serde(default)]
279 #[serde(with = "humantime_serde")]
280 pub update_frequency: Option<Duration>,
281
282 pub algo: Option<String>,
284
285 pub sni: Option<String>,
287
288 pub verify_cert: Option<bool>,
290
291 pub health_check: Option<String>,
293
294 pub ipv4_only: Option<bool>,
296
297 pub enable_tracer: Option<bool>,
299
300 pub enable_backend_stats: Option<bool>,
302
303 pub backend_failure_status_code: Option<String>,
306
307 pub circuit_break_max_consecutive_failures: Option<u32>,
309
310 pub circuit_break_max_failure_percent: Option<u16>,
312
313 pub circuit_break_min_requests_threshold: Option<u64>,
317
318 pub circuit_break_half_open_consecutive_success_threshold: Option<u32>,
320
321 #[serde(default)]
323 #[serde(with = "humantime_serde")]
324 pub circuit_break_open_duration: Option<Duration>,
325
326 #[serde(default)]
328 #[serde(with = "humantime_serde")]
329 pub backend_stats_interval: Option<Duration>,
330
331 pub alpn: Option<String>,
333
334 #[serde(default)]
336 #[serde(with = "humantime_serde")]
337 pub connection_timeout: Option<Duration>,
338
339 #[serde(default)]
341 #[serde(with = "humantime_serde")]
342 pub total_connection_timeout: Option<Duration>,
343
344 #[serde(default)]
346 #[serde(with = "humantime_serde")]
347 pub read_timeout: Option<Duration>,
348
349 #[serde(default)]
351 #[serde(with = "humantime_serde")]
352 pub idle_timeout: Option<Duration>,
353
354 #[serde(default)]
356 #[serde(with = "humantime_serde")]
357 pub write_timeout: Option<Duration>,
358
359 #[serde(default)]
361 #[serde(with = "humantime_serde")]
362 pub tcp_idle: Option<Duration>,
363
364 #[serde(default)]
366 #[serde(with = "humantime_serde")]
367 pub tcp_interval: Option<Duration>,
368
369 #[serde(default)]
371 #[serde(with = "humantime_serde")]
372 pub tcp_user_timeout: Option<Duration>,
373
374 pub tcp_probe_count: Option<usize>,
376
377 pub tcp_recv_buf: Option<ByteSize>,
379
380 pub tcp_fast_open: Option<bool>,
382
383 pub includes: Option<Vec<String>>,
385
386 pub remark: Option<String>,
388}
389
390impl Validate for UpstreamConf {
391 fn validate(&self) -> Result<()> {
397 self.validate_addresses()?;
399
400 self.validate_health_check()?;
402
403 self.validate_tcp_probe_count()?;
405
406 Ok(())
407 }
408}
409
410impl UpstreamConf {
411 pub fn guess_discovery(&self) -> String {
416 if let Some(discovery) = &self.discovery {
418 return discovery.clone();
419 }
420
421 let has_hostname = self.addrs.iter().any(|addr| {
423 let host =
425 addr.split_once(':').map_or(addr.as_str(), |(host, _)| host);
426
427 host.parse::<std::net::IpAddr>().is_err()
429 });
430
431 if has_hostname {
432 DNS_DISCOVERY.to_string()
433 } else {
434 String::new()
435 }
436 }
437
438 fn validate_addresses(&self) -> Result<()> {
439 if self.addrs.is_empty() {
440 return Err(Error::Invalid {
441 message: "upstream addrs is empty".to_string(),
442 });
443 }
444
445 if !is_static_discovery(&self.guess_discovery()) {
447 return Ok(());
448 }
449
450 for addr in &self.addrs {
451 let parts: Vec<_> = addr.split_whitespace().collect();
452 let host_port = parts[0].to_string();
453
454 let addr_to_check = if !host_port.contains(':') {
456 format!("{host_port}:80")
457 } else {
458 host_port
459 };
460
461 addr_to_check.to_socket_addrs().map_err(|e| Error::Io {
463 source: e,
464 file: addr_to_check,
465 })?;
466 }
467
468 Ok(())
469 }
470
471 fn validate_health_check(&self) -> Result<()> {
472 let health_check = match &self.health_check {
473 Some(url) if !url.is_empty() => url,
474 _ => return Ok(()),
475 };
476
477 Url::parse(health_check).map_err(|e| Error::UrlParse {
478 source: e,
479 url: health_check.to_string(),
480 })?;
481
482 Ok(())
483 }
484
485 fn validate_tcp_probe_count(&self) -> Result<()> {
486 const MAX_TCP_PROBE_COUNT: usize = 16;
487
488 if let Some(count) = self.tcp_probe_count {
489 if count > MAX_TCP_PROBE_COUNT {
490 return Err(Error::Invalid {
491 message: format!(
492 "tcp probe count should be <= {MAX_TCP_PROBE_COUNT}"
493 ),
494 });
495 }
496 }
497
498 Ok(())
499 }
500}
501
502impl Validate for LocationConf {
503 fn validate(&self) -> Result<()> {
504 self.validate_with_upstream(None)?;
505 Ok(())
506 }
507}
508
509#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
511pub struct LocationConf {
512 pub upstream: Option<String>,
514
515 pub path: Option<String>,
521
522 pub host: Option<String>,
524
525 pub proxy_set_headers: Option<Vec<String>>,
527
528 pub proxy_add_headers: Option<Vec<String>>,
530
531 pub rewrite: Option<String>,
533
534 pub weight: Option<u16>,
537
538 pub plugins: Option<Vec<String>>,
540
541 pub client_max_body_size: Option<ByteSize>,
543
544 pub max_processing: Option<i32>,
546
547 pub includes: Option<Vec<String>>,
549
550 pub grpc_web: Option<bool>,
552
553 pub enable_reverse_proxy_headers: Option<bool>,
555
556 pub max_retries: Option<u8>,
558
559 #[serde(default)]
561 #[serde(with = "humantime_serde")]
562 pub max_retry_window: Option<Duration>,
563
564 pub remark: Option<String>,
566}
567
568impl LocationConf {
569 fn validate_with_upstream(
575 &self,
576 upstream_names: Option<&[String]>,
577 ) -> Result<()> {
578 let validate = |headers: &Option<Vec<String>>| -> Result<()> {
580 if let Some(headers) = headers {
581 for header in headers.iter() {
582 let arr = header
584 .split_once(':')
585 .map(|(k, v)| (k.trim(), v.trim()));
586 let Some((header_name, header_value)) = arr else {
587 return Err(Error::Invalid {
588 message: format!("header {header} is invalid"),
589 });
590 };
591
592 HeaderName::from_bytes(header_name.as_bytes()).map_err(|err| Error::Invalid {
594 message: format!("header name({header_name}) is invalid, error: {err}"),
595 })?;
596
597 HeaderValue::from_str(header_value).map_err(|err| Error::Invalid {
599 message: format!("header value({header_value}) is invalid, error: {err}"),
600 })?;
601 }
602 }
603 Ok(())
604 };
605
606 if let Some(upstream_names) = upstream_names {
608 let upstream = self.upstream.clone().unwrap_or_default();
609 if !upstream.is_empty()
610 && !upstream.starts_with("$")
611 && !upstream_names.contains(&upstream)
612 {
613 return Err(Error::Invalid {
614 message: format!("upstream({upstream}) is not found"),
615 });
616 }
617 }
618
619 validate(&self.proxy_add_headers)?;
621 validate(&self.proxy_set_headers)?;
622
623 if let Some(value) = &self.rewrite {
625 let arr: Vec<&str> = value.split(' ').collect();
626 let _ =
627 Regex::new(arr[0]).map_err(|e| Error::Regex { source: e })?;
628 }
629
630 Ok(())
631 }
632
633 pub fn get_weight(&self) -> u16 {
642 if let Some(weight) = self.weight {
644 return weight;
645 }
646
647 let mut weight: u16 = 0;
648 let path = self.path.clone().unwrap_or("".to_string());
649
650 if path.len() > 1 {
652 if path.starts_with('=') {
653 weight += 1024; } else if path.starts_with('~') {
655 weight += 256; } else {
657 weight += 512; }
659 weight += path.len().min(64) as u16;
660 };
661 if let Some(host) = &self.host {
663 let exist_regex = host.split(',').any(|item| item.starts_with("~"));
664 if !exist_regex && !host.is_empty() {
667 weight += 128;
668 } else {
669 weight += host.len() as u16;
670 }
671 }
672
673 weight
674 }
675}
676
677#[derive(Debug, Default, Deserialize, Clone, Serialize)]
679pub struct ServerConf {
680 pub addr: String,
682
683 pub access_log: Option<String>,
685
686 pub locations: Option<Vec<String>>,
688
689 pub threads: Option<usize>,
691
692 pub tls_cipher_list: Option<String>,
694
695 pub tls_ciphersuites: Option<String>,
697
698 pub tls_min_version: Option<String>,
700
701 pub tls_max_version: Option<String>,
703
704 pub global_certificates: Option<bool>,
706
707 pub enabled_h2: Option<bool>,
709
710 #[serde(default)]
712 #[serde(with = "humantime_serde")]
713 pub tcp_idle: Option<Duration>,
714
715 #[serde(default)]
717 #[serde(with = "humantime_serde")]
718 pub tcp_interval: Option<Duration>,
719
720 #[serde(default)]
722 #[serde(with = "humantime_serde")]
723 pub tcp_user_timeout: Option<Duration>,
724
725 #[serde(default)]
727 #[serde(with = "humantime_serde")]
728 pub downstream_read_timeout: Option<Duration>,
729
730 #[serde(default)]
732 #[serde(with = "humantime_serde")]
733 pub downstream_write_timeout: Option<Duration>,
734
735 pub tcp_probe_count: Option<usize>,
737
738 pub tcp_fastopen: Option<usize>,
740
741 pub reuse_port: Option<bool>,
745
746 pub prometheus_metrics: Option<String>,
748
749 pub otlp_exporter: Option<String>,
751
752 pub includes: Option<Vec<String>>,
754
755 pub modules: Option<Vec<String>>,
757
758 pub enable_server_timing: Option<bool>,
760
761 pub remark: Option<String>,
763}
764
765impl Validate for ServerConf {
766 fn validate(&self) -> Result<()> {
767 self.validate_with_locations(&[])?;
768 Ok(())
769 }
770}
771
772impl ServerConf {
773 fn validate_with_locations(&self, location_names: &[String]) -> Result<()> {
778 for addr in self.addr.split(',') {
779 let _ = addr.to_socket_addrs().map_err(|e| Error::Io {
780 source: e,
781 file: self.addr.clone(),
782 })?;
783 }
784 if !location_names.is_empty() {
785 if let Some(locations) = &self.locations {
786 for item in locations {
787 if !location_names.contains(item) {
788 return Err(Error::Invalid {
789 message: format!("location({item}) is not found"),
790 });
791 }
792 }
793 }
794 }
795 let access_log = self.access_log.clone().unwrap_or_default();
796 if !access_log.is_empty() {
797 }
805
806 Ok(())
807 }
808}
809
810#[derive(Debug, Default, Deserialize, Clone, Serialize)]
812pub struct BasicConf {
813 pub name: Option<String>,
815 pub error_template: Option<String>,
817 pub pid_file: Option<String>,
819 pub upgrade_sock: Option<String>,
821 pub user: Option<String>,
823 pub group: Option<String>,
825 pub threads: Option<usize>,
827 pub work_stealing: Option<bool>,
829 pub listener_tasks_per_fd: Option<usize>,
831 #[serde(default)]
833 #[serde(with = "humantime_serde")]
834 pub grace_period: Option<Duration>,
835 #[serde(default)]
837 #[serde(with = "humantime_serde")]
838 pub graceful_shutdown_timeout: Option<Duration>,
839 pub upstream_keepalive_pool_size: Option<usize>,
841 pub webhook: Option<String>,
843 pub webhook_type: Option<String>,
845 pub webhook_notifications: Option<Vec<String>>,
847 pub log_level: Option<String>,
849 pub log_buffered_size: Option<ByteSize>,
851 pub log_format_json: Option<bool>,
853 pub sentry: Option<String>,
855 pub pyroscope: Option<String>,
857 #[serde(default)]
859 #[serde(with = "humantime_serde")]
860 pub auto_restart_check_interval: Option<Duration>,
861
862 pub log_compress_algorithm: Option<String>,
864 pub log_compress_level: Option<u8>,
866 pub log_compress_days_ago: Option<u16>,
868 pub log_compress_time_point_hour: Option<u8>,
870}
871
872impl Validate for BasicConf {
873 fn validate(&self) -> Result<()> {
874 Ok(())
875 }
876}
877
878impl BasicConf {
879 pub fn get_pid_file(&self) -> String {
884 if let Some(pid_file) = &self.pid_file {
885 return pid_file.clone();
886 }
887 for dir in ["/run", "/var/run"] {
888 if tempfile_in(dir).is_ok() {
889 return format!("{dir}/pingap.pid");
890 }
891 }
892 "/tmp/pingap.pid".to_string()
893 }
894}
895
896#[derive(Debug, Default, Deserialize, Clone, Serialize)]
897pub struct StorageConf {
898 pub category: String,
899 pub value: String,
900 pub secret: Option<String>,
901 pub remark: Option<String>,
902}
903
904impl Validate for StorageConf {
905 fn validate(&self) -> Result<()> {
906 Ok(())
907 }
908}
909
910pub trait Hashable: Hash {
911 fn hash_key(&self) -> String {
912 let mut hasher = DefaultHasher::new();
913 self.hash(&mut hasher);
914 format!("{:x}", hasher.finish())
915 }
916}
917impl Hashable for UpstreamConf {}
918impl Hashable for LocationConf {}
919
920#[derive(Deserialize, Debug, Serialize)]
921struct TomlConfig {
922 basic: Option<BasicConf>,
923 servers: Option<Map<String, Value>>,
924 upstreams: Option<Map<String, Value>>,
925 locations: Option<Map<String, Value>>,
926 plugins: Option<Map<String, Value>>,
927 certificates: Option<Map<String, Value>>,
928 storages: Option<Map<String, Value>>,
929}
930
931fn format_toml(value: &Value) -> String {
932 if let Some(value) = value.as_table() {
933 value.to_string()
934 } else {
935 "".to_string()
936 }
937}
938
939pub type PluginConf = Map<String, Value>;
940
941impl Validate for PluginConf {
942 fn validate(&self) -> Result<()> {
943 Ok(())
944 }
945}
946
947#[derive(Debug, Default, Clone, Deserialize, Serialize)]
948pub struct PingapConfig {
949 pub basic: BasicConf,
950 pub upstreams: HashMap<String, UpstreamConf>,
951 pub locations: HashMap<String, LocationConf>,
952 pub servers: HashMap<String, ServerConf>,
953 pub plugins: HashMap<String, PluginConf>,
954 pub certificates: HashMap<String, CertificateConf>,
955 pub storages: HashMap<String, StorageConf>,
956}
957
958impl PingapConfig {
959 fn get_value_for_category<T: Serialize>(
961 &self,
962 map: &HashMap<String, T>,
963 name: Option<&str>,
964 ) -> Result<toml::Value> {
965 match name {
966 Some(name) => {
968 if let Some(item) = map.get(name) {
969 let mut table = Map::new();
970 table.insert(
971 name.to_string(),
972 toml::Value::try_from(item)
973 .map_err(|e| Error::Ser { source: e })?,
974 );
975 Ok(toml::Value::Table(table))
976 } else {
977 Ok(toml::Value::Table(Map::new())) }
979 },
980 None => Ok(toml::Value::try_from(map)
982 .map_err(|e| Error::Ser { source: e })?),
983 }
984 }
985 pub fn get_toml(
986 &self,
987 category: &str,
988 name: Option<&str>,
989 ) -> Result<(String, String)> {
990 let (key, value_to_serialize) = match category {
991 CATEGORY_SERVER => {
992 ("servers", self.get_value_for_category(&self.servers, name)?)
993 },
994 CATEGORY_LOCATION => (
995 "locations",
996 self.get_value_for_category(&self.locations, name)?,
997 ),
998 CATEGORY_UPSTREAM => (
999 "upstreams",
1000 self.get_value_for_category(&self.upstreams, name)?,
1001 ),
1002 CATEGORY_PLUGIN => {
1003 ("plugins", self.get_value_for_category(&self.plugins, name)?)
1004 },
1005 CATEGORY_CERTIFICATE => (
1006 "certificates",
1007 self.get_value_for_category(&self.certificates, name)?,
1008 ),
1009 CATEGORY_STORAGE => (
1010 "storages",
1011 self.get_value_for_category(&self.storages, name)?,
1012 ),
1013 _ => (
1014 CATEGORY_BASIC,
1015 toml::Value::try_from(&self.basic)
1016 .map_err(|e| Error::Ser { source: e })?,
1017 ),
1018 };
1019
1020 let path = {
1021 let name = name.unwrap_or_default();
1022 if key == CATEGORY_BASIC || name.is_empty() {
1023 format!("/{key}.toml")
1024 } else {
1025 format!("/{key}/{name}.toml")
1026 }
1027 };
1028
1029 if let Some(table) = value_to_serialize.as_table() {
1030 if table.is_empty() {
1031 return Ok((path, "".to_string()));
1032 }
1033 }
1034
1035 let mut wrapper = Map::new();
1036 wrapper.insert(key.to_string(), value_to_serialize);
1037
1038 let toml_string = toml::to_string_pretty(&wrapper)
1039 .map_err(|e| Error::Ser { source: e })?;
1040
1041 Ok((path, toml_string))
1042 }
1043 pub fn get_storage_value(&self, name: &str) -> Result<String> {
1044 for (key, item) in self.storages.iter() {
1045 if key != name {
1046 continue;
1047 }
1048
1049 if let Some(key) = &item.secret {
1050 return pingap_util::aes_decrypt(key, &item.value).map_err(
1051 |e| Error::Invalid {
1052 message: e.to_string(),
1053 },
1054 );
1055 }
1056 return Ok(item.value.clone());
1057 }
1058 Ok("".to_string())
1059 }
1060}
1061
1062fn convert_include_toml(
1063 data: &HashMap<String, String>,
1064 replace_includes: bool,
1065 mut value: Value,
1066) -> String {
1067 let Some(m) = value.as_table_mut() else {
1068 return "".to_string();
1069 };
1070 if !replace_includes {
1071 return m.to_string();
1072 }
1073 if let Some(includes) = m.remove("includes") {
1074 if let Some(includes) = get_include_toml(data, includes) {
1075 if let Ok(includes) = toml::from_str::<Table>(&includes) {
1076 for (key, value) in includes.iter() {
1077 m.insert(key.to_string(), value.clone());
1078 }
1079 }
1080 }
1081 }
1082 m.to_string()
1083}
1084
1085fn get_include_toml(
1086 data: &HashMap<String, String>,
1087 includes: Value,
1088) -> Option<String> {
1089 let values = includes.as_array()?;
1090 let arr: Vec<String> = values
1091 .iter()
1092 .map(|item| {
1093 let key = item.as_str().unwrap_or_default();
1094 if let Some(value) = data.get(key) {
1095 value.clone()
1096 } else {
1097 "".to_string()
1098 }
1099 })
1100 .collect();
1101 Some(arr.join("\n"))
1102}
1103
1104pub(crate) fn convert_pingap_config(
1105 data: &[u8],
1106 replace_include: bool,
1107) -> Result<PingapConfig, Error> {
1108 let data: TomlConfig = toml::from_str(
1109 std::string::String::from_utf8_lossy(data)
1110 .to_string()
1111 .as_str(),
1112 )
1113 .map_err(|e| Error::De { source: e })?;
1114
1115 let mut conf = PingapConfig {
1116 basic: data.basic.unwrap_or_default(),
1117 ..Default::default()
1118 };
1119 let mut includes = HashMap::new();
1120 for (name, value) in data.storages.unwrap_or_default() {
1121 let toml = format_toml(&value);
1122 let storage: StorageConf = toml::from_str(toml.as_str())
1123 .map_err(|e| Error::De { source: e })?;
1124 includes.insert(name.clone(), storage.value.clone());
1125 conf.storages.insert(name, storage);
1126 }
1127
1128 for (name, value) in data.upstreams.unwrap_or_default() {
1129 let toml = convert_include_toml(&includes, replace_include, value);
1130
1131 let upstream: UpstreamConf = toml::from_str(toml.as_str())
1132 .map_err(|e| Error::De { source: e })?;
1133 conf.upstreams.insert(name, upstream);
1134 }
1135 for (name, value) in data.locations.unwrap_or_default() {
1136 let toml = convert_include_toml(&includes, replace_include, value);
1137
1138 let location: LocationConf = toml::from_str(toml.as_str())
1139 .map_err(|e| Error::De { source: e })?;
1140 conf.locations.insert(name, location);
1141 }
1142 for (name, value) in data.servers.unwrap_or_default() {
1143 let toml = convert_include_toml(&includes, replace_include, value);
1144
1145 let server: ServerConf = toml::from_str(toml.as_str())
1146 .map_err(|e| Error::De { source: e })?;
1147 conf.servers.insert(name, server);
1148 }
1149 for (name, value) in data.plugins.unwrap_or_default() {
1150 let plugin: PluginConf = toml::from_str(format_toml(&value).as_str())
1151 .map_err(|e| Error::De { source: e })?;
1152 conf.plugins.insert(name, plugin);
1153 }
1154
1155 for (name, value) in data.certificates.unwrap_or_default() {
1156 let certificate: CertificateConf =
1157 toml::from_str(format_toml(&value).as_str())
1158 .map_err(|e| Error::De { source: e })?;
1159 conf.certificates.insert(name, certificate);
1160 }
1161
1162 Ok(conf)
1163}
1164
1165#[derive(Debug, Default, Clone, Deserialize, Serialize)]
1166struct Description {
1167 category: String,
1168 name: String,
1169 data: String,
1170}
1171
1172impl PingapConfig {
1173 pub fn new(data: &[u8], replace_includes: bool) -> Result<Self> {
1174 convert_pingap_config(data, replace_includes)
1175 }
1176 pub fn validate(&self) -> Result<()> {
1178 let mut upstream_names = vec![];
1179 for (name, upstream) in self.upstreams.iter() {
1180 upstream.validate()?;
1181 upstream_names.push(name.to_string());
1182 }
1183 let mut location_names = vec![];
1184 for (name, location) in self.locations.iter() {
1185 location.validate_with_upstream(Some(&upstream_names))?;
1186 location_names.push(name.to_string());
1187 }
1188 let mut listen_addr_list = vec![];
1189 for server in self.servers.values() {
1190 for addr in server.addr.split(',') {
1191 if listen_addr_list.contains(&addr.to_string()) {
1192 return Err(Error::Invalid {
1193 message: format!("{addr} is inused by other server"),
1194 });
1195 }
1196 listen_addr_list.push(addr.to_string());
1197 }
1198 server.validate_with_locations(&location_names)?;
1199 }
1200 for (_, certificate) in self.certificates.iter() {
1209 certificate.validate()?;
1210 }
1211 let ping_conf = toml::to_string_pretty(self)
1212 .map_err(|e| Error::Ser { source: e })?;
1213 convert_pingap_config(ping_conf.as_bytes(), true)?;
1214 Ok(())
1215 }
1216 pub fn hash(&self) -> Result<String> {
1218 let mut lines = vec![];
1219 for desc in self.descriptions() {
1220 lines.push(desc.category);
1221 lines.push(desc.name);
1222 lines.push(desc.data);
1223 }
1224 let hash = crc32fast::hash(lines.join("\n").as_bytes());
1225 Ok(format!("{hash:X}"))
1226 }
1227 pub fn remove(&mut self, category: &str, name: &str) -> Result<()> {
1229 match category {
1230 CATEGORY_UPSTREAM => {
1231 for (location_name, location) in self.locations.iter() {
1232 if let Some(upstream) = &location.upstream {
1233 if upstream == name {
1234 return Err(Error::Invalid {
1235 message: format!(
1236 "upstream({name}) is in used by location({location_name})",
1237 ),
1238 });
1239 }
1240 }
1241 }
1242 self.upstreams.remove(name);
1243 },
1244 CATEGORY_LOCATION => {
1245 for (server_name, server) in self.servers.iter() {
1246 if let Some(locations) = &server.locations {
1247 if locations.contains(&name.to_string()) {
1248 return Err(Error::Invalid {
1249 message: format!(
1250 "location({name}) is in used by server({server_name})"
1251 ),
1252 });
1253 }
1254 }
1255 }
1256 self.locations.remove(name);
1257 },
1258 CATEGORY_SERVER => {
1259 self.servers.remove(name);
1260 },
1261 CATEGORY_PLUGIN => {
1262 for (location_name, location) in self.locations.iter() {
1263 if let Some(plugins) = &location.plugins {
1264 if plugins.contains(&name.to_string()) {
1265 return Err(Error::Invalid {
1266 message: format!(
1267 "proxy plugin({name}) is in used by location({location_name})"
1268 ),
1269 });
1270 }
1271 }
1272 }
1273 self.plugins.remove(name);
1274 },
1275 CATEGORY_CERTIFICATE => {
1276 self.certificates.remove(name);
1277 },
1278 _ => {},
1279 };
1280 Ok(())
1281 }
1282 fn descriptions(&self) -> Vec<Description> {
1283 let mut value = self.clone();
1284 let mut descriptions = vec![];
1285 for (name, data) in value.servers.iter() {
1286 descriptions.push(Description {
1287 category: CATEGORY_SERVER.to_string(),
1288 name: format!("server:{name}"),
1289 data: toml::to_string_pretty(data).unwrap_or_default(),
1290 });
1291 }
1292 for (name, data) in value.locations.iter() {
1293 descriptions.push(Description {
1294 category: CATEGORY_LOCATION.to_string(),
1295 name: format!("location:{name}"),
1296 data: toml::to_string_pretty(data).unwrap_or_default(),
1297 });
1298 }
1299 for (name, data) in value.upstreams.iter() {
1300 descriptions.push(Description {
1301 category: CATEGORY_UPSTREAM.to_string(),
1302 name: format!("upstream:{name}"),
1303 data: toml::to_string_pretty(data).unwrap_or_default(),
1304 });
1305 }
1306 for (name, data) in value.plugins.iter() {
1307 descriptions.push(Description {
1308 category: CATEGORY_PLUGIN.to_string(),
1309 name: format!("plugin:{name}"),
1310 data: toml::to_string_pretty(data).unwrap_or_default(),
1311 });
1312 }
1313 for (name, data) in value.certificates.iter() {
1314 let mut clone_data = data.clone();
1315 if let Some(cert) = &clone_data.tls_cert {
1316 clone_data.tls_cert = Some(format!(
1317 "crc32:{:X}",
1318 crc32fast::hash(cert.as_bytes())
1319 ));
1320 }
1321 if let Some(key) = &clone_data.tls_key {
1322 clone_data.tls_key = Some(format!(
1323 "crc32:{:X}",
1324 crc32fast::hash(key.as_bytes())
1325 ));
1326 }
1327 descriptions.push(Description {
1328 category: CATEGORY_CERTIFICATE.to_string(),
1329 name: format!("certificate:{name}"),
1330 data: toml::to_string_pretty(&clone_data).unwrap_or_default(),
1331 });
1332 }
1333 for (name, data) in value.storages.iter() {
1334 let mut clone_data = data.clone();
1335 if let Some(secret) = &clone_data.secret {
1336 clone_data.secret = Some(format!(
1337 "crc32:{:X}",
1338 crc32fast::hash(secret.as_bytes())
1339 ));
1340 }
1341 descriptions.push(Description {
1342 category: CATEGORY_STORAGE.to_string(),
1343 name: format!("storage:{name}"),
1344 data: toml::to_string_pretty(&clone_data).unwrap_or_default(),
1345 });
1346 }
1347 value.servers = HashMap::new();
1348 value.locations = HashMap::new();
1349 value.upstreams = HashMap::new();
1350 value.plugins = HashMap::new();
1351 value.certificates = HashMap::new();
1352 value.storages = HashMap::new();
1353 descriptions.push(Description {
1354 category: CATEGORY_BASIC.to_string(),
1355 name: CATEGORY_BASIC.to_string(),
1356 data: toml::to_string_pretty(&value).unwrap_or_default(),
1357 });
1358 descriptions.sort_by_key(|d| d.name.clone());
1359 descriptions
1360 }
1361 pub fn diff(&self, other: &PingapConfig) -> (Vec<String>, Vec<String>) {
1363 let current_map: HashMap<_, _> = self
1365 .descriptions()
1366 .into_iter()
1367 .map(|d| (d.name.clone(), d))
1368 .collect();
1369 let new_map: HashMap<_, _> = other
1370 .descriptions()
1371 .into_iter()
1372 .map(|d| (d.name.clone(), d))
1373 .collect();
1374
1375 let mut affected_categories = HashSet::new();
1377
1378 let mut added_items = vec![];
1380 let mut removed_items = vec![];
1381 let mut modified_items = vec![];
1382
1383 for (name, current_item) in ¤t_map {
1385 match new_map.get(name) {
1386 Some(new_item) => {
1387 if current_item.data != new_item.data {
1389 affected_categories
1390 .insert(current_item.category.clone());
1391
1392 let mut item_diff_result = vec![];
1394 for diff in
1395 diff::lines(¤t_item.data, &new_item.data)
1396 {
1397 match diff {
1398 diff::Result::Left(l) => {
1399 item_diff_result.push(format!("- {l}"))
1400 },
1401 diff::Result::Right(r) => {
1402 item_diff_result.push(format!("+ {r}"))
1403 },
1404 _ => (),
1405 }
1406 }
1407
1408 if !item_diff_result.is_empty() {
1409 modified_items.push(format!("[MODIFIED] {name}"));
1410 modified_items.extend(item_diff_result);
1411 modified_items.push("".to_string()); }
1413 }
1414 },
1415 None => {
1416 removed_items.push(format!("-- [REMOVED] {name}"));
1418 affected_categories.insert(current_item.category.clone());
1419 },
1420 }
1421 }
1422
1423 for (name, new_item) in &new_map {
1425 if !current_map.contains_key(name) {
1426 added_items.push(format!("++ [ADDED] {name}"));
1427 affected_categories.insert(new_item.category.clone());
1428 }
1429 }
1430
1431 let mut final_diff = Vec::new();
1433 if !added_items.is_empty() {
1434 final_diff.extend(added_items);
1435 final_diff.push("".to_string()); }
1437 if !removed_items.is_empty() {
1438 final_diff.extend(removed_items);
1439 final_diff.push("".to_string());
1440 }
1441 if !modified_items.is_empty() {
1442 final_diff.extend(modified_items);
1443 }
1444
1445 (affected_categories.into_iter().collect(), final_diff)
1447 }
1448}
1449
1450#[cfg(test)]
1451mod tests {
1452 use super::{CertificateConf, Hashable, Validate, validate_cert};
1453 use super::{LocationConf, PluginCategory, ServerConf, UpstreamConf};
1454 use pingap_core::PluginStep;
1455 use pingap_util::base64_encode;
1456 use pretty_assertions::assert_eq;
1457 use serde::{Deserialize, Serialize};
1458 use std::str::FromStr;
1459
1460 #[test]
1461 fn test_plugin_step() {
1462 let step = PluginStep::from_str("early_request").unwrap();
1463 assert_eq!(step, PluginStep::EarlyRequest);
1464
1465 assert_eq!("early_request", step.to_string());
1466 }
1467
1468 #[test]
1469 fn test_validate_cert() {
1470 let pem = r#"-----BEGIN CERTIFICATE-----
1472MIIEljCCAv6gAwIBAgIQeYUdeFj3gpzhQes3aGaMZTANBgkqhkiG9w0BAQsFADCB
1473pTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMT0wOwYDVQQLDDR4aWVz
1474aHV6aG91QHhpZXNodXpob3VzLU1hY0Jvb2stQWlyLmxvY2FsICjosKLmoJHmtLIp
1475MUQwQgYDVQQDDDtta2NlcnQgeGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29r
1476LUFpci5sb2NhbCAo6LCi5qCR5rSyKTAeFw0yMzA5MjQxMzA1MjdaFw0yNTEyMjQx
1477MzA1MjdaMGgxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0
1478ZTE9MDsGA1UECww0eGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29rLUFpci5s
1479b2NhbCAo6LCi5qCR5rSyKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
1480ALuJ8lYEj9uf4iE9hguASq7re87Np+zJc2x/eqr1cR/SgXRStBsjxqI7i3xwMRqX
1481AuhAnM6ktlGuqidl7D9y6AN/UchqgX8AetslRJTpCcEDfL/q24zy0MqOS0FlYEgh
1482s4PIjWsSNoglBDeaIdUpN9cM/64IkAAtHndNt2p2vPfjrPeixLjese096SKEnZM/
1483xBdWF491hx06IyzjtWKqLm9OUmYZB9d/gDGnDsKpqClw8m95opKD4TBHAoE//WvI
1484m1mZnjNTNR27vVbmnc57d2Lx2Ib2eqJG5zMsP2hPBoqS8CKEwMRFLHAcclNkI67U
1485kcSEGaWgr15QGHJPN/FtjDsCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud
1486JQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFJo0y9bYUM/OuenDjsJ1RyHJfL3n
1487MDQGA1UdEQQtMCuCBm1lLmRldoIJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAA
1488AAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBgQAlQbow3+4UyQx+E+J0RwmHBltU6i+K
1489soFfza6FWRfAbTyv+4KEWl2mx51IfHhJHYZvsZqPqGWxm5UvBecskegDExFMNFVm
1490O5QixydQzHHY2krmBwmDZ6Ao88oW/qw4xmMUhzKAZbsqeQyE/uiUdyI4pfDcduLB
1491rol31g9OFsgwZrZr0d1ZiezeYEhemnSlh9xRZW3veKx9axgFttzCMmWdpGTCvnav
1492ZVc3rB+KBMjdCwsS37zmrNm9syCjW1O5a1qphwuMpqSnDHBgKWNpbsgqyZM0oyOc
14939Bkja+BV5wFO+4zH5WtestcrNMeoQ83a5lI0m42u/bUEJ/T/5BQBSFidNuvS7Ylw
1494IZpXa00xvlnm1BOHOfRI4Ehlfa5jmfcdnrGkQLGjiyygQtKcc7rOXGK+mSeyxwhs
1495sIARwslSQd4q0dbYTPKvvUHxTYiCv78vQBAsE15T2GGS80pAFDBW9vOf3upANvOf
1496EHjKf0Dweb4ppL4ddgeAKU5V0qn76K2fFaE=
1497-----END CERTIFICATE-----"#;
1498 let result = validate_cert(pem);
1500 assert_eq!(true, result.is_ok());
1501
1502 let value = base64_encode(pem);
1503 let result = validate_cert(&value);
1504 assert_eq!(true, result.is_ok());
1505 }
1506
1507 #[test]
1508 fn test_plugin_category_serde() {
1509 #[derive(Deserialize, Serialize)]
1510 struct TmpPluginCategory {
1511 category: PluginCategory,
1512 }
1513 let tmp = TmpPluginCategory {
1514 category: PluginCategory::RequestId,
1515 };
1516 let data = serde_json::to_string(&tmp).unwrap();
1517 assert_eq!(r#"{"category":"request_id"}"#, data);
1518
1519 let tmp: TmpPluginCategory = serde_json::from_str(&data).unwrap();
1520 assert_eq!(PluginCategory::RequestId, tmp.category);
1521 }
1522
1523 #[test]
1524 fn test_upstream_conf() {
1525 let mut conf = UpstreamConf::default();
1526
1527 let result = conf.validate();
1528 assert_eq!(true, result.is_err());
1529 assert_eq!(
1530 "Invalid error upstream addrs is empty",
1531 result.expect_err("").to_string()
1532 );
1533
1534 conf.addrs = vec!["127.0.0.1".to_string(), "github".to_string()];
1535 conf.discovery = Some("static".to_string());
1536 let result = conf.validate();
1537 assert_eq!(true, result.is_err());
1538 assert_eq!(
1539 true,
1540 result
1541 .expect_err("")
1542 .to_string()
1543 .contains("Io error failed to lookup address information")
1544 );
1545
1546 conf.addrs = vec!["127.0.0.1".to_string(), "github.com".to_string()];
1547 conf.health_check = Some("http:///".to_string());
1548 let result = conf.validate();
1549 assert_eq!(true, result.is_err());
1550 assert_eq!(
1551 "Url parse error empty host, http:///",
1552 result.expect_err("").to_string()
1553 );
1554
1555 conf.health_check = Some("http://github.com/".to_string());
1556 let result = conf.validate();
1557 assert_eq!(true, result.is_ok());
1558 }
1559
1560 #[test]
1561 fn test_location_conf() {
1562 let mut conf = LocationConf::default();
1563 let upstream_names = vec!["upstream1".to_string()];
1564
1565 conf.upstream = Some("upstream2".to_string());
1566 let result = conf.validate_with_upstream(Some(&upstream_names));
1567 assert_eq!(true, result.is_err());
1568 assert_eq!(
1569 "Invalid error upstream(upstream2) is not found",
1570 result.expect_err("").to_string()
1571 );
1572
1573 conf.upstream = Some("upstream1".to_string());
1574 conf.proxy_set_headers = Some(vec!["X-Request-Id".to_string()]);
1575 let result = conf.validate_with_upstream(Some(&upstream_names));
1576 assert_eq!(true, result.is_err());
1577 assert_eq!(
1578 "Invalid error header X-Request-Id is invalid",
1579 result.expect_err("").to_string()
1580 );
1581
1582 conf.proxy_set_headers = Some(vec!["请求:响应".to_string()]);
1583 let result = conf.validate_with_upstream(Some(&upstream_names));
1584 assert_eq!(true, result.is_err());
1585 assert_eq!(
1586 "Invalid error header name(请求) is invalid, error: invalid HTTP header name",
1587 result.expect_err("").to_string()
1588 );
1589
1590 conf.proxy_set_headers = Some(vec!["X-Request-Id: abcd".to_string()]);
1591 let result = conf.validate_with_upstream(Some(&upstream_names));
1592 assert_eq!(true, result.is_ok());
1593
1594 conf.rewrite = Some(r"foo(bar".to_string());
1595 let result = conf.validate_with_upstream(Some(&upstream_names));
1596 assert_eq!(true, result.is_err());
1597 assert_eq!(
1598 true,
1599 result
1600 .expect_err("")
1601 .to_string()
1602 .starts_with("Regex error regex parse error")
1603 );
1604
1605 conf.rewrite = Some(r"^/api /".to_string());
1606 let result = conf.validate_with_upstream(Some(&upstream_names));
1607 assert_eq!(true, result.is_ok());
1608 }
1609
1610 #[test]
1611 fn test_location_get_wegiht() {
1612 let mut conf = LocationConf {
1613 weight: Some(2048),
1614 ..Default::default()
1615 };
1616
1617 assert_eq!(2048, conf.get_weight());
1618
1619 conf.weight = None;
1620 conf.path = Some("=/api".to_string());
1621 assert_eq!(1029, conf.get_weight());
1622
1623 conf.path = Some("~/api".to_string());
1624 assert_eq!(261, conf.get_weight());
1625
1626 conf.path = Some("/api".to_string());
1627 assert_eq!(516, conf.get_weight());
1628
1629 conf.path = None;
1630 conf.host = Some("github.com".to_string());
1631 assert_eq!(128, conf.get_weight());
1632
1633 conf.host = Some("~github.com".to_string());
1634 assert_eq!(11, conf.get_weight());
1635
1636 conf.host = Some("".to_string());
1637 assert_eq!(0, conf.get_weight());
1638 }
1639
1640 #[test]
1641 fn test_server_conf() {
1642 let mut conf = ServerConf::default();
1643 let location_names = vec!["lo".to_string()];
1644
1645 let result = conf.validate_with_locations(&location_names);
1646 assert_eq!(true, result.is_err());
1647 assert_eq!(
1648 "Io error invalid socket address, ",
1649 result.expect_err("").to_string()
1650 );
1651
1652 conf.addr = "127.0.0.1:3001".to_string();
1653 conf.locations = Some(vec!["lo1".to_string()]);
1654 let result = conf.validate_with_locations(&location_names);
1655 assert_eq!(true, result.is_err());
1656 assert_eq!(
1657 "Invalid error location(lo1) is not found",
1658 result.expect_err("").to_string()
1659 );
1660
1661 conf.locations = Some(vec!["lo".to_string()]);
1662 let result = conf.validate_with_locations(&location_names);
1663 assert_eq!(true, result.is_ok());
1664 }
1665
1666 #[test]
1667 fn test_certificate_conf() {
1668 let pem = r#"-----BEGIN CERTIFICATE-----
1670MIIEljCCAv6gAwIBAgIQeYUdeFj3gpzhQes3aGaMZTANBgkqhkiG9w0BAQsFADCB
1671pTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMT0wOwYDVQQLDDR4aWVz
1672aHV6aG91QHhpZXNodXpob3VzLU1hY0Jvb2stQWlyLmxvY2FsICjosKLmoJHmtLIp
1673MUQwQgYDVQQDDDtta2NlcnQgeGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29r
1674LUFpci5sb2NhbCAo6LCi5qCR5rSyKTAeFw0yMzA5MjQxMzA1MjdaFw0yNTEyMjQx
1675MzA1MjdaMGgxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0
1676ZTE9MDsGA1UECww0eGllc2h1emhvdUB4aWVzaHV6aG91cy1NYWNCb29rLUFpci5s
1677b2NhbCAo6LCi5qCR5rSyKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
1678ALuJ8lYEj9uf4iE9hguASq7re87Np+zJc2x/eqr1cR/SgXRStBsjxqI7i3xwMRqX
1679AuhAnM6ktlGuqidl7D9y6AN/UchqgX8AetslRJTpCcEDfL/q24zy0MqOS0FlYEgh
1680s4PIjWsSNoglBDeaIdUpN9cM/64IkAAtHndNt2p2vPfjrPeixLjese096SKEnZM/
1681xBdWF491hx06IyzjtWKqLm9OUmYZB9d/gDGnDsKpqClw8m95opKD4TBHAoE//WvI
1682m1mZnjNTNR27vVbmnc57d2Lx2Ib2eqJG5zMsP2hPBoqS8CKEwMRFLHAcclNkI67U
1683kcSEGaWgr15QGHJPN/FtjDsCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud
1684JQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFJo0y9bYUM/OuenDjsJ1RyHJfL3n
1685MDQGA1UdEQQtMCuCBm1lLmRldoIJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAA
1686AAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBgQAlQbow3+4UyQx+E+J0RwmHBltU6i+K
1687soFfza6FWRfAbTyv+4KEWl2mx51IfHhJHYZvsZqPqGWxm5UvBecskegDExFMNFVm
1688O5QixydQzHHY2krmBwmDZ6Ao88oW/qw4xmMUhzKAZbsqeQyE/uiUdyI4pfDcduLB
1689rol31g9OFsgwZrZr0d1ZiezeYEhemnSlh9xRZW3veKx9axgFttzCMmWdpGTCvnav
1690ZVc3rB+KBMjdCwsS37zmrNm9syCjW1O5a1qphwuMpqSnDHBgKWNpbsgqyZM0oyOc
16919Bkja+BV5wFO+4zH5WtestcrNMeoQ83a5lI0m42u/bUEJ/T/5BQBSFidNuvS7Ylw
1692IZpXa00xvlnm1BOHOfRI4Ehlfa5jmfcdnrGkQLGjiyygQtKcc7rOXGK+mSeyxwhs
1693sIARwslSQd4q0dbYTPKvvUHxTYiCv78vQBAsE15T2GGS80pAFDBW9vOf3upANvOf
1694EHjKf0Dweb4ppL4ddgeAKU5V0qn76K2fFaE=
1695-----END CERTIFICATE-----"#;
1696 let key = r#"-----BEGIN PRIVATE KEY-----
1697MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7ifJWBI/bn+Ih
1698PYYLgEqu63vOzafsyXNsf3qq9XEf0oF0UrQbI8aiO4t8cDEalwLoQJzOpLZRrqon
1699Zew/cugDf1HIaoF/AHrbJUSU6QnBA3y/6tuM8tDKjktBZWBIIbODyI1rEjaIJQQ3
1700miHVKTfXDP+uCJAALR53Tbdqdrz346z3osS43rHtPekihJ2TP8QXVhePdYcdOiMs
170147Viqi5vTlJmGQfXf4Axpw7CqagpcPJveaKSg+EwRwKBP/1ryJtZmZ4zUzUdu71W
17025p3Oe3di8diG9nqiRuczLD9oTwaKkvAihMDERSxwHHJTZCOu1JHEhBmloK9eUBhy
1703TzfxbYw7AgMBAAECggEALjed0FMJfO+XE+gMm9L/FMKV3W5TXwh6eJemDHG2ckg3
1704fQpQtouHjT2tb3par5ndro0V19tBzzmDV3hH048m3I3JAuI0ja75l/5EO4p+y+Fn
1705IgjoGIFSsUiGBVTNeJlNm0GWkHeJlt3Af09t3RFuYIIklKgpjNGRu4ccl5ExmslF
1706WHv7/1dwzeJCi8iOY2gJZz6N7qHD95VkgVyDj/EtLltONAtIGVdorgq70CYmtwSM
17079XgXszqOTtSJxle+UBmeQTL4ZkUR0W+h6JSpcTn0P9c3fiNDrHSKFZbbpAhO/wHd
1708Ab4IK8IksVyg+tem3m5W9QiXn3WbgcvjJTi83Y3syQKBgQD5IsaSbqwEG3ruttQe
1709yfMeq9NUGVfmj7qkj2JiF4niqXwTpvoaSq/5gM/p7lAtSMzhCKtlekP8VLuwx8ih
1710n4hJAr8pGfyu/9IUghXsvP2DXsCKyypbhzY/F2m4WNIjtyLmed62Nt1PwWWUlo9Q
1711igHI6pieT45vJTBICsRyqC/a/wKBgQDAtLXUsCABQDTPHdy/M/dHZA/QQ/xU8NOs
1712ul5UMJCkSfFNk7b2etQG/iLlMSNup3bY3OPvaCGwwEy/gZ31tTSymgooXQMFxJ7G
17131S/DF45yKD6xJEmAUhwz/Hzor1cM95g78UpZFCEVMnEmkBNb9pmrXRLDuWb0vLE6
1714B6YgiEP6xQKBgBOXuooVjg2co6RWWIQ7WZVV6f65J4KIVyNN62zPcRaUQZ/CB/U9
1715Xm1+xdsd1Mxa51HjPqdyYBpeB4y1iX+8bhlfz+zJkGeq0riuKk895aoJL5c6txAP
1716qCJ6EuReh9grNOFvQCaQVgNJsFVpKcgpsk48tNfuZcMz54Ii5qQlue29AoGAA2Sr
1717Nv2K8rqws1zxQCSoHAe1B5PK46wB7i6x7oWUZnAu4ZDSTfDHvv/GmYaN+yrTuunY
17180aRhw3z/XPfpUiRIs0RnHWLV5MobiaDDYIoPpg7zW6cp7CqF+JxfjrFXtRC/C38q
1719MftawcbLm0Q6MwpallvjMrMXDwQrkrwDvtrnZ4kCgYEA0oSvmSK5ADD0nqYFdaro
1720K+hM90AVD1xmU7mxy3EDPwzjK1wZTj7u0fvcAtZJztIfL+lmVpkvK8KDLQ9wCWE7
1721SGToOzVHYX7VazxioA9nhNne9kaixvnIUg3iowAz07J7o6EU8tfYsnHxsvjlIkBU
1722ai02RHnemmqJaNepfmCdyec=
1723-----END PRIVATE KEY-----"#;
1724 let conf = CertificateConf {
1726 tls_cert: Some(pem.to_string()),
1727 tls_key: Some(key.to_string()),
1728 ..Default::default()
1729 };
1730 let result = conf.validate();
1731 assert_eq!(true, result.is_ok());
1732
1733 assert_eq!("15ba921aee80abc3", conf.hash_key());
1735 }
1737}