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(Clone)]
273pub struct BaseConfig {
274 pub base_url: String,
276 pub port: Option<u16>,
278 pub mtls_key: Option<String>,
280 pub mtls_cert: Option<String>,
282 pub server_ca: String,
284 pub public_key: Option<String>,
286 pub personal_keypair: Option<String>,
288}
289
290impl BaseConfig {
291 pub fn get_base_url(&self) -> String {
296 let (host, embedded_port) = parse_server_address(&self.base_url);
298
299 let resolved_port = self.port.or(embedded_port);
301
302 match resolved_port {
303 Some(port) => format!("{host}:{port}"),
304 None => host,
305 }
306 }
307}
308
309pub struct Http1Client {
311 config: BaseConfig,
313 client: reqwest::Client,
315}
316
317impl Http1Client {
318 pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
320 let certs =
322 reqwest::Certificate::from_pem_bundle(config.server_ca.as_bytes()).map_err(|e| {
323 ApiError::SslConfig(format!("Failed to parse CA certificate chain: {e}"))
324 })?;
325
326 let mut client_builder = reqwest::ClientBuilder::new().use_rustls_tls();
328
329 for cert in certs {
331 client_builder = client_builder.add_root_certificate(cert);
332 }
333
334 if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
336 let identity_str = format!("{cert}{key}");
337 let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
338 ApiError::SslConfig(format!(
339 "Failed to create identity from certificate and key: {e}"
340 ))
341 })?;
342 client_builder = client_builder.identity(identity);
343 }
344
345 let client = client_builder
346 .build()
347 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
348
349 Ok(Self { config, client })
350 }
351
352 pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
354 where
355 T: Serialize,
356 R: for<'de> Deserialize<'de>,
357 {
358 let base_url = self.config.get_base_url();
359 let url = format!("https://{base_url}/{endpoint}");
360
361 let response = self
362 .client
363 .post(&url)
364 .json(request_body)
365 .send()
366 .await
367 .map_err(ApiError::HttpClient)?;
368
369 if !response.status().is_success() {
370 let status = response.status();
371 let error_text = response.text().await.unwrap_or_default();
372 return Err(ApiError::InvalidResponse(format!(
373 "HTTP error: {status} - {error_text}"
374 )));
375 }
376
377 let result = response
378 .json::<R>()
379 .await
380 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
381
382 Ok(result)
383 }
384
385 pub async fn send_request_with_auth<T, R>(
386 &self,
387 endpoint: &str,
388 request_body: &T,
389 auth_header: &str,
390 ) -> Result<R, ApiError>
391 where
392 T: Serialize,
393 R: for<'de> Deserialize<'de>,
394 {
395 let base_url = self.config.get_base_url();
396 let url = format!("https://{base_url}/{endpoint}");
397
398 let response = self
399 .client
400 .post(&url)
401 .header("Authorization", auth_header)
402 .json(request_body)
403 .send()
404 .await
405 .map_err(ApiError::HttpClient)?;
406
407 if !response.status().is_success() {
408 let status = response.status();
409 let error_text = response.text().await.unwrap_or_default();
410 return Err(ApiError::InvalidResponse(format!(
411 "HTTP error: {status} - {error_text}"
412 )));
413 }
414
415 let result = response
416 .json::<R>()
417 .await
418 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
419
420 Ok(result)
421 }
422}
423
424#[cfg(feature = "http3")]
426pub struct Http3Client {
427 config: BaseConfig,
429 client: reqwest::Client,
431}
432
433#[cfg(feature = "http3")]
434impl Http3Client {
435 pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
437 let certs =
439 reqwest::Certificate::from_pem_bundle(config.server_ca.as_bytes()).map_err(|e| {
440 ApiError::SslConfig(format!("Failed to parse CA certificate chain: {e}"))
441 })?;
442
443 let mut client_builder = reqwest::ClientBuilder::new()
445 .use_rustls_tls()
446 .http3_prior_knowledge();
447
448 for cert in certs {
450 client_builder = client_builder.add_root_certificate(cert);
451 }
452
453 if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
455 let identity_str = format!("{}{}", cert, key);
456 let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
457 ApiError::SslConfig(format!(
458 "Failed to create identity from certificate and key: {e}"
459 ))
460 })?;
461 client_builder = client_builder.identity(identity);
462 }
463
464 let client = client_builder
465 .build()
466 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
467
468 Ok(Self { config, client })
469 }
470
471 pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
473 where
474 T: Serialize,
475 R: for<'de> Deserialize<'de>,
476 {
477 let base_url = self.config.get_base_url();
478 let url = format!("https://{base_url}/{endpoint}");
479
480 let response = self
481 .client
482 .post(&url)
483 .json(request_body)
484 .send()
485 .await
486 .map_err(ApiError::HttpClient)?;
487
488 if !response.status().is_success() {
489 let status = response.status();
490 let error_text = response.text().await.unwrap_or_default();
491 return Err(ApiError::InvalidResponse(format!(
492 "HTTP error: {status} - {error_text}"
493 )));
494 }
495
496 let result = response
497 .json::<R>()
498 .await
499 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
500
501 Ok(result)
502 }
503
504 pub async fn send_request_with_auth<T, R>(
505 &self,
506 endpoint: &str,
507 request_body: &T,
508 auth_header: &str,
509 ) -> Result<R, ApiError>
510 where
511 T: Serialize,
512 R: for<'de> Deserialize<'de>,
513 {
514 let base_url = self.config.get_base_url();
515 let url = format!("https://{base_url}/{endpoint}");
516
517 let response = self
518 .client
519 .post(&url)
520 .header("Authorization", auth_header)
521 .json(request_body)
522 .send()
523 .await
524 .map_err(ApiError::HttpClient)?;
525
526 if !response.status().is_success() {
527 let status = response.status();
528 let error_text = response.text().await.unwrap_or_default();
529 return Err(ApiError::InvalidResponse(format!(
530 "HTTP error: {status} - {error_text}"
531 )));
532 }
533
534 let result = response
535 .json::<R>()
536 .await
537 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
538
539 Ok(result)
540 }
541}
542
543pub enum HessraClient {
545 Http1(Http1Client),
547 #[cfg(feature = "http3")]
549 Http3(Http3Client),
550}
551
552pub struct HessraClientBuilder {
554 config: BaseConfig,
556 protocol: hessra_config::Protocol,
558}
559
560impl HessraClientBuilder {
561 pub fn new() -> Self {
563 Self {
564 config: BaseConfig {
565 base_url: String::new(),
566 port: None,
567 mtls_key: None,
568 mtls_cert: None,
569 server_ca: String::new(),
570 public_key: None,
571 personal_keypair: None,
572 },
573 protocol: Protocol::Http1,
574 }
575 }
576
577 pub fn from_config(mut self, config: &HessraConfig) -> Self {
579 self.config.base_url = config.base_url.clone();
580 self.config.port = config.port;
581 self.config.mtls_key = config.mtls_key.clone();
582 self.config.mtls_cert = config.mtls_cert.clone();
583 self.config.server_ca = config.server_ca.clone();
584 self.config.public_key = config.public_key.clone();
585 self.config.personal_keypair = config.personal_keypair.clone();
586 self.protocol = config.protocol.clone();
587 self
588 }
589
590 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
592 self.config.base_url = base_url.into();
593 self
594 }
595
596 pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
599 self.config.mtls_key = Some(mtls_key.into());
600 self
601 }
602
603 pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
606 self.config.mtls_cert = Some(mtls_cert.into());
607 self
608 }
609
610 pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
613 self.config.server_ca = server_ca.into();
614 self
615 }
616
617 pub fn port(mut self, port: u16) -> Self {
619 self.config.port = Some(port);
620 self
621 }
622
623 pub fn protocol(mut self, protocol: Protocol) -> Self {
625 self.protocol = protocol;
626 self
627 }
628
629 pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
632 self.config.public_key = Some(public_key.into());
633 self
634 }
635
636 pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
640 self.config.personal_keypair = Some(keypair.into());
641 self
642 }
643
644 fn build_http1(&self) -> Result<Http1Client, ApiError> {
646 Http1Client::new(self.config.clone())
647 }
648
649 #[cfg(feature = "http3")]
651 fn build_http3(&self) -> Result<Http3Client, ApiError> {
652 Http3Client::new(self.config.clone())
653 }
654
655 pub fn build(self) -> Result<HessraClient, ApiError> {
657 match self.protocol {
658 Protocol::Http1 => Ok(HessraClient::Http1(self.build_http1()?)),
659 #[cfg(feature = "http3")]
660 Protocol::Http3 => Ok(HessraClient::Http3(self.build_http3()?)),
661 #[allow(unreachable_patterns)]
662 _ => Err(ApiError::Internal("Unsupported protocol".to_string())),
663 }
664 }
665}
666
667impl Default for HessraClientBuilder {
668 fn default() -> Self {
669 Self::new()
670 }
671}
672
673impl HessraClient {
674 pub fn builder() -> HessraClientBuilder {
676 HessraClientBuilder::new()
677 }
678
679 pub async fn fetch_public_key(
683 base_url: impl Into<String>,
684 port: Option<u16>,
685 server_ca: impl Into<String>,
686 ) -> Result<String, ApiError> {
687 let base_url_str = base_url.into();
688 let server_ca = server_ca.into();
689
690 let (host, embedded_port) = parse_server_address(&base_url_str);
692 let resolved_port = embedded_port.or(port);
694
695 let cert_pem = server_ca.as_bytes();
697 let certs = reqwest::Certificate::from_pem_bundle(cert_pem)
698 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
699
700 let mut client_builder = reqwest::ClientBuilder::new().use_rustls_tls();
701 for cert in certs {
702 client_builder = client_builder.add_root_certificate(cert);
703 }
704
705 let client = client_builder
706 .build()
707 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
708
709 let url = match resolved_port {
711 Some(port) => format!("https://{host}:{port}/public_key"),
712 None => format!("https://{host}/public_key"),
713 };
714
715 let response = client
717 .get(&url)
718 .send()
719 .await
720 .map_err(ApiError::HttpClient)?;
721
722 if !response.status().is_success() {
723 let status = response.status();
724 let error_text = response.text().await.unwrap_or_default();
725 return Err(ApiError::InvalidResponse(format!(
726 "HTTP error: {status} - {error_text}"
727 )));
728 }
729
730 let result = response
732 .json::<PublicKeyResponse>()
733 .await
734 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
735
736 Ok(result.public_key)
737 }
738
739 pub async fn fetch_ca_cert(
751 base_url: impl Into<String>,
752 port: Option<u16>,
753 ) -> Result<String, ApiError> {
754 let base_url_str = base_url.into();
755
756 let (host, embedded_port) = parse_server_address(&base_url_str);
758 let resolved_port = embedded_port.or(port);
760
761 let client = reqwest::ClientBuilder::new()
763 .use_rustls_tls()
764 .build()
765 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
766
767 let url = match resolved_port {
769 Some(port) => format!("https://{host}:{port}/ca_cert"),
770 None => format!("https://{host}/ca_cert"),
771 };
772
773 let response = client
775 .get(&url)
776 .send()
777 .await
778 .map_err(ApiError::HttpClient)?;
779
780 if !response.status().is_success() {
781 let status = response.status();
782 let error_text = response.text().await.unwrap_or_default();
783 return Err(ApiError::InvalidResponse(format!(
784 "HTTP error: {status} - {error_text}"
785 )));
786 }
787
788 let result = response
790 .json::<CaCertResponse>()
791 .await
792 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
793
794 if result.ca_cert_pem.is_empty() {
796 return Err(ApiError::InvalidResponse(
797 "Server returned empty CA certificate".to_string(),
798 ));
799 }
800
801 if !result.ca_cert_pem.contains("-----BEGIN CERTIFICATE-----") {
802 return Err(ApiError::InvalidResponse(
803 "Server returned invalid PEM format".to_string(),
804 ));
805 }
806
807 Ok(result.ca_cert_pem)
808 }
809
810 #[cfg(feature = "http3")]
811 pub async fn fetch_public_key_http3(
812 base_url: impl Into<String>,
813 port: Option<u16>,
814 server_ca: impl Into<String>,
815 ) -> Result<String, ApiError> {
816 let base_url_str = base_url.into();
817 let server_ca = server_ca.into();
818
819 let (host, embedded_port) = parse_server_address(&base_url_str);
821 let resolved_port = embedded_port.or(port);
823
824 let cert_pem = server_ca.as_bytes();
826 let certs = reqwest::Certificate::from_pem_bundle(cert_pem)
827 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
828
829 let mut client_builder = reqwest::ClientBuilder::new()
830 .use_rustls_tls()
831 .http3_prior_knowledge();
832 for cert in certs {
833 client_builder = client_builder.add_root_certificate(cert);
834 }
835
836 let client = client_builder
837 .build()
838 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
839
840 let url = match resolved_port {
842 Some(port) => format!("https://{host}:{port}/public_key"),
843 None => format!("https://{host}/public_key"),
844 };
845
846 let response = client
848 .get(&url)
849 .send()
850 .await
851 .map_err(ApiError::HttpClient)?;
852
853 if !response.status().is_success() {
854 let status = response.status();
855 let error_text = response.text().await.unwrap_or_default();
856 return Err(ApiError::InvalidResponse(format!(
857 "HTTP error: {status} - {error_text}"
858 )));
859 }
860
861 let result = response
863 .json::<PublicKeyResponse>()
864 .await
865 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
866
867 Ok(result.public_key)
868 }
869
870 pub async fn request_token(
878 &self,
879 resource: String,
880 operation: String,
881 domain: Option<String>,
882 ) -> Result<TokenResponse, ApiError> {
883 let request = TokenRequest {
884 resource,
885 operation,
886 domain,
887 };
888
889 let response = match self {
890 HessraClient::Http1(client) => {
891 client
892 .send_request::<_, TokenResponse>("request_token", &request)
893 .await?
894 }
895 #[cfg(feature = "http3")]
896 HessraClient::Http3(client) => {
897 client
898 .send_request::<_, TokenResponse>("request_token", &request)
899 .await?
900 }
901 };
902
903 Ok(response)
904 }
905
906 pub async fn request_token_with_identity(
916 &self,
917 resource: String,
918 operation: String,
919 identity_token: String,
920 domain: Option<String>,
921 ) -> Result<TokenResponse, ApiError> {
922 let request = TokenRequest {
923 resource,
924 operation,
925 domain,
926 };
927
928 let response = match self {
929 HessraClient::Http1(client) => {
930 client
931 .send_request_with_auth::<_, TokenResponse>(
932 "request_token",
933 &request,
934 &format!("Bearer {identity_token}"),
935 )
936 .await?
937 }
938 #[cfg(feature = "http3")]
939 HessraClient::Http3(client) => {
940 client
941 .send_request_with_auth::<_, TokenResponse>(
942 "request_token",
943 &request,
944 &format!("Bearer {identity_token}"),
945 )
946 .await?
947 }
948 };
949
950 Ok(response)
951 }
952
953 pub async fn request_token_simple(
956 &self,
957 resource: String,
958 operation: String,
959 ) -> Result<String, ApiError> {
960 let response = self.request_token(resource, operation, None).await?;
961
962 match response.token {
963 Some(token) => Ok(token),
964 None => Err(ApiError::TokenRequest(format!(
965 "Failed to get token: {}",
966 response.response_msg
967 ))),
968 }
969 }
970
971 pub async fn verify_token(
974 &self,
975 token: String,
976 subject: String,
977 resource: String,
978 operation: String,
979 ) -> Result<String, ApiError> {
980 let request = VerifyTokenRequest {
981 token,
982 subject,
983 resource,
984 operation,
985 };
986
987 let response = match self {
988 HessraClient::Http1(client) => {
989 client
990 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
991 .await?
992 }
993 #[cfg(feature = "http3")]
994 HessraClient::Http3(client) => {
995 client
996 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
997 .await?
998 }
999 };
1000
1001 Ok(response.response_msg)
1002 }
1003
1004 pub async fn verify_service_chain_token(
1011 &self,
1012 token: String,
1013 subject: String,
1014 resource: String,
1015 component: Option<String>,
1016 ) -> Result<String, ApiError> {
1017 let request = VerifyServiceChainTokenRequest {
1018 token,
1019 subject,
1020 resource,
1021 component,
1022 };
1023
1024 let response = match self {
1025 HessraClient::Http1(client) => {
1026 client
1027 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
1028 .await?
1029 }
1030 #[cfg(feature = "http3")]
1031 HessraClient::Http3(client) => {
1032 client
1033 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
1034 .await?
1035 }
1036 };
1037
1038 Ok(response.response_msg)
1039 }
1040
1041 pub async fn sign_token(
1043 &self,
1044 token: &str,
1045 resource: &str,
1046 operation: &str,
1047 ) -> Result<SignTokenResponse, ApiError> {
1048 let request = SignTokenRequest {
1049 token: token.to_string(),
1050 resource: resource.to_string(),
1051 operation: operation.to_string(),
1052 };
1053
1054 let response = match self {
1055 HessraClient::Http1(client) => {
1056 client
1057 .send_request::<_, SignTokenResponse>("sign_token", &request)
1058 .await?
1059 }
1060 #[cfg(feature = "http3")]
1061 HessraClient::Http3(client) => {
1062 client
1063 .send_request::<_, SignTokenResponse>("sign_token", &request)
1064 .await?
1065 }
1066 };
1067
1068 Ok(response)
1069 }
1070
1071 pub async fn get_public_key(&self) -> Result<String, ApiError> {
1073 let url_path = "public_key";
1074
1075 let response = match self {
1076 HessraClient::Http1(client) => {
1077 let base_url = client.config.get_base_url();
1079 let full_url = format!("https://{base_url}/{url_path}");
1080
1081 let response = client
1082 .client
1083 .get(&full_url)
1084 .send()
1085 .await
1086 .map_err(ApiError::HttpClient)?;
1087
1088 if !response.status().is_success() {
1089 let status = response.status();
1090 let error_text = response.text().await.unwrap_or_default();
1091 return Err(ApiError::InvalidResponse(format!(
1092 "HTTP error: {status} - {error_text}"
1093 )));
1094 }
1095
1096 response.json::<PublicKeyResponse>().await.map_err(|e| {
1097 ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
1098 })?
1099 }
1100 #[cfg(feature = "http3")]
1101 HessraClient::Http3(client) => {
1102 let base_url = client.config.get_base_url();
1103 let full_url = format!("https://{base_url}/{url_path}");
1104
1105 let response = client
1106 .client
1107 .get(&full_url)
1108 .send()
1109 .await
1110 .map_err(ApiError::HttpClient)?;
1111
1112 if !response.status().is_success() {
1113 let status = response.status();
1114 let error_text = response.text().await.unwrap_or_default();
1115 return Err(ApiError::InvalidResponse(format!(
1116 "HTTP error: {status} - {error_text}"
1117 )));
1118 }
1119
1120 response.json::<PublicKeyResponse>().await.map_err(|e| {
1121 ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
1122 })?
1123 }
1124 };
1125
1126 Ok(response.public_key)
1127 }
1128
1129 pub async fn request_identity_token(
1137 &self,
1138 identifier: Option<String>,
1139 ) -> Result<IdentityTokenResponse, ApiError> {
1140 let request = IdentityTokenRequest { identifier };
1141
1142 let response = match self {
1143 HessraClient::Http1(client) => {
1144 client
1145 .send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
1146 .await?
1147 }
1148 #[cfg(feature = "http3")]
1149 HessraClient::Http3(client) => {
1150 client
1151 .send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
1152 .await?
1153 }
1154 };
1155
1156 Ok(response)
1157 }
1158
1159 pub async fn refresh_identity_token(
1169 &self,
1170 current_token: String,
1171 identifier: Option<String>,
1172 ) -> Result<IdentityTokenResponse, ApiError> {
1173 let request = RefreshIdentityTokenRequest {
1174 current_token,
1175 identifier,
1176 };
1177
1178 let response = match self {
1179 HessraClient::Http1(client) => {
1180 client
1181 .send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
1182 .await?
1183 }
1184 #[cfg(feature = "http3")]
1185 HessraClient::Http3(client) => {
1186 client
1187 .send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
1188 .await?
1189 }
1190 };
1191
1192 Ok(response)
1193 }
1194
1195 pub async fn mint_domain_restricted_identity_token(
1205 &self,
1206 subject: String,
1207 duration: Option<u64>,
1208 ) -> Result<MintIdentityTokenResponse, ApiError> {
1209 let request = MintIdentityTokenRequest { subject, duration };
1210
1211 let response = match self {
1212 HessraClient::Http1(client) => {
1213 client
1214 .send_request::<_, MintIdentityTokenResponse>("mint_identity_token", &request)
1215 .await?
1216 }
1217 #[cfg(feature = "http3")]
1218 HessraClient::Http3(client) => {
1219 client
1220 .send_request::<_, MintIdentityTokenResponse>("mint_identity_token", &request)
1221 .await?
1222 }
1223 };
1224
1225 Ok(response)
1226 }
1227}
1228
1229#[cfg(test)]
1230mod tests {
1231 use super::*;
1232
1233 #[test]
1235 fn test_base_config_get_base_url_with_port() {
1236 let config = BaseConfig {
1237 base_url: "test.hessra.net".to_string(),
1238 port: Some(443),
1239 mtls_key: None,
1240 mtls_cert: None,
1241 server_ca: "".to_string(),
1242 public_key: None,
1243 personal_keypair: None,
1244 };
1245
1246 assert_eq!(config.get_base_url(), "test.hessra.net:443");
1247 }
1248
1249 #[test]
1250 fn test_base_config_get_base_url_without_port() {
1251 let config = BaseConfig {
1252 base_url: "test.hessra.net".to_string(),
1253 port: None,
1254 mtls_key: None,
1255 mtls_cert: None,
1256 server_ca: "".to_string(),
1257 public_key: None,
1258 personal_keypair: None,
1259 };
1260
1261 assert_eq!(config.get_base_url(), "test.hessra.net");
1262 }
1263
1264 #[test]
1266 fn test_client_builder_methods() {
1267 let builder = HessraClientBuilder::new()
1268 .base_url("test.hessra.net")
1269 .port(443)
1270 .protocol(Protocol::Http1)
1271 .mtls_cert("CERT")
1272 .mtls_key("KEY")
1273 .server_ca("CA")
1274 .public_key("PUBKEY")
1275 .personal_keypair("KEYPAIR");
1276
1277 assert_eq!(builder.config.base_url, "test.hessra.net");
1278 assert_eq!(builder.config.port, Some(443));
1279 assert_eq!(builder.config.mtls_cert, Some("CERT".to_string()));
1280 assert_eq!(builder.config.mtls_key, Some("KEY".to_string()));
1281 assert_eq!(builder.config.server_ca, "CA");
1282 assert_eq!(builder.config.public_key, Some("PUBKEY".to_string()));
1283 assert_eq!(builder.config.personal_keypair, Some("KEYPAIR".to_string()));
1284 }
1285
1286 #[test]
1288 fn test_parse_server_address_ip_with_port() {
1289 let (host, port) = parse_server_address("127.0.0.1:4433");
1290 assert_eq!(host, "127.0.0.1");
1291 assert_eq!(port, Some(4433));
1292 }
1293
1294 #[test]
1295 fn test_parse_server_address_ip_only() {
1296 let (host, port) = parse_server_address("127.0.0.1");
1297 assert_eq!(host, "127.0.0.1");
1298 assert_eq!(port, None);
1299 }
1300
1301 #[test]
1302 fn test_parse_server_address_hostname_with_port() {
1303 let (host, port) = parse_server_address("test.hessra.net:443");
1304 assert_eq!(host, "test.hessra.net");
1305 assert_eq!(port, Some(443));
1306 }
1307
1308 #[test]
1309 fn test_parse_server_address_hostname_only() {
1310 let (host, port) = parse_server_address("test.hessra.net");
1311 assert_eq!(host, "test.hessra.net");
1312 assert_eq!(port, None);
1313 }
1314
1315 #[test]
1316 fn test_parse_server_address_with_https_protocol() {
1317 let (host, port) = parse_server_address("https://example.com:8443");
1318 assert_eq!(host, "example.com");
1319 assert_eq!(port, Some(8443));
1320 }
1321
1322 #[test]
1323 fn test_parse_server_address_with_https_protocol_no_port() {
1324 let (host, port) = parse_server_address("https://example.com");
1325 assert_eq!(host, "example.com");
1326 assert_eq!(port, None);
1327 }
1328
1329 #[test]
1330 fn test_parse_server_address_with_path() {
1331 let (host, port) = parse_server_address("https://example.com:8443/some/path");
1332 assert_eq!(host, "example.com");
1333 assert_eq!(port, Some(8443));
1334 }
1335
1336 #[test]
1337 fn test_parse_server_address_ipv6_with_brackets_and_port() {
1338 let (host, port) = parse_server_address("[::1]:8443");
1339 assert_eq!(host, "::1");
1340 assert_eq!(port, Some(8443));
1341 }
1342
1343 #[test]
1344 fn test_parse_server_address_ipv6_with_brackets_no_port() {
1345 let (host, port) = parse_server_address("[::1]");
1346 assert_eq!(host, "::1");
1347 assert_eq!(port, None);
1348 }
1349
1350 #[test]
1351 fn test_parse_server_address_ipv6_full_with_port() {
1352 let (host, port) = parse_server_address("[2001:db8::1]:4433");
1353 assert_eq!(host, "2001:db8::1");
1354 assert_eq!(port, Some(4433));
1355 }
1356
1357 #[test]
1358 fn test_parse_server_address_with_whitespace() {
1359 let (host, port) = parse_server_address(" 127.0.0.1:4433 ");
1360 assert_eq!(host, "127.0.0.1");
1361 assert_eq!(port, Some(4433));
1362 }
1363
1364 #[test]
1365 fn test_base_config_get_base_url_with_embedded_port() {
1366 let config = BaseConfig {
1368 base_url: "127.0.0.1:4433".to_string(),
1369 port: None, mtls_key: None,
1371 mtls_cert: None,
1372 server_ca: "".to_string(),
1373 public_key: None,
1374 personal_keypair: None,
1375 };
1376 assert_eq!(config.get_base_url(), "127.0.0.1:4433");
1378 }
1379
1380 #[test]
1381 fn test_base_config_get_base_url_explicit_port_overrides_embedded() {
1382 let config = BaseConfig {
1384 base_url: "127.0.0.1:4433".to_string(),
1385 port: Some(8080), mtls_key: None,
1387 mtls_cert: None,
1388 server_ca: "".to_string(),
1389 public_key: None,
1390 personal_keypair: None,
1391 };
1392 assert_eq!(config.get_base_url(), "127.0.0.1:8080");
1393 }
1394}