Skip to main content

conjure_runtime/
user_agent.rs

1// Copyright 2020 Palantir Technologies, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14use conjure_object::log_safety::AssertLogSafe;
15use once_cell::sync::Lazy;
16use regex::Regex;
17use std::fmt;
18use witchcraft_log::warn;
19
20static VALID_NODE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9.\-]*$").unwrap());
21static VALID_NAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z][a-zA-Z0-9\-]*$").unwrap());
22static VALID_VERSION: Lazy<Regex> =
23    Lazy::new(|| Regex::new(r"^[0-9]+(\.[0-9]+)*(-rc[0-9]+)?(-[0-9]+-g[a-f0-9]+)?$").unwrap());
24
25const DEFAULT_VERSION: &str = "0.0.0";
26
27/// A representation of an HTTP `User-Agent` header value.
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct UserAgent {
30    node_id: Option<String>,
31    primary: Agent,
32    informational: Vec<Agent>,
33}
34
35impl fmt::Display for UserAgent {
36    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(fmt, "{}", self.primary)?;
38
39        if let Some(ref node_id) = self.node_id {
40            write!(fmt, " (nodeId:{node_id})")?;
41        }
42
43        for agent in &self.informational {
44            write!(fmt, " {agent}")?;
45        }
46
47        Ok(())
48    }
49}
50
51impl UserAgent {
52    /// Creates a new `UserAgent`.
53    pub fn new(primary: Agent) -> UserAgent {
54        UserAgent {
55            node_id: None,
56            primary,
57            informational: vec![],
58        }
59    }
60
61    /// Adds an additional informational agent to the `User-Agent`.
62    pub fn push_agent(&mut self, agent: Agent) {
63        self.informational.push(agent);
64    }
65
66    /// Sets the identifier of this node.
67    ///
68    /// For example, this could be the node's IP address.
69    pub fn set_node_id(&mut self, node_id: &str) {
70        assert!(
71            VALID_NODE.is_match(node_id),
72            "invalid user agent node ID `{}`",
73            node_id
74        );
75        self.node_id = Some(node_id.to_string());
76    }
77
78    /// Returns the identifier of this node, if provided.
79    pub fn node_id(&self) -> Option<&str> {
80        self.node_id.as_deref()
81    }
82
83    /// Returns the primary agent.
84    pub fn primary(&self) -> &Agent {
85        &self.primary
86    }
87
88    /// Returns additional informational agents.
89    pub fn informational(&self) -> &[Agent] {
90        &self.informational
91    }
92}
93
94/// A component of a [`UserAgent`].
95#[derive(Debug, Clone, PartialEq, Eq, Hash)]
96pub struct Agent {
97    name: String,
98    version: String,
99}
100
101impl fmt::Display for Agent {
102    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(fmt, "{}/{}", self.name, self.version)
104    }
105}
106
107impl Agent {
108    /// Creates a new `Agent`.
109    pub fn new(name: &str, mut version: &str) -> Agent {
110        assert!(VALID_NAME.is_match(name), "invalid agent name `{}`", name);
111
112        if !VALID_VERSION.is_match(version) {
113            warn!(
114                "encountered invalid user agent version",
115                safe: {
116                    version: AssertLogSafe(version),
117                }
118            );
119            version = DEFAULT_VERSION;
120        }
121
122        Agent {
123            name: name.to_string(),
124            version: version.to_string(),
125        }
126    }
127
128    /// Returns the agent's name.
129    pub fn name(&self) -> &str {
130        &self.name
131    }
132
133    /// Returns the agent's version.
134    pub fn version(&self) -> &str {
135        &self.version
136    }
137}
138
139#[cfg(test)]
140mod test {
141    use super::*;
142
143    #[test]
144    fn fmt() {
145        let mut agent = UserAgent::new(Agent::new("foobar", "1.2.3"));
146        agent.set_node_id("127.0.0.1");
147        agent.push_agent(Agent::new("fizzbuzz", "0.0.0-1-g12345"));
148        agent.push_agent(Agent::new("btob", "1.0.0-rc1"));
149        assert_eq!(
150            agent.to_string(),
151            "foobar/1.2.3 (nodeId:127.0.0.1) fizzbuzz/0.0.0-1-g12345 btob/1.0.0-rc1"
152        );
153    }
154
155    #[test]
156    fn version_fallback() {
157        let agent = Agent::new("foobar", "some-invalid-version");
158        assert_eq!(agent.version(), "0.0.0");
159    }
160}