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}