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: {}",
112 value
113 ))
114 })
115 }
116}
117
118#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
120pub struct CertificateConf {
121 pub domains: Option<String>,
123 pub tls_cert: Option<String>,
125 pub tls_key: Option<String>,
127 pub tls_chain: Option<String>,
129 pub is_default: Option<bool>,
131 pub is_ca: Option<bool>,
133 pub acme: Option<String>,
135 pub buffer_days: Option<u16>,
137 pub remark: Option<String>,
139}
140
141fn validate_cert(value: &str) -> Result<()> {
143 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 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 Ok(())
164}
165
166impl CertificateConf {
167 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 pub fn validate(&self) -> Result<()> {
180 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 let tls_cert = self.tls_cert.clone().unwrap_or_default();
198 if !tls_cert.is_empty() {
199 validate_cert(&tls_cert)?;
200 }
201
202 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#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
213pub struct UpstreamConf {
214 pub addrs: Vec<String>,
216
217 pub discovery: Option<String>,
219
220 #[serde(default)]
222 #[serde(with = "humantime_serde")]
223 pub update_frequency: Option<Duration>,
224
225 pub algo: Option<String>,
227
228 pub sni: Option<String>,
230
231 pub verify_cert: Option<bool>,
233
234 pub health_check: Option<String>,
236
237 pub ipv4_only: Option<bool>,
239
240 pub enable_tracer: Option<bool>,
242
243 pub alpn: Option<String>,
245
246 #[serde(default)]
248 #[serde(with = "humantime_serde")]
249 pub connection_timeout: Option<Duration>,
250
251 #[serde(default)]
253 #[serde(with = "humantime_serde")]
254 pub total_connection_timeout: Option<Duration>,
255
256 #[serde(default)]
258 #[serde(with = "humantime_serde")]
259 pub read_timeout: Option<Duration>,
260
261 #[serde(default)]
263 #[serde(with = "humantime_serde")]
264 pub idle_timeout: Option<Duration>,
265
266 #[serde(default)]
268 #[serde(with = "humantime_serde")]
269 pub write_timeout: Option<Duration>,
270
271 #[serde(default)]
273 #[serde(with = "humantime_serde")]
274 pub tcp_idle: Option<Duration>,
275
276 #[serde(default)]
278 #[serde(with = "humantime_serde")]
279 pub tcp_interval: Option<Duration>,
280
281 pub tcp_probe_count: Option<usize>,
283
284 pub tcp_recv_buf: Option<ByteSize>,
286
287 pub tcp_fast_open: Option<bool>,
289
290 pub includes: Option<Vec<String>>,
292
293 pub remark: Option<String>,
295}
296
297impl UpstreamConf {
298 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 pub fn guess_discovery(&self) -> String {
311 if let Some(discovery) = &self.discovery {
313 return discovery.clone();
314 }
315
316 let has_hostname = self.addrs.iter().any(|addr| {
318 let host =
320 addr.split_once(':').map_or(addr.as_str(), |(host, _)| host);
321
322 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 pub fn validate(&self, name: &str) -> Result<()> {
339 self.validate_addresses(name)?;
341
342 self.validate_health_check()?;
344
345 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 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 let addr_to_check = if !host_port.contains(':') {
369 format!("{host_port}:80")
370 } else {
371 host_port
372 };
373
374 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#[derive(Debug, Default, Deserialize, Clone, Serialize, Hash)]
417pub struct LocationConf {
418 pub upstream: Option<String>,
420
421 pub path: Option<String>,
427
428 pub host: Option<String>,
430
431 pub proxy_set_headers: Option<Vec<String>>,
433
434 pub proxy_add_headers: Option<Vec<String>>,
436
437 pub rewrite: Option<String>,
439
440 pub weight: Option<u16>,
443
444 pub plugins: Option<Vec<String>>,
446
447 pub client_max_body_size: Option<ByteSize>,
449
450 pub max_processing: Option<i32>,
452
453 pub includes: Option<Vec<String>>,
455
456 pub grpc_web: Option<bool>,
458
459 pub enable_reverse_proxy_headers: Option<bool>,
461
462 pub remark: Option<String>,
464}
465
466impl LocationConf {
467 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 fn validate(&self, name: &str, upstream_names: &[String]) -> Result<()> {
481 let validate = |headers: &Option<Vec<String>>| -> Result<()> {
483 if let Some(headers) = headers {
484 for header in headers.iter() {
485 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 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 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 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(&self.proxy_add_headers)?;
527 validate(&self.proxy_set_headers)?;
528
529 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 pub fn get_weight(&self) -> u16 {
548 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 if path.len() > 1 {
558 if path.starts_with('=') {
559 weight += 1024; } else if path.starts_with('~') {
561 weight += 256; } else {
563 weight += 512; }
565 weight += path.len().min(64) as u16;
566 };
567 if let Some(host) = &self.host {
569 let exist_regex = host.split(',').any(|item| item.starts_with("~"));
570 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#[derive(Debug, Default, Deserialize, Clone, Serialize)]
585pub struct ServerConf {
586 pub addr: String,
588
589 pub access_log: Option<String>,
591
592 pub locations: Option<Vec<String>>,
594
595 pub threads: Option<usize>,
597
598 pub tls_cipher_list: Option<String>,
600
601 pub tls_ciphersuites: Option<String>,
603
604 pub tls_min_version: Option<String>,
606
607 pub tls_max_version: Option<String>,
609
610 pub global_certificates: Option<bool>,
612
613 pub enabled_h2: Option<bool>,
615
616 #[serde(default)]
618 #[serde(with = "humantime_serde")]
619 pub tcp_idle: Option<Duration>,
620
621 #[serde(default)]
623 #[serde(with = "humantime_serde")]
624 pub tcp_interval: Option<Duration>,
625
626 pub tcp_probe_count: Option<usize>,
628
629 pub tcp_fastopen: Option<usize>,
631
632 pub prometheus_metrics: Option<String>,
634
635 pub otlp_exporter: Option<String>,
637
638 pub includes: Option<Vec<String>>,
640
641 pub modules: Option<Vec<String>>,
643
644 pub enable_server_timing: Option<bool>,
646
647 pub remark: Option<String>,
649}
650
651impl ServerConf {
652 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 }
684
685 Ok(())
686 }
687}
688
689#[derive(Debug, Default, Deserialize, Clone, Serialize)]
691pub struct BasicConf {
692 pub name: Option<String>,
694 pub error_template: Option<String>,
696 pub pid_file: Option<String>,
698 pub upgrade_sock: Option<String>,
700 pub user: Option<String>,
702 pub group: Option<String>,
704 pub threads: Option<usize>,
706 pub work_stealing: Option<bool>,
708 pub listener_tasks_per_fd: Option<usize>,
710 #[serde(default)]
712 #[serde(with = "humantime_serde")]
713 pub grace_period: Option<Duration>,
714 #[serde(default)]
716 #[serde(with = "humantime_serde")]
717 pub graceful_shutdown_timeout: Option<Duration>,
718 pub upstream_keepalive_pool_size: Option<usize>,
720 pub webhook: Option<String>,
722 pub webhook_type: Option<String>,
724 pub webhook_notifications: Option<Vec<String>>,
726 pub log_level: Option<String>,
728 pub log_buffered_size: Option<ByteSize>,
730 pub log_format_json: Option<bool>,
732 pub sentry: Option<String>,
734 pub pyroscope: Option<String>,
736 #[serde(default)]
738 #[serde(with = "humantime_serde")]
739 pub auto_restart_check_interval: Option<Duration>,
740 pub cache_directory: Option<String>,
742 pub cache_max_size: Option<ByteSize>,
744}
745
746impl BasicConf {
747 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 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 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 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 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 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 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 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()));
1273pub fn set_current_config(value: &PingapConf) {
1275 CURRENT_CONFIG.store(Arc::new(value.clone()));
1276}
1277
1278pub fn get_current_config() -> Arc<PingapConf> {
1280 CURRENT_CONFIG.load().clone()
1281}
1282
1283pub 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 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 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 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 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}