axum_conf/
utils.rs

1//!
2//! Utility types and functions for common patterns in the service.
3//!
4//! This module provides:
5//! - [`Sensitive`] - A wrapper type for sensitive data that hides values in debug output
6//! - [`RequestIdGenerator`] - Generates or preserves request IDs for distributed tracing
7//! - [`replace_handlebars_with_env`] - Template substitution for environment variables
8//! - [`ApiVersion`] - API version extraction and management for versioned APIs
9//!
10
11use {
12    http::{HeaderValue, Request},
13    regex::{Captures, Regex},
14    serde::Deserialize,
15    std::{env, sync::LazyLock},
16    tower_http::request_id::{MakeRequestId, RequestId},
17    uuid::{ContextV7, Timestamp, Uuid},
18    zeroize::{Zeroize, ZeroizeOnDrop},
19};
20
21/// Regular expression pattern for matching handlebars-style environment variable references.
22/// Matches patterns like `{{ VAR_NAME }}` with optional whitespace around the variable name.
23/// Variable names must be uppercase letters, digits, or underscores (standard env var naming).
24static HANDLEBAR_REGEXP: LazyLock<Regex> =
25    LazyLock::new(|| Regex::new(r"\{\{\s*([A-Z0-9_]+)\s*\}\}").unwrap());
26
27/// A wrapper type for sensitive data that obscures the value in debug output
28/// and securely zeros memory when dropped.
29///
30/// This type is useful for wrapping secrets, passwords, API keys, and other
31/// sensitive information that should not be accidentally exposed in logs,
32/// error messages, or debug output.
33///
34/// The inner value remains accessible through the public field `0`, but when
35/// formatted using `Debug`, it displays as `Sensitive(****)` instead of the
36/// actual value.
37///
38/// # Type Parameters
39///
40/// - `T`: The type of the sensitive value, which must implement `Default`
41///
42/// # Examples
43///
44/// ```
45/// use axum_conf::Sensitive;
46///
47/// let api_key = Sensitive::from("secret-key-12345");
48/// println!("{:?}", api_key);  // Prints: Sensitive(****)
49///
50/// // Access the actual value when needed
51/// let key_value: &str = &api_key.0;
52/// ```
53///
54/// # Security Features
55///
56/// - **Debug hiding**: Debug output shows `Sensitive(****)` instead of the value
57/// - **Memory zeroing**: When `Sensitive<String>` is dropped, the memory is securely
58///   overwritten with zeros to prevent secrets from lingering in memory
59///
60/// # Security Limitations
61///
62/// This type does NOT:
63/// - Prevent the value from being read if you have access to the `Sensitive` instance
64/// - Encrypt or secure the value in memory while in use
65/// - Prevent the value from being serialized if using `Serialize`
66/// - Prevent the compiler from copying the value (use with care in generic contexts)
67///
68/// For true security, combine with other security measures like secure memory handling.
69///
70/// # Derive Macros
71///
72/// Uses `ZeroizeOnDrop` from the `zeroize` crate to automatically zero memory when dropped.
73#[derive(Clone, Deserialize, Default, Zeroize, ZeroizeOnDrop)]
74pub struct Sensitive<T: Default + Zeroize>(pub T);
75
76impl Sensitive<String> {
77    /// Creates a new `Sensitive<String>` from a string slice.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use axum_conf::Sensitive;
83    ///
84    /// let password = Sensitive::from("my-secret-password");
85    /// ```
86    pub fn from(s: &str) -> Self {
87        Self(s.to_string())
88    }
89}
90
91impl<T: Default + Zeroize + PartialEq> PartialEq for Sensitive<T> {
92    fn eq(&self, other: &Self) -> bool {
93        self.0 == other.0
94    }
95}
96
97impl<T: Default + Zeroize> std::fmt::Debug for Sensitive<T> {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        write!(f, "Sensitive(****)")
100    }
101}
102
103/// Request ID generator for distributed tracing and request correlation.
104///
105/// This generator implements the `MakeRequestId` trait from `tower-http` to either:
106/// 1. Preserve an existing `x-request-id` header from the incoming request, or
107/// 2. Generate a new UUIDv7 if no request ID is present
108///
109/// Using UUIDv7 provides several benefits:
110/// - Time-ordered: IDs are sortable by creation time
111/// - Unique: Collision-resistant across distributed systems
112/// - Traceable: Can correlate requests across multiple services
113///
114/// # Request ID Flow
115///
116/// ```text
117/// Client Request
118///     │
119///     ├─ Has x-request-id header? ─> Preserve it
120///     │
121///     └─ No header? ─> Generate new UUIDv7
122/// ```
123///
124/// # Examples
125///
126/// ```
127/// use axum_conf::RequestIdGenerator;
128/// use tower_http::request_id::SetRequestIdLayer;
129///
130/// // Add to your Axum router
131/// let layer = SetRequestIdLayer::x_request_id(RequestIdGenerator);
132/// ```
133///
134/// # Use Cases
135///
136/// - **Distributed Tracing**: Track a request across multiple microservices
137/// - **Debugging**: Correlate logs from different components of a request
138/// - **Auditing**: Track the lifecycle of a request for compliance
139/// - **Monitoring**: Measure end-to-end request latency
140#[derive(Debug, Clone, Copy)]
141pub struct RequestIdGenerator;
142
143impl MakeRequestId for RequestIdGenerator {
144    /// Generates or extracts a request ID from an HTTP request.
145    ///
146    /// If the request already has an `x-request-id` header, that value is preserved.
147    /// Otherwise, a new UUIDv7 is generated with high-precision timestamp context.
148    ///
149    /// # Arguments
150    ///
151    /// * `req` - The HTTP request to process
152    ///
153    /// # Returns
154    ///
155    /// An `Option<RequestId>` containing either the existing or newly generated ID.
156    /// Returns `None` only if UUID generation or header value creation fails
157    /// (which is extremely rare in practice).
158    fn make_request_id<B>(&mut self, req: &Request<B>) -> Option<RequestId> {
159        match req.headers().get("x-request-id") {
160            Some(value) => Some(RequestId::new(value.clone())),
161            None => {
162                let cx = ContextV7::new().with_additional_precision();
163                let uuid = Uuid::new_v7(Timestamp::now(cx));
164                let value = HeaderValue::from_str(&uuid.to_string()).ok()?;
165                Some(RequestId::new(value))
166            }
167        }
168    }
169}
170
171/// Replaces handlebars-style placeholders with environment variable values.
172///
173/// Searches through the input string for patterns like `{{ VAR_NAME }}` and replaces
174/// them with the corresponding environment variable value. Variable names are
175/// case-sensitive and must consist of uppercase letters, digits, or underscores.
176///
177/// Whitespace around the variable name is allowed: `{{VAR}}`, `{{ VAR }}`, and
178/// `{{  VAR  }}` are all valid and equivalent.
179///
180/// # Arguments
181///
182/// * `input` - A string slice containing the template text with placeholders
183///
184/// # Returns
185///
186/// A new `String` with all placeholders replaced by their environment variable values.
187/// If an environment variable is not set, it is replaced with an empty string.
188///
189/// # Examples
190///
191/// ```
192/// use axum_conf::replace_handlebars_with_env;
193///
194/// // Assume HOME environment variable exists (standard on Unix systems)
195/// let template = "Path: {{ HOME }}/config";
196/// let result = replace_handlebars_with_env(template);
197/// // Result will be something like "Path: /home/user/config"
198/// assert!(result.starts_with("Path: "));
199///
200/// // Missing variables become empty strings
201/// let template = "Value: {{ MISSING_VAR }}";
202/// let result = replace_handlebars_with_env(template);
203/// assert_eq!(result, "Value: ");
204/// ```
205///
206/// # Use Cases
207///
208/// This function is primarily used for:
209/// - **Configuration files**: Keep sensitive values out of TOML files
210/// - **Connection strings**: Inject credentials from environment
211/// - **Dynamic configuration**: Support different values per environment
212///
213/// # Pattern Details
214///
215/// The function uses a regular expression that matches:
216/// - Opening braces: `{{`
217/// - Optional whitespace: `\s*`
218/// - Variable name: `[A-Z0-9_]+` (uppercase alphanumeric and underscores)
219/// - Optional whitespace: `\s*`
220/// - Closing braces: `}}`
221///
222/// # Security Considerations
223///
224/// - Environment variables are NOT encrypted in memory
225/// - Substituted values appear in the returned string in plain text
226/// - Consider using [`Sensitive`] wrapper for secrets after substitution
227/// - Be cautious when logging or displaying the result
228pub fn replace_handlebars_with_env(input: &str) -> String {
229    HANDLEBAR_REGEXP
230        .replace_all(input, |caps: &Captures| {
231            let var_name = &caps[1];
232            env::var(var_name).unwrap_or_else(|_| {
233                tracing::warn!(
234                    variable = %var_name,
235                    "Environment variable not found, substituting with empty string"
236                );
237                String::new()
238            })
239        })
240        .to_string()
241}
242
243/// API version extracted from request headers or path.
244///
245/// This type is used to track which version of the API a request is targeting.
246/// It can be inserted into request extensions by versioning middleware and
247/// extracted in handlers for version-specific logic.
248///
249/// # Examples
250///
251/// ```
252/// use axum_conf::ApiVersion;
253///
254/// let version = ApiVersion::new(2);
255/// assert_eq!(version.as_u32(), 2);
256/// assert_eq!(version.to_string(), "v2");
257/// ```
258#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
259pub struct ApiVersion(u32);
260
261impl ApiVersion {
262    /// Creates a new API version
263    pub fn new(version: u32) -> Self {
264        Self(version)
265    }
266
267    /// Returns the version number as u32
268    #[allow(unused)]
269    pub fn as_u32(&self) -> u32 {
270        self.0
271    }
272
273    /// Extracts API version from request path.
274    ///
275    /// Looks for patterns like `/v1/`, `/v2/`, `/api/v1/`, etc.
276    pub fn from_path(path: &str) -> Option<Self> {
277        static VERSION_PATH_REGEX: LazyLock<Regex> =
278            LazyLock::new(|| Regex::new(r"/v(\d+)(?:/|$)").unwrap());
279
280        VERSION_PATH_REGEX
281            .captures(path)
282            .and_then(|caps| caps.get(1))
283            .and_then(|m| m.as_str().parse::<u32>().ok())
284            .map(ApiVersion::new)
285    }
286
287    /// Extracts API version from request header.
288    ///
289    /// Supports headers like:
290    /// - `X-API-Version: 2`
291    /// - `Accept: application/vnd.api+json;version=2`
292    pub fn from_header(header_value: &str) -> Option<Self> {
293        // Try direct version number first (X-API-Version: 2)
294        if let Ok(version) = header_value.trim().parse::<u32>() {
295            return Some(ApiVersion::new(version));
296        }
297
298        // Try Accept header format (version=2)
299        static VERSION_HEADER_REGEX: LazyLock<Regex> =
300            LazyLock::new(|| Regex::new(r"version=(\d+)").unwrap());
301
302        VERSION_HEADER_REGEX
303            .captures(header_value)
304            .and_then(|caps| caps.get(1))
305            .and_then(|m| m.as_str().parse::<u32>().ok())
306            .map(ApiVersion::new)
307    }
308
309    /// Extracts API version from query parameter.
310    ///
311    /// Looks for `?version=2` or `&version=2` in the query string.
312    pub fn from_query(query: &str) -> Option<Self> {
313        static VERSION_QUERY_REGEX: LazyLock<Regex> =
314            LazyLock::new(|| Regex::new(r"[?&]version=(\d+)").unwrap());
315
316        VERSION_QUERY_REGEX
317            .captures(query)
318            .and_then(|caps| caps.get(1))
319            .and_then(|m| m.as_str().parse::<u32>().ok())
320            .map(ApiVersion::new)
321    }
322}
323
324impl std::fmt::Display for ApiVersion {
325    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326        write!(f, "v{}", self.0)
327    }
328}
329
330impl From<u32> for ApiVersion {
331    fn from(version: u32) -> Self {
332        ApiVersion::new(version)
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use proptest::prelude::*;
340
341    // ========================================================================
342    // Property-based tests for replace_handlebars_with_env
343    // ========================================================================
344
345    proptest! {
346        /// Strings without handlebars patterns should pass through unchanged
347        #[test]
348        fn handlebars_no_pattern_unchanged(s in "[^{}]*") {
349            // Input without any braces should be unchanged
350            let result = replace_handlebars_with_env(&s);
351            prop_assert_eq!(result, s);
352        }
353
354        /// The function should never panic on arbitrary input
355        #[test]
356        fn handlebars_never_panics(s in ".*") {
357            // Just verify it doesn't panic - we don't care about the result
358            let _ = replace_handlebars_with_env(&s);
359        }
360
361        /// Single braces should pass through unchanged
362        #[test]
363        fn handlebars_single_braces_unchanged(
364            prefix in "[^{}]*",
365            middle in "[^{}]*",
366            suffix in "[^{}]*"
367        ) {
368            let input = format!("{prefix}{{{middle}}}{suffix}");
369            let result = replace_handlebars_with_env(&input);
370            // Single braces aren't our pattern, should be unchanged
371            prop_assert_eq!(result, input);
372        }
373
374        /// Valid patterns with set env vars should be substituted
375        #[test]
376        fn handlebars_valid_pattern_substituted(
377            var_name in "[A-Z][A-Z0-9_]{0,10}",
378            var_value in "[a-zA-Z0-9_]{1,20}",
379            prefix in "[^{}]{0,10}",
380            suffix in "[^{}]{0,10}"
381        ) {
382            // Set up test env var with unique name to avoid conflicts
383            let test_var = format!("PROPTEST_{var_name}");
384            unsafe { std::env::set_var(&test_var, &var_value); }
385
386            let input = format!("{prefix}{{{{ {test_var} }}}}{suffix}");
387            let result = replace_handlebars_with_env(&input);
388            let expected = format!("{prefix}{var_value}{suffix}");
389
390            unsafe { std::env::remove_var(&test_var); }
391
392            prop_assert_eq!(result, expected);
393        }
394
395        /// Multiple patterns in one string should all be substituted
396        #[test]
397        fn handlebars_multiple_patterns(
398            var1 in "[A-Z][A-Z0-9_]{0,5}",
399            var2 in "[A-Z][A-Z0-9_]{0,5}",
400            val1 in "[a-z]{1,10}",
401            val2 in "[a-z]{1,10}"
402        ) {
403            let test_var1 = format!("PROPTEST_MULTI1_{var1}");
404            let test_var2 = format!("PROPTEST_MULTI2_{var2}");
405
406            unsafe {
407                std::env::set_var(&test_var1, &val1);
408                std::env::set_var(&test_var2, &val2);
409            }
410
411            let input = format!("a={{{{ {test_var1} }}}} b={{{{ {test_var2} }}}}");
412            let result = replace_handlebars_with_env(&input);
413            let expected = format!("a={val1} b={val2}");
414
415            unsafe {
416                std::env::remove_var(&test_var1);
417                std::env::remove_var(&test_var2);
418            }
419
420            prop_assert_eq!(result, expected);
421        }
422
423        /// Missing env vars should become empty strings
424        #[test]
425        fn handlebars_missing_var_empty(
426            var_name in "[A-Z][A-Z0-9_]{5,15}"  // Use longer names to avoid collisions
427        ) {
428            let test_var = format!("PROPTEST_MISSING_{var_name}");
429            // Ensure it's not set
430            unsafe { std::env::remove_var(&test_var); }
431
432            let input = format!("value={{{{ {test_var} }}}}");
433            let result = replace_handlebars_with_env(&input);
434
435            prop_assert_eq!(result, "value=");
436        }
437    }
438
439    // ========================================================================
440    // Property-based tests for ApiVersion
441    // ========================================================================
442
443    proptest! {
444        /// ApiVersion round-trips through u32
445        #[test]
446        fn api_version_roundtrip(version in 0u32..1000) {
447            let api_version = ApiVersion::new(version);
448            prop_assert_eq!(api_version.as_u32(), version);
449        }
450
451        /// ApiVersion from_path extracts version correctly
452        #[test]
453        fn api_version_from_path(version in 1u32..100) {
454            let path = format!("/v{version}/resource");
455            let result = ApiVersion::from_path(&path);
456            prop_assert_eq!(result, Some(ApiVersion::new(version)));
457        }
458
459        /// ApiVersion from_header with direct number
460        #[test]
461        fn api_version_from_header_direct(version in 1u32..100) {
462            let header = format!("{version}");
463            let result = ApiVersion::from_header(&header);
464            prop_assert_eq!(result, Some(ApiVersion::new(version)));
465        }
466
467        /// ApiVersion from_header with version= format
468        #[test]
469        fn api_version_from_header_param(version in 1u32..100) {
470            let header = format!("application/json; version={version}");
471            let result = ApiVersion::from_header(&header);
472            prop_assert_eq!(result, Some(ApiVersion::new(version)));
473        }
474
475        /// ApiVersion from_query extracts version correctly
476        #[test]
477        fn api_version_from_query(version in 1u32..100) {
478            let query = format!("?foo=bar&version={version}&baz=qux");
479            let result = ApiVersion::from_query(&query);
480            prop_assert_eq!(result, Some(ApiVersion::new(version)));
481        }
482
483        /// ApiVersion Display format is correct
484        #[test]
485        fn api_version_display(version in 0u32..1000) {
486            let api_version = ApiVersion::new(version);
487            let display = api_version.to_string();
488            prop_assert_eq!(display, format!("v{version}"));
489        }
490    }
491
492    // ========================================================================
493    // Property-based tests for Sensitive wrapper
494    // ========================================================================
495
496    proptest! {
497        /// Sensitive wrapper preserves the inner value
498        #[test]
499        fn sensitive_preserves_value(s in ".*") {
500            let sensitive = Sensitive::from(s.as_str());
501            prop_assert_eq!(&sensitive.0, &s);
502        }
503
504        /// Sensitive Debug output never contains the actual value
505        #[test]
506        fn sensitive_debug_hides_value(s in "[a-zA-Z0-9]{1,50}") {
507            let sensitive = Sensitive::from(s.as_str());
508            let debug_output = format!("{:?}", sensitive);
509
510            // Debug output should contain "****" and NOT the actual value
511            prop_assert!(debug_output.contains("****"));
512            // Only check non-trivial strings to avoid false positives
513            if s.len() > 4 {
514                prop_assert!(!debug_output.contains(&s));
515            }
516        }
517    }
518
519    // ========================================================================
520    // Memory zeroing tests for Sensitive
521    // ========================================================================
522
523    #[test]
524    fn sensitive_drop_zeros_memory() {
525        // We can't directly inspect memory after drop in safe Rust,
526        // but we can verify the Drop implementation runs without panicking
527        // and that the inner value is accessible before drop.
528        let secret = "super-secret-password-12345";
529        let sensitive = Sensitive::from(secret);
530
531        // Value is accessible before drop
532        assert_eq!(sensitive.0, secret);
533
534        // Drop runs without panic (zeroize is called)
535        drop(sensitive);
536
537        // If we got here, Drop ran successfully
538    }
539
540    #[test]
541    fn sensitive_clone_creates_independent_copy() {
542        let original = Sensitive::from("original-secret");
543        let cloned = original.clone();
544
545        // Both have the same value
546        assert_eq!(original.0, cloned.0);
547
548        // Dropping one doesn't affect the other
549        drop(original);
550        assert_eq!(cloned.0, "original-secret");
551    }
552
553    #[test]
554    fn sensitive_zeroize_trait_is_used() {
555        use zeroize::Zeroize;
556
557        // Verify that String implements Zeroize (which is required by our Sensitive)
558        let mut s = String::from("secret");
559        s.zeroize();
560        assert!(s.is_empty(), "Zeroize should clear the string");
561    }
562}