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.
- checksums.yaml +4 -4
- data/Cargo.lock +257 -102
- data/ext/wreq_rb/Cargo.toml +7 -4
- data/ext/wreq_rb/src/client.rs +105 -15
- data/ext/wreq_rb/src/response.rs +28 -3
- data/lib/wreq-rb/version.rb +1 -1
- data/patches/0001-add-transfer-size-tracking.patch +11 -15
- data/vendor/wreq/Cargo.toml +9 -8
- data/vendor/wreq/README.md +5 -5
- data/vendor/wreq/bench/support/bench.rs +6 -2
- data/vendor/wreq/bench/support/client.rs +88 -1
- data/vendor/wreq/bench/support/exec.rs +0 -0
- data/vendor/wreq/bench/support/rt.rs +34 -0
- data/vendor/wreq/bench/support/server.rs +1 -1
- data/vendor/wreq/bench/support.rs +1 -15
- data/vendor/wreq/examples/cert_store.rs +13 -13
- data/vendor/wreq/examples/request_with_emulate.rs +1 -1
- data/vendor/wreq/examples/tcp_linger.rs +22 -0
- data/vendor/wreq/src/client/layer/client/pool.rs +17 -17
- data/vendor/wreq/src/client/layer/client.rs +2 -0
- data/vendor/wreq/src/client/layer/decoder.rs +71 -17
- data/vendor/wreq/src/client/layer/redirect/future.rs +49 -63
- data/vendor/wreq/src/client/layer/redirect/policy.rs +2 -26
- data/vendor/wreq/src/client/layer/redirect.rs +48 -60
- data/vendor/wreq/src/client/layer/retry.rs +12 -15
- data/vendor/wreq/src/client/layer/timeout/body.rs +27 -21
- data/vendor/wreq/src/client/layer/timeout/future.rs +33 -58
- data/vendor/wreq/src/client/layer/timeout.rs +8 -14
- data/vendor/wreq/src/client/request.rs +4 -0
- data/vendor/wreq/src/client.rs +53 -31
- data/vendor/wreq/src/conn/connector.rs +99 -129
- data/vendor/wreq/src/conn/http.rs +25 -18
- data/vendor/wreq/src/conn/net/tcp.rs +601 -107
- data/vendor/wreq/src/conn/proxy/socks.rs +6 -6
- data/vendor/wreq/src/conn/timeout.rs +166 -0
- data/vendor/wreq/src/conn.rs +5 -4
- data/vendor/wreq/src/cookie/jar.rs +1225 -0
- data/vendor/wreq/src/cookie/store.rs +321 -0
- data/vendor/wreq/src/cookie.rs +108 -612
- data/vendor/wreq/src/dns/resolve.rs +8 -2
- data/vendor/wreq/src/dns.rs +4 -4
- data/vendor/wreq/src/error.rs +53 -20
- data/vendor/wreq/src/lib.rs +1 -0
- data/vendor/wreq/src/proxy/matcher.rs +26 -12
- data/vendor/wreq/src/proxy/win.rs +39 -9
- data/vendor/wreq/src/redirect.rs +515 -100
- data/vendor/wreq/src/tls/conn.rs +3 -11
- data/vendor/wreq/src/tls/session.rs +7 -8
- data/vendor/wreq/src/tls/trust/store.rs +4 -4
- data/vendor/wreq/src/util.rs +23 -0
- data/vendor/wreq/tests/badssl.rs +72 -7
- data/vendor/wreq/tests/brotli.rs +1 -1
- data/vendor/wreq/tests/client.rs +24 -0
- data/vendor/wreq/tests/connector_layers.rs +8 -4
- data/vendor/wreq/tests/cookie.rs +59 -0
- data/vendor/wreq/tests/deflate.rs +1 -1
- data/vendor/wreq/tests/gzip.rs +53 -1
- data/vendor/wreq/tests/layers.rs +8 -4
- data/vendor/wreq/tests/redirect.rs +180 -97
- data/vendor/wreq/tests/timeouts.rs +47 -12
- data/vendor/wreq/tests/zstd.rs +1 -1
- metadata +8 -2
data/vendor/wreq/src/cookie.rs
CHANGED
|
@@ -1,98 +1,27 @@
|
|
|
1
|
-
//! HTTP
|
|
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
|
-
|
|
10
|
+
mod jar;
|
|
11
|
+
mod store;
|
|
4
12
|
|
|
5
|
-
use
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
///
|
|
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
|
-
///
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
///
|
|
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
|
-
///
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
///
|
|
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
|
-
///
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
///
|
|
373
|
-
|
|
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
|
-
///
|
|
158
|
+
/// Serializes the cookies that apply to `uri` for the requested HTTP version.
|
|
396
159
|
///
|
|
397
|
-
///
|
|
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
|
-
/// #
|
|
400
|
-
///
|
|
401
|
-
///
|
|
402
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
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
|
-
///
|
|
199
|
+
/// Converts cookie-like values into an owned [`Cookie`].
|
|
532
200
|
///
|
|
533
|
-
///
|
|
534
|
-
|
|
535
|
-
///
|
|
536
|
-
fn
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
}
|