openzeppelin_relayer/utils/
url.rs

1//! URL utility functions.
2//!
3//! This module provides utility functions for working with URLs,
4//! including masking sensitive information from URLs.
5
6/// Masks a URL by showing only the scheme and host, hiding the path and query parameters.
7///
8/// This is used to safely display RPC URLs in API responses and logs without exposing
9/// sensitive API keys that are often embedded in the URL path or query string.
10///
11/// # Examples
12/// - `https://eth-mainnet.g.alchemy.com/v2/abc123` → `https://eth-mainnet.g.alchemy.com/***`
13/// - `https://mainnet.infura.io/v3/PROJECT_ID` → `https://mainnet.infura.io/***`
14/// - `http://localhost:8545` → `http://localhost:8545` (no path to mask)
15/// - `invalid-url` → `***` (fallback for unparsable URLs)
16pub fn mask_url(url: &str) -> String {
17    // Find the scheme separator "://"
18    let Some(scheme_end) = url.find("://") else {
19        // No valid scheme, mask entirely for safety
20        return "***".to_string();
21    };
22
23    // Find where the host ends (first "/" after "://")
24    let host_start = scheme_end + 3; // Skip "://"
25    let rest = &url[host_start..];
26
27    // Find the first "/" which marks the start of the path
28    if let Some(path_start) = rest.find('/') {
29        // Check if there's actually content in the path (not just "/")
30        let path_and_beyond = &rest[path_start..];
31        if path_and_beyond.len() > 1 || url.contains('?') {
32            // There's a path or query to mask
33            let host_end = host_start + path_start;
34            format!("{}/***", &url[..host_end])
35        } else {
36            // Just a trailing "/" with no real path content
37            url.to_string()
38        }
39    } else if url.contains('?') {
40        // No path but has query parameters - mask those
41        let query_start = url.find('?').unwrap();
42        format!("{}?***", &url[..query_start])
43    } else {
44        // No path or query to mask, return original
45        url.to_string()
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn test_mask_url_alchemy_with_api_key() {
55        let url = "https://eth-mainnet.g.alchemy.com/v2/abc123xyz";
56        let masked = mask_url(url);
57        assert_eq!(masked, "https://eth-mainnet.g.alchemy.com/***");
58    }
59
60    #[test]
61    fn test_mask_url_infura_with_project_id() {
62        let url = "https://mainnet.infura.io/v3/my-project-id";
63        let masked = mask_url(url);
64        assert_eq!(masked, "https://mainnet.infura.io/***");
65    }
66
67    #[test]
68    fn test_mask_url_quicknode_with_api_key() {
69        let url = "https://my-node.quiknode.pro/secret-api-key/";
70        let masked = mask_url(url);
71        assert_eq!(masked, "https://my-node.quiknode.pro/***");
72    }
73
74    #[test]
75    fn test_mask_url_localhost_no_path() {
76        // No path to mask, should return original
77        let url = "http://localhost:8545";
78        let masked = mask_url(url);
79        assert_eq!(masked, "http://localhost:8545");
80    }
81
82    #[test]
83    fn test_mask_url_localhost_with_trailing_slash() {
84        // Just a trailing slash with no real path content
85        let url = "http://localhost:8545/";
86        let masked = mask_url(url);
87        assert_eq!(masked, "http://localhost:8545/");
88    }
89
90    #[test]
91    fn test_mask_url_with_query_params() {
92        let url = "https://rpc.example.com/v1?api_key=secret123&network=mainnet";
93        let masked = mask_url(url);
94        assert_eq!(masked, "https://rpc.example.com/***");
95    }
96
97    #[test]
98    fn test_mask_url_query_params_no_path() {
99        let url = "https://rpc.example.com?api_key=secret123";
100        let masked = mask_url(url);
101        assert_eq!(masked, "https://rpc.example.com?***");
102    }
103
104    #[test]
105    fn test_mask_url_invalid_url_no_scheme() {
106        // Invalid URL without scheme should be fully masked for safety
107        let url = "invalid-url";
108        let masked = mask_url(url);
109        assert_eq!(masked, "***");
110    }
111
112    #[test]
113    fn test_mask_url_empty_string() {
114        let url = "";
115        let masked = mask_url(url);
116        assert_eq!(masked, "***");
117    }
118
119    #[test]
120    fn test_mask_url_with_port_and_path() {
121        let url = "https://rpc.example.com:8080/api/v1/secret";
122        let masked = mask_url(url);
123        assert_eq!(masked, "https://rpc.example.com:8080/***");
124    }
125
126    #[test]
127    fn test_mask_url_ankr_with_api_key() {
128        let url = "https://rpc.ankr.com/eth/my-api-key-here";
129        let masked = mask_url(url);
130        assert_eq!(masked, "https://rpc.ankr.com/***");
131    }
132}