fw_client/
headers.rs

1use std::{str::FromStr, sync::LazyLock};
2
3use regex::Regex;
4
5use crate::FWClientError;
6
7static API_KEY_RE: LazyLock<Regex> = LazyLock::new(|| {
8    Regex::new(
9        r"(?i)((?P<api_key_type>bearer|scitran-user) )?((?P<scheme>https?://)?(?P<host>[^:]+)(?P<port>:\d+)?:)?(?P<api_key>.+)"
10    ).expect("Failed to compile API key regex")
11});
12
13pub fn get_user_agent(name: Option<String>, version: Option<String>) -> String {
14    let version = version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
15    let name = name.unwrap_or_else(|| env!("CARGO_PKG_NAME").to_string());
16    let os = std::env::consts::OS;
17    let family = std::env::consts::FAMILY;
18    let arch = std::env::consts::ARCH;
19    let comments = [("family", family), ("arch", arch), ("os", os)]
20        .iter()
21        .map(|(k, v)| format!("{}:{}", k, v))
22        .collect::<Vec<_>>()
23        .join(";");
24    format!("{}/{} ({})", name, version, comments)
25}
26
27/// Struct representing an API key.
28///
29/// Requires at least a host and an API key.
30/// The API key can be in the format:
31/// `[<api-key-type>][<scheme>]://<host>[:<port>]:<key>`, where `api-key-type` can either
32///  be `Bearer` or `scitran-user`
33#[derive(Debug, Clone)]
34pub struct ApiKey {
35    pub api_key_type: Option<String>,
36    pub scheme: Option<String>,
37    pub host: String,
38    pub port: Option<String>,
39    pub api_key: String,
40}
41
42impl<'de> serde::Deserialize<'de> for ApiKey {
43    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
44    where
45        D: serde::Deserializer<'de>,
46    {
47        let api_key = String::deserialize(deserializer)?;
48        ApiKey::from_str(api_key.as_ref()).map_err(serde::de::Error::custom)
49    }
50}
51
52impl FromStr for ApiKey {
53    type Err = FWClientError;
54
55    /// Try to parse API key from a string.
56    /// The expected format is: `[<scheme>]://<host>[:<port>][:__force_insecure]:<key>`
57    fn from_str(api_key: &str) -> Result<Self, Self::Err> {
58        let caps = API_KEY_RE
59            .captures(api_key.as_ref())
60            .ok_or_else(|| FWClientError::InvalidApiKey(api_key.to_string()))?;
61        Ok(ApiKey {
62            api_key_type: caps.name("api_key_type").map(|m| m.as_str().to_string()),
63            scheme: caps.name("scheme").map(|m| m.as_str().to_string()),
64            host: caps
65                .name("host")
66                .map(|m| m.as_str())
67                .ok_or_else(|| FWClientError::MissingComponent("host".to_string()))?
68                .to_string(),
69            port: caps.name("port").map(|m| m.as_str().to_string()),
70            api_key: caps
71                .name("api_key")
72                .map(|m| m.as_str())
73                .ok_or_else(|| FWClientError::MissingComponent("api_key".to_string()))?
74                .to_string(),
75        })
76    }
77}
78
79impl std::fmt::Display for ApiKey {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        let mut api_key = String::new();
82        match &self.api_key_type {
83            Some(api_key_type) => api_key.push_str(format!("{} ", api_key_type).as_str()),
84            None => api_key.push_str("scitran-user "),
85        }
86        api_key.push_str(&self.host);
87        api_key.push_str(&format!(":{}", self.api_key));
88        write!(f, "{}", api_key)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use rstest::rstest;
96
97    #[rstest]
98    #[case::with_api_key_type(
99        "scitran-user flywheel.io:my-api-key",
100        "scitran-user flywheel.io:my-api-key"
101    )]
102    #[case::with_bearer(
103        "bearer flywheel.io:my-bearer-token",
104        "bearer flywheel.io:my-bearer-token"
105    )]
106    #[case::with_scheme(
107        "https://api.flywheel.io:my-api-key",
108        "scitran-user api.flywheel.io:my-api-key"
109    )]
110    #[case::with_port(
111        "api.flywheel.io:8000:my-api-key",
112        "scitran-user api.flywheel.io:my-api-key"
113    )]
114    #[case::with_all_components(
115        "bearer https://api.flywheel.io:8000:my-api-key",
116        "bearer api.flywheel.io:my-api-key"
117    )]
118    #[case::simple_format("flywheel.io:my-api-key", "scitran-user flywheel.io:my-api-key")]
119    // Some weird cases we may want to be errors in the future
120    #[case::colon_in_key("fw_instance:443::", "scitran-user fw_instance::")]
121    #[case::port_is_key("fw_instance:443", "scitran-user fw_instance:443")]
122    fn test_parse_api_key_success(#[case] input_key: &str, #[case] expected_display: &str) {
123        // Parse the input key string into an ApiKey struct
124        let api_key = ApiKey::from_str(input_key).unwrap();
125
126        // Assert that the string representation matches the expected value
127        assert_eq!(api_key.to_string(), expected_display);
128    }
129
130    #[rstest]
131    #[case::without_host(":my-api-key", FWClientError::MissingComponent("host".to_string()))]
132    #[case::invalid_format("invalid-format", FWClientError::MissingComponent("host".to_string()))]
133    fn test_parse_api_key_failure(
134        #[case] api_key_str: &str,
135        #[case] expected_error: FWClientError,
136    ) {
137        let result = ApiKey::from_str(api_key_str);
138
139        assert!(result.is_err());
140        match (result.unwrap_err(), expected_error) {
141            (
142                FWClientError::MissingComponent(component),
143                FWClientError::MissingComponent(expected),
144            ) => {
145                assert_eq!(component, expected);
146            }
147            (FWClientError::InvalidApiKey(key), FWClientError::InvalidApiKey(expected)) => {
148                assert_eq!(key, expected);
149            }
150            (err, expected) => panic!("Expected {:?}, got {:?}", expected, err),
151        }
152    }
153
154    #[test]
155    fn test_user_agent() {
156        let user_agent = get_user_agent(None, None);
157        assert!(user_agent.contains("fw-client"));
158        assert!(user_agent.contains("os:"));
159        assert!(user_agent.contains("arch:"));
160        assert!(user_agent.contains("family:"));
161        println!("User agent: {}", user_agent);
162    }
163}