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() .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() .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 {
333 self.config.mtls_key = mtls_key.into();
334 self
335 }
336
337 pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
339 self.config.mtls_cert = mtls_cert.into();
340 self
341 }
342
343 pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
345 self.config.server_ca = server_ca.into();
346 self
347 }
348
349 pub fn port(mut self, port: u16) -> Self {
351 self.config.port = Some(port);
352 self
353 }
354
355 pub fn protocol(mut self, protocol: Protocol) -> Self {
357 self.protocol = protocol;
358 self
359 }
360
361 pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
363 self.config.public_key = Some(public_key.into());
364 self
365 }
366
367 pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
369 self.config.personal_keypair = Some(keypair.into());
370 self
371 }
372
373 fn build_http1(&self) -> Result<Http1Client, ApiError> {
375 Http1Client::new(self.config.clone())
376 }
377
378 #[cfg(feature = "http3")]
380 fn build_http3(&self) -> Result<Http3Client, ApiError> {
381 Http3Client::new(self.config.clone())
382 }
383
384 pub fn build(self) -> Result<HessraClient, ApiError> {
386 match self.protocol {
387 Protocol::Http1 => Ok(HessraClient::Http1(self.build_http1()?)),
388 #[cfg(feature = "http3")]
389 Protocol::Http3 => Ok(HessraClient::Http3(self.build_http3()?)),
390 #[allow(unreachable_patterns)]
391 _ => Err(ApiError::Internal("Unsupported protocol".to_string())),
392 }
393 }
394}
395
396impl Default for HessraClientBuilder {
397 fn default() -> Self {
398 Self::new()
399 }
400}
401
402impl HessraClient {
403 pub fn builder() -> HessraClientBuilder {
405 HessraClientBuilder::new()
406 }
407
408 pub async fn fetch_public_key(
410 base_url: impl Into<String>,
411 port: Option<u16>,
412 server_ca: impl Into<String>,
413 ) -> Result<String, ApiError> {
414 let base_url = base_url.into();
415 let server_ca = server_ca.into();
416
417 let cert_pem = server_ca.as_bytes();
419 let cert_der = reqwest::Certificate::from_pem(cert_pem)
420 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
421
422 let client = reqwest::ClientBuilder::new()
423 .use_rustls_tls()
424 .add_root_certificate(cert_der)
425 .build()
426 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
427
428 let url = match port {
430 Some(port) => format!("https://{}:{}/public_key", base_url, port),
431 None => format!("https://{}/public_key", base_url),
432 };
433
434 let response = client
436 .get(&url)
437 .send()
438 .await
439 .map_err(ApiError::HttpClient)?;
440
441 if !response.status().is_success() {
442 let status = response.status();
443 let error_text = response.text().await.unwrap_or_default();
444 return Err(ApiError::InvalidResponse(format!(
445 "HTTP error: {} - {}",
446 status, error_text
447 )));
448 }
449
450 let result = response
452 .json::<PublicKeyResponse>()
453 .await
454 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
455
456 Ok(result.public_key)
457 }
458
459 #[cfg(feature = "http3")]
460 pub async fn fetch_public_key_http3(
461 base_url: impl Into<String>,
462 port: Option<u16>,
463 server_ca: impl Into<String>,
464 ) -> Result<String, ApiError> {
465 let base_url = base_url.into();
466 let server_ca = server_ca.into();
467
468 let cert_pem = server_ca.as_bytes();
470 let cert_der = reqwest::Certificate::from_pem(cert_pem)
471 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
472
473 let client = reqwest::ClientBuilder::new()
474 .use_rustls_tls()
475 .add_root_certificate(cert_der)
476 .http3_prior_knowledge()
477 .build()
478 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
479
480 let url = match port {
482 Some(port) => format!("https://{}:{}/public_key", base_url, port),
483 None => format!("https://{}/public_key", base_url),
484 };
485
486 let response = client
488 .get(&url)
489 .version(http::Version::HTTP_3)
490 .send()
491 .await
492 .map_err(ApiError::HttpClient)?;
493
494 if !response.status().is_success() {
495 let status = response.status();
496 let error_text = response.text().await.unwrap_or_default();
497 return Err(ApiError::InvalidResponse(format!(
498 "HTTP error: {} - {}",
499 status, error_text
500 )));
501 }
502
503 let result = response
505 .json::<PublicKeyResponse>()
506 .await
507 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
508
509 Ok(result.public_key)
510 }
511
512 pub async fn request_token(
514 &self,
515 resource: String,
516 operation: String,
517 ) -> Result<String, ApiError> {
518 let request = TokenRequest {
519 resource,
520 operation,
521 };
522
523 let response = match self {
524 HessraClient::Http1(client) => {
525 client
526 .send_request::<_, TokenResponse>("request_token", &request)
527 .await?
528 }
529 #[cfg(feature = "http3")]
530 HessraClient::Http3(client) => {
531 client
532 .send_request::<_, TokenResponse>("request_token", &request)
533 .await?
534 }
535 };
536
537 match response.token {
538 Some(token) => Ok(token),
539 None => Err(ApiError::TokenRequest(format!(
540 "Failed to get token: {}",
541 response.response_msg
542 ))),
543 }
544 }
545
546 pub async fn verify_token(
548 &self,
549 token: String,
550 subject: String,
551 resource: String,
552 operation: String,
553 ) -> Result<String, ApiError> {
554 let request = VerifyTokenRequest {
555 token,
556 subject,
557 resource,
558 operation,
559 };
560
561 let response = match self {
562 HessraClient::Http1(client) => {
563 client
564 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
565 .await?
566 }
567 #[cfg(feature = "http3")]
568 HessraClient::Http3(client) => {
569 client
570 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
571 .await?
572 }
573 };
574
575 Ok(response.response_msg)
576 }
577
578 pub async fn verify_service_chain_token(
580 &self,
581 token: String,
582 subject: String,
583 resource: String,
584 component: Option<String>,
585 ) -> Result<String, ApiError> {
586 let request = VerifyServiceChainTokenRequest {
587 token,
588 subject,
589 resource,
590 component,
591 };
592
593 let response = match self {
594 HessraClient::Http1(client) => {
595 client
596 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
597 .await?
598 }
599 #[cfg(feature = "http3")]
600 HessraClient::Http3(client) => {
601 client
602 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
603 .await?
604 }
605 };
606
607 Ok(response.response_msg)
608 }
609
610 pub async fn get_public_key(&self) -> Result<String, ApiError> {
612 let url_path = "public_key";
613
614 let response = match self {
615 HessraClient::Http1(client) => {
616 let base_url = client.config.get_base_url();
618 let full_url = format!("https://{}/{}", base_url, url_path);
619
620 let response = client
621 .client
622 .get(&full_url)
623 .send()
624 .await
625 .map_err(ApiError::HttpClient)?;
626
627 if !response.status().is_success() {
628 let status = response.status();
629 let error_text = response.text().await.unwrap_or_default();
630 return Err(ApiError::InvalidResponse(format!(
631 "HTTP error: {} - {}",
632 status, error_text
633 )));
634 }
635
636 response.json::<PublicKeyResponse>().await.map_err(|e| {
637 ApiError::InvalidResponse(format!("Failed to parse response: {}", e))
638 })?
639 }
640 #[cfg(feature = "http3")]
641 HessraClient::Http3(client) => {
642 let base_url = client.config.get_base_url();
643 let full_url = format!("https://{}/{}", base_url, url_path);
644
645 let response = client
646 .client
647 .get(&full_url)
648 .version(http::Version::HTTP_3)
649 .send()
650 .await
651 .map_err(ApiError::HttpClient)?;
652
653 if !response.status().is_success() {
654 let status = response.status();
655 let error_text = response.text().await.unwrap_or_default();
656 return Err(ApiError::InvalidResponse(format!(
657 "HTTP error: {} - {}",
658 status, error_text
659 )));
660 }
661
662 response.json::<PublicKeyResponse>().await.map_err(|e| {
663 ApiError::InvalidResponse(format!("Failed to parse response: {}", e))
664 })?
665 }
666 };
667
668 Ok(response.public_key)
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675
676 #[test]
678 fn test_base_config_get_base_url_with_port() {
679 let config = BaseConfig {
680 base_url: "test.hessra.net".to_string(),
681 port: Some(443),
682 mtls_key: "".to_string(),
683 mtls_cert: "".to_string(),
684 server_ca: "".to_string(),
685 public_key: None,
686 personal_keypair: None,
687 };
688
689 assert_eq!(config.get_base_url(), "test.hessra.net:443");
690 }
691
692 #[test]
693 fn test_base_config_get_base_url_without_port() {
694 let config = BaseConfig {
695 base_url: "test.hessra.net".to_string(),
696 port: None,
697 mtls_key: "".to_string(),
698 mtls_cert: "".to_string(),
699 server_ca: "".to_string(),
700 public_key: None,
701 personal_keypair: None,
702 };
703
704 assert_eq!(config.get_base_url(), "test.hessra.net");
705 }
706
707 #[test]
709 fn test_client_builder_methods() {
710 let builder = HessraClientBuilder::new()
711 .base_url("test.hessra.net")
712 .port(443)
713 .protocol(Protocol::Http1)
714 .mtls_cert("CERT")
715 .mtls_key("KEY")
716 .server_ca("CA")
717 .public_key("PUBKEY")
718 .personal_keypair("KEYPAIR");
719
720 assert_eq!(builder.config.base_url, "test.hessra.net");
721 assert_eq!(builder.config.port, Some(443));
722 assert_eq!(builder.config.mtls_cert, "CERT");
723 assert_eq!(builder.config.mtls_key, "KEY");
724 assert_eq!(builder.config.server_ca, "CA");
725 assert_eq!(builder.config.public_key, Some("PUBKEY".to_string()));
726 assert_eq!(builder.config.personal_keypair, Some("KEYPAIR".to_string()));
727 }
728}