hessra_config/lib.rs
1//! # Hessra Config
2//!
3//! Configuration management for Hessra SDK.
4//!
5//! This crate provides structures and utilities for loading and managing
6//! configuration for the Hessra authentication system. It supports loading
7//! configuration from various sources including environment variables,
8//! files, and programmatic configuration.
9//!
10//! ## Features
11//!
12//! - Configuration loading from JSON files
13//! - Configuration loading from environment variables
14//! - Optional TOML file support
15//! - Builder pattern for programmatic configuration
16//! - Validation of configuration parameters
17
18use std::env;
19use std::fs;
20use std::path::Path;
21use std::sync::OnceLock;
22
23use base64::Engine;
24use serde::{Deserialize, Serialize};
25use thiserror::Error;
26
27/// Protocol options for Hessra client communication
28#[derive(Clone, Debug, Serialize, Deserialize)]
29pub enum Protocol {
30 /// HTTP/1.1 protocol (always available)
31 Http1,
32 /// HTTP/3 protocol (only available with the "http3" feature)
33 #[cfg(feature = "http3")]
34 Http3,
35}
36
37fn default_protocol() -> Protocol {
38 Protocol::Http1
39}
40
41/// Configuration for Hessra SDK client
42///
43/// This structure contains all the configuration parameters needed
44/// to create a Hessra client. It can be created manually or loaded
45/// from various sources.
46///
47/// # Examples
48///
49/// ## Creating a configuration manually
50///
51/// ```
52/// use hessra_config::{HessraConfig, Protocol};
53///
54/// let config = HessraConfig::new(
55/// "https://test.hessra.net", // base URL
56/// Some(443), // port (optional)
57/// Protocol::Http1, // protocol
58/// "client-cert-content", // mTLS certificate
59/// "client-key-content", // mTLS key
60/// "ca-cert-content", // Server CA certificate
61/// );
62/// ```
63///
64/// ## Loading from a JSON file
65///
66/// ```no_run
67/// use hessra_config::HessraConfig;
68/// use std::path::Path;
69///
70/// let config = HessraConfig::from_file(Path::new("./config.json"))
71/// .expect("Failed to load configuration");
72/// ```
73///
74/// ## Loading from environment variables
75///
76/// ```no_run
77/// use hessra_config::HessraConfig;
78///
79/// // Assuming the following environment variables are set:
80/// // HESSRA_BASE_URL=https://test.hessra.net
81/// // HESSRA_PORT=443
82/// // HESSRA_MTLS_CERT=<certificate content>
83/// // HESSRA_MTLS_KEY=<key content>
84/// // HESSRA_SERVER_CA=<CA certificate content>
85/// let config = HessraConfig::from_env("HESSRA")
86/// .expect("Failed to load configuration from environment");
87/// ```
88///
89/// ## Using the global configuration
90///
91/// ```no_run
92/// use hessra_config::{HessraConfig, Protocol, set_default_config, get_default_config};
93///
94/// // Set up the global configuration
95/// let config = HessraConfig::new(
96/// "https://test.hessra.net",
97/// Some(443),
98/// Protocol::Http1,
99/// "<certificate content>",
100/// "<key content>",
101/// "<CA certificate content>",
102/// );
103///
104/// // Set as the default configuration
105/// set_default_config(config).expect("Failed to set default configuration");
106///
107/// // Later in your code, get the default configuration
108/// let default_config = get_default_config()
109/// .expect("No default configuration set");
110/// ```
111#[derive(Clone, Debug, Serialize, Deserialize)]
112pub struct HessraConfig {
113 pub base_url: String,
114 pub port: Option<u16>,
115 /// Optional mTLS client certificate in PEM format
116 /// Required for mTLS authentication, optional for identity token authentication
117 #[serde(default)]
118 pub mtls_cert: Option<String>,
119 /// Optional mTLS private key in PEM format
120 /// Required for mTLS authentication, optional for identity token authentication
121 #[serde(default)]
122 pub mtls_key: Option<String>,
123 pub server_ca: String,
124 #[serde(default = "default_protocol")]
125 pub protocol: Protocol,
126 /// The server's public key for token verification
127 /// as a PEM formatted string
128 #[serde(default)]
129 pub public_key: Option<String>,
130 /// The personal keypair for the user as a PEM formatted string
131 ///
132 /// This is used for service chain attestations. When acting as a node in a service chain,
133 /// this keypair is used to sign attestations that this node has processed the request.
134 /// The private key should be kept secret and only the public key should be shared with
135 /// the authorization service.
136 #[serde(default)]
137 pub personal_keypair: Option<String>,
138}
139
140/// Errors that can occur when working with Hessra configuration
141#[derive(Debug, Error)]
142pub enum ConfigError {
143 #[error("Base URL is required but was not provided")]
144 MissingBaseUrl,
145 #[error("Invalid port number. Port must be a valid number between 1-65535")]
146 InvalidPort,
147 #[error("mTLS certificate is required but was not provided")]
148 MissingCertificate,
149 #[error("mTLS key is required but was not provided")]
150 MissingKey,
151 #[error("Server CA certificate is required but was not provided")]
152 MissingServerCA,
153 #[error("Invalid certificate format: {0}")]
154 InvalidCertificate(String),
155 #[error("I/O error occurred while reading configuration: {0}")]
156 IOError(String),
157 #[error("Failed to parse configuration data: {0}")]
158 ParseError(String),
159 #[error("Global configuration has already been initialized")]
160 AlreadyInitialized,
161 #[error("Environment variable error: {0}")]
162 EnvVarError(String),
163}
164
165impl From<std::io::Error> for ConfigError {
166 fn from(error: std::io::Error) -> Self {
167 ConfigError::IOError(error.to_string())
168 }
169}
170
171impl From<serde_json::Error> for ConfigError {
172 fn from(error: serde_json::Error) -> Self {
173 ConfigError::ParseError(error.to_string())
174 }
175}
176
177#[cfg(feature = "toml")]
178impl From<toml::de::Error> for ConfigError {
179 fn from(error: toml::de::Error) -> Self {
180 ConfigError::ParseError(error.to_string())
181 }
182}
183
184impl From<std::env::VarError> for ConfigError {
185 fn from(error: std::env::VarError) -> Self {
186 ConfigError::EnvVarError(error.to_string())
187 }
188}
189
190/// Builder for HessraConfig
191///
192/// This struct provides a more flexible way to construct a HessraConfig object.
193///
194/// # Examples
195///
196/// ```no_run
197/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
198/// use hessra_config::{HessraConfigBuilder, Protocol};
199///
200/// // Create a new config using the builder pattern
201/// let config = HessraConfigBuilder::new()
202/// .base_url("https://test.hessra.net")
203/// .port(443)
204/// .protocol(Protocol::Http1)
205/// .mtls_cert("client-cert-content")
206/// .mtls_key("client-key-content")
207/// .server_ca("ca-cert-content")
208/// .build()?;
209///
210/// # Ok(())
211/// # }
212/// ```
213///
214/// You can also modify an existing configuration:
215///
216/// ```no_run
217/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
218/// # use hessra_config::{HessraConfig, Protocol};
219/// # let config = HessraConfig::new(
220/// # "https://test.hessra.net",
221/// # Some(443),
222/// # Protocol::Http1,
223/// # "CERT",
224/// # "KEY",
225/// # "CA"
226/// # );
227/// // Convert existing config to a builder
228/// let new_config = config.to_builder()
229/// .port(8443) // Change the port
230/// .build()?;
231/// # Ok(())
232/// # }
233/// ```
234#[derive(Default, Debug)]
235pub struct HessraConfigBuilder {
236 base_url: Option<String>,
237 port: Option<u16>,
238 mtls_cert: Option<String>,
239 mtls_key: Option<String>,
240 server_ca: Option<String>,
241 protocol: Option<Protocol>,
242 public_key: Option<String>,
243 personal_keypair: Option<String>,
244}
245
246impl HessraConfigBuilder {
247 /// Create a new HessraConfigBuilder with default values
248 pub fn new() -> Self {
249 Self {
250 base_url: None,
251 port: None,
252 mtls_cert: None,
253 mtls_key: None,
254 server_ca: None,
255 protocol: None,
256 public_key: None,
257 personal_keypair: None,
258 }
259 }
260
261 /// Create a new HessraConfigBuilder from an existing HessraConfig
262 ///
263 /// # Arguments
264 ///
265 /// * `config` - The existing HessraConfig to use as a starting point
266 pub fn from_config(config: &HessraConfig) -> Self {
267 Self {
268 base_url: Some(config.base_url.clone()),
269 port: config.port,
270 mtls_cert: config.mtls_cert.clone(),
271 mtls_key: config.mtls_key.clone(),
272 server_ca: Some(config.server_ca.clone()),
273 protocol: Some(config.protocol.clone()),
274 public_key: config.public_key.clone(),
275 personal_keypair: config.personal_keypair.clone(),
276 }
277 }
278
279 /// Set the base URL for the Hessra service
280 ///
281 /// # Arguments
282 ///
283 /// * `base_url` - The base URL for the Hessra service, e.g. "test.hessra.net"
284 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
285 self.base_url = Some(base_url.into());
286 self
287 }
288
289 /// Set the port for the Hessra service
290 ///
291 /// # Arguments
292 ///
293 /// * `port` - The port to use for the Hessra service, e.g. 443
294 pub fn port(mut self, port: u16) -> Self {
295 self.port = Some(port);
296 self
297 }
298
299 /// Set the mTLS certificate for client authentication
300 ///
301 /// # Arguments
302 ///
303 /// * `cert` - The client certificate in PEM format, e.g. "-----BEGIN CERTIFICATE-----\nENV CERT\n-----END CERTIFICATE-----"
304 pub fn mtls_cert(mut self, cert: impl Into<String>) -> Self {
305 self.mtls_cert = Some(cert.into());
306 self
307 }
308
309 /// Set the mTLS private key for client authentication
310 ///
311 /// # Arguments
312 ///
313 /// * `key` - The client private key in PEM format, e.g. "-----BEGIN PRIVATE KEY-----\nENV KEY\n-----END PRIVATE KEY-----"
314 pub fn mtls_key(mut self, key: impl Into<String>) -> Self {
315 self.mtls_key = Some(key.into());
316 self
317 }
318
319 /// Set the server CA certificate for server validation
320 ///
321 /// # Arguments
322 ///
323 /// * `ca` - The server CA certificate in PEM format, e.g. "-----BEGIN CERTIFICATE-----\nENV CA\n-----END CERTIFICATE-----"
324 pub fn server_ca(mut self, ca: impl Into<String>) -> Self {
325 self.server_ca = Some(ca.into());
326 self
327 }
328
329 /// Set the protocol to use for the Hessra service
330 ///
331 /// # Arguments
332 ///
333 /// * `protocol` - The protocol to use (HTTP/1.1 or HTTP/3)
334 pub fn protocol(mut self, protocol: Protocol) -> Self {
335 self.protocol = Some(protocol);
336 self
337 }
338
339 /// Set the server's public key for token verification
340 ///
341 /// # Arguments
342 ///
343 /// * `public_key` - The server's public key in PEM format
344 pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
345 self.public_key = Some(public_key.into());
346 self
347 }
348
349 /// Set the ed25519 or P-256 personal keypair if this is a service node in a service chain
350 ///
351 /// # Arguments
352 ///
353 /// * `personal_keypair` - The personal keypair in PEM format, e.g. "-----BEGIN PRIVATE KEY-----\nENV KEY\n-----END PRIVATE KEY-----"
354 /// Note: Only ed25519 or P-256 keypairs are supported.
355 pub fn personal_keypair(mut self, personal_keypair: impl Into<String>) -> Self {
356 self.personal_keypair = Some(personal_keypair.into());
357 self
358 }
359
360 /// Build a HessraConfig from this builder
361 ///
362 /// # Returns
363 ///
364 /// A Result containing either the built HessraConfig or a ConfigError
365 pub fn build(self) -> Result<HessraConfig, ConfigError> {
366 let base_url = self.base_url.ok_or(ConfigError::MissingBaseUrl)?;
367 let server_ca = self.server_ca.ok_or(ConfigError::MissingServerCA)?;
368
369 // mTLS is now optional - both cert and key must be provided together if any
370 match (&self.mtls_cert, &self.mtls_key) {
371 (Some(_), None) => return Err(ConfigError::MissingKey),
372 (None, Some(_)) => return Err(ConfigError::MissingCertificate),
373 _ => {}
374 }
375
376 let config = HessraConfig {
377 base_url,
378 port: self.port,
379 protocol: self.protocol.unwrap_or_else(default_protocol),
380 mtls_cert: self.mtls_cert,
381 mtls_key: self.mtls_key,
382 server_ca,
383 public_key: self.public_key,
384 personal_keypair: self.personal_keypair,
385 };
386
387 config.validate()?;
388 Ok(config)
389 }
390}
391
392// Global configuration instance
393static GLOBAL_CONFIG: OnceLock<HessraConfig> = OnceLock::new();
394
395impl HessraConfig {
396 /// Create a new HessraConfig with the given parameters
397 ///
398 /// # Arguments
399 ///
400 /// * `base_url` - The base URL for the Hessra service, e.g. "test.hessra.net"
401 /// * `port` - Optional port to use for the Hessra service, e.g. 443
402 /// * `protocol` - The protocol to use (HTTP/1.1 or HTTP/3)
403 /// * `mtls_cert` - The client certificate in PEM format, e.g. "-----BEGIN CERTIFICATE-----\nENV CERT\n-----END CERTIFICATE-----"
404 /// * `mtls_key` - The client private key in PEM format, e.g. "-----BEGIN PRIVATE KEY-----\nENV KEY\n-----END PRIVATE KEY-----"
405 /// * `server_ca` - The server CA certificate in PEM format, e.g. "-----BEGIN CERTIFICATE-----\nENV CA\n-----END CERTIFICATE-----"
406 pub fn new(
407 base_url: impl Into<String>,
408 port: Option<u16>,
409 protocol: Protocol,
410 mtls_cert: impl Into<String>,
411 mtls_key: impl Into<String>,
412 server_ca: impl Into<String>,
413 ) -> Self {
414 Self {
415 base_url: base_url.into(),
416 port,
417 protocol,
418 mtls_cert: Some(mtls_cert.into()),
419 mtls_key: Some(mtls_key.into()),
420 server_ca: server_ca.into(),
421 public_key: None,
422 personal_keypair: None,
423 }
424 }
425
426 /// Create a new HessraConfig for TLS-only connections (no mTLS)
427 ///
428 /// This is useful when using identity tokens for authentication
429 ///
430 /// # Arguments
431 ///
432 /// * `base_url` - The base URL for the Hessra service, e.g. "test.hessra.net"
433 /// * `port` - The port to use for the Hessra service, e.g. 443
434 /// * `protocol` - The protocol to use, Http1 or Http3 (with the "http3" feature)
435 /// * `server_ca` - The CA certificate for verifying the server
436 pub fn new_tls_only(
437 base_url: impl Into<String>,
438 port: Option<u16>,
439 protocol: Protocol,
440 server_ca: impl Into<String>,
441 ) -> Self {
442 Self {
443 base_url: base_url.into(),
444 port,
445 protocol,
446 mtls_cert: None,
447 mtls_key: None,
448 server_ca: server_ca.into(),
449 public_key: None,
450 personal_keypair: None,
451 }
452 }
453
454 /// Create a new HessraConfigBuilder
455 pub fn builder() -> HessraConfigBuilder {
456 HessraConfigBuilder::new()
457 }
458
459 /// Convert this config to a builder for modification
460 pub fn to_builder(&self) -> HessraConfigBuilder {
461 HessraConfigBuilder::from_config(self)
462 }
463
464 /// Load configuration from a JSON file
465 ///
466 /// # Arguments
467 ///
468 /// * `path` - Path to the JSON configuration file
469 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
470 let content = fs::read_to_string(path)?;
471 let config: HessraConfig = serde_json::from_str(&content)?;
472 config.validate()?;
473 Ok(config)
474 }
475
476 /// Load configuration from a TOML file
477 ///
478 /// # Arguments
479 ///
480 /// * `path` - Path to the TOML configuration file
481 #[cfg(feature = "toml")]
482 pub fn from_toml(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
483 let content = fs::read_to_string(path)?;
484 let config: HessraConfig = toml::from_str(&content)?;
485 config.validate()?;
486 Ok(config)
487 }
488
489 /// Load configuration from environment variables
490 ///
491 /// # Arguments
492 ///
493 /// * `prefix` - Prefix for environment variables (e.g., "HESSRA")
494 ///
495 /// # Environment Variables
496 ///
497 /// The following environment variables are used (with the given prefix):
498 ///
499 /// * `{PREFIX}_BASE_URL` - Base URL for the Hessra service
500 /// * `{PREFIX}_PORT` - Port for the Hessra service (optional)
501 /// * `{PREFIX}_PROTOCOL` - Protocol to use ("http1" or "http3")
502 /// * `{PREFIX}_MTLS_CERT` - Base64-encoded client certificate in PEM format
503 /// * `{PREFIX}_MTLS_KEY` - Base64-encoded client private key in PEM format
504 /// * `{PREFIX}_SERVER_CA` - Base64-encoded server CA certificate in PEM format
505 /// * `{PREFIX}_PUBLIC_KEY` - Base64-encoded server's public key for token verification (optional)
506 /// * `{PREFIX}_PERSONAL_KEYPAIR` - Base64-encoded personal keypair in PEM format (optional)
507 ///
508 /// The certificate and key values can also be loaded from files by using:
509 ///
510 /// * `{PREFIX}_MTLS_CERT_FILE` - Path to client certificate file
511 /// * `{PREFIX}_MTLS_KEY_FILE` - Path to client private key file
512 /// * `{PREFIX}_SERVER_CA_FILE` - Path to server CA certificate file
513 /// * `{PREFIX}_PUBLIC_KEY_FILE` - Path to server's public key file
514 /// * `{PREFIX}_PERSONAL_KEYPAIR_FILE` - Path to personal keypair file
515 ///
516 /// Note: When using environment variables for certificates and keys, the PEM content should be
517 /// base64-encoded to avoid issues with newlines and special characters in environment variables.
518 pub fn from_env(prefix: &str) -> Result<Self, ConfigError> {
519 let mut builder = HessraConfigBuilder::new();
520
521 // Base URL is required
522 if let Ok(base_url) = env::var(format!("{prefix}_BASE_URL")) {
523 builder = builder.base_url(base_url);
524 }
525
526 // Port is optional
527 if let Ok(port_str) = env::var(format!("{prefix}_PORT")) {
528 if let Ok(port) = port_str.parse::<u16>() {
529 builder = builder.port(port);
530 } else {
531 return Err(ConfigError::InvalidPort);
532 }
533 }
534
535 // Protocol is optional (defaults to HTTP/1.1)
536 if let Ok(protocol_str) = env::var(format!("{prefix}_PROTOCOL")) {
537 let protocol = match protocol_str.to_lowercase().as_str() {
538 "http1" => Protocol::Http1,
539 #[cfg(feature = "http3")]
540 "http3" => Protocol::Http3,
541 _ => {
542 return Err(ConfigError::ParseError(format!(
543 "Invalid protocol: {protocol_str}"
544 )))
545 }
546 };
547 builder = builder.protocol(protocol);
548 }
549
550 // Helper function to decode base64 and validate PEM format
551 fn decode_base64_pem(value: &str) -> Result<String, ConfigError> {
552 base64::engine::general_purpose::STANDARD
553 .decode(value)
554 .map_err(|e| ConfigError::ParseError(format!("Invalid base64 encoding: {e}")))
555 .and_then(|decoded| {
556 String::from_utf8(decoded)
557 .map_err(|e| ConfigError::ParseError(format!("Invalid UTF-8: {e}")))
558 })
559 }
560
561 // Client certificate (either direct or from file)
562 if let Ok(mtls_cert) = env::var(format!("{prefix}_MTLS_CERT")) {
563 let decoded_cert = decode_base64_pem(&mtls_cert)?;
564 builder = builder.mtls_cert(decoded_cert);
565 } else if let Ok(mtls_cert_file) = env::var(format!("{prefix}_MTLS_CERT_FILE")) {
566 let cert_content = fs::read_to_string(mtls_cert_file)?;
567 builder = builder.mtls_cert(cert_content);
568 }
569
570 // Client key (either direct or from file)
571 if let Ok(mtls_key) = env::var(format!("{prefix}_MTLS_KEY")) {
572 let decoded_key = decode_base64_pem(&mtls_key)?;
573 builder = builder.mtls_key(decoded_key);
574 } else if let Ok(mtls_key_file) = env::var(format!("{prefix}_MTLS_KEY_FILE")) {
575 let key_content = fs::read_to_string(mtls_key_file)?;
576 builder = builder.mtls_key(key_content);
577 }
578
579 // Server CA certificate (either direct or from file)
580 if let Ok(server_ca) = env::var(format!("{prefix}_SERVER_CA")) {
581 let decoded_ca = decode_base64_pem(&server_ca)?;
582 builder = builder.server_ca(decoded_ca);
583 } else if let Ok(server_ca_file) = env::var(format!("{prefix}_SERVER_CA_FILE")) {
584 let ca_content = fs::read_to_string(server_ca_file)?;
585 builder = builder.server_ca(ca_content);
586 }
587
588 // Public key (optional, either direct or from file)
589 if let Ok(public_key) = env::var(format!("{prefix}_PUBLIC_KEY")) {
590 let decoded_key = decode_base64_pem(&public_key)?;
591 builder = builder.public_key(decoded_key);
592 } else if let Ok(public_key_file) = env::var(format!("{prefix}_PUBLIC_KEY_FILE")) {
593 let key_content = fs::read_to_string(public_key_file)?;
594 builder = builder.public_key(key_content);
595 }
596
597 // Personal keypair (optional, either direct or from file)
598 if let Ok(personal_keypair) = env::var(format!("{prefix}_PERSONAL_KEYPAIR")) {
599 let decoded_keypair = decode_base64_pem(&personal_keypair)?;
600 builder = builder.personal_keypair(decoded_keypair);
601 } else if let Ok(personal_keypair_file) =
602 env::var(format!("{prefix}_PERSONAL_KEYPAIR_FILE"))
603 {
604 let keypair_content = fs::read_to_string(personal_keypair_file)?;
605 builder = builder.personal_keypair(keypair_content);
606 }
607
608 builder.build()
609 }
610
611 /// Load configuration from environment variables or fall back to a config file
612 ///
613 /// This method will first attempt to load the configuration from environment variables.
614 /// If that fails, it will look for a configuration file in the following locations:
615 ///
616 /// 1. Path specified in the `{PREFIX}_CONFIG_FILE` environment variable
617 /// 2. `./hessra.json` in the current directory
618 /// 3. `~/.config/hessra/config.json` in the user's home directory
619 ///
620 /// # Arguments
621 ///
622 /// * `prefix` - Prefix for environment variables (e.g., "HESSRA")
623 pub fn from_env_or_file(prefix: &str) -> Result<Self, ConfigError> {
624 // First try to load from environment variables
625 match Self::from_env(prefix) {
626 Ok(config) => return Ok(config),
627 Err(e) => {
628 if !matches!(
629 e,
630 ConfigError::MissingBaseUrl
631 | ConfigError::MissingCertificate
632 | ConfigError::MissingKey
633 | ConfigError::MissingServerCA
634 ) {
635 return Err(e);
636 }
637 // If the error is just missing required fields, try loading from file
638 }
639 }
640
641 // Check if a config file path is specified in the environment
642 if let Ok(config_file) = env::var(format!("{prefix}_CONFIG_FILE")) {
643 return Self::load_from_file(&config_file);
644 }
645
646 // Try current directory
647 if let Ok(config) = Self::load_from_file("./hessra.json") {
648 return Ok(config);
649 }
650
651 // Try user's config directory
652 if let Some(mut config_dir) = dirs::config_dir() {
653 config_dir.push("hessra");
654 config_dir.push("config.json");
655 if let Ok(config) = Self::load_from_file(config_dir) {
656 return Ok(config);
657 }
658 }
659
660 // Couldn't find a valid configuration
661 Err(ConfigError::MissingBaseUrl)
662 }
663
664 // Helper method to load from either JSON or TOML file based on extension
665 fn load_from_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
666 let path = path.as_ref();
667
668 if let Some(ext) = path.extension() {
669 match ext.to_str() {
670 Some("json") => Self::from_file(path),
671 #[cfg(feature = "toml")]
672 Some("toml") | Some("tml") => Self::from_toml(path),
673 _ => Self::from_file(path), // Default to JSON if extension is unknown
674 }
675 } else {
676 // No extension, try as JSON
677 Self::from_file(path)
678 }
679 }
680
681 /// Validate the current configuration
682 ///
683 /// Checks that all required fields are present and have valid values.
684 pub fn validate(&self) -> Result<(), ConfigError> {
685 // Check required fields
686 if self.base_url.is_empty() {
687 return Err(ConfigError::MissingBaseUrl);
688 }
689
690 // Validate mTLS certificates if provided
691 // Both cert and key must be provided together
692 match (&self.mtls_cert, &self.mtls_key) {
693 (Some(cert), Some(key)) => {
694 if cert.is_empty() {
695 return Err(ConfigError::MissingCertificate);
696 }
697 if key.is_empty() {
698 return Err(ConfigError::MissingKey);
699 }
700 }
701 (Some(_), None) => return Err(ConfigError::MissingKey),
702 (None, Some(_)) => return Err(ConfigError::MissingCertificate),
703 (None, None) => {} // TLS-only mode, no mTLS required
704 }
705
706 if self.server_ca.is_empty() {
707 return Err(ConfigError::MissingServerCA);
708 }
709
710 // Validate PEM formats if mTLS is configured
711 if let Some(cert) = &self.mtls_cert {
712 if !cert.contains("-----BEGIN CERTIFICATE-----") {
713 return Err(ConfigError::InvalidCertificate(
714 "Client certificate does not appear to be in PEM format".into(),
715 ));
716 }
717 }
718
719 if let Some(key) = &self.mtls_key {
720 if !key.contains("-----BEGIN") {
721 return Err(ConfigError::InvalidCertificate(
722 "Client key does not appear to be in PEM format".into(),
723 ));
724 }
725 }
726
727 // Validate PEM CA certificate format (basic check)
728 if !self.server_ca.contains("-----BEGIN CERTIFICATE-----") {
729 return Err(ConfigError::InvalidCertificate(
730 "Server CA certificate does not appear to be in PEM format".into(),
731 ));
732 }
733
734 // If a public key is provided, validate its format
735 if let Some(public_key) = &self.public_key {
736 if !public_key.contains("-----BEGIN PUBLIC KEY-----") {
737 return Err(ConfigError::InvalidCertificate(
738 "Server public key does not appear to be in PEM format".into(),
739 ));
740 }
741 }
742
743 // If a personal keypair is provided, validate its format
744 if let Some(keypair) = &self.personal_keypair {
745 if !keypair.contains("-----BEGIN") {
746 return Err(ConfigError::InvalidCertificate(
747 "Personal keypair does not appear to be in PEM format".into(),
748 ));
749 }
750 }
751
752 Ok(())
753 }
754}
755
756/// Set the global default configuration
757///
758/// This function sets a global configuration that can be used across the application.
759/// It can only be set once; attempting to set it again will result in an error.
760///
761/// # Arguments
762///
763/// * `config` - The configuration to set as the global default
764pub fn set_default_config(config: HessraConfig) -> Result<(), ConfigError> {
765 if GLOBAL_CONFIG.set(config).is_err() {
766 return Err(ConfigError::AlreadyInitialized);
767 }
768 Ok(())
769}
770
771/// Get the global default configuration, if set
772///
773/// # Returns
774///
775/// An Option containing a reference to the global configuration, or None if not set
776pub fn get_default_config() -> Option<&'static HessraConfig> {
777 GLOBAL_CONFIG.get()
778}
779
780/// Try to load a default configuration from environment or files
781///
782/// This function attempts to load a configuration from the environment or from
783/// standard configuration file locations. It does not set the global configuration.
784///
785/// # Returns
786///
787/// An Option containing the loaded configuration, or None if no valid configuration could be found
788pub fn try_load_default_config() -> Option<HessraConfig> {
789 // Try to load from HESSRA_ environment variables or standard file locations
790 HessraConfig::from_env_or_file("HESSRA").ok()
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796 use std::io::Write;
797 use tempfile::NamedTempFile;
798
799 const VALID_CERT: &str = r#"-----BEGIN CERTIFICATE-----
800MIICZjCCAc+gAwIBAgIUJlq+zz4mN3zoNfbMkKqLQ9BS79UwDQYJKoZIhvcNAQEL
801BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
802GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA0MTUwMDAwMDBaFw0yNDA0
803MTUwMDAwMDBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
804HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwgZ8wDQYJKoZIhvcNAQEB
805BQADgY0AMIGJAoGBAONH4+1QZPmY3zWP/Yjt5UJeuR0IGF5q8TYHTGw2kzbTPLTa
806XMo/JohB/duKFRYvZEbGlmK0xQtLrLhBF8MUoN+kUxG9UkbQHk5xNL0eLmDOy4bm
807OLtIfCIoQZyKMJFIRAgLcNv6Z9q1l+mfBCz9ZIPzVZRyCv/YsHEJUkJfrfg9AgMB
808AAGjUzBRMB0GA1UdDgQWBBQCQ7Ui9CeMRzZzLeTHzYJbPT9rkjAfBgNVHSMEGDAW
809gBQCQ7Ui9CeMRzZzLeTHzYJbPT9rkjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
810DQEBCwUAA4GBADbM7a5bQjQK7JKaFaXqiueiv4qM7fhZ1O3icLzLYBzrO8vGRQo9
811FM9zgPOiqpjzLGfDhbUJvN3hbjPZmzJzyyRM9XHdKKwYH/ErY6vRbciuO7qbD6Hx
812CKZ0ORbMdmc0TRF6+5s6p3bhDvZ2ZpUVsXzMz5ZxMnpQMpfh3AbEV2Yw
813-----END CERTIFICATE-----"#;
814
815 const VALID_KEY: &str = r#"-----BEGIN PRIVATE KEY-----
816MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAONH4+1QZPmY3zWP
817/Yjt5UJeuR0IGF5q8TYHTGw2kzbTPLTaXMo/JohB/duKFRYvZEbGlmK0xQtLrLhB
818F8MUoN+kUxG9UkbQHk5xNL0eLmDOy4bmOLtIfCIoQZyKMJFIRAgLcNv6Z9q1l+mf
819BCz9ZIPzVZRyCv/YsHEJUkJfrfg9AgMBAAECgYEAw5tgq6t1QRUDNaZsNQ4QYkgI
820CjVekg0XMR/WK6NmmKUkOI2aTaA+CwU0ZYERLvGZMLOVPHJKQZdLLbsl8CvhDtT1
821HXpxKR1EJ8vuCPlfZ3LVdUVQeV3QcUpBQGvzWGHl2R3LM/RrW4cS4eP4SMNdVVF4
822jdmGpMDvPm/0VoUtRwECQQD/EY/RTGlU9oXnSwEUfK9Gg0OBXDVEPGrNJ4JAoMHn
823MJQywP0IZxHfr8A9uk9U8L+5LCFgXcFZ8fgYOFvrElxBAkEA5Ca+Tq5k4IEpyW+Q
824wPrKu77SmAKiT3JlIslGUO0OXUHYqZbRHWCAQTZWZuaOJG8I82I5EWyLrVaJbA4X
825OgbebQJABHBmZA3TF1zRci73OWUc7pK2K8PSx38tPPAIg5dP5y8SGpiKpf+8HijD
826EjKvY+0K1Py1Q7nHU4GbqE9juOS0AQJAFDIYzaGuJwdNZRIAS2h5uqZmIpKieLfc
8275c3JVVzkBFXfKQME6KsAdIrlpwCmzU5vUEQzGWNXCes2uBGp2XpXxQJAGvf5IVWF
828+ZkVB5GKbj0DGOw3rH7QYhbJVAeCJbzBqI+euvtVK4xrDdWZsK8IGy6NCxMA//Qf
829Tz0nftszeCrCGw==
830-----END PRIVATE KEY-----"#;
831
832 #[test]
833 fn test_config_builder() {
834 let config = HessraConfigBuilder::new()
835 .base_url("https://test.hessra.net")
836 .port(443)
837 .protocol(Protocol::Http1)
838 .mtls_cert(VALID_CERT)
839 .mtls_key(VALID_KEY)
840 .server_ca(VALID_CERT)
841 .build()
842 .unwrap();
843
844 assert_eq!(config.base_url, "https://test.hessra.net");
845 assert_eq!(config.port, Some(443));
846 assert!(matches!(config.protocol, Protocol::Http1));
847 }
848
849 #[test]
850 fn test_config_validation() {
851 // Missing base URL
852 let result = HessraConfigBuilder::new()
853 .mtls_cert(VALID_CERT)
854 .mtls_key(VALID_KEY)
855 .server_ca(VALID_CERT)
856 .build();
857 assert!(matches!(result, Err(ConfigError::MissingBaseUrl)));
858
859 // Missing certificate
860 let result = HessraConfigBuilder::new()
861 .base_url("https://test.hessra.net")
862 .mtls_key(VALID_KEY)
863 .server_ca(VALID_CERT)
864 .build();
865 assert!(matches!(result, Err(ConfigError::MissingCertificate)));
866
867 // Invalid certificate format
868 let result = HessraConfigBuilder::new()
869 .base_url("https://test.hessra.net")
870 .mtls_cert("not-a-cert")
871 .mtls_key(VALID_KEY)
872 .server_ca(VALID_CERT)
873 .build();
874 assert!(matches!(result, Err(ConfigError::InvalidCertificate(_))));
875 }
876
877 #[test]
878 fn test_load_from_json() {
879 let config_json = r#"{
880 "base_url": "https://test.hessra.net",
881 "port": 443,
882 "mtls_cert": "-----BEGIN CERTIFICATE-----\nMIICZjCCAc+gAwIBAgIUJlq+zz4mN3zoNfbMkKqLQ9BS79UwDQYJKoZIhvcNAQEL\n-----END CERTIFICATE-----",
883 "mtls_key": "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAONH4+1QZPmY3zWP\n-----END PRIVATE KEY-----",
884 "server_ca": "-----BEGIN CERTIFICATE-----\nMIICZjCCAc+gAwIBAgIUJlq+zz4mN3zoNfbMkKqLQ9BS79UwDQYJKoZIhvcNAQEL\n-----END CERTIFICATE-----"
885 }"#;
886
887 let mut temp_file = NamedTempFile::new().unwrap();
888 temp_file.write_all(config_json.as_bytes()).unwrap();
889
890 let config = HessraConfig::from_file(temp_file.path()).unwrap();
891 assert_eq!(config.base_url, "https://test.hessra.net");
892 assert_eq!(config.port, Some(443));
893 }
894
895 #[test]
896 fn test_from_env() {
897 // This test would set environment variables, but that's not ideal for unit tests
898 // Instead, we'll just test the builder conversion
899
900 let original = HessraConfig::new(
901 "https://test.hessra.net",
902 Some(443),
903 Protocol::Http1,
904 VALID_CERT,
905 VALID_KEY,
906 VALID_CERT,
907 );
908
909 let builder = original.to_builder();
910 let new_config = builder.port(8443).build().unwrap();
911
912 assert_eq!(new_config.base_url, original.base_url);
913 assert_eq!(new_config.port, Some(8443));
914 }
915
916 #[test]
917 fn test_global_config() {
918 // Test setting and getting global config
919 // Note: this test is not ideal since it modifies global state
920 // In a real test suite, you'd want to run these in isolation
921
922 let config = HessraConfig::new(
923 "https://test.hessra.net",
924 Some(443),
925 Protocol::Http1,
926 VALID_CERT,
927 VALID_KEY,
928 VALID_CERT,
929 );
930
931 // First time should succeed
932 assert!(set_default_config(config.clone()).is_ok());
933
934 // Second time should fail
935 assert!(matches!(
936 set_default_config(config.clone()),
937 Err(ConfigError::AlreadyInitialized)
938 ));
939
940 // Getting should return our config
941 let global = get_default_config().unwrap();
942 assert_eq!(global.base_url, "https://test.hessra.net");
943 }
944}