wreq-rb 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +257 -102
  3. data/ext/wreq_rb/Cargo.toml +7 -4
  4. data/ext/wreq_rb/src/client.rs +105 -15
  5. data/ext/wreq_rb/src/response.rs +28 -3
  6. data/lib/wreq-rb/version.rb +1 -1
  7. data/patches/0001-add-transfer-size-tracking.patch +11 -15
  8. data/vendor/wreq/Cargo.toml +9 -8
  9. data/vendor/wreq/README.md +5 -5
  10. data/vendor/wreq/bench/support/bench.rs +6 -2
  11. data/vendor/wreq/bench/support/client.rs +88 -1
  12. data/vendor/wreq/bench/support/exec.rs +0 -0
  13. data/vendor/wreq/bench/support/rt.rs +34 -0
  14. data/vendor/wreq/bench/support/server.rs +1 -1
  15. data/vendor/wreq/bench/support.rs +1 -15
  16. data/vendor/wreq/examples/cert_store.rs +13 -13
  17. data/vendor/wreq/examples/request_with_emulate.rs +1 -1
  18. data/vendor/wreq/examples/tcp_linger.rs +22 -0
  19. data/vendor/wreq/src/client/layer/client/pool.rs +17 -17
  20. data/vendor/wreq/src/client/layer/client.rs +2 -0
  21. data/vendor/wreq/src/client/layer/decoder.rs +71 -17
  22. data/vendor/wreq/src/client/layer/redirect/future.rs +49 -63
  23. data/vendor/wreq/src/client/layer/redirect/policy.rs +2 -26
  24. data/vendor/wreq/src/client/layer/redirect.rs +48 -60
  25. data/vendor/wreq/src/client/layer/retry.rs +12 -15
  26. data/vendor/wreq/src/client/layer/timeout/body.rs +27 -21
  27. data/vendor/wreq/src/client/layer/timeout/future.rs +33 -58
  28. data/vendor/wreq/src/client/layer/timeout.rs +8 -14
  29. data/vendor/wreq/src/client/request.rs +4 -0
  30. data/vendor/wreq/src/client.rs +53 -31
  31. data/vendor/wreq/src/conn/connector.rs +99 -129
  32. data/vendor/wreq/src/conn/http.rs +25 -18
  33. data/vendor/wreq/src/conn/net/tcp.rs +601 -107
  34. data/vendor/wreq/src/conn/proxy/socks.rs +6 -6
  35. data/vendor/wreq/src/conn/timeout.rs +166 -0
  36. data/vendor/wreq/src/conn.rs +5 -4
  37. data/vendor/wreq/src/cookie/jar.rs +1225 -0
  38. data/vendor/wreq/src/cookie/store.rs +321 -0
  39. data/vendor/wreq/src/cookie.rs +108 -612
  40. data/vendor/wreq/src/dns/resolve.rs +8 -2
  41. data/vendor/wreq/src/dns.rs +4 -4
  42. data/vendor/wreq/src/error.rs +53 -20
  43. data/vendor/wreq/src/lib.rs +1 -0
  44. data/vendor/wreq/src/proxy/matcher.rs +26 -12
  45. data/vendor/wreq/src/proxy/win.rs +39 -9
  46. data/vendor/wreq/src/redirect.rs +515 -100
  47. data/vendor/wreq/src/tls/conn.rs +3 -11
  48. data/vendor/wreq/src/tls/session.rs +7 -8
  49. data/vendor/wreq/src/tls/trust/store.rs +4 -4
  50. data/vendor/wreq/src/util.rs +23 -0
  51. data/vendor/wreq/tests/badssl.rs +72 -7
  52. data/vendor/wreq/tests/brotli.rs +1 -1
  53. data/vendor/wreq/tests/client.rs +24 -0
  54. data/vendor/wreq/tests/connector_layers.rs +8 -4
  55. data/vendor/wreq/tests/cookie.rs +59 -0
  56. data/vendor/wreq/tests/deflate.rs +1 -1
  57. data/vendor/wreq/tests/gzip.rs +53 -1
  58. data/vendor/wreq/tests/layers.rs +8 -4
  59. data/vendor/wreq/tests/redirect.rs +180 -97
  60. data/vendor/wreq/tests/timeouts.rs +47 -12
  61. data/vendor/wreq/tests/zstd.rs +1 -1
  62. metadata +8 -2
@@ -1,98 +1,27 @@
1
- //! HTTP Cookies
1
+ //! HTTP cookie storage and request selection.
2
+ //!
3
+ //! [`Jar`] stores cookies received through `Set-Cookie` response fields and selects the cookies
4
+ //! that apply to later requests. Domain, path, security, and expiration checks follow the
5
+ //! [RFC 6265 storage and retrieval model]. [`CookieStore`] can be implemented to provide another
6
+ //! storage backend while keeping the same client integration.
7
+ //!
8
+ //! [RFC 6265 storage and retrieval model]: https://www.rfc-editor.org/rfc/rfc6265.html#section-5
2
9
 
3
- use std::{collections::HashMap, convert::TryInto, fmt, sync::Arc, time::SystemTime};
10
+ mod jar;
11
+ mod store;
4
12
 
5
- use bytes::Bytes;
6
- use cookie::{Cookie as RawCookie, CookieJar, Expiration, SameSite, time::Duration};
7
- use http::{Uri, Version};
8
-
9
- use crate::{IntoUri, error::Error, ext::UriExt, header::HeaderValue, sync::RwLock};
10
-
11
- /// Cookie header values in two forms.
12
- #[derive(Debug, Clone)]
13
- #[non_exhaustive]
14
- pub enum Cookies {
15
- /// All cookies combined into one header (compressed).
16
- Compressed(HeaderValue),
13
+ use std::{convert::TryInto, fmt, sync::Arc, time::SystemTime};
17
14
 
18
- /// Each cookie sent as its own header (uncompressed).
19
- Uncompressed(Vec<HeaderValue>),
20
-
21
- /// No cookies.
22
- Empty,
23
- }
24
-
25
- /// Actions for a persistent cookie store providing session support.
26
- pub trait CookieStore: Send + Sync {
27
- /// Store a set of Set-Cookie header values received from `uri`
28
- fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, uri: &Uri);
29
-
30
- /// Returns cookies for the given URI and HTTP version.
31
- ///
32
- /// Following [RFC 9112 §5.6.3], HTTP/1.1 combines all cookies into a single header.
33
- /// For [HTTP/2] and above, cookies are sent as separate header fields
34
- /// as per [RFC 9113 §8.1.2.5].
35
- ///
36
- /// [RFC 9112 §5.6.3]: https://www.rfc-editor.org/rfc/rfc9112#section-5.6.3
37
- /// [RFC 9113 §8.1.2.5]: https://www.rfc-editor.org/rfc/rfc9113#section-8.1.2.5
38
- /// [HTTP/2]: https://datatracker.ietf.org/doc/html/rfc9113
39
- fn cookies(&self, uri: &Uri, version: Version) -> Cookies;
40
- }
15
+ use cookie::{Cookie as RawCookie, Expiration, SameSite};
16
+ use http::{Uri, Version};
41
17
 
42
- impl_into_shared!(
43
- /// Trait for converting types into a shared cookie store ([`Arc<dyn CookieStore>`]).
44
- ///
45
- /// Implemented for any [`CookieStore`] type, [`Arc<T>`] where `T: CookieStore`, and [`Arc<dyn
46
- /// CookieStore>`]. Enables ergonomic conversion to a trait object for use in APIs without manual
47
- /// boxing.
48
- pub trait IntoCookieStore => CookieStore
49
- );
18
+ pub use self::jar::Jar;
19
+ use crate::{error::Error, header::HeaderValue};
50
20
 
51
- impl_request_config_value!(Arc<dyn CookieStore>);
52
-
53
- /// Trait for converting types into an owned cookie ([`Cookie<'static>`]).
54
- pub trait IntoCookie {
55
- /// Converts the implementor into a optional owned [`Cookie<'static>`].
56
- fn into_cookie(self) -> Option<Cookie<'static>>;
57
- }
58
-
59
- /// A single HTTP cookie.
21
+ /// A parsed HTTP cookie.
60
22
  #[derive(Debug, Clone)]
61
23
  pub struct Cookie<'a>(RawCookie<'a>);
62
24
 
63
- /// A good default `CookieStore` implementation.
64
- ///
65
- /// This is the implementation used when simply calling `cookie_store(true)`.
66
- /// This type is exposed to allow creating one and filling it with some
67
- /// existing cookies more easily, before creating a [`crate::Client`].
68
- #[derive(Debug, Default)]
69
- pub struct Jar(RwLock<HashMap<String, HashMap<String, CookieJar>>>);
70
-
71
- // ===== impl IntoCookie =====
72
-
73
- impl IntoCookie for Cookie<'_> {
74
- #[inline]
75
- fn into_cookie(self) -> Option<Cookie<'static>> {
76
- Some(self.into_owned())
77
- }
78
- }
79
-
80
- impl IntoCookie for RawCookie<'_> {
81
- #[inline]
82
- fn into_cookie(self) -> Option<Cookie<'static>> {
83
- Some(Cookie(self.into_owned()))
84
- }
85
- }
86
-
87
- impl IntoCookie for &str {
88
- #[inline]
89
- fn into_cookie(self) -> Option<Cookie<'static>> {
90
- RawCookie::parse(self).map(|c| Cookie(c.into_owned())).ok()
91
- }
92
- }
93
-
94
- // ===== impl Cookie =====
95
-
96
25
  impl<'a> Cookie<'a> {
97
26
  pub(crate) fn parse(value: &'a HeaderValue) -> crate::Result<Cookie<'a>> {
98
27
  std::str::from_utf8(value.as_bytes())
@@ -102,61 +31,69 @@ impl<'a> Cookie<'a> {
102
31
  .map(Cookie)
103
32
  }
104
33
 
105
- /// The name of the cookie.
34
+ /// Returns the name of `self`.
106
35
  #[inline]
107
36
  pub fn name(&self) -> &str {
108
37
  self.0.name()
109
38
  }
110
39
 
111
- /// The value of the cookie.
40
+ /// Returns the value of `self`.
41
+ ///
42
+ /// Does not strip surrounding quotes.
112
43
  #[inline]
113
44
  pub fn value(&self) -> &str {
114
45
  self.0.value()
115
46
  }
116
47
 
117
- /// Returns true if the 'HttpOnly' directive is enabled.
48
+ /// Returns whether this cookie was marked `HttpOnly` or not.
118
49
  #[inline]
119
50
  pub fn http_only(&self) -> bool {
120
51
  self.0.http_only().unwrap_or(false)
121
52
  }
122
53
 
123
- /// Returns true if the 'Secure' directive is enabled.
54
+ /// Returns whether this cookie was marked `Secure` or not.
124
55
  #[inline]
125
56
  pub fn secure(&self) -> bool {
126
57
  self.0.secure().unwrap_or(false)
127
58
  }
128
59
 
129
- /// Returns true if 'SameSite' directive is 'Lax'.
60
+ /// Returns whether the `SameSite` attribute of this cookie is `Lax`.
130
61
  #[inline]
131
62
  pub fn same_site_lax(&self) -> bool {
132
63
  self.0.same_site() == Some(SameSite::Lax)
133
64
  }
134
65
 
135
- /// Returns true if 'SameSite' directive is 'Strict'.
66
+ /// Returns whether the `SameSite` attribute of this cookie is `Strict`.
136
67
  #[inline]
137
68
  pub fn same_site_strict(&self) -> bool {
138
69
  self.0.same_site() == Some(SameSite::Strict)
139
70
  }
140
71
 
141
- /// Returns the path directive of the cookie, if set.
72
+ /// Returns the `Path` of the cookie if one was specified.
142
73
  #[inline]
143
74
  pub fn path(&self) -> Option<&str> {
144
75
  self.0.path()
145
76
  }
146
77
 
147
- /// Returns the domain directive of the cookie, if set.
78
+ /// Returns the `Domain` of the cookie if one was specified.
79
+ ///
80
+ /// This does not consider whether the `Domain` is valid; validation is left to higher-level
81
+ /// libraries, as needed. However, if the `Domain` starts with a leading `.`, the leading `.` is
82
+ /// stripped.
148
83
  #[inline]
149
84
  pub fn domain(&self) -> Option<&str> {
150
85
  self.0.domain()
151
86
  }
152
87
 
153
- /// Get the Max-Age information.
88
+ /// Returns the specified max-age of the cookie if it is non-negative and representable.
154
89
  #[inline]
155
90
  pub fn max_age(&self) -> Option<std::time::Duration> {
156
91
  self.0.max_age().and_then(|d| d.try_into().ok())
157
92
  }
158
93
 
159
- /// The cookie expiration time.
94
+ /// Returns the expiration date-time of the cookie if one was specified.
95
+ ///
96
+ /// Session cookies return `None`.
160
97
  #[inline]
161
98
  pub fn expires(&self) -> Option<SystemTime> {
162
99
  match self.0.expires() {
@@ -165,8 +102,7 @@ impl<'a> Cookie<'a> {
165
102
  }
166
103
  }
167
104
 
168
- /// Converts `self` into a `Cookie` with a static lifetime with as few
169
- /// allocations as possible.
105
+ /// Converts `self` into a `Cookie` with a static lifetime with as few allocations as possible.
170
106
  #[inline]
171
107
  pub fn into_owned(self) -> Cookie<'static> {
172
108
  Cookie(self.0.into_owned())
@@ -194,546 +130,106 @@ impl<'c> From<Cookie<'c>> for RawCookie<'c> {
194
130
  }
195
131
  }
196
132
 
197
- // ===== impl Jar =====
198
-
199
- macro_rules! into_uri {
200
- ($expr:expr) => {
201
- match $expr.into_uri() {
202
- Ok(u) => u,
203
- Err(_) => return,
204
- }
205
- };
206
- }
207
-
208
- impl Jar {
209
- /// Get a cookie by name for a given Uri.
210
- ///
211
- /// Returns the cookie with the specified name for the domain and path
212
- /// derived from the given Uri, if it exists.
213
- ///
214
- /// # Example
215
- /// ```
216
- /// use wreq::cookie::Jar;
217
- /// let jar = Jar::default();
218
- /// jar.add("foo=bar; Path=/foo; Domain=example.com", "http://example.com/foo");
219
- /// let cookie = jar.get("foo", "http://example.com/foo").unwrap();
220
- /// assert_eq!(cookie.value(), "bar");
221
- /// ```
222
- pub fn get<U: IntoUri>(&self, name: &str, uri: U) -> Option<Cookie<'static>> {
223
- let uri = uri.into_uri().ok()?;
224
- let host = normalize_domain(uri.host()?);
225
- let cookie = self
226
- .0
227
- .read()
228
- .get(host)?
229
- .get(uri.path())?
230
- .get(name)?
231
- .clone()
232
- .into_owned();
233
- Some(Cookie(cookie))
234
- }
133
+ /// Serialized `Cookie` field values selected for an outgoing request.
134
+ #[derive(Debug, Clone)]
135
+ #[non_exhaustive]
136
+ pub enum Cookies {
137
+ /// All cookie pairs combined into one field value.
138
+ Compressed(HeaderValue),
235
139
 
236
- /// Get all cookies in this jar.
237
- ///
238
- /// Returns an iterator over all cookies currently stored in the jar,
239
- /// regardless of domain or path.
240
- ///
241
- /// # Example
242
- /// ```
243
- /// use wreq::cookie::Jar;
244
- /// let jar = Jar::default();
245
- /// jar.add("foo=bar; Domain=example.com", "http://example.com");
246
- /// for cookie in jar.get_all() {
247
- /// println!("{}={}", cookie.name(), cookie.value());
248
- /// }
249
- /// ```
250
- pub fn get_all(&self) -> impl Iterator<Item = Cookie<'static>> {
251
- self.0
252
- .read()
253
- .iter()
254
- .flat_map(|(domain, path_map)| {
255
- path_map.iter().flat_map(|(path, name_map)| {
256
- name_map.iter().map(|cookie| {
257
- let mut cookie = cookie.clone().into_owned();
258
-
259
- if cookie.domain().is_none() {
260
- cookie.set_domain(domain.to_owned());
261
- }
262
-
263
- if cookie.path().is_none() {
264
- cookie.set_path(path.to_owned());
265
- }
266
-
267
- Cookie(cookie)
268
- })
269
- })
270
- })
271
- .collect::<Vec<_>>()
272
- .into_iter()
273
- }
140
+ /// Each cookie pair stored as a separate field value.
141
+ Uncompressed(Vec<HeaderValue>),
274
142
 
275
- /// Add a cookie to this jar.
276
- ///
277
- /// # Example
278
- ///
279
- /// ```
280
- /// use wreq::cookie::Jar;
281
- /// use cookie::CookieBuilder;
282
- /// let jar = Jar::default();
283
- /// let cookie = CookieBuilder::new("foo", "bar")
284
- /// .domain("example.com")
285
- /// .path("/")
286
- /// .build();
287
- /// jar.add(cookie, "http://example.com");
288
- ///
289
- /// let cookie = CookieBuilder::new("foo", "bar")
290
- /// .domain("example.com")
291
- /// .path("/")
292
- /// .build();
293
- /// jar.add(cookie, "http://example.com");
294
- /// ```
295
- pub fn add<C, U>(&self, cookie: C, uri: U)
296
- where
297
- C: IntoCookie,
298
- U: IntoUri,
299
- {
300
- if let Some(cookie) = cookie.into_cookie() {
301
- let uri = into_uri!(uri);
302
- let mut cookie: RawCookie<'static> = cookie.into();
303
-
304
- // If the request-uri contains no host component:
305
- let Some(host) = uri.host() else {
306
- return;
307
- };
308
-
309
- // If the canonicalized request-host does not domain-match the
310
- // domain-attribute:
311
- // Ignore the cookie entirely and abort these steps.
312
- //
313
- // RFC 6265 §5.3 + §5.1.3:
314
- // https://datatracker.ietf.org/doc/html/rfc6265#section-5.3
315
- // https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3
316
- let domain = if let Some(domain) = cookie.domain() {
317
- let domain = normalize_domain(domain);
318
- if domain.is_empty() || !domain_match(normalize_domain(host), domain) {
319
- return;
320
- }
321
- domain
322
- } else {
323
- normalize_domain(host)
324
- };
325
-
326
- // If the request-uri contains no path component or if the first character of the
327
- // path component of the request-uri is not a %x2F ("/") OR if the cookie's path-
328
- // attribute is missing or does not start with a %x2F ("/"):
329
- // Let cookie-path be the default-path of the request-uri.
330
- // Otherwise:
331
- // Let cookie-path be the substring of the request-uri's path from the first
332
- // character up to, not including, the right-most %x2F ("/").
333
- //
334
- // RFC 6265 §5.2.4 + §5.1.4:
335
- // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4
336
- // https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
337
- let path = cookie
338
- .path()
339
- .filter(|path| path.starts_with(DEFAULT_PATH))
340
- .unwrap_or_else(|| normalize_path(uri.path()));
341
-
342
- let mut inner = self.0.write();
343
- let name_map = inner
344
- .entry(domain.to_owned())
345
- .or_default()
346
- .entry(path.to_owned())
347
- .or_default();
348
-
349
- // RFC 6265: If Max-Age=0 or Expires in the past, remove the cookie
350
- let expired = cookie
351
- .expires_datetime()
352
- .is_some_and(|dt| dt <= SystemTime::now())
353
- || cookie.max_age().is_some_and(Duration::is_zero);
354
-
355
- if expired {
356
- name_map.remove(cookie);
357
- } else {
358
- cookie.set_path(path.to_owned());
359
- name_map.add(cookie);
360
- }
361
- }
362
- }
143
+ /// No cookie applies to the request.
144
+ Empty,
145
+ }
363
146
 
364
- /// Remove a cookie by name for a given Uri.
365
- ///
366
- /// Removes the cookie with the specified name for the domain and path
367
- /// derived from the given Uri, if it exists.
368
- ///
369
- /// # Example
370
- /// ```
371
- /// use wreq::cookie::Jar;
372
- /// let jar = Jar::default();
373
- /// jar.add("foo=bar; Path=/foo; Domain=example.com", "http://example.com/foo");
374
- /// assert!(jar.get("foo", "http://example.com/foo").is_some());
375
- /// jar.remove("foo", "http://example.com/foo");
376
- /// assert!(jar.get("foo", "http://example.com/foo").is_none());
377
- /// ```
378
- pub fn remove<C, U>(&self, cookie: C, uri: U)
379
- where
380
- C: Into<RawCookie<'static>>,
381
- U: IntoUri,
382
- {
383
- let uri = into_uri!(uri);
384
- if let Some(host) = uri.host() {
385
- let host = normalize_domain(host);
386
- let mut inner = self.0.write();
387
- if let Some(path_map) = inner.get_mut(host) {
388
- if let Some(name_map) = path_map.get_mut(uri.path()) {
389
- name_map.remove(cookie.into());
390
- }
391
- }
392
- }
393
- }
147
+ /// Thread-safe cookie storage used by [`crate::Client`].
148
+ ///
149
+ /// Implementors receive `Set-Cookie` response fields through [`set_cookies`](Self::set_cookies)
150
+ /// and produce `Cookie` request fields through [`cookies`](Self::cookies). Implementations are
151
+ /// responsible for applying the [RFC 6265 storage and retrieval model].
152
+ ///
153
+ /// [RFC 6265 storage and retrieval model]: https://www.rfc-editor.org/rfc/rfc6265.html#section-5
154
+ pub trait CookieStore: Send + Sync {
155
+ /// Stores `Set-Cookie` field values received from `uri`.
156
+ fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, uri: &Uri);
394
157
 
395
- /// Clear all cookies from this jar.
158
+ /// Serializes the cookies that apply to `uri` for the requested HTTP version.
396
159
  ///
397
- /// Removes all cookies from the jar, leaving it empty.
160
+ /// HTTP/1.1 combines cookie pairs into one field value using the format from
161
+ /// [RFC 6265 section 5.4]. HTTP/2 and HTTP/3 may split them across field lines to improve
162
+ /// compression, as described by [RFC 9113 section 8.2.3] and [RFC 9114 section 4.2.1].
398
163
  ///
399
- /// # Example
400
- /// ```
401
- /// use wreq::cookie::Jar;
402
- /// let jar = Jar::default();
403
- /// jar.add("foo=bar; Domain=example.com", "http://example.com");
404
- /// assert_eq!(jar.get_all().count(), 1);
405
- /// jar.clear();
406
- /// assert_eq!(jar.get_all().count(), 0);
407
- /// ```
408
- pub fn clear(&self) {
409
- self.0.write().clear();
410
- }
164
+ /// [RFC 6265 section 5.4]: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.4
165
+ /// [RFC 9113 section 8.2.3]: https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.3
166
+ /// [RFC 9114 section 4.2.1]: https://www.rfc-editor.org/rfc/rfc9114.html#section-4.2.1
167
+ fn cookies(&self, uri: &Uri, version: Version) -> Cookies;
411
168
  }
412
169
 
413
- impl CookieStore for Jar {
414
- fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, uri: &Uri) {
415
- let cookies = cookie_headers
416
- .map(Cookie::parse)
417
- .filter_map(Result::ok)
418
- .map(|cookie| cookie.0.into_owned());
170
+ /// Converts cookie stores into a shared [`CookieStore`].
171
+ pub trait IntoCookieStore {
172
+ /// Converts this value into a shared cookie store.
173
+ fn into_shared(self) -> Arc<dyn CookieStore>;
174
+ }
419
175
 
420
- for cookie in cookies {
421
- self.add(cookie, uri);
422
- }
176
+ impl IntoCookieStore for Arc<dyn CookieStore> {
177
+ #[inline]
178
+ fn into_shared(self) -> Arc<dyn CookieStore> {
179
+ self
423
180
  }
181
+ }
424
182
 
425
- fn cookies(&self, uri: &Uri, version: Version) -> Cookies {
426
- let host = match uri.host() {
427
- Some(h) => normalize_domain(h),
428
- None => return Cookies::Empty,
429
- };
430
-
431
- let store = self.0.read();
432
- let iter = store
433
- .iter()
434
- .filter(|(domain, _)| domain_match(host, domain))
435
- .flat_map(|(_, path_map)| {
436
- path_map
437
- .iter()
438
- .filter(|(path, _)| path_match(uri.path(), path))
439
- .flat_map(|(_, name_map)| {
440
- name_map.iter().filter(|cookie| {
441
- if cookie.secure() == Some(true) && uri.is_http() {
442
- return false;
443
- }
444
-
445
- if cookie
446
- .expires_datetime()
447
- .is_some_and(|dt| dt <= SystemTime::now())
448
- {
449
- return false;
450
- }
451
-
452
- true
453
- })
454
- })
455
- });
456
-
457
- if matches!(version, Version::HTTP_2 | Version::HTTP_3) {
458
- let cookies = iter
459
- .map(|cookie| {
460
- let name = cookie.name();
461
- let value = cookie.value();
462
-
463
- let mut cookie_str = String::with_capacity(name.len() + 1 + value.len());
464
- cookie_str.push_str(name);
465
- cookie_str.push('=');
466
- cookie_str.push_str(value);
467
-
468
- HeaderValue::from_maybe_shared(Bytes::from(cookie_str))
469
- })
470
- .filter_map(Result::ok)
471
- .collect();
472
-
473
- Cookies::Uncompressed(cookies)
474
- } else {
475
- let cookies = iter.fold(String::new(), |mut cookies, cookie| {
476
- if !cookies.is_empty() {
477
- cookies.push_str("; ");
478
- }
479
- cookies.push_str(cookie.name());
480
- cookies.push('=');
481
- cookies.push_str(cookie.value());
482
- cookies
483
- });
484
-
485
- if cookies.is_empty() {
486
- return Cookies::Empty;
487
- }
488
-
489
- HeaderValue::from_maybe_shared(Bytes::from(cookies))
490
- .map(Cookies::Compressed)
491
- .unwrap_or(Cookies::Empty)
492
- }
183
+ impl<S: CookieStore + 'static> IntoCookieStore for Arc<S> {
184
+ #[inline]
185
+ fn into_shared(self) -> Arc<dyn CookieStore> {
186
+ self
493
187
  }
494
188
  }
495
189
 
496
- const DEFAULT_PATH: &str = "/";
497
-
498
- /// Determines if the given `host` matches the cookie `domain` according to
499
- /// [RFC 6265 section 5.1.3](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3).
500
- ///
501
- /// - Returns true if the host and domain are identical.
502
- /// - Returns true if the host is a subdomain of the domain (host ends with ".domain").
503
- /// - Returns false otherwise.
504
- fn domain_match(host: &str, domain: &str) -> bool {
505
- if domain.is_empty() {
506
- return false;
507
- }
508
- if host == domain {
509
- return true;
190
+ impl<S: CookieStore + 'static> IntoCookieStore for S {
191
+ #[inline]
192
+ fn into_shared(self) -> Arc<dyn CookieStore> {
193
+ Arc::new(self)
510
194
  }
511
- host.len() > domain.len()
512
- && host.as_bytes()[host.len() - domain.len() - 1] == b'.'
513
- && host.ends_with(domain)
514
195
  }
515
196
 
516
- /// Determines if the request path matches the cookie path according to
517
- /// [RFC 6265 section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4).
518
- ///
519
- /// - Returns true if the request path and cookie path are identical.
520
- /// - Returns true if the request path starts with the cookie path, and
521
- /// - the cookie path ends with '/', or
522
- /// - the next character in the request path after the cookie path is '/'.
523
- /// - Returns false otherwise.
524
- fn path_match(req_path: &str, cookie_path: &str) -> bool {
525
- req_path == cookie_path
526
- || req_path.starts_with(cookie_path)
527
- && (cookie_path.ends_with(DEFAULT_PATH)
528
- || req_path[cookie_path.len()..].starts_with(DEFAULT_PATH))
529
- }
197
+ impl_request_config_value!(Arc<dyn CookieStore>);
530
198
 
531
- /// Normalizes a domain by stripping any port information.
199
+ /// Converts cookie-like values into an owned [`Cookie`].
532
200
  ///
533
- /// According to [RFC 6265 section 5.2.3](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3),
534
- /// the domain attribute of a cookie must not include a port. If a port is present (non-standard),
535
- /// it will be ignored for domain matching purposes.
536
- fn normalize_domain(domain: &str) -> &str {
537
- let host_without_port = domain.split(':').next().unwrap_or(domain);
538
- let without_leading = host_without_port
539
- .strip_prefix(".")
540
- .unwrap_or(host_without_port);
541
- without_leading.strip_suffix(".").unwrap_or(without_leading)
201
+ /// String implementations return `None` when the value cannot be parsed as a `Set-Cookie` value.
202
+ pub trait IntoCookie {
203
+ /// Converts this value into an optional owned [`Cookie`].
204
+ fn into_cookie(self) -> Option<Cookie<'static>>;
542
205
  }
543
206
 
544
- /// Computes the normalized default path for a cookie as specified in
545
- /// [RFC 6265 section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4).
546
- ///
547
- /// This function normalizes the path for a cookie, ensuring it matches
548
- /// browser and server expectations for default cookie scope.
549
- fn normalize_path(path: &str) -> &str {
550
- if !path.starts_with(DEFAULT_PATH) {
551
- return DEFAULT_PATH;
552
- }
553
- if let Some(pos) = path.rfind(DEFAULT_PATH) {
554
- if pos == 0 {
555
- return DEFAULT_PATH;
556
- }
557
- return &path[..pos];
207
+ impl IntoCookie for Cookie<'_> {
208
+ #[inline]
209
+ fn into_cookie(self) -> Option<Cookie<'static>> {
210
+ Some(self.into_owned())
558
211
  }
559
- DEFAULT_PATH
560
212
  }
561
213
 
562
- #[cfg(test)]
563
- mod tests {
564
- use http::{Uri, Version};
565
-
566
- use super::{CookieStore, Cookies, Jar};
567
-
568
- #[test]
569
- fn jar_get_all_backfills_domain_and_path() {
570
- let jar = Jar::default();
571
- jar.add("session=abc", "http://example.com/foo/bar");
572
-
573
- let cookies = jar.get_all().collect::<Vec<_>>();
574
- assert_eq!(cookies.len(), 1);
575
-
576
- let cookie = &cookies[0];
577
- assert_eq!(cookie.name(), "session");
578
- assert_eq!(cookie.value(), "abc");
579
- assert_eq!(cookie.domain(), Some("example.com"));
580
- assert_eq!(cookie.path(), Some("/foo"));
581
- }
582
-
583
- #[test]
584
- fn jar_get_all_keeps_existing_domain_and_path() {
585
- let jar = Jar::default();
586
- jar.add(
587
- "session=abc; Domain=example.com; Path=/custom",
588
- "http://example.com/foo/bar",
589
- );
590
-
591
- let cookies = jar.get_all().collect::<Vec<_>>();
592
- assert_eq!(cookies.len(), 1);
593
-
594
- let cookie = &cookies[0];
595
- assert_eq!(cookie.name(), "session");
596
- assert_eq!(cookie.value(), "abc");
597
- assert_eq!(cookie.domain(), Some("example.com"));
598
- assert_eq!(cookie.path(), Some("/custom"));
599
- }
600
-
601
- #[test]
602
- fn jar_get_all_backfills_only_missing_field() {
603
- let jar = Jar::default();
604
- jar.add("a=1; Domain=example.com", "http://example.com/foo/bar");
605
- jar.add("b=2; Path=/fixed", "http://example.com/foo/bar");
606
-
607
- let mut cookies = jar.get_all().collect::<Vec<_>>();
608
- cookies.sort_by(|left, right| left.name().cmp(right.name()));
609
-
610
- let a = &cookies[0];
611
- assert_eq!(a.name(), "a");
612
- assert_eq!(a.domain(), Some("example.com"));
613
- assert_eq!(a.path(), Some("/foo"));
614
-
615
- let b = &cookies[1];
616
- assert_eq!(b.name(), "b");
617
- assert_eq!(b.domain(), Some("example.com"));
618
- assert_eq!(b.path(), Some("/fixed"));
619
- }
620
-
621
- #[test]
622
- fn jar_add_rejects_mismatched_domain() {
623
- let jar = Jar::default();
624
- jar.add("session=abc; Domain=other.com", "http://example.com/foo");
625
-
626
- assert_eq!(jar.get_all().count(), 0);
627
- }
628
-
629
- #[test]
630
- fn jar_add_accepts_matching_parent_domain() {
631
- let jar = Jar::default();
632
- jar.add(
633
- "session=abc; Domain=example.com",
634
- "http://api.example.com/foo",
635
- );
636
-
637
- let cookies = jar.get_all().collect::<Vec<_>>();
638
- assert_eq!(cookies.len(), 1);
639
- assert_eq!(cookies[0].domain(), Some("example.com"));
640
- }
641
-
642
- #[test]
643
- fn jar_get_all_export_import_keeps_effective_path() {
644
- let source = Jar::default();
645
- source.add("session=abc", "http://example.com/foo/bar");
646
-
647
- let exported = source.get_all().collect::<Vec<_>>();
648
- assert_eq!(exported.len(), 1);
649
- assert_eq!(exported[0].path(), Some("/foo"));
650
-
651
- let target = Jar::default();
652
- for cookie in exported {
653
- target.add(cookie, "http://example.com/another/deeper");
654
- }
655
-
656
- let imported = target.get_all().collect::<Vec<_>>();
657
- assert_eq!(imported.len(), 1);
658
- assert_eq!(imported[0].path(), Some("/foo"));
659
- }
660
-
661
- #[test]
662
- fn cookie_store_invalid_explicit_path_falls_back_to_default_path() {
663
- let jar = Jar::default();
664
- jar.add("key=val; Path=noslash", "http://example.com/foo/bar");
665
-
666
- assert!(jar.get("key", "http://example.com/foo").is_some());
667
- assert!(jar.get("key", "http://example.com/noslash").is_none());
668
-
669
- let cookies = jar.get_all().collect::<Vec<_>>();
670
- assert_eq!(cookies.len(), 1);
671
- assert_eq!(cookies[0].path(), Some("/foo"));
214
+ impl IntoCookie for cookie::Cookie<'_> {
215
+ #[inline]
216
+ fn into_cookie(self) -> Option<Cookie<'static>> {
217
+ Some(Cookie::from(self.into_owned()))
672
218
  }
219
+ }
673
220
 
674
- #[test]
675
- fn jar_sends_parent_domain_cookie_to_subdomain() {
676
- let jar = Jar::default();
677
- jar.add(
678
- "session=abc; Domain=example.com; Path=/",
679
- "http://example.com/login",
680
- );
681
-
682
- let should_receive = [
683
- "http://example.com/dashboard",
684
- "http://api.example.com/dashboard",
685
- "http://sub.api.example.com/dashboard",
686
- ];
687
- for uri_str in &should_receive {
688
- let uri = Uri::from_static(uri_str);
689
- match jar.cookies(&uri, Version::HTTP_11) {
690
- Cookies::Compressed(v) => assert_eq!(
691
- v.to_str().unwrap(),
692
- "session=abc",
693
- "expected cookie to be sent to {uri_str}"
694
- ),
695
- other => panic!("expected Compressed cookie for {uri_str}, got {other:?}"),
696
- }
697
- }
698
-
699
- let should_not_receive = [
700
- "http://notexample.com/dashboard",
701
- "http://fakeexample.com/dashboard",
702
- ];
703
- for uri_str in &should_not_receive {
704
- let uri = Uri::from_static(uri_str);
705
- assert!(
706
- matches!(jar.cookies(&uri, Version::HTTP_11), Cookies::Empty),
707
- "cookie must NOT be sent to {uri_str}"
708
- );
709
- }
221
+ impl IntoCookie for &str {
222
+ #[inline]
223
+ fn into_cookie(self) -> Option<Cookie<'static>> {
224
+ cookie::Cookie::parse(self)
225
+ .map(|cookie| Cookie::from(cookie.into_owned()))
226
+ .ok()
710
227
  }
228
+ }
711
229
 
712
- #[test]
713
- fn jar_subdomain_cookie_does_not_leak_to_parent_or_sibling() {
714
- let jar = Jar::default();
715
- jar.add(
716
- "token=xyz; Domain=api.example.com; Path=/",
717
- "http://api.example.com/",
718
- );
719
-
720
- let uri = Uri::from_static("http://api.example.com/");
721
- assert!(
722
- matches!(jar.cookies(&uri, Version::HTTP_11), Cookies::Compressed(_)),
723
- "cookie must be sent to api.example.com"
724
- );
725
-
726
- let must_not_receive = [
727
- "http://example.com/",
728
- "http://other.example.com/",
729
- "http://notapi.example.com/",
730
- ];
731
- for uri_str in &must_not_receive {
732
- let uri = Uri::from_static(uri_str);
733
- assert!(
734
- matches!(jar.cookies(&uri, Version::HTTP_11), Cookies::Empty),
735
- "cookie must NOT leak to {uri_str}"
736
- );
737
- }
230
+ impl IntoCookie for String {
231
+ #[inline]
232
+ fn into_cookie(self) -> Option<Cookie<'static>> {
233
+ cookie::Cookie::parse(self).map(Cookie::from).ok()
738
234
  }
739
235
  }