spire_core/identity/
mod.rs1mod identities;
35mod pool;
36mod weak;
37
38use std::fmt;
39use std::hash::{Hash, Hasher};
40use std::sync::Arc;
41
42use ::http::header::IntoHeaderName;
43use ::http::{HeaderMap, HeaderValue, Uri};
44#[cfg(feature = "user-agents")]
45#[cfg_attr(docsrs, doc(cfg(feature = "user-agents")))]
46#[doc(inline)]
47pub use uagent::{Browser, Platform, Profile, UserAgents};
48
49pub use self::identities::Identities;
50pub(crate) use self::pool::{Health, IdentityPool};
51pub use self::weak::WeakIdentity;
52use crate::http::{Request, RequestMeta};
53use crate::{Error, Result};
54
55#[derive(Clone)]
60pub struct Identity(Arc<Inner>);
61
62#[derive(Clone)]
63struct Inner {
64 name: Box<str>,
65 proxy: Option<Uri>,
66 headers: HeaderMap,
67 #[cfg(feature = "user-agents")]
68 profile: Option<Profile>,
69}
70
71impl Identity {
72 #[must_use]
74 pub fn new(name: impl Into<Box<str>>) -> Self {
75 Self(Arc::new(Inner {
76 name: name.into(),
77 proxy: None,
78 headers: HeaderMap::new(),
79 #[cfg(feature = "user-agents")]
80 profile: None,
81 }))
82 }
83
84 pub fn require_no_proxy(&self) -> Result<()> {
93 match self.proxy() {
94 Some(_) => Err(Error::invalid(format!(
95 "identity `{}` has a proxy, which this backend cannot send through",
96 self.name()
97 ))),
98 None => Ok(()),
99 }
100 }
101
102 pub fn with_proxy<U>(mut self, proxy: U) -> Result<Self>
110 where
111 U: TryInto<Uri>,
112 U::Error: Into<Error>,
113 {
114 let proxy: Uri = proxy.try_into().map_err(Into::into)?;
115 if proxy.scheme().is_none() || proxy.authority().is_none() {
116 let message = format!("proxy of identity `{}` is not absolute", self.name());
117 return Err(Error::invalid(message));
118 }
119 Arc::make_mut(&mut self.0).proxy = Some(proxy);
120 Ok(self)
121 }
122
123 #[must_use]
126 pub fn with_header(mut self, name: impl IntoHeaderName, value: HeaderValue) -> Self {
127 Arc::make_mut(&mut self.0).headers.insert(name, value);
128 self
129 }
130
131 #[cfg(feature = "user-agents")]
137 #[cfg_attr(docsrs, doc(cfg(feature = "user-agents")))]
138 #[must_use]
139 pub fn with_profile(mut self, profile: Profile) -> Self {
140 Arc::make_mut(&mut self.0).profile = Some(profile);
141 self
142 }
143
144 #[cfg(feature = "user-agents")]
146 #[cfg_attr(docsrs, doc(cfg(feature = "user-agents")))]
147 #[must_use]
148 pub fn profile(&self) -> Option<&Profile> {
149 self.0.profile.as_ref()
150 }
151
152 pub fn require_no_profile(&self) -> Result<()> {
160 #[cfg(feature = "user-agents")]
161 if self.profile().is_some() {
162 return Err(Error::invalid(format!(
163 "identity `{}` has a user-agent profile, which a real browser cannot use",
164 self.name()
165 )));
166 }
167 Ok(())
168 }
169
170 #[must_use]
172 pub fn name(&self) -> &str {
173 &self.0.name
174 }
175
176 #[must_use]
178 pub fn proxy(&self) -> Option<&Uri> {
179 self.0.proxy.as_ref()
180 }
181
182 #[must_use]
184 pub fn headers(&self) -> &HeaderMap {
185 &self.0.headers
186 }
187
188 #[must_use]
193 pub fn downgrade(&self) -> WeakIdentity {
194 WeakIdentity(Arc::downgrade(&self.0))
195 }
196
197 pub(crate) fn apply(&self, request: &mut Request) {
201 let headers = request.headers_mut();
202 for (name, value) in self.headers() {
203 if !headers.contains_key(name) {
204 headers.insert(name.clone(), value.clone());
205 }
206 }
207 #[cfg(feature = "user-agents")]
208 if let Some(profile) = self.profile() {
209 for header in profile.headers() {
210 if !headers.contains_key(header.name) {
211 let name = ::http::HeaderName::from_static(header.name);
213 let value = HeaderValue::from_str(&header.value)
214 .expect("uagent profile headers are valid HTTP headers");
215 headers.insert(name, value);
216 }
217 }
218 }
219 let meta = request
220 .extensions_mut()
221 .get_or_insert_default::<RequestMeta>();
222 meta.identity = Some(self.clone());
223 }
224}
225
226impl fmt::Debug for Identity {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 let proxy = self.proxy().map(|proxy| {
230 let host = proxy.host().unwrap_or_default();
231 match proxy.port_u16() {
232 Some(port) => format!("{host}:{port}"),
233 None => host.to_owned(),
234 }
235 });
236 f.debug_struct("Identity")
237 .field("name", &self.name())
238 .field("proxy", &proxy)
239 .field("headers", &self.headers().keys().collect::<Vec<_>>())
240 .finish()
241 }
242}
243
244impl PartialEq for Identity {
245 fn eq(&self, other: &Self) -> bool {
246 self.name() == other.name()
247 }
248}
249
250impl Eq for Identity {}
251
252impl Hash for Identity {
253 fn hash<H: Hasher>(&self, state: &mut H) {
254 self.name().hash(state);
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use ::http::header::USER_AGENT;
261
262 use super::*;
263
264 #[cfg(feature = "user-agents")]
265 #[test]
266 fn replacing_a_profile_preserves_explicit_headers_and_clones() {
267 let chrome = Profile::new(Browser::Chrome, Platform::Windows).unwrap();
268 let firefox = Profile::new(Browser::Firefox, Platform::Linux).unwrap();
269 let original = Identity::new("a")
270 .with_profile(chrome.clone())
271 .with_header(USER_AGENT, HeaderValue::from_static("explicit"));
272 let changed = original.clone().with_profile(firefox.clone());
273 assert_eq!(original.profile(), Some(&chrome));
274 assert_eq!(changed.profile(), Some(&firefox));
275 assert_eq!(changed.headers()[USER_AGENT], "explicit");
276 assert_eq!(changed.headers().len(), 1);
277 assert!(changed.require_no_profile().is_err());
278 assert!(Identity::new("plain").require_no_profile().is_ok());
279 }
280
281 #[test]
282 fn debug_hides_proxy_credentials() {
283 let identity = Identity::new("a")
284 .with_proxy("http://user:secret@proxy.test:8080") .unwrap()
287 .with_header(USER_AGENT, HeaderValue::from_static("Bot"));
288 let debug = format!("{identity:?}");
289 assert!(debug.contains("proxy.test:8080"), "{debug}");
290 assert!(!debug.contains("secret"), "{debug}");
291 assert_eq!(identity.proxy().unwrap().host(), Some("proxy.test"));
292 }
293
294 #[test]
295 fn only_proxied_identities_require_a_proxy() {
296 assert!(Identity::new("a").require_no_proxy().is_ok());
297 let proxied = Identity::new("a")
298 .with_proxy("http://proxy.test:8080")
299 .unwrap();
300 assert!(proxied.require_no_proxy().is_err());
301 }
302
303 #[test]
304 fn rejects_relative_proxies() {
305 assert!(Identity::new("a").with_proxy("/proxy").is_err());
306 assert!(Identity::new("a").with_proxy("not a url").is_err());
307 }
308}