1use serde::{Deserialize, Serialize};
19use thiserror::Error;
20
21use hessra_config::{HessraConfig, Protocol};
22
23pub fn parse_server_address(address: &str) -> (String, Option<u16>) {
37 let address = address.trim();
38
39 let without_protocol = address
41 .strip_prefix("https://")
42 .or_else(|| address.strip_prefix("http://"))
43 .unwrap_or(address);
44
45 let host_port = without_protocol
47 .split('/')
48 .next()
49 .unwrap_or(without_protocol);
50
51 if host_port.starts_with('[') {
53 if let Some(bracket_end) = host_port.find(']') {
55 let host = &host_port[1..bracket_end]; let after_bracket = &host_port[bracket_end + 1..];
57
58 if let Some(port_str) = after_bracket.strip_prefix(':') {
59 if let Ok(port) = port_str.parse::<u16>() {
61 return (host.to_string(), Some(port));
62 }
63 }
64 return (host.to_string(), None);
66 }
67 return (host_port.trim_start_matches('[').to_string(), None);
69 }
70
71 let colon_count = host_port.chars().filter(|c| *c == ':').count();
74
75 if colon_count == 1 {
76 let parts: Vec<&str> = host_port.splitn(2, ':').collect();
78 if parts.len() == 2 {
79 if let Ok(port) = parts[1].parse::<u16>() {
80 return (parts[0].to_string(), Some(port));
81 }
82 }
83 }
84
85 (host_port.to_string(), None)
87}
88
89#[derive(Error, Debug)]
91pub enum ApiError {
92 #[error("HTTP client error: {0}")]
93 HttpClient(#[from] reqwest::Error),
94
95 #[error("SSL configuration error: {0}")]
96 SslConfig(String),
97
98 #[error("Invalid response: {0}")]
99 InvalidResponse(String),
100
101 #[error("Token request error: {0}")]
102 TokenRequest(String),
103
104 #[error("Token verification error: {0}")]
105 TokenVerification(String),
106
107 #[error("Service chain error: {0}")]
108 ServiceChain(String),
109
110 #[error("Internal error: {0}")]
111 Internal(String),
112
113 #[error("Signoff failed: {0}")]
114 SignoffFailed(String),
115
116 #[error("Missing signoff configuration for service: {0}")]
117 MissingSignoffConfig(String),
118
119 #[error("Invalid signoff response from {service}: {reason}")]
120 InvalidSignoffResponse { service: String, reason: String },
121
122 #[error("Signoff collection incomplete: {missing_signoffs} signoffs remaining")]
123 IncompleteSignoffs { missing_signoffs: usize },
124}
125
126#[derive(Serialize, Deserialize)]
129pub struct TokenRequest {
130 pub resource: String,
132 pub operation: String,
134 #[serde(skip_serializing_if = "Option::is_none")]
139 pub domain: Option<String>,
140}
141
142#[derive(Serialize, Deserialize)]
144pub struct VerifyTokenRequest {
145 pub token: String,
147 pub subject: String,
149 pub resource: String,
151 pub operation: String,
153}
154
155#[derive(Serialize, Deserialize, Debug, Clone)]
157pub struct SignoffInfo {
158 pub component: String,
159 pub authorization_service: String,
160 pub public_key: String,
161}
162
163#[derive(Serialize, Deserialize, Debug, Clone)]
165pub struct SignTokenRequest {
166 pub token: String,
167 pub resource: String,
168 pub operation: String,
169}
170
171#[derive(Serialize, Deserialize, Debug, Clone)]
173pub struct SignTokenResponse {
174 pub response_msg: String,
175 pub signed_token: Option<String>,
176}
177
178#[derive(Serialize, Deserialize, Debug, Clone)]
180pub struct TokenResponse {
181 pub response_msg: String,
183 pub token: Option<String>,
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub pending_signoffs: Option<Vec<SignoffInfo>>,
188}
189
190#[derive(Serialize, Deserialize)]
192pub struct VerifyTokenResponse {
193 pub response_msg: String,
195}
196
197#[derive(Serialize, Deserialize)]
199pub struct PublicKeyResponse {
200 pub response_msg: String,
201 pub public_key: String,
202}
203
204#[derive(Serialize, Deserialize)]
206pub struct CaCertResponse {
207 pub response_msg: String,
208 pub ca_cert_pem: String,
209}
210
211#[derive(Serialize, Deserialize)]
213pub struct VerifyServiceChainTokenRequest {
214 pub token: String,
215 pub subject: String,
216 pub resource: String,
217 pub component: Option<String>,
218}
219
220#[derive(Serialize, Deserialize)]
222pub struct IdentityTokenRequest {
223 pub identifier: Option<String>,
225}
226
227#[derive(Serialize, Deserialize)]
229pub struct RefreshIdentityTokenRequest {
230 pub current_token: String,
232 pub identifier: Option<String>,
234}
235
236#[derive(Serialize, Deserialize, Debug, Clone)]
238pub struct IdentityTokenResponse {
239 pub response_msg: String,
241 pub token: Option<String>,
243 pub expires_in: Option<u64>,
245 pub identity: Option<String>,
247}
248
249#[derive(Serialize, Deserialize)]
251pub struct MintIdentityTokenRequest {
252 pub subject: String,
254 pub duration: Option<u64>,
256}
257
258#[derive(Serialize, Deserialize, Debug, Clone)]
260pub struct MintIdentityTokenResponse {
261 pub response_msg: String,
263 pub token: Option<String>,
265 pub expires_in: Option<u64>,
267 pub identity: Option<String>,
269}
270
271#[derive(Serialize, Deserialize, Debug, Clone)]
278pub struct StubTokenRequest {
279 pub target_identity: String,
281 pub resource: String,
283 pub operation: String,
285 pub prefix_attenuator_key: String,
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub duration: Option<u64>,
290}
291
292#[derive(Serialize, Deserialize, Debug, Clone, Default)]
294pub struct StubTokenResponse {
295 pub response_msg: String,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub token: Option<String>,
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub expires_in: Option<u64>,
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub target_identity: Option<String>,
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub prefix_attenuator_key: Option<String>,
309}
310
311#[derive(Clone)]
313pub struct BaseConfig {
314 pub base_url: String,
316 pub port: Option<u16>,
318 pub mtls_key: Option<String>,
320 pub mtls_cert: Option<String>,
322 pub server_ca: String,
324 pub public_key: Option<String>,
326 pub personal_keypair: Option<String>,
328}
329
330impl BaseConfig {
331 pub fn get_base_url(&self) -> String {
336 let (host, embedded_port) = parse_server_address(&self.base_url);
338
339 let resolved_port = self.port.or(embedded_port);
341
342 match resolved_port {
343 Some(port) => format!("{host}:{port}"),
344 None => host,
345 }
346 }
347}
348
349pub struct Http1Client {
351 config: BaseConfig,
353 client: reqwest::Client,
355}
356
357impl Http1Client {
358 pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
360 let certs =
362 reqwest::Certificate::from_pem_bundle(config.server_ca.as_bytes()).map_err(|e| {
363 ApiError::SslConfig(format!("Failed to parse CA certificate chain: {e}"))
364 })?;
365
366 let mut client_builder = reqwest::ClientBuilder::new().use_rustls_tls();
368
369 for cert in certs {
371 client_builder = client_builder.add_root_certificate(cert);
372 }
373
374 if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
376 let identity_str = format!("{cert}{key}");
377 let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
378 ApiError::SslConfig(format!(
379 "Failed to create identity from certificate and key: {e}"
380 ))
381 })?;
382 client_builder = client_builder.identity(identity);
383 }
384
385 let client = client_builder
386 .build()
387 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
388
389 Ok(Self { config, client })
390 }
391
392 pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
394 where
395 T: Serialize,
396 R: for<'de> Deserialize<'de>,
397 {
398 let base_url = self.config.get_base_url();
399 let url = format!("https://{base_url}/{endpoint}");
400
401 let response = self
402 .client
403 .post(&url)
404 .json(request_body)
405 .send()
406 .await
407 .map_err(ApiError::HttpClient)?;
408
409 if !response.status().is_success() {
410 let status = response.status();
411 let error_text = response.text().await.unwrap_or_default();
412 return Err(ApiError::InvalidResponse(format!(
413 "HTTP error: {status} - {error_text}"
414 )));
415 }
416
417 let result = response
418 .json::<R>()
419 .await
420 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
421
422 Ok(result)
423 }
424
425 pub async fn send_request_with_auth<T, R>(
426 &self,
427 endpoint: &str,
428 request_body: &T,
429 auth_header: &str,
430 ) -> Result<R, ApiError>
431 where
432 T: Serialize,
433 R: for<'de> Deserialize<'de>,
434 {
435 let base_url = self.config.get_base_url();
436 let url = format!("https://{base_url}/{endpoint}");
437
438 let response = self
439 .client
440 .post(&url)
441 .header("Authorization", auth_header)
442 .json(request_body)
443 .send()
444 .await
445 .map_err(ApiError::HttpClient)?;
446
447 if !response.status().is_success() {
448 let status = response.status();
449 let error_text = response.text().await.unwrap_or_default();
450 return Err(ApiError::InvalidResponse(format!(
451 "HTTP error: {status} - {error_text}"
452 )));
453 }
454
455 let result = response
456 .json::<R>()
457 .await
458 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
459
460 Ok(result)
461 }
462}
463
464#[cfg(feature = "http3")]
466pub struct Http3Client {
467 config: BaseConfig,
469 client: reqwest::Client,
471}
472
473#[cfg(feature = "http3")]
474impl Http3Client {
475 pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
477 let certs =
479 reqwest::Certificate::from_pem_bundle(config.server_ca.as_bytes()).map_err(|e| {
480 ApiError::SslConfig(format!("Failed to parse CA certificate chain: {e}"))
481 })?;
482
483 let mut client_builder = reqwest::ClientBuilder::new()
485 .use_rustls_tls()
486 .http3_prior_knowledge();
487
488 for cert in certs {
490 client_builder = client_builder.add_root_certificate(cert);
491 }
492
493 if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
495 let identity_str = format!("{}{}", cert, key);
496 let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
497 ApiError::SslConfig(format!(
498 "Failed to create identity from certificate and key: {e}"
499 ))
500 })?;
501 client_builder = client_builder.identity(identity);
502 }
503
504 let client = client_builder
505 .build()
506 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
507
508 Ok(Self { config, client })
509 }
510
511 pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
513 where
514 T: Serialize,
515 R: for<'de> Deserialize<'de>,
516 {
517 let base_url = self.config.get_base_url();
518 let url = format!("https://{base_url}/{endpoint}");
519
520 let response = self
521 .client
522 .post(&url)
523 .json(request_body)
524 .send()
525 .await
526 .map_err(ApiError::HttpClient)?;
527
528 if !response.status().is_success() {
529 let status = response.status();
530 let error_text = response.text().await.unwrap_or_default();
531 return Err(ApiError::InvalidResponse(format!(
532 "HTTP error: {status} - {error_text}"
533 )));
534 }
535
536 let result = response
537 .json::<R>()
538 .await
539 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
540
541 Ok(result)
542 }
543
544 pub async fn send_request_with_auth<T, R>(
545 &self,
546 endpoint: &str,
547 request_body: &T,
548 auth_header: &str,
549 ) -> Result<R, ApiError>
550 where
551 T: Serialize,
552 R: for<'de> Deserialize<'de>,
553 {
554 let base_url = self.config.get_base_url();
555 let url = format!("https://{base_url}/{endpoint}");
556
557 let response = self
558 .client
559 .post(&url)
560 .header("Authorization", auth_header)
561 .json(request_body)
562 .send()
563 .await
564 .map_err(ApiError::HttpClient)?;
565
566 if !response.status().is_success() {
567 let status = response.status();
568 let error_text = response.text().await.unwrap_or_default();
569 return Err(ApiError::InvalidResponse(format!(
570 "HTTP error: {status} - {error_text}"
571 )));
572 }
573
574 let result = response
575 .json::<R>()
576 .await
577 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
578
579 Ok(result)
580 }
581}
582
583pub enum HessraClient {
585 Http1(Http1Client),
587 #[cfg(feature = "http3")]
589 Http3(Http3Client),
590}
591
592pub struct HessraClientBuilder {
594 config: BaseConfig,
596 protocol: hessra_config::Protocol,
598}
599
600impl HessraClientBuilder {
601 pub fn new() -> Self {
603 Self {
604 config: BaseConfig {
605 base_url: String::new(),
606 port: None,
607 mtls_key: None,
608 mtls_cert: None,
609 server_ca: String::new(),
610 public_key: None,
611 personal_keypair: None,
612 },
613 protocol: Protocol::Http1,
614 }
615 }
616
617 pub fn from_config(mut self, config: &HessraConfig) -> Self {
619 self.config.base_url = config.base_url.clone();
620 self.config.port = config.port;
621 self.config.mtls_key = config.mtls_key.clone();
622 self.config.mtls_cert = config.mtls_cert.clone();
623 self.config.server_ca = config.server_ca.clone();
624 self.config.public_key = config.public_key.clone();
625 self.config.personal_keypair = config.personal_keypair.clone();
626 self.protocol = config.protocol.clone();
627 self
628 }
629
630 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
632 self.config.base_url = base_url.into();
633 self
634 }
635
636 pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
639 self.config.mtls_key = Some(mtls_key.into());
640 self
641 }
642
643 pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
646 self.config.mtls_cert = Some(mtls_cert.into());
647 self
648 }
649
650 pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
653 self.config.server_ca = server_ca.into();
654 self
655 }
656
657 pub fn port(mut self, port: u16) -> Self {
659 self.config.port = Some(port);
660 self
661 }
662
663 pub fn protocol(mut self, protocol: Protocol) -> Self {
665 self.protocol = protocol;
666 self
667 }
668
669 pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
672 self.config.public_key = Some(public_key.into());
673 self
674 }
675
676 pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
680 self.config.personal_keypair = Some(keypair.into());
681 self
682 }
683
684 fn build_http1(&self) -> Result<Http1Client, ApiError> {
686 Http1Client::new(self.config.clone())
687 }
688
689 #[cfg(feature = "http3")]
691 fn build_http3(&self) -> Result<Http3Client, ApiError> {
692 Http3Client::new(self.config.clone())
693 }
694
695 pub fn build(self) -> Result<HessraClient, ApiError> {
697 match self.protocol {
698 Protocol::Http1 => Ok(HessraClient::Http1(self.build_http1()?)),
699 #[cfg(feature = "http3")]
700 Protocol::Http3 => Ok(HessraClient::Http3(self.build_http3()?)),
701 #[allow(unreachable_patterns)]
702 _ => Err(ApiError::Internal("Unsupported protocol".to_string())),
703 }
704 }
705}
706
707impl Default for HessraClientBuilder {
708 fn default() -> Self {
709 Self::new()
710 }
711}
712
713impl HessraClient {
714 pub fn builder() -> HessraClientBuilder {
716 HessraClientBuilder::new()
717 }
718
719 pub async fn fetch_public_key(
723 base_url: impl Into<String>,
724 port: Option<u16>,
725 server_ca: impl Into<String>,
726 ) -> Result<String, ApiError> {
727 let base_url_str = base_url.into();
728 let server_ca = server_ca.into();
729
730 let (host, embedded_port) = parse_server_address(&base_url_str);
732 let resolved_port = embedded_port.or(port);
734
735 let cert_pem = server_ca.as_bytes();
737 let certs = reqwest::Certificate::from_pem_bundle(cert_pem)
738 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
739
740 let mut client_builder = reqwest::ClientBuilder::new().use_rustls_tls();
741 for cert in certs {
742 client_builder = client_builder.add_root_certificate(cert);
743 }
744
745 let client = client_builder
746 .build()
747 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
748
749 let url = match resolved_port {
751 Some(port) => format!("https://{host}:{port}/public_key"),
752 None => format!("https://{host}/public_key"),
753 };
754
755 let response = client
757 .get(&url)
758 .send()
759 .await
760 .map_err(ApiError::HttpClient)?;
761
762 if !response.status().is_success() {
763 let status = response.status();
764 let error_text = response.text().await.unwrap_or_default();
765 return Err(ApiError::InvalidResponse(format!(
766 "HTTP error: {status} - {error_text}"
767 )));
768 }
769
770 let result = response
772 .json::<PublicKeyResponse>()
773 .await
774 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
775
776 Ok(result.public_key)
777 }
778
779 pub async fn fetch_ca_cert(
791 base_url: impl Into<String>,
792 port: Option<u16>,
793 ) -> Result<String, ApiError> {
794 let base_url_str = base_url.into();
795
796 let (host, embedded_port) = parse_server_address(&base_url_str);
798 let resolved_port = embedded_port.or(port);
800
801 let client = reqwest::ClientBuilder::new()
803 .use_rustls_tls()
804 .build()
805 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
806
807 let url = match resolved_port {
809 Some(port) => format!("https://{host}:{port}/ca_cert"),
810 None => format!("https://{host}/ca_cert"),
811 };
812
813 let response = client
815 .get(&url)
816 .send()
817 .await
818 .map_err(ApiError::HttpClient)?;
819
820 if !response.status().is_success() {
821 let status = response.status();
822 let error_text = response.text().await.unwrap_or_default();
823 return Err(ApiError::InvalidResponse(format!(
824 "HTTP error: {status} - {error_text}"
825 )));
826 }
827
828 let result = response
830 .json::<CaCertResponse>()
831 .await
832 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
833
834 if result.ca_cert_pem.is_empty() {
836 return Err(ApiError::InvalidResponse(
837 "Server returned empty CA certificate".to_string(),
838 ));
839 }
840
841 if !result.ca_cert_pem.contains("-----BEGIN CERTIFICATE-----") {
842 return Err(ApiError::InvalidResponse(
843 "Server returned invalid PEM format".to_string(),
844 ));
845 }
846
847 Ok(result.ca_cert_pem)
848 }
849
850 #[cfg(feature = "http3")]
851 pub async fn fetch_public_key_http3(
852 base_url: impl Into<String>,
853 port: Option<u16>,
854 server_ca: impl Into<String>,
855 ) -> Result<String, ApiError> {
856 let base_url_str = base_url.into();
857 let server_ca = server_ca.into();
858
859 let (host, embedded_port) = parse_server_address(&base_url_str);
861 let resolved_port = embedded_port.or(port);
863
864 let cert_pem = server_ca.as_bytes();
866 let certs = reqwest::Certificate::from_pem_bundle(cert_pem)
867 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
868
869 let mut client_builder = reqwest::ClientBuilder::new()
870 .use_rustls_tls()
871 .http3_prior_knowledge();
872 for cert in certs {
873 client_builder = client_builder.add_root_certificate(cert);
874 }
875
876 let client = client_builder
877 .build()
878 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
879
880 let url = match resolved_port {
882 Some(port) => format!("https://{host}:{port}/public_key"),
883 None => format!("https://{host}/public_key"),
884 };
885
886 let response = client
888 .get(&url)
889 .send()
890 .await
891 .map_err(ApiError::HttpClient)?;
892
893 if !response.status().is_success() {
894 let status = response.status();
895 let error_text = response.text().await.unwrap_or_default();
896 return Err(ApiError::InvalidResponse(format!(
897 "HTTP error: {status} - {error_text}"
898 )));
899 }
900
901 let result = response
903 .json::<PublicKeyResponse>()
904 .await
905 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
906
907 Ok(result.public_key)
908 }
909
910 pub async fn request_token(
918 &self,
919 resource: String,
920 operation: String,
921 domain: Option<String>,
922 ) -> Result<TokenResponse, ApiError> {
923 let request = TokenRequest {
924 resource,
925 operation,
926 domain,
927 };
928
929 let response = match self {
930 HessraClient::Http1(client) => {
931 client
932 .send_request::<_, TokenResponse>("request_token", &request)
933 .await?
934 }
935 #[cfg(feature = "http3")]
936 HessraClient::Http3(client) => {
937 client
938 .send_request::<_, TokenResponse>("request_token", &request)
939 .await?
940 }
941 };
942
943 Ok(response)
944 }
945
946 pub async fn request_token_with_identity(
956 &self,
957 resource: String,
958 operation: String,
959 identity_token: String,
960 domain: Option<String>,
961 ) -> Result<TokenResponse, ApiError> {
962 let request = TokenRequest {
963 resource,
964 operation,
965 domain,
966 };
967
968 let response = match self {
969 HessraClient::Http1(client) => {
970 client
971 .send_request_with_auth::<_, TokenResponse>(
972 "request_token",
973 &request,
974 &format!("Bearer {identity_token}"),
975 )
976 .await?
977 }
978 #[cfg(feature = "http3")]
979 HessraClient::Http3(client) => {
980 client
981 .send_request_with_auth::<_, TokenResponse>(
982 "request_token",
983 &request,
984 &format!("Bearer {identity_token}"),
985 )
986 .await?
987 }
988 };
989
990 Ok(response)
991 }
992
993 pub async fn request_token_simple(
996 &self,
997 resource: String,
998 operation: String,
999 ) -> Result<String, ApiError> {
1000 let response = self.request_token(resource, operation, None).await?;
1001
1002 match response.token {
1003 Some(token) => Ok(token),
1004 None => Err(ApiError::TokenRequest(format!(
1005 "Failed to get token: {}",
1006 response.response_msg
1007 ))),
1008 }
1009 }
1010
1011 pub async fn verify_token(
1014 &self,
1015 token: String,
1016 subject: String,
1017 resource: String,
1018 operation: String,
1019 ) -> Result<String, ApiError> {
1020 let request = VerifyTokenRequest {
1021 token,
1022 subject,
1023 resource,
1024 operation,
1025 };
1026
1027 let response = match self {
1028 HessraClient::Http1(client) => {
1029 client
1030 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
1031 .await?
1032 }
1033 #[cfg(feature = "http3")]
1034 HessraClient::Http3(client) => {
1035 client
1036 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
1037 .await?
1038 }
1039 };
1040
1041 Ok(response.response_msg)
1042 }
1043
1044 pub async fn verify_service_chain_token(
1051 &self,
1052 token: String,
1053 subject: String,
1054 resource: String,
1055 component: Option<String>,
1056 ) -> Result<String, ApiError> {
1057 let request = VerifyServiceChainTokenRequest {
1058 token,
1059 subject,
1060 resource,
1061 component,
1062 };
1063
1064 let response = match self {
1065 HessraClient::Http1(client) => {
1066 client
1067 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
1068 .await?
1069 }
1070 #[cfg(feature = "http3")]
1071 HessraClient::Http3(client) => {
1072 client
1073 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
1074 .await?
1075 }
1076 };
1077
1078 Ok(response.response_msg)
1079 }
1080
1081 pub async fn sign_token(
1083 &self,
1084 token: &str,
1085 resource: &str,
1086 operation: &str,
1087 ) -> Result<SignTokenResponse, ApiError> {
1088 let request = SignTokenRequest {
1089 token: token.to_string(),
1090 resource: resource.to_string(),
1091 operation: operation.to_string(),
1092 };
1093
1094 let response = match self {
1095 HessraClient::Http1(client) => {
1096 client
1097 .send_request::<_, SignTokenResponse>("sign_token", &request)
1098 .await?
1099 }
1100 #[cfg(feature = "http3")]
1101 HessraClient::Http3(client) => {
1102 client
1103 .send_request::<_, SignTokenResponse>("sign_token", &request)
1104 .await?
1105 }
1106 };
1107
1108 Ok(response)
1109 }
1110
1111 pub async fn get_public_key(&self) -> Result<String, ApiError> {
1113 let url_path = "public_key";
1114
1115 let response = match self {
1116 HessraClient::Http1(client) => {
1117 let base_url = client.config.get_base_url();
1119 let full_url = format!("https://{base_url}/{url_path}");
1120
1121 let response = client
1122 .client
1123 .get(&full_url)
1124 .send()
1125 .await
1126 .map_err(ApiError::HttpClient)?;
1127
1128 if !response.status().is_success() {
1129 let status = response.status();
1130 let error_text = response.text().await.unwrap_or_default();
1131 return Err(ApiError::InvalidResponse(format!(
1132 "HTTP error: {status} - {error_text}"
1133 )));
1134 }
1135
1136 response.json::<PublicKeyResponse>().await.map_err(|e| {
1137 ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
1138 })?
1139 }
1140 #[cfg(feature = "http3")]
1141 HessraClient::Http3(client) => {
1142 let base_url = client.config.get_base_url();
1143 let full_url = format!("https://{base_url}/{url_path}");
1144
1145 let response = client
1146 .client
1147 .get(&full_url)
1148 .send()
1149 .await
1150 .map_err(ApiError::HttpClient)?;
1151
1152 if !response.status().is_success() {
1153 let status = response.status();
1154 let error_text = response.text().await.unwrap_or_default();
1155 return Err(ApiError::InvalidResponse(format!(
1156 "HTTP error: {status} - {error_text}"
1157 )));
1158 }
1159
1160 response.json::<PublicKeyResponse>().await.map_err(|e| {
1161 ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
1162 })?
1163 }
1164 };
1165
1166 Ok(response.public_key)
1167 }
1168
1169 pub async fn request_identity_token(
1177 &self,
1178 identifier: Option<String>,
1179 ) -> Result<IdentityTokenResponse, ApiError> {
1180 let request = IdentityTokenRequest { identifier };
1181
1182 let response = match self {
1183 HessraClient::Http1(client) => {
1184 client
1185 .send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
1186 .await?
1187 }
1188 #[cfg(feature = "http3")]
1189 HessraClient::Http3(client) => {
1190 client
1191 .send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
1192 .await?
1193 }
1194 };
1195
1196 Ok(response)
1197 }
1198
1199 pub async fn refresh_identity_token(
1209 &self,
1210 current_token: String,
1211 identifier: Option<String>,
1212 ) -> Result<IdentityTokenResponse, ApiError> {
1213 let request = RefreshIdentityTokenRequest {
1214 current_token,
1215 identifier,
1216 };
1217
1218 let response = match self {
1219 HessraClient::Http1(client) => {
1220 client
1221 .send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
1222 .await?
1223 }
1224 #[cfg(feature = "http3")]
1225 HessraClient::Http3(client) => {
1226 client
1227 .send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
1228 .await?
1229 }
1230 };
1231
1232 Ok(response)
1233 }
1234
1235 pub async fn mint_domain_restricted_identity_token(
1245 &self,
1246 subject: String,
1247 duration: Option<u64>,
1248 ) -> Result<MintIdentityTokenResponse, ApiError> {
1249 let request = MintIdentityTokenRequest { subject, duration };
1250
1251 let response = match self {
1252 HessraClient::Http1(client) => {
1253 client
1254 .send_request::<_, MintIdentityTokenResponse>("mint_identity_token", &request)
1255 .await?
1256 }
1257 #[cfg(feature = "http3")]
1258 HessraClient::Http3(client) => {
1259 client
1260 .send_request::<_, MintIdentityTokenResponse>("mint_identity_token", &request)
1261 .await?
1262 }
1263 };
1264
1265 Ok(response)
1266 }
1267
1268 pub async fn request_stub_token(
1282 &self,
1283 target_identity: String,
1284 resource: String,
1285 operation: String,
1286 prefix_attenuator_key: String,
1287 duration: Option<u64>,
1288 ) -> Result<StubTokenResponse, ApiError> {
1289 let request = StubTokenRequest {
1290 target_identity,
1291 resource,
1292 operation,
1293 prefix_attenuator_key,
1294 duration,
1295 };
1296
1297 let response = match self {
1298 HessraClient::Http1(client) => {
1299 client
1300 .send_request::<_, StubTokenResponse>("request_stub", &request)
1301 .await?
1302 }
1303 #[cfg(feature = "http3")]
1304 HessraClient::Http3(client) => {
1305 client
1306 .send_request::<_, StubTokenResponse>("request_stub", &request)
1307 .await?
1308 }
1309 };
1310
1311 Ok(response)
1312 }
1313
1314 pub async fn request_stub_token_with_identity(
1328 &self,
1329 target_identity: String,
1330 resource: String,
1331 operation: String,
1332 prefix_attenuator_key: String,
1333 identity_token: String,
1334 duration: Option<u64>,
1335 ) -> Result<StubTokenResponse, ApiError> {
1336 let request = StubTokenRequest {
1337 target_identity,
1338 resource,
1339 operation,
1340 prefix_attenuator_key,
1341 duration,
1342 };
1343
1344 let response = match self {
1345 HessraClient::Http1(client) => {
1346 client
1347 .send_request_with_auth::<_, StubTokenResponse>(
1348 "request_stub",
1349 &request,
1350 &format!("Bearer {identity_token}"),
1351 )
1352 .await?
1353 }
1354 #[cfg(feature = "http3")]
1355 HessraClient::Http3(client) => {
1356 client
1357 .send_request_with_auth::<_, StubTokenResponse>(
1358 "request_stub",
1359 &request,
1360 &format!("Bearer {identity_token}"),
1361 )
1362 .await?
1363 }
1364 };
1365
1366 Ok(response)
1367 }
1368}
1369
1370#[cfg(test)]
1371mod tests {
1372 use super::*;
1373
1374 #[test]
1376 fn test_base_config_get_base_url_with_port() {
1377 let config = BaseConfig {
1378 base_url: "test.hessra.net".to_string(),
1379 port: Some(443),
1380 mtls_key: None,
1381 mtls_cert: None,
1382 server_ca: "".to_string(),
1383 public_key: None,
1384 personal_keypair: None,
1385 };
1386
1387 assert_eq!(config.get_base_url(), "test.hessra.net:443");
1388 }
1389
1390 #[test]
1391 fn test_base_config_get_base_url_without_port() {
1392 let config = BaseConfig {
1393 base_url: "test.hessra.net".to_string(),
1394 port: None,
1395 mtls_key: None,
1396 mtls_cert: None,
1397 server_ca: "".to_string(),
1398 public_key: None,
1399 personal_keypair: None,
1400 };
1401
1402 assert_eq!(config.get_base_url(), "test.hessra.net");
1403 }
1404
1405 #[test]
1407 fn test_client_builder_methods() {
1408 let builder = HessraClientBuilder::new()
1409 .base_url("test.hessra.net")
1410 .port(443)
1411 .protocol(Protocol::Http1)
1412 .mtls_cert("CERT")
1413 .mtls_key("KEY")
1414 .server_ca("CA")
1415 .public_key("PUBKEY")
1416 .personal_keypair("KEYPAIR");
1417
1418 assert_eq!(builder.config.base_url, "test.hessra.net");
1419 assert_eq!(builder.config.port, Some(443));
1420 assert_eq!(builder.config.mtls_cert, Some("CERT".to_string()));
1421 assert_eq!(builder.config.mtls_key, Some("KEY".to_string()));
1422 assert_eq!(builder.config.server_ca, "CA");
1423 assert_eq!(builder.config.public_key, Some("PUBKEY".to_string()));
1424 assert_eq!(builder.config.personal_keypair, Some("KEYPAIR".to_string()));
1425 }
1426
1427 #[test]
1429 fn test_parse_server_address_ip_with_port() {
1430 let (host, port) = parse_server_address("127.0.0.1:4433");
1431 assert_eq!(host, "127.0.0.1");
1432 assert_eq!(port, Some(4433));
1433 }
1434
1435 #[test]
1436 fn test_parse_server_address_ip_only() {
1437 let (host, port) = parse_server_address("127.0.0.1");
1438 assert_eq!(host, "127.0.0.1");
1439 assert_eq!(port, None);
1440 }
1441
1442 #[test]
1443 fn test_parse_server_address_hostname_with_port() {
1444 let (host, port) = parse_server_address("test.hessra.net:443");
1445 assert_eq!(host, "test.hessra.net");
1446 assert_eq!(port, Some(443));
1447 }
1448
1449 #[test]
1450 fn test_parse_server_address_hostname_only() {
1451 let (host, port) = parse_server_address("test.hessra.net");
1452 assert_eq!(host, "test.hessra.net");
1453 assert_eq!(port, None);
1454 }
1455
1456 #[test]
1457 fn test_parse_server_address_with_https_protocol() {
1458 let (host, port) = parse_server_address("https://example.com:8443");
1459 assert_eq!(host, "example.com");
1460 assert_eq!(port, Some(8443));
1461 }
1462
1463 #[test]
1464 fn test_parse_server_address_with_https_protocol_no_port() {
1465 let (host, port) = parse_server_address("https://example.com");
1466 assert_eq!(host, "example.com");
1467 assert_eq!(port, None);
1468 }
1469
1470 #[test]
1471 fn test_parse_server_address_with_path() {
1472 let (host, port) = parse_server_address("https://example.com:8443/some/path");
1473 assert_eq!(host, "example.com");
1474 assert_eq!(port, Some(8443));
1475 }
1476
1477 #[test]
1478 fn test_parse_server_address_ipv6_with_brackets_and_port() {
1479 let (host, port) = parse_server_address("[::1]:8443");
1480 assert_eq!(host, "::1");
1481 assert_eq!(port, Some(8443));
1482 }
1483
1484 #[test]
1485 fn test_parse_server_address_ipv6_with_brackets_no_port() {
1486 let (host, port) = parse_server_address("[::1]");
1487 assert_eq!(host, "::1");
1488 assert_eq!(port, None);
1489 }
1490
1491 #[test]
1492 fn test_parse_server_address_ipv6_full_with_port() {
1493 let (host, port) = parse_server_address("[2001:db8::1]:4433");
1494 assert_eq!(host, "2001:db8::1");
1495 assert_eq!(port, Some(4433));
1496 }
1497
1498 #[test]
1499 fn test_parse_server_address_with_whitespace() {
1500 let (host, port) = parse_server_address(" 127.0.0.1:4433 ");
1501 assert_eq!(host, "127.0.0.1");
1502 assert_eq!(port, Some(4433));
1503 }
1504
1505 #[test]
1506 fn test_base_config_get_base_url_with_embedded_port() {
1507 let config = BaseConfig {
1509 base_url: "127.0.0.1:4433".to_string(),
1510 port: None, mtls_key: None,
1512 mtls_cert: None,
1513 server_ca: "".to_string(),
1514 public_key: None,
1515 personal_keypair: None,
1516 };
1517 assert_eq!(config.get_base_url(), "127.0.0.1:4433");
1519 }
1520
1521 #[test]
1522 fn test_base_config_get_base_url_explicit_port_overrides_embedded() {
1523 let config = BaseConfig {
1525 base_url: "127.0.0.1:4433".to_string(),
1526 port: Some(8080), mtls_key: None,
1528 mtls_cert: None,
1529 server_ca: "".to_string(),
1530 public_key: None,
1531 personal_keypair: None,
1532 };
1533 assert_eq!(config.get_base_url(), "127.0.0.1:8080");
1534 }
1535}