Skip to main content

rustfs_targets/sys/
user_agent.rs

1// Copyright 2024 RustFS Team
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.
14
15use rustfs_config::VERSION;
16use std::borrow::Cow;
17use std::env;
18use std::fmt;
19use std::sync::OnceLock;
20#[cfg(not(target_os = "openbsd"))]
21use sysinfo::System;
22
23/// Business Type Enumeration
24#[derive(Debug, Clone, PartialEq)]
25pub enum ServiceType {
26    Basis,
27    Core,
28    Event,
29    Logger,
30    Custom(Cow<'static, str>),
31}
32
33impl ServiceType {
34    fn as_str(&self) -> &str {
35        match self {
36            ServiceType::Basis => "basis",
37            ServiceType::Core => "core",
38            ServiceType::Event => "event",
39            ServiceType::Logger => "logger",
40            ServiceType::Custom(s) => s,
41        }
42    }
43}
44
45/// UserAgent structure to hold User-Agent information
46/// including OS platform, architecture, version, and service type.
47#[derive(Debug)]
48struct UserAgent {
49    os_platform: &'static str,
50    arch: &'static str,
51    version: &'static str,
52    service: ServiceType,
53}
54
55static OS_PLATFORM: OnceLock<String> = OnceLock::new();
56
57impl UserAgent {
58    /// Create a new UserAgent instance and accept business type parameters
59    fn new(service: ServiceType) -> Self {
60        UserAgent {
61            os_platform: Self::get_os_platform(),
62            arch: env::consts::ARCH,
63            version: VERSION,
64            service,
65        }
66    }
67
68    /// Obtain operating system platform information using a thread-safe cache.
69    ///
70    /// The value is computed once on first use via `OnceLock` and then reused
71    /// for all subsequent calls for the lifetime of the program.
72    fn get_os_platform() -> &'static str {
73        OS_PLATFORM.get_or_init(|| {
74            if cfg!(target_os = "windows") {
75                Self::get_windows_platform()
76            } else if cfg!(target_os = "macos") {
77                Self::get_macos_platform()
78            } else if cfg!(target_os = "linux") {
79                Self::get_linux_platform()
80            } else if cfg!(target_os = "freebsd") {
81                Self::get_freebsd_platform()
82            } else if cfg!(target_os = "netbsd") {
83                Self::get_netbsd_platform()
84            } else {
85                "Unknown".to_string()
86            }
87        })
88    }
89
90    /// Get Windows platform information
91    #[cfg(windows)]
92    fn get_windows_platform() -> String {
93        let version = System::os_version().unwrap_or_else(|| "NT Unknown".to_string());
94        if version.starts_with("Windows") {
95            version
96        } else {
97            format!("Windows NT {version}")
98        }
99    }
100
101    #[cfg(not(windows))]
102    fn get_windows_platform() -> String {
103        "N/A".to_string()
104    }
105
106    /// Get macOS platform information
107    #[cfg(target_os = "macos")]
108    fn get_macos_platform() -> String {
109        let version_str = System::os_version().unwrap_or_else(|| "14.0.0".to_string());
110        let mut parts = version_str.split('.');
111        let major = parts.next().unwrap_or("14");
112        let minor = parts.next().unwrap_or("0");
113        let patch = parts.next().unwrap_or("0");
114
115        let cpu_info = if env::consts::ARCH == "aarch64" { "Apple" } else { "Intel" };
116
117        format!("Macintosh; {cpu_info} Mac OS X {major}_{minor}_{patch}")
118    }
119
120    #[cfg(not(target_os = "macos"))]
121    fn get_macos_platform() -> String {
122        "N/A".to_string()
123    }
124
125    /// Get Linux platform information
126    #[cfg(target_os = "linux")]
127    fn get_linux_platform() -> String {
128        let os_name = System::long_os_version().unwrap_or_else(|| "Linux Unknown".to_string());
129        format!("X11; {os_name}")
130    }
131
132    #[cfg(not(target_os = "linux"))]
133    fn get_linux_platform() -> String {
134        "N/A".to_string()
135    }
136
137    #[cfg(target_os = "freebsd")]
138    fn get_freebsd_platform() -> String {
139        format!("FreeBSD; {}", env::consts::ARCH)
140    }
141
142    #[cfg(not(target_os = "freebsd"))]
143    fn get_freebsd_platform() -> String {
144        "N/A".to_string()
145    }
146
147    #[cfg(target_os = "netbsd")]
148    fn get_netbsd_platform() -> String {
149        format!("NetBSD; {}", env::consts::ARCH)
150    }
151
152    #[cfg(not(target_os = "netbsd"))]
153    fn get_netbsd_platform() -> String {
154        "N/A".to_string()
155    }
156}
157
158impl fmt::Display for UserAgent {
159    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
160        write!(f, "Mozilla/5.0 ({}; {}) RustFS/{}", self.os_platform, self.arch, self.version)?;
161        if self.service != ServiceType::Basis {
162            write!(f, " ({})", self.service.as_str())?;
163        }
164        Ok(())
165    }
166}
167
168/// Get the User-Agent string and accept business type parameters
169pub fn get_user_agent(service: ServiceType) -> String {
170    UserAgent::new(service).to_string()
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use rustfs_config::VERSION;
177
178    #[test]
179    fn test_user_agent_format_basis() {
180        let ua = get_user_agent(ServiceType::Basis);
181        assert!(ua.starts_with("Mozilla/5.0"));
182        assert!(ua.contains(&format!("RustFS/{VERSION}")));
183        assert!(!ua.contains("(basis)"));
184    }
185
186    #[test]
187    fn test_user_agent_format_core() {
188        let ua = get_user_agent(ServiceType::Core);
189        assert!(ua.contains(&format!("RustFS/{VERSION} (core)")));
190    }
191
192    #[test]
193    fn test_user_agent_format_custom() {
194        let ua = get_user_agent(ServiceType::Custom("monitor".into()));
195        assert!(ua.contains(&format!("RustFS/{VERSION} (monitor)")));
196    }
197
198    #[test]
199    fn test_os_platform_caching() {
200        let ua1 = UserAgent::new(ServiceType::Basis);
201        let ua2 = UserAgent::new(ServiceType::Basis);
202        assert_eq!(ua1.os_platform, ua2.os_platform);
203        assert!(std::ptr::eq(ua1.os_platform.as_ptr(), ua2.os_platform.as_ptr()));
204    }
205}