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
46#[derive(Serialize, Deserialize)]
49pub struct TokenRequest {
50 pub resource: String,
52 pub operation: String,
54}
55
56#[derive(Serialize, Deserialize)]
58pub struct VerifyTokenRequest {
59 pub token: String,
61 pub subject: String,
63 pub resource: String,
65 pub operation: String,
67}
68
69#[derive(Serialize, Deserialize)]
71pub struct TokenResponse {
72 pub response_msg: String,
74 pub token: Option<String>,
76}
77
78#[derive(Serialize, Deserialize)]
80pub struct VerifyTokenResponse {
81 pub response_msg: String,
83}
84
85#[derive(Serialize, Deserialize)]
87pub struct PublicKeyResponse {
88 pub response_msg: String,
89 pub public_key: String,
90}
91
92#[derive(Serialize, Deserialize)]
94pub struct VerifyServiceChainTokenRequest {
95 pub token: String,
96 pub subject: String,
97 pub resource: String,
98 pub component: Option<String>,
99}
100
101#[derive(Clone)]
103pub struct BaseConfig {
104 pub base_url: String,
106 pub port: Option<u16>,
108 pub mtls_key: String,
110 pub mtls_cert: String,
112 pub server_ca: String,
114 pub public_key: Option<String>,
116 pub personal_keypair: Option<String>,
118}
119
120impl BaseConfig {
121 pub fn get_base_url(&self) -> String {
123 match self.port {
124 Some(port) => format!("{}:{}", self.base_url, port),
125 None => self.base_url.clone(),
126 }
127 }
128}
129
130pub struct Http1Client {
132 config: BaseConfig,
134 client: reqwest::Client,
136}
137
138impl Http1Client {
139 pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
141 let identity_str = format!("{}{}", config.mtls_cert, config.mtls_key);
144
145 let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
146 ApiError::SslConfig(format!(
147 "Failed to create identity from certificate and key: {}",
148 e
149 ))
150 })?;
151
152 let cert_der = reqwest::Certificate::from_pem(config.server_ca.as_bytes())
154 .map_err(|e| ApiError::SslConfig(format!("Failed to parse CA certificate: {}", e)))?;
155
156 let client = reqwest::ClientBuilder::new()
158 .use_rustls_tls()
159 .identity(identity)
160 .add_root_certificate(cert_der)
161 .build()
162 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
163
164 Ok(Self { config, client })
165 }
166
167 pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
169 where
170 T: Serialize,
171 R: for<'de> Deserialize<'de>,
172 {
173 let base_url = self.config.get_base_url();
174 let url = format!("https://{}/{}", base_url, endpoint);
175
176 let response = self
177 .client
178 .post(&url)
179 .json(request_body)
180 .send()
181 .await
182 .map_err(ApiError::HttpClient)?;
183
184 if !response.status().is_success() {
185 let status = response.status();
186 let error_text = response.text().await.unwrap_or_default();
187 return Err(ApiError::InvalidResponse(format!(
188 "HTTP error: {} - {}",
189 status, error_text
190 )));
191 }
192
193 let result = response
194 .json::<R>()
195 .await
196 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
197
198 Ok(result)
199 }
200}
201
202#[cfg(feature = "http3")]
204pub struct Http3Client {
205 config: BaseConfig,
207 client: reqwest::Client,
209}
210
211#[cfg(feature = "http3")]
212impl Http3Client {
213 pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
215 let identity_str = format!("{}{}", config.mtls_cert, config.mtls_key);
218
219 let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
220 ApiError::SslConfig(format!(
221 "Failed to create identity from certificate and key: {}",
222 e
223 ))
224 })?;
225
226 let cert_der = reqwest::Certificate::from_pem(config.server_ca.as_bytes())
228 .map_err(|e| ApiError::SslConfig(format!("Failed to parse CA certificate: {}", e)))?;
229
230 let client = reqwest::ClientBuilder::new()
232 .use_rustls_tls()
233 .http3_prior_knowledge()
234 .identity(identity)
235 .add_root_certificate(cert_der)
236 .build()
237 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
238
239 Ok(Self { config, client })
240 }
241
242 pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
244 where
245 T: Serialize,
246 R: for<'de> Deserialize<'de>,
247 {
248 let base_url = self.config.get_base_url();
249 let url = format!("https://{}/{}", base_url, endpoint);
250
251 let response = self
252 .client
253 .post(&url)
254 .version(http::Version::HTTP_3)
255 .json(request_body)
256 .send()
257 .await
258 .map_err(ApiError::HttpClient)?;
259
260 if !response.status().is_success() {
261 let status = response.status();
262 let error_text = response.text().await.unwrap_or_default();
263 return Err(ApiError::InvalidResponse(format!(
264 "HTTP error: {} - {}",
265 status, error_text
266 )));
267 }
268
269 let result = response
270 .json::<R>()
271 .await
272 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
273
274 Ok(result)
275 }
276}
277
278pub enum HessraClient {
280 Http1(Http1Client),
282 #[cfg(feature = "http3")]
284 Http3(Http3Client),
285}
286
287pub struct HessraClientBuilder {
289 config: BaseConfig,
291 protocol: hessra_config::Protocol,
293}
294
295impl HessraClientBuilder {
296 pub fn new() -> Self {
298 Self {
299 config: BaseConfig {
300 base_url: String::new(),
301 port: None,
302 mtls_key: String::new(),
303 mtls_cert: String::new(),
304 server_ca: String::new(),
305 public_key: None,
306 personal_keypair: None,
307 },
308 protocol: Protocol::Http1,
309 }
310 }
311
312 pub fn from_config(mut self, config: &HessraConfig) -> Self {
314 self.config.base_url = config.base_url.clone();
315 self.config.port = config.port;
316 self.config.mtls_key = config.mtls_key.clone();
317 self.config.mtls_cert = config.mtls_cert.clone();
318 self.config.server_ca = config.server_ca.clone();
319 self.config.public_key = config.public_key.clone();
320 self.config.personal_keypair = config.personal_keypair.clone();
321 self.protocol = config.protocol.clone();
322 self
323 }
324
325 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
327 self.config.base_url = base_url.into();
328 self
329 }
330
331 pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
334 self.config.mtls_key = mtls_key.into();
335 self
336 }
337
338 pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
341 self.config.mtls_cert = mtls_cert.into();
342 self
343 }
344
345 pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
348 self.config.server_ca = server_ca.into();
349 self
350 }
351
352 pub fn port(mut self, port: u16) -> Self {
354 self.config.port = Some(port);
355 self
356 }
357
358 pub fn protocol(mut self, protocol: Protocol) -> Self {
360 self.protocol = protocol;
361 self
362 }
363
364 pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
367 self.config.public_key = Some(public_key.into());
368 self
369 }
370
371 pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
375 self.config.personal_keypair = Some(keypair.into());
376 self
377 }
378
379 fn build_http1(&self) -> Result<Http1Client, ApiError> {
381 Http1Client::new(self.config.clone())
382 }
383
384 #[cfg(feature = "http3")]
386 fn build_http3(&self) -> Result<Http3Client, ApiError> {
387 Http3Client::new(self.config.clone())
388 }
389
390 pub fn build(self) -> Result<HessraClient, ApiError> {
392 match self.protocol {
393 Protocol::Http1 => Ok(HessraClient::Http1(self.build_http1()?)),
394 #[cfg(feature = "http3")]
395 Protocol::Http3 => Ok(HessraClient::Http3(self.build_http3()?)),
396 #[allow(unreachable_patterns)]
397 _ => Err(ApiError::Internal("Unsupported protocol".to_string())),
398 }
399 }
400}
401
402impl Default for HessraClientBuilder {
403 fn default() -> Self {
404 Self::new()
405 }
406}
407
408impl HessraClient {
409 pub fn builder() -> HessraClientBuilder {
411 HessraClientBuilder::new()
412 }
413
414 pub async fn fetch_public_key(
418 base_url: impl Into<String>,
419 port: Option<u16>,
420 server_ca: impl Into<String>,
421 ) -> Result<String, ApiError> {
422 let base_url = base_url.into();
423 let server_ca = server_ca.into();
424
425 let cert_pem = server_ca.as_bytes();
427 let cert_der = reqwest::Certificate::from_pem(cert_pem)
428 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
429
430 let client = reqwest::ClientBuilder::new()
431 .use_rustls_tls()
432 .add_root_certificate(cert_der)
433 .build()
434 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
435
436 let url = match port {
438 Some(port) => format!("https://{}:{}/public_key", base_url, port),
439 None => format!("https://{}/public_key", base_url),
440 };
441
442 let response = client
444 .get(&url)
445 .send()
446 .await
447 .map_err(ApiError::HttpClient)?;
448
449 if !response.status().is_success() {
450 let status = response.status();
451 let error_text = response.text().await.unwrap_or_default();
452 return Err(ApiError::InvalidResponse(format!(
453 "HTTP error: {} - {}",
454 status, error_text
455 )));
456 }
457
458 let result = response
460 .json::<PublicKeyResponse>()
461 .await
462 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
463
464 Ok(result.public_key)
465 }
466
467 #[cfg(feature = "http3")]
468 pub async fn fetch_public_key_http3(
469 base_url: impl Into<String>,
470 port: Option<u16>,
471 server_ca: impl Into<String>,
472 ) -> Result<String, ApiError> {
473 let base_url = base_url.into();
474 let server_ca = server_ca.into();
475
476 let cert_pem = server_ca.as_bytes();
478 let cert_der = reqwest::Certificate::from_pem(cert_pem)
479 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
480
481 let client = reqwest::ClientBuilder::new()
482 .use_rustls_tls()
483 .add_root_certificate(cert_der)
484 .http3_prior_knowledge()
485 .build()
486 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
487
488 let url = match port {
490 Some(port) => format!("https://{}:{}/public_key", base_url, port),
491 None => format!("https://{}/public_key", base_url),
492 };
493
494 let response = client
496 .get(&url)
497 .version(http::Version::HTTP_3)
498 .send()
499 .await
500 .map_err(ApiError::HttpClient)?;
501
502 if !response.status().is_success() {
503 let status = response.status();
504 let error_text = response.text().await.unwrap_or_default();
505 return Err(ApiError::InvalidResponse(format!(
506 "HTTP error: {} - {}",
507 status, error_text
508 )));
509 }
510
511 let result = response
513 .json::<PublicKeyResponse>()
514 .await
515 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
516
517 Ok(result.public_key)
518 }
519
520 pub async fn request_token(
522 &self,
523 resource: String,
524 operation: String,
525 ) -> Result<String, ApiError> {
526 let request = TokenRequest {
527 resource,
528 operation,
529 };
530
531 let response = match self {
532 HessraClient::Http1(client) => {
533 client
534 .send_request::<_, TokenResponse>("request_token", &request)
535 .await?
536 }
537 #[cfg(feature = "http3")]
538 HessraClient::Http3(client) => {
539 client
540 .send_request::<_, TokenResponse>("request_token", &request)
541 .await?
542 }
543 };
544
545 match response.token {
546 Some(token) => Ok(token),
547 None => Err(ApiError::TokenRequest(format!(
548 "Failed to get token: {}",
549 response.response_msg
550 ))),
551 }
552 }
553
554 pub async fn verify_token(
557 &self,
558 token: String,
559 subject: String,
560 resource: String,
561 operation: String,
562 ) -> Result<String, ApiError> {
563 let request = VerifyTokenRequest {
564 token,
565 subject,
566 resource,
567 operation,
568 };
569
570 let response = match self {
571 HessraClient::Http1(client) => {
572 client
573 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
574 .await?
575 }
576 #[cfg(feature = "http3")]
577 HessraClient::Http3(client) => {
578 client
579 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
580 .await?
581 }
582 };
583
584 Ok(response.response_msg)
585 }
586
587 pub async fn verify_service_chain_token(
594 &self,
595 token: String,
596 subject: String,
597 resource: String,
598 component: Option<String>,
599 ) -> Result<String, ApiError> {
600 let request = VerifyServiceChainTokenRequest {
601 token,
602 subject,
603 resource,
604 component,
605 };
606
607 let response = match self {
608 HessraClient::Http1(client) => {
609 client
610 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
611 .await?
612 }
613 #[cfg(feature = "http3")]
614 HessraClient::Http3(client) => {
615 client
616 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
617 .await?
618 }
619 };
620
621 Ok(response.response_msg)
622 }
623
624 pub async fn get_public_key(&self) -> Result<String, ApiError> {
626 let url_path = "public_key";
627
628 let response = match self {
629 HessraClient::Http1(client) => {
630 let base_url = client.config.get_base_url();
632 let full_url = format!("https://{}/{}", base_url, url_path);
633
634 let response = client
635 .client
636 .get(&full_url)
637 .send()
638 .await
639 .map_err(ApiError::HttpClient)?;
640
641 if !response.status().is_success() {
642 let status = response.status();
643 let error_text = response.text().await.unwrap_or_default();
644 return Err(ApiError::InvalidResponse(format!(
645 "HTTP error: {} - {}",
646 status, error_text
647 )));
648 }
649
650 response.json::<PublicKeyResponse>().await.map_err(|e| {
651 ApiError::InvalidResponse(format!("Failed to parse response: {}", e))
652 })?
653 }
654 #[cfg(feature = "http3")]
655 HessraClient::Http3(client) => {
656 let base_url = client.config.get_base_url();
657 let full_url = format!("https://{}/{}", base_url, url_path);
658
659 let response = client
660 .client
661 .get(&full_url)
662 .version(http::Version::HTTP_3)
663 .send()
664 .await
665 .map_err(ApiError::HttpClient)?;
666
667 if !response.status().is_success() {
668 let status = response.status();
669 let error_text = response.text().await.unwrap_or_default();
670 return Err(ApiError::InvalidResponse(format!(
671 "HTTP error: {} - {}",
672 status, error_text
673 )));
674 }
675
676 response.json::<PublicKeyResponse>().await.map_err(|e| {
677 ApiError::InvalidResponse(format!("Failed to parse response: {}", e))
678 })?
679 }
680 };
681
682 Ok(response.public_key)
683 }
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689
690 #[test]
692 fn test_base_config_get_base_url_with_port() {
693 let config = BaseConfig {
694 base_url: "test.hessra.net".to_string(),
695 port: Some(443),
696 mtls_key: "".to_string(),
697 mtls_cert: "".to_string(),
698 server_ca: "".to_string(),
699 public_key: None,
700 personal_keypair: None,
701 };
702
703 assert_eq!(config.get_base_url(), "test.hessra.net:443");
704 }
705
706 #[test]
707 fn test_base_config_get_base_url_without_port() {
708 let config = BaseConfig {
709 base_url: "test.hessra.net".to_string(),
710 port: None,
711 mtls_key: "".to_string(),
712 mtls_cert: "".to_string(),
713 server_ca: "".to_string(),
714 public_key: None,
715 personal_keypair: None,
716 };
717
718 assert_eq!(config.get_base_url(), "test.hessra.net");
719 }
720
721 #[test]
723 fn test_client_builder_methods() {
724 let builder = HessraClientBuilder::new()
725 .base_url("test.hessra.net")
726 .port(443)
727 .protocol(Protocol::Http1)
728 .mtls_cert("CERT")
729 .mtls_key("KEY")
730 .server_ca("CA")
731 .public_key("PUBKEY")
732 .personal_keypair("KEYPAIR");
733
734 assert_eq!(builder.config.base_url, "test.hessra.net");
735 assert_eq!(builder.config.port, Some(443));
736 assert_eq!(builder.config.mtls_cert, "CERT");
737 assert_eq!(builder.config.mtls_key, "KEY");
738 assert_eq!(builder.config.server_ca, "CA");
739 assert_eq!(builder.config.public_key, Some("PUBKEY".to_string()));
740 assert_eq!(builder.config.personal_keypair, Some("KEYPAIR".to_string()));
741 }
742}