1use serde::{Deserialize, Serialize};
17use thiserror::Error;
18
19use hessra_config::{HessraConfig, Protocol};
20
21#[derive(Error, Debug)]
23pub enum ApiError {
24 #[error("HTTP client error: {0}")]
25 HttpClient(#[from] reqwest::Error),
26
27 #[error("SSL configuration error: {0}")]
28 SslConfig(String),
29
30 #[error("Invalid response: {0}")]
31 InvalidResponse(String),
32
33 #[error("Token request error: {0}")]
34 TokenRequest(String),
35
36 #[error("Token verification error: {0}")]
37 TokenVerification(String),
38
39 #[error("Service chain error: {0}")]
40 ServiceChain(String),
41
42 #[error("Internal error: {0}")]
43 Internal(String),
44
45 #[error("Signoff failed: {0}")]
46 SignoffFailed(String),
47
48 #[error("Missing signoff configuration for service: {0}")]
49 MissingSignoffConfig(String),
50
51 #[error("Invalid signoff response from {service}: {reason}")]
52 InvalidSignoffResponse { service: String, reason: String },
53
54 #[error("Signoff collection incomplete: {missing_signoffs} signoffs remaining")]
55 IncompleteSignoffs { missing_signoffs: usize },
56}
57
58#[derive(Serialize, Deserialize)]
61pub struct TokenRequest {
62 pub resource: String,
64 pub operation: String,
66}
67
68#[derive(Serialize, Deserialize)]
70pub struct VerifyTokenRequest {
71 pub token: String,
73 pub subject: String,
75 pub resource: String,
77 pub operation: String,
79}
80
81#[derive(Serialize, Deserialize, Debug, Clone)]
83pub struct SignoffInfo {
84 pub component: String,
85 pub authorization_service: String,
86 pub public_key: String,
87}
88
89#[derive(Serialize, Deserialize, Debug, Clone)]
91pub struct SignTokenRequest {
92 pub token: String,
93 pub resource: String,
94 pub operation: String,
95}
96
97#[derive(Serialize, Deserialize, Debug, Clone)]
99pub struct SignTokenResponse {
100 pub response_msg: String,
101 pub signed_token: Option<String>,
102}
103
104#[derive(Serialize, Deserialize, Debug, Clone)]
106pub struct TokenResponse {
107 pub response_msg: String,
109 pub token: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub pending_signoffs: Option<Vec<SignoffInfo>>,
114}
115
116#[derive(Serialize, Deserialize)]
118pub struct VerifyTokenResponse {
119 pub response_msg: String,
121}
122
123#[derive(Serialize, Deserialize)]
125pub struct PublicKeyResponse {
126 pub response_msg: String,
127 pub public_key: String,
128}
129
130#[derive(Serialize, Deserialize)]
132pub struct VerifyServiceChainTokenRequest {
133 pub token: String,
134 pub subject: String,
135 pub resource: String,
136 pub component: Option<String>,
137}
138
139#[derive(Clone)]
141pub struct BaseConfig {
142 pub base_url: String,
144 pub port: Option<u16>,
146 pub mtls_key: String,
148 pub mtls_cert: String,
150 pub server_ca: String,
152 pub public_key: Option<String>,
154 pub personal_keypair: Option<String>,
156}
157
158impl BaseConfig {
159 pub fn get_base_url(&self) -> String {
161 match self.port {
162 Some(port) => format!("{}:{port}", self.base_url),
163 None => self.base_url.clone(),
164 }
165 }
166}
167
168pub struct Http1Client {
170 config: BaseConfig,
172 client: reqwest::Client,
174}
175
176impl Http1Client {
177 pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
179 let identity_str = format!("{}{}", config.mtls_cert, config.mtls_key);
182
183 let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
184 ApiError::SslConfig(format!(
185 "Failed to create identity from certificate and key: {e}"
186 ))
187 })?;
188
189 let cert_der = reqwest::Certificate::from_pem(config.server_ca.as_bytes())
191 .map_err(|e| ApiError::SslConfig(format!("Failed to parse CA certificate: {e}")))?;
192
193 let client = reqwest::ClientBuilder::new()
195 .use_rustls_tls()
196 .identity(identity)
197 .add_root_certificate(cert_der)
198 .build()
199 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
200
201 Ok(Self { config, client })
202 }
203
204 pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
206 where
207 T: Serialize,
208 R: for<'de> Deserialize<'de>,
209 {
210 let base_url = self.config.get_base_url();
211 let url = format!("https://{base_url}/{endpoint}");
212
213 let response = self
214 .client
215 .post(&url)
216 .json(request_body)
217 .send()
218 .await
219 .map_err(ApiError::HttpClient)?;
220
221 if !response.status().is_success() {
222 let status = response.status();
223 let error_text = response.text().await.unwrap_or_default();
224 return Err(ApiError::InvalidResponse(format!(
225 "HTTP error: {status} - {error_text}"
226 )));
227 }
228
229 let result = response
230 .json::<R>()
231 .await
232 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
233
234 Ok(result)
235 }
236}
237
238#[cfg(feature = "http3")]
240pub struct Http3Client {
241 config: BaseConfig,
243 client: reqwest::Client,
245}
246
247#[cfg(feature = "http3")]
248impl Http3Client {
249 pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
251 let identity_str = format!("{}{}", config.mtls_cert, config.mtls_key);
254
255 let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
256 ApiError::SslConfig(format!(
257 "Failed to create identity from certificate and key: {e}"
258 ))
259 })?;
260
261 let cert_der = reqwest::Certificate::from_pem(config.server_ca.as_bytes())
263 .map_err(|e| ApiError::SslConfig(format!("Failed to parse CA certificate: {e}")))?;
264
265 let client = reqwest::ClientBuilder::new()
267 .use_rustls_tls()
268 .http3_prior_knowledge()
269 .identity(identity)
270 .add_root_certificate(cert_der)
271 .build()
272 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
273
274 Ok(Self { config, client })
275 }
276
277 pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
279 where
280 T: Serialize,
281 R: for<'de> Deserialize<'de>,
282 {
283 let base_url = self.config.get_base_url();
284 let url = format!("https://{base_url}/{endpoint}");
285
286 let response = self
287 .client
288 .post(&url)
289 .version(http::Version::HTTP_3)
290 .json(request_body)
291 .send()
292 .await
293 .map_err(ApiError::HttpClient)?;
294
295 if !response.status().is_success() {
296 let status = response.status();
297 let error_text = response.text().await.unwrap_or_default();
298 return Err(ApiError::InvalidResponse(format!(
299 "HTTP error: {status} - {error_text}"
300 )));
301 }
302
303 let result = response
304 .json::<R>()
305 .await
306 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
307
308 Ok(result)
309 }
310}
311
312pub enum HessraClient {
314 Http1(Http1Client),
316 #[cfg(feature = "http3")]
318 Http3(Http3Client),
319}
320
321pub struct HessraClientBuilder {
323 config: BaseConfig,
325 protocol: hessra_config::Protocol,
327}
328
329impl HessraClientBuilder {
330 pub fn new() -> Self {
332 Self {
333 config: BaseConfig {
334 base_url: String::new(),
335 port: None,
336 mtls_key: String::new(),
337 mtls_cert: String::new(),
338 server_ca: String::new(),
339 public_key: None,
340 personal_keypair: None,
341 },
342 protocol: Protocol::Http1,
343 }
344 }
345
346 pub fn from_config(mut self, config: &HessraConfig) -> Self {
348 self.config.base_url = config.base_url.clone();
349 self.config.port = config.port;
350 self.config.mtls_key = config.mtls_key.clone();
351 self.config.mtls_cert = config.mtls_cert.clone();
352 self.config.server_ca = config.server_ca.clone();
353 self.config.public_key = config.public_key.clone();
354 self.config.personal_keypair = config.personal_keypair.clone();
355 self.protocol = config.protocol.clone();
356 self
357 }
358
359 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
361 self.config.base_url = base_url.into();
362 self
363 }
364
365 pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
368 self.config.mtls_key = mtls_key.into();
369 self
370 }
371
372 pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
375 self.config.mtls_cert = mtls_cert.into();
376 self
377 }
378
379 pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
382 self.config.server_ca = server_ca.into();
383 self
384 }
385
386 pub fn port(mut self, port: u16) -> Self {
388 self.config.port = Some(port);
389 self
390 }
391
392 pub fn protocol(mut self, protocol: Protocol) -> Self {
394 self.protocol = protocol;
395 self
396 }
397
398 pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
401 self.config.public_key = Some(public_key.into());
402 self
403 }
404
405 pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
409 self.config.personal_keypair = Some(keypair.into());
410 self
411 }
412
413 fn build_http1(&self) -> Result<Http1Client, ApiError> {
415 Http1Client::new(self.config.clone())
416 }
417
418 #[cfg(feature = "http3")]
420 fn build_http3(&self) -> Result<Http3Client, ApiError> {
421 Http3Client::new(self.config.clone())
422 }
423
424 pub fn build(self) -> Result<HessraClient, ApiError> {
426 match self.protocol {
427 Protocol::Http1 => Ok(HessraClient::Http1(self.build_http1()?)),
428 #[cfg(feature = "http3")]
429 Protocol::Http3 => Ok(HessraClient::Http3(self.build_http3()?)),
430 #[allow(unreachable_patterns)]
431 _ => Err(ApiError::Internal("Unsupported protocol".to_string())),
432 }
433 }
434}
435
436impl Default for HessraClientBuilder {
437 fn default() -> Self {
438 Self::new()
439 }
440}
441
442impl HessraClient {
443 pub fn builder() -> HessraClientBuilder {
445 HessraClientBuilder::new()
446 }
447
448 pub async fn fetch_public_key(
452 base_url: impl Into<String>,
453 port: Option<u16>,
454 server_ca: impl Into<String>,
455 ) -> Result<String, ApiError> {
456 let base_url = base_url.into();
457 let server_ca = server_ca.into();
458
459 let cert_pem = server_ca.as_bytes();
461 let cert_der = reqwest::Certificate::from_pem(cert_pem)
462 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
463
464 let client = reqwest::ClientBuilder::new()
465 .use_rustls_tls()
466 .add_root_certificate(cert_der)
467 .build()
468 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
469
470 let url = match port {
472 Some(port) => format!("https://{base_url}:{port}/public_key"),
473 None => format!("https://{base_url}/public_key"),
474 };
475
476 let response = client
478 .get(&url)
479 .send()
480 .await
481 .map_err(ApiError::HttpClient)?;
482
483 if !response.status().is_success() {
484 let status = response.status();
485 let error_text = response.text().await.unwrap_or_default();
486 return Err(ApiError::InvalidResponse(format!(
487 "HTTP error: {status} - {error_text}"
488 )));
489 }
490
491 let result = response
493 .json::<PublicKeyResponse>()
494 .await
495 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
496
497 Ok(result.public_key)
498 }
499
500 #[cfg(feature = "http3")]
501 pub async fn fetch_public_key_http3(
502 base_url: impl Into<String>,
503 port: Option<u16>,
504 server_ca: impl Into<String>,
505 ) -> Result<String, ApiError> {
506 let base_url = base_url.into();
507 let server_ca = server_ca.into();
508
509 let cert_pem = server_ca.as_bytes();
511 let cert_der = reqwest::Certificate::from_pem(cert_pem)
512 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
513
514 let client = reqwest::ClientBuilder::new()
515 .use_rustls_tls()
516 .add_root_certificate(cert_der)
517 .http3_prior_knowledge()
518 .build()
519 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
520
521 let url = match port {
523 Some(port) => format!("https://{base_url}:{port}/public_key"),
524 None => format!("https://{base_url}/public_key"),
525 };
526
527 let response = client
529 .get(&url)
530 .version(http::Version::HTTP_3)
531 .send()
532 .await
533 .map_err(ApiError::HttpClient)?;
534
535 if !response.status().is_success() {
536 let status = response.status();
537 let error_text = response.text().await.unwrap_or_default();
538 return Err(ApiError::InvalidResponse(format!(
539 "HTTP error: {status} - {error_text}"
540 )));
541 }
542
543 let result = response
545 .json::<PublicKeyResponse>()
546 .await
547 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
548
549 Ok(result.public_key)
550 }
551
552 pub async fn request_token(
555 &self,
556 resource: String,
557 operation: String,
558 ) -> Result<TokenResponse, ApiError> {
559 let request = TokenRequest {
560 resource,
561 operation,
562 };
563
564 let response = match self {
565 HessraClient::Http1(client) => {
566 client
567 .send_request::<_, TokenResponse>("request_token", &request)
568 .await?
569 }
570 #[cfg(feature = "http3")]
571 HessraClient::Http3(client) => {
572 client
573 .send_request::<_, TokenResponse>("request_token", &request)
574 .await?
575 }
576 };
577
578 Ok(response)
579 }
580
581 pub async fn request_token_simple(
584 &self,
585 resource: String,
586 operation: String,
587 ) -> Result<String, ApiError> {
588 let response = self.request_token(resource, operation).await?;
589
590 match response.token {
591 Some(token) => Ok(token),
592 None => Err(ApiError::TokenRequest(format!(
593 "Failed to get token: {}",
594 response.response_msg
595 ))),
596 }
597 }
598
599 pub async fn verify_token(
602 &self,
603 token: String,
604 subject: String,
605 resource: String,
606 operation: String,
607 ) -> Result<String, ApiError> {
608 let request = VerifyTokenRequest {
609 token,
610 subject,
611 resource,
612 operation,
613 };
614
615 let response = match self {
616 HessraClient::Http1(client) => {
617 client
618 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
619 .await?
620 }
621 #[cfg(feature = "http3")]
622 HessraClient::Http3(client) => {
623 client
624 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
625 .await?
626 }
627 };
628
629 Ok(response.response_msg)
630 }
631
632 pub async fn verify_service_chain_token(
639 &self,
640 token: String,
641 subject: String,
642 resource: String,
643 component: Option<String>,
644 ) -> Result<String, ApiError> {
645 let request = VerifyServiceChainTokenRequest {
646 token,
647 subject,
648 resource,
649 component,
650 };
651
652 let response = match self {
653 HessraClient::Http1(client) => {
654 client
655 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
656 .await?
657 }
658 #[cfg(feature = "http3")]
659 HessraClient::Http3(client) => {
660 client
661 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
662 .await?
663 }
664 };
665
666 Ok(response.response_msg)
667 }
668
669 pub async fn sign_token(
671 &self,
672 token: &str,
673 resource: &str,
674 operation: &str,
675 ) -> Result<SignTokenResponse, ApiError> {
676 let request = SignTokenRequest {
677 token: token.to_string(),
678 resource: resource.to_string(),
679 operation: operation.to_string(),
680 };
681
682 let response = match self {
683 HessraClient::Http1(client) => {
684 client
685 .send_request::<_, SignTokenResponse>("sign_token", &request)
686 .await?
687 }
688 #[cfg(feature = "http3")]
689 HessraClient::Http3(client) => {
690 client
691 .send_request::<_, SignTokenResponse>("sign_token", &request)
692 .await?
693 }
694 };
695
696 Ok(response)
697 }
698
699 pub async fn get_public_key(&self) -> Result<String, ApiError> {
701 let url_path = "public_key";
702
703 let response = match self {
704 HessraClient::Http1(client) => {
705 let base_url = client.config.get_base_url();
707 let full_url = format!("https://{base_url}/{url_path}");
708
709 let response = client
710 .client
711 .get(&full_url)
712 .send()
713 .await
714 .map_err(ApiError::HttpClient)?;
715
716 if !response.status().is_success() {
717 let status = response.status();
718 let error_text = response.text().await.unwrap_or_default();
719 return Err(ApiError::InvalidResponse(format!(
720 "HTTP error: {status} - {error_text}"
721 )));
722 }
723
724 response.json::<PublicKeyResponse>().await.map_err(|e| {
725 ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
726 })?
727 }
728 #[cfg(feature = "http3")]
729 HessraClient::Http3(client) => {
730 let base_url = client.config.get_base_url();
731 let full_url = format!("https://{base_url}/{url_path}");
732
733 let response = client
734 .client
735 .get(&full_url)
736 .version(http::Version::HTTP_3)
737 .send()
738 .await
739 .map_err(ApiError::HttpClient)?;
740
741 if !response.status().is_success() {
742 let status = response.status();
743 let error_text = response.text().await.unwrap_or_default();
744 return Err(ApiError::InvalidResponse(format!(
745 "HTTP error: {status} - {error_text}"
746 )));
747 }
748
749 response.json::<PublicKeyResponse>().await.map_err(|e| {
750 ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
751 })?
752 }
753 };
754
755 Ok(response.public_key)
756 }
757}
758
759#[cfg(test)]
760mod tests {
761 use super::*;
762
763 #[test]
765 fn test_base_config_get_base_url_with_port() {
766 let config = BaseConfig {
767 base_url: "test.hessra.net".to_string(),
768 port: Some(443),
769 mtls_key: "".to_string(),
770 mtls_cert: "".to_string(),
771 server_ca: "".to_string(),
772 public_key: None,
773 personal_keypair: None,
774 };
775
776 assert_eq!(config.get_base_url(), "test.hessra.net:443");
777 }
778
779 #[test]
780 fn test_base_config_get_base_url_without_port() {
781 let config = BaseConfig {
782 base_url: "test.hessra.net".to_string(),
783 port: None,
784 mtls_key: "".to_string(),
785 mtls_cert: "".to_string(),
786 server_ca: "".to_string(),
787 public_key: None,
788 personal_keypair: None,
789 };
790
791 assert_eq!(config.get_base_url(), "test.hessra.net");
792 }
793
794 #[test]
796 fn test_client_builder_methods() {
797 let builder = HessraClientBuilder::new()
798 .base_url("test.hessra.net")
799 .port(443)
800 .protocol(Protocol::Http1)
801 .mtls_cert("CERT")
802 .mtls_key("KEY")
803 .server_ca("CA")
804 .public_key("PUBKEY")
805 .personal_keypair("KEYPAIR");
806
807 assert_eq!(builder.config.base_url, "test.hessra.net");
808 assert_eq!(builder.config.port, Some(443));
809 assert_eq!(builder.config.mtls_cert, "CERT");
810 assert_eq!(builder.config.mtls_key, "KEY");
811 assert_eq!(builder.config.server_ca, "CA");
812 assert_eq!(builder.config.public_key, Some("PUBKEY".to_string()));
813 assert_eq!(builder.config.personal_keypair, Some("KEYPAIR".to_string()));
814 }
815}