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