wreq-rb 0.6.0 → 0.6.1
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 +4 -4
- data/ext/wreq_rb/src/client.rs +1 -1
- 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 +7 -1
|
@@ -0,0 +1,1225 @@
|
|
|
1
|
+
use bytes::Bytes;
|
|
2
|
+
use cookie::{
|
|
3
|
+
Cookie as RawCookie,
|
|
4
|
+
time::{Duration, OffsetDateTime},
|
|
5
|
+
};
|
|
6
|
+
use http::{Uri, Version};
|
|
7
|
+
|
|
8
|
+
use super::{
|
|
9
|
+
Cookie, CookieStore, Cookies, IntoCookie,
|
|
10
|
+
store::{DEFAULT_PATH, Store, canonical_host, cookie_is_expired, domain_match, normalize_path},
|
|
11
|
+
};
|
|
12
|
+
use crate::{IntoUri, ext::UriExt, header::HeaderValue, sync::RwLock};
|
|
13
|
+
|
|
14
|
+
/// A good default `CookieStore` implementation.
|
|
15
|
+
///
|
|
16
|
+
/// This is the implementation used when simply calling `cookie_store(true)`.
|
|
17
|
+
/// This type is exposed to allow creating one and filling it with some
|
|
18
|
+
/// existing cookies more easily, before creating a [`crate::Client`].
|
|
19
|
+
#[derive(Debug, Default)]
|
|
20
|
+
pub struct Jar(RwLock<Store>);
|
|
21
|
+
|
|
22
|
+
macro_rules! into_uri {
|
|
23
|
+
($expr:expr) => {
|
|
24
|
+
match $expr.into_uri() {
|
|
25
|
+
Ok(u) => u,
|
|
26
|
+
Err(_) => return,
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl Jar {
|
|
32
|
+
/// Returns an unexpired cookie by name for an exact URI scope.
|
|
33
|
+
///
|
|
34
|
+
/// The URI host is canonicalized and its path is used as the exact stored cookie path. Use
|
|
35
|
+
/// [`matches`](Self::matches) to select every cookie that would apply to a request URI through
|
|
36
|
+
/// RFC domain and path matching. When both storage scopes contain the name, the older cookie is
|
|
37
|
+
/// returned.
|
|
38
|
+
///
|
|
39
|
+
/// # Example
|
|
40
|
+
/// ```
|
|
41
|
+
/// use wreq::cookie::Jar;
|
|
42
|
+
/// let jar = Jar::default();
|
|
43
|
+
/// jar.add("foo=bar; Path=/foo; Domain=example.com", "http://example.com/foo");
|
|
44
|
+
/// let cookie = jar.get("foo", "http://example.com/foo").unwrap();
|
|
45
|
+
/// assert_eq!(cookie.value(), "bar");
|
|
46
|
+
/// ```
|
|
47
|
+
pub fn get<U: IntoUri>(&self, name: &str, uri: U) -> Option<Cookie<'static>> {
|
|
48
|
+
let uri = uri.into_uri().ok()?;
|
|
49
|
+
let host = canonical_host(uri.host()?)?;
|
|
50
|
+
let now = OffsetDateTime::now_utc();
|
|
51
|
+
let store = self.0.read();
|
|
52
|
+
let cookie = store
|
|
53
|
+
.cookies
|
|
54
|
+
.get(&host)?
|
|
55
|
+
.get(uri.path())?
|
|
56
|
+
.entries(name)
|
|
57
|
+
.filter(|entry| !cookie_is_expired(&entry.cookie, now))
|
|
58
|
+
.min_by_key(|entry| entry.creation_index)?;
|
|
59
|
+
|
|
60
|
+
Some(Cookie::from(cookie.cookie.clone()))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Returns whether an unexpired cookie exists for an exact URI scope.
|
|
64
|
+
///
|
|
65
|
+
/// This performs the same scope lookup as [`get`](Self::get) without cloning the cookie.
|
|
66
|
+
pub fn contains<U: IntoUri>(&self, name: &str, uri: U) -> bool {
|
|
67
|
+
let Ok(uri) = uri.into_uri() else {
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
let Some(host) = uri.host().and_then(canonical_host) else {
|
|
71
|
+
return false;
|
|
72
|
+
};
|
|
73
|
+
let now = OffsetDateTime::now_utc();
|
|
74
|
+
|
|
75
|
+
self.0
|
|
76
|
+
.read()
|
|
77
|
+
.cookies
|
|
78
|
+
.get(&host)
|
|
79
|
+
.and_then(|path_map| path_map.get(uri.path()))
|
|
80
|
+
.is_some_and(|cookie_map| {
|
|
81
|
+
cookie_map
|
|
82
|
+
.entries(name)
|
|
83
|
+
.any(|entry| !cookie_is_expired(&entry.cookie, now))
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Returns all unexpired cookies in the jar.
|
|
88
|
+
///
|
|
89
|
+
/// The returned cookies are owned snapshots with their effective stored `Path`. Host-only
|
|
90
|
+
/// cookies keep their `Domain` attribute absent, so importing a snapshot into another jar does
|
|
91
|
+
/// not broaden its domain scope. Snapshots are returned in creation order.
|
|
92
|
+
///
|
|
93
|
+
/// # Example
|
|
94
|
+
/// ```
|
|
95
|
+
/// use wreq::cookie::Jar;
|
|
96
|
+
/// let jar = Jar::default();
|
|
97
|
+
/// jar.add("foo=bar; Domain=example.com", "http://example.com");
|
|
98
|
+
/// for cookie in jar.get_all() {
|
|
99
|
+
/// println!("{}={}", cookie.name(), cookie.value());
|
|
100
|
+
/// }
|
|
101
|
+
/// ```
|
|
102
|
+
pub fn get_all(&self) -> impl Iterator<Item = Cookie<'static>> {
|
|
103
|
+
let now = OffsetDateTime::now_utc();
|
|
104
|
+
let mut cookies = self
|
|
105
|
+
.0
|
|
106
|
+
.read()
|
|
107
|
+
.cookies
|
|
108
|
+
.values()
|
|
109
|
+
.flat_map(|path_map| {
|
|
110
|
+
path_map.values().flat_map(|cookie_map| {
|
|
111
|
+
cookie_map.values().filter_map(|entry| {
|
|
112
|
+
if cookie_is_expired(&entry.cookie, now) {
|
|
113
|
+
return None;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Some((entry.creation_index, Cookie::from(entry.cookie.clone())))
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
.collect::<Vec<_>>();
|
|
121
|
+
cookies.sort_unstable_by_key(|(creation_index, _)| *creation_index);
|
|
122
|
+
cookies.into_iter().map(|(_, cookie)| cookie)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Returns the number of unexpired cookies in the jar.
|
|
126
|
+
///
|
|
127
|
+
/// Expired cookies are excluded even if their internal entries have not yet been overwritten or
|
|
128
|
+
/// cleared.
|
|
129
|
+
pub fn len(&self) -> usize {
|
|
130
|
+
let now = OffsetDateTime::now_utc();
|
|
131
|
+
self.0
|
|
132
|
+
.read()
|
|
133
|
+
.cookies
|
|
134
|
+
.values()
|
|
135
|
+
.flat_map(|path_map| path_map.values())
|
|
136
|
+
.flat_map(|cookie_map| cookie_map.values())
|
|
137
|
+
.filter(|entry| !cookie_is_expired(&entry.cookie, now))
|
|
138
|
+
.count()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Returns `true` when the jar has no unexpired cookies.
|
|
142
|
+
pub fn is_empty(&self) -> bool {
|
|
143
|
+
let now = OffsetDateTime::now_utc();
|
|
144
|
+
!self
|
|
145
|
+
.0
|
|
146
|
+
.read()
|
|
147
|
+
.cookies
|
|
148
|
+
.values()
|
|
149
|
+
.flat_map(|path_map| path_map.values())
|
|
150
|
+
.flat_map(|cookie_map| cookie_map.values())
|
|
151
|
+
.any(|entry| !cookie_is_expired(&entry.cookie, now))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Returns the unexpired cookies that apply to an HTTP or HTTPS request URI.
|
|
155
|
+
///
|
|
156
|
+
/// Selection applies domain matching, path matching, host-only scope, the `Secure` attribute,
|
|
157
|
+
/// and expiration rules from the [RFC 6265 retrieval model]. Returned cookies are owned
|
|
158
|
+
/// snapshots that preserve host-only scope. Unsupported URI schemes do not match any cookies.
|
|
159
|
+
///
|
|
160
|
+
/// # Example
|
|
161
|
+
///
|
|
162
|
+
/// ```
|
|
163
|
+
/// use wreq::cookie::Jar;
|
|
164
|
+
///
|
|
165
|
+
/// let jar = Jar::default();
|
|
166
|
+
/// jar.add("root=1; Domain=example.com; Path=/", "https://example.com/");
|
|
167
|
+
/// jar.add("api=2; Domain=example.com; Path=/api", "https://example.com/api");
|
|
168
|
+
///
|
|
169
|
+
/// let cookies = jar
|
|
170
|
+
/// .matches("https://www.example.com/api/users")
|
|
171
|
+
/// .collect::<Vec<_>>();
|
|
172
|
+
/// assert_eq!(cookies.len(), 2);
|
|
173
|
+
/// assert!(cookies.iter().any(|cookie| cookie.name() == "root"));
|
|
174
|
+
/// assert!(cookies.iter().any(|cookie| cookie.name() == "api"));
|
|
175
|
+
/// ```
|
|
176
|
+
///
|
|
177
|
+
/// [RFC 6265 retrieval model]: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.4
|
|
178
|
+
pub fn matches<U: IntoUri>(&self, uri: U) -> impl Iterator<Item = Cookie<'static>> {
|
|
179
|
+
let Ok(uri) = uri.into_uri() else {
|
|
180
|
+
return Vec::new().into_iter();
|
|
181
|
+
};
|
|
182
|
+
let Some(host) = uri.host().and_then(canonical_host) else {
|
|
183
|
+
return Vec::new().into_iter();
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
let now = OffsetDateTime::now_utc();
|
|
187
|
+
let store = self.0.read();
|
|
188
|
+
store
|
|
189
|
+
.matching_cookies(&uri, &host, now)
|
|
190
|
+
.map(|(_, _, entry)| Cookie::from(entry.cookie.clone()))
|
|
191
|
+
.collect::<Vec<_>>()
|
|
192
|
+
.into_iter()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Stores a cookie received from a URI.
|
|
196
|
+
///
|
|
197
|
+
/// The URI supplies the host-only domain and default path when those attributes are absent.
|
|
198
|
+
/// Cookies with an invalid URI or an invalid or mismatched `Domain` are ignored. A non-positive
|
|
199
|
+
/// `Max-Age` removes an existing cookie in the same scope, as required by the
|
|
200
|
+
/// [RFC 6265 storage model]. A positive `Max-Age` is stored as an absolute deadline. Insecure
|
|
201
|
+
/// origins cannot set `Secure` cookies or overlay an existing `Secure` cookie, following the
|
|
202
|
+
/// [RFC 6265bis storage model].
|
|
203
|
+
///
|
|
204
|
+
/// # Example
|
|
205
|
+
///
|
|
206
|
+
/// ```
|
|
207
|
+
/// use wreq::cookie::Jar;
|
|
208
|
+
/// use cookie::CookieBuilder;
|
|
209
|
+
/// let jar = Jar::default();
|
|
210
|
+
/// let cookie = CookieBuilder::new("foo", "bar")
|
|
211
|
+
/// .domain("example.com")
|
|
212
|
+
/// .path("/")
|
|
213
|
+
/// .build();
|
|
214
|
+
/// jar.add(cookie, "http://example.com");
|
|
215
|
+
/// ```
|
|
216
|
+
///
|
|
217
|
+
/// [RFC 6265 storage model]: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.3
|
|
218
|
+
/// [RFC 6265bis storage model]: https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#section-5.7
|
|
219
|
+
pub fn add<C, U>(&self, cookie: C, uri: U)
|
|
220
|
+
where
|
|
221
|
+
C: IntoCookie,
|
|
222
|
+
U: IntoUri,
|
|
223
|
+
{
|
|
224
|
+
if let Some(cookie) = cookie.into_cookie() {
|
|
225
|
+
let uri = into_uri!(uri);
|
|
226
|
+
let mut cookie: RawCookie<'static> = cookie.into();
|
|
227
|
+
let secure_origin = uri.is_https();
|
|
228
|
+
|
|
229
|
+
if cookie.secure() == Some(true) && !secure_origin {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// If the request-uri contains no host component:
|
|
234
|
+
let Some(host) = uri.host().and_then(canonical_host) else {
|
|
235
|
+
return;
|
|
236
|
+
};
|
|
237
|
+
let host_only = cookie.domain().is_none();
|
|
238
|
+
|
|
239
|
+
// If the canonicalized request-host does not domain-match the
|
|
240
|
+
// domain-attribute:
|
|
241
|
+
// Ignore the cookie entirely and abort these steps.
|
|
242
|
+
//
|
|
243
|
+
// RFC 6265 §5.3 + §5.1.3:
|
|
244
|
+
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.3
|
|
245
|
+
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3
|
|
246
|
+
let domain = if let Some(raw_domain) = cookie.domain() {
|
|
247
|
+
let Some(domain) = canonical_host(raw_domain) else {
|
|
248
|
+
return;
|
|
249
|
+
};
|
|
250
|
+
if !domain_match(&host, &domain) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
cookie.set_domain(domain.to_string());
|
|
255
|
+
domain
|
|
256
|
+
} else {
|
|
257
|
+
host
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Max-Age takes precedence over Expires and is relative to when the cookie is
|
|
261
|
+
// received. Store its effective deadline so every read path applies the same
|
|
262
|
+
// expiration decision. RFC 6265 sections 5.2.2 and 5.3:
|
|
263
|
+
// https://www.rfc-editor.org/rfc/rfc6265.html#section-5.2.2
|
|
264
|
+
// https://www.rfc-editor.org/rfc/rfc6265.html#section-5.3
|
|
265
|
+
let now = OffsetDateTime::now_utc();
|
|
266
|
+
let expired = match cookie.max_age() {
|
|
267
|
+
Some(max_age) if max_age <= Duration::ZERO => true,
|
|
268
|
+
Some(max_age) => {
|
|
269
|
+
let deadline = now.saturating_add(max_age);
|
|
270
|
+
cookie.set_max_age(None);
|
|
271
|
+
cookie.set_expires(deadline);
|
|
272
|
+
false
|
|
273
|
+
}
|
|
274
|
+
None => cookie
|
|
275
|
+
.expires_datetime()
|
|
276
|
+
.is_some_and(|deadline| deadline <= now),
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// If the request-uri contains no path component or if the first character of the
|
|
280
|
+
// path component of the request-uri is not a %x2F ("/") OR if the cookie's path-
|
|
281
|
+
// attribute is missing or does not start with a %x2F ("/"):
|
|
282
|
+
// Let cookie-path be the default-path of the request-uri.
|
|
283
|
+
// Otherwise:
|
|
284
|
+
// Let cookie-path be the substring of the request-uri's path from the first
|
|
285
|
+
// character up to, not including, the right-most %x2F ("/").
|
|
286
|
+
//
|
|
287
|
+
// RFC 6265 §5.2.4 + §5.1.4:
|
|
288
|
+
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4
|
|
289
|
+
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
|
|
290
|
+
let path = cookie
|
|
291
|
+
.path()
|
|
292
|
+
.filter(|path| path.starts_with(DEFAULT_PATH))
|
|
293
|
+
.unwrap_or_else(|| normalize_path(uri.path()))
|
|
294
|
+
.to_owned();
|
|
295
|
+
|
|
296
|
+
let mut store = self.0.write();
|
|
297
|
+
if !secure_origin
|
|
298
|
+
&& store.would_overlay_secure_cookie(cookie.name(), &domain, &path, now)
|
|
299
|
+
{
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if expired {
|
|
304
|
+
store.remove_stored_cookie(&domain, &path, cookie.name(), host_only);
|
|
305
|
+
} else {
|
|
306
|
+
cookie.set_path(path.clone());
|
|
307
|
+
store.insert_stored_cookie(domain, path, cookie);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/// Removes a cookie from an exact URI scope.
|
|
313
|
+
///
|
|
314
|
+
/// The URI host and path identify the stored scope. Both host-only and `Domain` cookies with
|
|
315
|
+
/// the same name in that scope are removed; other domains and paths are left unchanged.
|
|
316
|
+
///
|
|
317
|
+
/// # Example
|
|
318
|
+
/// ```
|
|
319
|
+
/// use wreq::cookie::Jar;
|
|
320
|
+
/// let jar = Jar::default();
|
|
321
|
+
/// jar.add("foo=bar; Path=/foo; Domain=example.com", "http://example.com/foo");
|
|
322
|
+
/// assert!(jar.get("foo", "http://example.com/foo").is_some());
|
|
323
|
+
/// jar.remove("foo", "http://example.com/foo");
|
|
324
|
+
/// assert!(jar.get("foo", "http://example.com/foo").is_none());
|
|
325
|
+
/// ```
|
|
326
|
+
pub fn remove<C, U>(&self, cookie: C, uri: U)
|
|
327
|
+
where
|
|
328
|
+
C: Into<RawCookie<'static>>,
|
|
329
|
+
U: IntoUri,
|
|
330
|
+
{
|
|
331
|
+
let uri = into_uri!(uri);
|
|
332
|
+
if let Some(host) = uri.host() {
|
|
333
|
+
let Some(host) = canonical_host(host) else {
|
|
334
|
+
return;
|
|
335
|
+
};
|
|
336
|
+
let cookie = cookie.into();
|
|
337
|
+
let mut store = self.0.write();
|
|
338
|
+
store.remove_stored_cookies(&host, uri.path(), cookie.name());
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// Removes every cookie from the jar, leaving it empty.
|
|
343
|
+
///
|
|
344
|
+
/// # Example
|
|
345
|
+
/// ```
|
|
346
|
+
/// use wreq::cookie::Jar;
|
|
347
|
+
/// let jar = Jar::default();
|
|
348
|
+
/// jar.add("foo=bar; Domain=example.com", "http://example.com");
|
|
349
|
+
/// assert_eq!(jar.get_all().count(), 1);
|
|
350
|
+
/// jar.clear();
|
|
351
|
+
/// assert_eq!(jar.get_all().count(), 0);
|
|
352
|
+
/// ```
|
|
353
|
+
pub fn clear(&self) {
|
|
354
|
+
*self.0.write() = Store::default();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
impl CookieStore for Jar {
|
|
359
|
+
fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, uri: &Uri) {
|
|
360
|
+
let cookies = cookie_headers
|
|
361
|
+
.map(Cookie::parse)
|
|
362
|
+
.filter_map(Result::ok)
|
|
363
|
+
.map(|cookie| RawCookie::from(cookie).into_owned());
|
|
364
|
+
|
|
365
|
+
for cookie in cookies {
|
|
366
|
+
self.add(cookie, uri);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
fn cookies(&self, uri: &Uri, version: Version) -> Cookies {
|
|
371
|
+
let host = match uri.host() {
|
|
372
|
+
Some(host) => match canonical_host(host) {
|
|
373
|
+
Some(host) => host,
|
|
374
|
+
None => return Cookies::Empty,
|
|
375
|
+
},
|
|
376
|
+
None => return Cookies::Empty,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
let now = OffsetDateTime::now_utc();
|
|
380
|
+
let store = self.0.read();
|
|
381
|
+
let mut matches = store.matching_cookies(uri, &host, now).collect::<Vec<_>>();
|
|
382
|
+
|
|
383
|
+
// Chromium sorts cookies selected for a request by longest path first, then oldest
|
|
384
|
+
// creation time. Cookie names and domains do not participate in the comparison.
|
|
385
|
+
// RFC 6265 section 5.4: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.4
|
|
386
|
+
// Chromium: https://chromium.googlesource.com/chromium/src/+/main/net/cookies/cookie_monster.cc
|
|
387
|
+
matches.sort_unstable_by(|(_, left_path, left), (_, right_path, right)| {
|
|
388
|
+
right_path
|
|
389
|
+
.len()
|
|
390
|
+
.cmp(&left_path.len())
|
|
391
|
+
.then_with(|| left.creation_index.cmp(&right.creation_index))
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
let iter = matches.into_iter().map(|(_, _, entry)| &entry.cookie);
|
|
395
|
+
|
|
396
|
+
if matches!(version, Version::HTTP_2 | Version::HTTP_3) {
|
|
397
|
+
let cookies = iter
|
|
398
|
+
.map(|cookie| {
|
|
399
|
+
let name = cookie.name();
|
|
400
|
+
let value = cookie.value();
|
|
401
|
+
|
|
402
|
+
let mut cookie_str = String::with_capacity(name.len() + 1 + value.len());
|
|
403
|
+
cookie_str.push_str(name);
|
|
404
|
+
cookie_str.push('=');
|
|
405
|
+
cookie_str.push_str(value);
|
|
406
|
+
|
|
407
|
+
HeaderValue::from_maybe_shared(Bytes::from(cookie_str))
|
|
408
|
+
})
|
|
409
|
+
.filter_map(Result::ok)
|
|
410
|
+
.collect();
|
|
411
|
+
|
|
412
|
+
Cookies::Uncompressed(cookies)
|
|
413
|
+
} else {
|
|
414
|
+
let cookies = iter.fold(String::new(), |mut cookies, cookie| {
|
|
415
|
+
if !cookies.is_empty() {
|
|
416
|
+
cookies.push_str("; ");
|
|
417
|
+
}
|
|
418
|
+
cookies.push_str(cookie.name());
|
|
419
|
+
cookies.push('=');
|
|
420
|
+
cookies.push_str(cookie.value());
|
|
421
|
+
cookies
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
if cookies.is_empty() {
|
|
425
|
+
return Cookies::Empty;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
HeaderValue::from_maybe_shared(Bytes::from(cookies))
|
|
429
|
+
.map(Cookies::Compressed)
|
|
430
|
+
.unwrap_or(Cookies::Empty)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
#[cfg(test)]
|
|
436
|
+
mod tests {
|
|
437
|
+
use std::{thread, time::Duration as StdDuration};
|
|
438
|
+
|
|
439
|
+
use http::{Uri, Version};
|
|
440
|
+
|
|
441
|
+
use super::{CookieStore, Cookies, Jar};
|
|
442
|
+
|
|
443
|
+
#[test]
|
|
444
|
+
fn jar_get_all_preserves_host_only_scope_and_effective_path() {
|
|
445
|
+
let jar = Jar::default();
|
|
446
|
+
jar.add("session=abc", "http://example.com/foo/bar");
|
|
447
|
+
|
|
448
|
+
let cookies = jar.get_all().collect::<Vec<_>>();
|
|
449
|
+
assert_eq!(cookies.len(), 1);
|
|
450
|
+
|
|
451
|
+
let cookie = &cookies[0];
|
|
452
|
+
assert_eq!(cookie.name(), "session");
|
|
453
|
+
assert_eq!(cookie.value(), "abc");
|
|
454
|
+
assert_eq!(cookie.domain(), None);
|
|
455
|
+
assert_eq!(cookie.path(), Some("/foo"));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#[test]
|
|
459
|
+
fn jar_get_all_keeps_existing_domain_and_path() {
|
|
460
|
+
let jar = Jar::default();
|
|
461
|
+
jar.add(
|
|
462
|
+
"session=abc; Domain=example.com; Path=/custom",
|
|
463
|
+
"http://example.com/foo/bar",
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
let cookies = jar.get_all().collect::<Vec<_>>();
|
|
467
|
+
assert_eq!(cookies.len(), 1);
|
|
468
|
+
|
|
469
|
+
let cookie = &cookies[0];
|
|
470
|
+
assert_eq!(cookie.name(), "session");
|
|
471
|
+
assert_eq!(cookie.value(), "abc");
|
|
472
|
+
assert_eq!(cookie.domain(), Some("example.com"));
|
|
473
|
+
assert_eq!(cookie.path(), Some("/custom"));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
#[test]
|
|
477
|
+
fn jar_get_all_preserves_explicit_domain_and_host_only_scope() {
|
|
478
|
+
let jar = Jar::default();
|
|
479
|
+
jar.add("a=1; Domain=example.com", "http://example.com/foo/bar");
|
|
480
|
+
jar.add("b=2; Path=/fixed", "http://example.com/foo/bar");
|
|
481
|
+
|
|
482
|
+
let mut cookies = jar.get_all().collect::<Vec<_>>();
|
|
483
|
+
cookies.sort_by(|left, right| left.name().cmp(right.name()));
|
|
484
|
+
|
|
485
|
+
let a = &cookies[0];
|
|
486
|
+
assert_eq!(a.name(), "a");
|
|
487
|
+
assert_eq!(a.domain(), Some("example.com"));
|
|
488
|
+
assert_eq!(a.path(), Some("/foo"));
|
|
489
|
+
|
|
490
|
+
let b = &cookies[1];
|
|
491
|
+
assert_eq!(b.name(), "b");
|
|
492
|
+
assert_eq!(b.domain(), None);
|
|
493
|
+
assert_eq!(b.path(), Some("/fixed"));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
#[test]
|
|
497
|
+
fn jar_add_rejects_mismatched_domain() {
|
|
498
|
+
let jar = Jar::default();
|
|
499
|
+
jar.add("session=abc; Domain=other.com", "http://example.com/foo");
|
|
500
|
+
|
|
501
|
+
assert_eq!(jar.get_all().count(), 0);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
#[test]
|
|
505
|
+
fn jar_add_accepts_matching_parent_domain() {
|
|
506
|
+
let jar = Jar::default();
|
|
507
|
+
jar.add(
|
|
508
|
+
"session=abc; Domain=example.com",
|
|
509
|
+
"http://api.example.com/foo",
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
let cookies = jar.get_all().collect::<Vec<_>>();
|
|
513
|
+
assert_eq!(cookies.len(), 1);
|
|
514
|
+
assert_eq!(cookies[0].domain(), Some("example.com"));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
#[test]
|
|
518
|
+
fn jar_rejects_secure_cookie_from_insecure_origin() {
|
|
519
|
+
let jar = Jar::default();
|
|
520
|
+
|
|
521
|
+
jar.add("session=secure; Secure; Path=/", "http://example.com/");
|
|
522
|
+
assert!(jar.is_empty());
|
|
523
|
+
|
|
524
|
+
jar.add("session=plain; Path=/", "https://example.com/");
|
|
525
|
+
jar.add(
|
|
526
|
+
"session=gone; Secure; Max-Age=0; Path=/",
|
|
527
|
+
"http://example.com/",
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
let cookie = jar
|
|
531
|
+
.get("session", "https://example.com/")
|
|
532
|
+
.expect("insecure deletion must not remove the stored cookie");
|
|
533
|
+
assert_eq!(cookie.value(), "plain");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#[test]
|
|
537
|
+
fn jar_blocks_insecure_cookie_that_overlays_secure_path() {
|
|
538
|
+
let jar = Jar::default();
|
|
539
|
+
jar.add(
|
|
540
|
+
"session=secure; Secure; Domain=example.com; Path=/login",
|
|
541
|
+
"https://example.com/login",
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
for cookie in [
|
|
545
|
+
"session=exact; Domain=example.com; Path=/login",
|
|
546
|
+
"session=deeper; Domain=example.com; Path=/login/profile",
|
|
547
|
+
"session=gone; Domain=example.com; Path=/login; Max-Age=0",
|
|
548
|
+
] {
|
|
549
|
+
jar.add(cookie, "http://example.com/login/profile");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// A shorter path does not overlay the existing Secure cookie.
|
|
553
|
+
jar.add(
|
|
554
|
+
"session=root; Domain=example.com; Path=/",
|
|
555
|
+
"http://example.com/",
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
let uri = Uri::from_static("https://example.com/login/profile");
|
|
559
|
+
match jar.cookies(&uri, Version::HTTP_11) {
|
|
560
|
+
Cookies::Compressed(value) => assert_eq!(value, "session=secure; session=root"),
|
|
561
|
+
other => panic!("expected protected Secure cookie, got {other:?}"),
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
#[test]
|
|
566
|
+
fn jar_blocks_secure_cookie_overlay_across_related_domains() {
|
|
567
|
+
let parent = Jar::default();
|
|
568
|
+
parent.add(
|
|
569
|
+
"session=secure; Secure; Domain=example.com; Path=/",
|
|
570
|
+
"https://example.com/",
|
|
571
|
+
);
|
|
572
|
+
parent.add(
|
|
573
|
+
"session=child; Domain=api.example.com; Path=/",
|
|
574
|
+
"http://api.example.com/",
|
|
575
|
+
);
|
|
576
|
+
assert_eq!(parent.get_all().count(), 1);
|
|
577
|
+
|
|
578
|
+
let child = Jar::default();
|
|
579
|
+
child.add(
|
|
580
|
+
"session=secure; Secure; Domain=api.example.com; Path=/",
|
|
581
|
+
"https://api.example.com/",
|
|
582
|
+
);
|
|
583
|
+
child.add(
|
|
584
|
+
"session=parent; Domain=example.com; Path=/",
|
|
585
|
+
"http://example.com/",
|
|
586
|
+
);
|
|
587
|
+
assert_eq!(child.get_all().count(), 1);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
#[test]
|
|
591
|
+
fn jar_allows_secure_origin_to_replace_secure_cookie() {
|
|
592
|
+
let jar = Jar::default();
|
|
593
|
+
let uri = "https://example.com/";
|
|
594
|
+
jar.add("session=secure; Secure; Path=/", uri);
|
|
595
|
+
jar.add("session=plain; Path=/", uri);
|
|
596
|
+
|
|
597
|
+
let cookie = jar
|
|
598
|
+
.get("session", uri)
|
|
599
|
+
.expect("secure origin should replace the cookie");
|
|
600
|
+
assert_eq!(cookie.value(), "plain");
|
|
601
|
+
assert!(!cookie.secure());
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
#[test]
|
|
605
|
+
fn jar_get_all_export_import_keeps_host_only_scope_and_effective_path() {
|
|
606
|
+
let source = Jar::default();
|
|
607
|
+
source.add("session=abc", "http://example.com/foo/bar");
|
|
608
|
+
|
|
609
|
+
let exported = source.get_all().collect::<Vec<_>>();
|
|
610
|
+
assert_eq!(exported.len(), 1);
|
|
611
|
+
assert_eq!(exported[0].domain(), None);
|
|
612
|
+
assert_eq!(exported[0].path(), Some("/foo"));
|
|
613
|
+
|
|
614
|
+
let target = Jar::default();
|
|
615
|
+
for cookie in exported {
|
|
616
|
+
target.add(cookie, "http://example.com/another/deeper");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
let imported = target.get_all().collect::<Vec<_>>();
|
|
620
|
+
assert_eq!(imported.len(), 1);
|
|
621
|
+
assert_eq!(imported[0].domain(), None);
|
|
622
|
+
assert_eq!(imported[0].path(), Some("/foo"));
|
|
623
|
+
|
|
624
|
+
let subdomain = Uri::from_static("http://api.example.com/foo/resource");
|
|
625
|
+
assert!(matches!(
|
|
626
|
+
target.cookies(&subdomain, Version::HTTP_11),
|
|
627
|
+
Cookies::Empty
|
|
628
|
+
));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
#[test]
|
|
632
|
+
fn jar_get_all_export_import_preserves_absolute_expiration() {
|
|
633
|
+
let source = Jar::default();
|
|
634
|
+
let uri = "http://example.com/";
|
|
635
|
+
source.add("session=abc; Max-Age=60; Path=/", uri);
|
|
636
|
+
|
|
637
|
+
let exported = source
|
|
638
|
+
.get_all()
|
|
639
|
+
.next()
|
|
640
|
+
.expect("source cookie should be stored");
|
|
641
|
+
let expires = exported.expires();
|
|
642
|
+
assert_eq!(exported.max_age(), None);
|
|
643
|
+
assert!(expires.is_some());
|
|
644
|
+
|
|
645
|
+
let target = Jar::default();
|
|
646
|
+
target.add(exported, uri);
|
|
647
|
+
|
|
648
|
+
let imported = target
|
|
649
|
+
.get("session", uri)
|
|
650
|
+
.expect("snapshot should be imported");
|
|
651
|
+
assert_eq!(imported.max_age(), None);
|
|
652
|
+
assert_eq!(imported.expires(), expires);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
#[test]
|
|
656
|
+
fn jar_get_all_export_import_preserves_creation_order() {
|
|
657
|
+
let source = Jar::default();
|
|
658
|
+
let uri = "http://example.com/";
|
|
659
|
+
source.add("B=first; Path=/", uri);
|
|
660
|
+
source.add("A=second; Path=/", uri);
|
|
661
|
+
|
|
662
|
+
let exported = source.get_all().collect::<Vec<_>>();
|
|
663
|
+
assert_eq!(
|
|
664
|
+
exported
|
|
665
|
+
.iter()
|
|
666
|
+
.map(|cookie| cookie.name())
|
|
667
|
+
.collect::<Vec<_>>(),
|
|
668
|
+
["B", "A"]
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
let target = Jar::default();
|
|
672
|
+
for cookie in exported {
|
|
673
|
+
target.add(cookie, uri);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
let uri = Uri::from_static("http://example.com/");
|
|
677
|
+
match target.cookies(&uri, Version::HTTP_11) {
|
|
678
|
+
Cookies::Compressed(value) => assert_eq!(value, "B=first; A=second"),
|
|
679
|
+
other => panic!("expected imported Cookie field, got {other:?}"),
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
#[test]
|
|
684
|
+
fn cookie_store_invalid_explicit_path_falls_back_to_default_path() {
|
|
685
|
+
let jar = Jar::default();
|
|
686
|
+
jar.add("key=val; Path=noslash", "http://example.com/foo/bar");
|
|
687
|
+
|
|
688
|
+
assert!(jar.get("key", "http://example.com/foo").is_some());
|
|
689
|
+
assert!(jar.get("key", "http://example.com/noslash").is_none());
|
|
690
|
+
|
|
691
|
+
let cookies = jar.get_all().collect::<Vec<_>>();
|
|
692
|
+
assert_eq!(cookies.len(), 1);
|
|
693
|
+
assert_eq!(cookies[0].path(), Some("/foo"));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
#[test]
|
|
697
|
+
fn jar_collection_queries_use_unexpired_exact_scopes() {
|
|
698
|
+
let jar = Jar::default();
|
|
699
|
+
assert!(jar.is_empty());
|
|
700
|
+
assert_eq!(jar.len(), 0);
|
|
701
|
+
|
|
702
|
+
jar.add("first=1; Path=/one", "https://example.com/one");
|
|
703
|
+
jar.add("second=2; Path=/two", "https://example.com/two");
|
|
704
|
+
|
|
705
|
+
assert_eq!(jar.len(), 2);
|
|
706
|
+
assert!(!jar.is_empty());
|
|
707
|
+
assert!(jar.contains("first", "https://example.com/one"));
|
|
708
|
+
assert!(jar.contains("first", "https://EXAMPLE.COM/one"));
|
|
709
|
+
assert!(!jar.contains("first", "https://example.com/one/child"));
|
|
710
|
+
assert!(!jar.contains("first", "https://other.example/one"));
|
|
711
|
+
assert!(!jar.contains("missing", "https://example.com/one"));
|
|
712
|
+
assert!(!jar.contains("first", "/relative"));
|
|
713
|
+
|
|
714
|
+
jar.add("first=updated; Path=/one", "https://example.com/one");
|
|
715
|
+
assert_eq!(jar.len(), 2);
|
|
716
|
+
assert_eq!(
|
|
717
|
+
jar.get("first", "https://example.com/one")
|
|
718
|
+
.map(|cookie| cookie.value().to_owned()),
|
|
719
|
+
Some("updated".to_owned())
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
jar.remove("first", "https://example.com/one");
|
|
723
|
+
assert_eq!(jar.len(), 1);
|
|
724
|
+
assert!(!jar.contains("first", "https://example.com/one"));
|
|
725
|
+
|
|
726
|
+
jar.clear();
|
|
727
|
+
assert!(jar.is_empty());
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
#[test]
|
|
731
|
+
fn jar_matches_request_scope() {
|
|
732
|
+
let jar = Jar::default();
|
|
733
|
+
jar.add("host=1; Path=/", "https://example.com/");
|
|
734
|
+
jar.add(
|
|
735
|
+
"domain=2; Domain=example.com; Path=/api",
|
|
736
|
+
"https://example.com/api",
|
|
737
|
+
);
|
|
738
|
+
jar.add(
|
|
739
|
+
"secure=3; Domain=example.com; Path=/; Secure",
|
|
740
|
+
"https://example.com/",
|
|
741
|
+
);
|
|
742
|
+
jar.add(
|
|
743
|
+
"http_only=4; Domain=example.com; Path=/api; HttpOnly",
|
|
744
|
+
"https://example.com/api",
|
|
745
|
+
);
|
|
746
|
+
jar.add(
|
|
747
|
+
"admin=5; Domain=example.com; Path=/admin",
|
|
748
|
+
"https://example.com/admin",
|
|
749
|
+
);
|
|
750
|
+
jar.add("other=6; Domain=other.com; Path=/", "https://other.com/");
|
|
751
|
+
|
|
752
|
+
let names = |uri| {
|
|
753
|
+
let mut names = jar
|
|
754
|
+
.matches(uri)
|
|
755
|
+
.map(|cookie| cookie.name().to_owned())
|
|
756
|
+
.collect::<Vec<_>>();
|
|
757
|
+
names.sort();
|
|
758
|
+
names
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
assert_eq!(
|
|
762
|
+
names("https://api.example.com/api/users"),
|
|
763
|
+
["domain", "http_only", "secure"]
|
|
764
|
+
);
|
|
765
|
+
assert_eq!(
|
|
766
|
+
names("http://api.example.com/api/users"),
|
|
767
|
+
["domain", "http_only"]
|
|
768
|
+
);
|
|
769
|
+
assert_eq!(
|
|
770
|
+
names("https://example.com/api/users"),
|
|
771
|
+
["domain", "host", "http_only", "secure"]
|
|
772
|
+
);
|
|
773
|
+
assert!(jar.matches("ftp://example.com/api/users").next().is_none());
|
|
774
|
+
assert!(jar.matches("/relative").next().is_none());
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
#[test]
|
|
778
|
+
fn jar_matches_same_name_across_domain_and_path_scopes() {
|
|
779
|
+
let jar = Jar::default();
|
|
780
|
+
jar.add("id=host; Path=/", "https://bus.example.com/");
|
|
781
|
+
jar.add(
|
|
782
|
+
"id=domain; Domain=example.com; Path=/",
|
|
783
|
+
"https://example.com/",
|
|
784
|
+
);
|
|
785
|
+
jar.add(
|
|
786
|
+
"id=path; Domain=example.com; Path=/api",
|
|
787
|
+
"https://example.com/api",
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
let values = |uri| {
|
|
791
|
+
let mut values = jar
|
|
792
|
+
.matches(uri)
|
|
793
|
+
.map(|cookie| cookie.value().to_owned())
|
|
794
|
+
.collect::<Vec<_>>();
|
|
795
|
+
values.sort();
|
|
796
|
+
values
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
assert_eq!(
|
|
800
|
+
values("https://bus.example.com/api/users"),
|
|
801
|
+
["domain", "host", "path"]
|
|
802
|
+
);
|
|
803
|
+
assert_eq!(
|
|
804
|
+
values("https://foo.bus.example.com/api/users"),
|
|
805
|
+
["domain", "path"]
|
|
806
|
+
);
|
|
807
|
+
assert_eq!(values("https://example.com/api/users"), ["domain", "path"]);
|
|
808
|
+
assert!(values("https://other.example/api/users").is_empty());
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
#[test]
|
|
812
|
+
fn jar_keeps_host_only_and_domain_cookies_with_the_same_key() {
|
|
813
|
+
let jar = Jar::default();
|
|
814
|
+
let origin = "https://example.com/";
|
|
815
|
+
jar.add("id=host; Path=/", origin);
|
|
816
|
+
jar.add("id=domain; Domain=example.com; Path=/", origin);
|
|
817
|
+
|
|
818
|
+
assert_eq!(jar.len(), 2);
|
|
819
|
+
assert_eq!(
|
|
820
|
+
jar.get("id", origin)
|
|
821
|
+
.map(|cookie| cookie.value().to_owned()),
|
|
822
|
+
Some("host".to_owned())
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
let origin_uri = Uri::from_static("https://example.com/");
|
|
826
|
+
match jar.cookies(&origin_uri, Version::HTTP_11) {
|
|
827
|
+
Cookies::Compressed(value) => assert_eq!(value, "id=host; id=domain"),
|
|
828
|
+
other => panic!("expected both origin cookies, got {other:?}"),
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
let subdomain = Uri::from_static("https://api.example.com/");
|
|
832
|
+
match jar.cookies(&subdomain, Version::HTTP_11) {
|
|
833
|
+
Cookies::Compressed(value) => assert_eq!(value, "id=domain"),
|
|
834
|
+
other => panic!("expected only the domain cookie, got {other:?}"),
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
jar.add("id=gone; Max-Age=0; Path=/", origin);
|
|
838
|
+
assert_eq!(jar.len(), 1);
|
|
839
|
+
assert!(jar.contains("id", origin));
|
|
840
|
+
assert_eq!(
|
|
841
|
+
jar.get("id", origin)
|
|
842
|
+
.map(|cookie| cookie.value().to_owned()),
|
|
843
|
+
Some("domain".to_owned())
|
|
844
|
+
);
|
|
845
|
+
match jar.cookies(&origin_uri, Version::HTTP_11) {
|
|
846
|
+
Cookies::Compressed(value) => assert_eq!(value, "id=domain"),
|
|
847
|
+
other => panic!("expected the remaining domain cookie, got {other:?}"),
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
jar.add("id=gone; Max-Age=0; Domain=example.com; Path=/", origin);
|
|
851
|
+
assert!(jar.is_empty());
|
|
852
|
+
|
|
853
|
+
jar.add("id=host; Path=/", origin);
|
|
854
|
+
jar.add("id=domain; Domain=example.com; Path=/", origin);
|
|
855
|
+
jar.remove("id", origin);
|
|
856
|
+
assert!(jar.is_empty());
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
#[test]
|
|
860
|
+
fn jar_matches_rfc_path_boundaries() {
|
|
861
|
+
let jar = Jar::default();
|
|
862
|
+
jar.add(
|
|
863
|
+
"plain=1; Domain=example.com; Path=/foo",
|
|
864
|
+
"https://example.com/foo",
|
|
865
|
+
);
|
|
866
|
+
jar.add(
|
|
867
|
+
"slash=2; Domain=example.com; Path=/foo/",
|
|
868
|
+
"https://example.com/foo/",
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
let names = |uri| {
|
|
872
|
+
let mut names = jar
|
|
873
|
+
.matches(uri)
|
|
874
|
+
.map(|cookie| cookie.name().to_owned())
|
|
875
|
+
.collect::<Vec<_>>();
|
|
876
|
+
names.sort();
|
|
877
|
+
names
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
assert_eq!(names("https://example.com/foo"), ["plain"]);
|
|
881
|
+
assert_eq!(names("https://example.com/foo/"), ["plain", "slash"]);
|
|
882
|
+
assert_eq!(names("https://example.com/foo/bar"), ["plain", "slash"]);
|
|
883
|
+
assert!(names("https://example.com/foobar").is_empty());
|
|
884
|
+
assert!(names("https://example.com/fo").is_empty());
|
|
885
|
+
assert!(names("https://example.com/").is_empty());
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
#[test]
|
|
889
|
+
fn jar_orders_request_cookies_by_path_length_then_creation() {
|
|
890
|
+
let jar = Jar::default();
|
|
891
|
+
let origin = "https://example.com/";
|
|
892
|
+
|
|
893
|
+
for cookie in [
|
|
894
|
+
"B=B1; Path=/",
|
|
895
|
+
"B=B2; Path=/foo",
|
|
896
|
+
"B=B3; Path=/foo/bar",
|
|
897
|
+
"A=A1; Path=/",
|
|
898
|
+
"A=A2; Path=/foo",
|
|
899
|
+
"A=A3; Path=/foo/bar",
|
|
900
|
+
] {
|
|
901
|
+
jar.add(cookie, origin);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
let uri = Uri::from_static("https://example.com/foo/bar/resource");
|
|
905
|
+
match jar.cookies(&uri, Version::HTTP_11) {
|
|
906
|
+
Cookies::Compressed(value) => {
|
|
907
|
+
assert_eq!(value, "B=B3; A=A3; B=B2; A=A2; B=B1; A=A1");
|
|
908
|
+
}
|
|
909
|
+
other => panic!("expected compressed Cookie field, got {other:?}"),
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
for version in [Version::HTTP_2, Version::HTTP_3] {
|
|
913
|
+
match jar.cookies(&uri, version) {
|
|
914
|
+
Cookies::Uncompressed(values) => {
|
|
915
|
+
let values = values
|
|
916
|
+
.iter()
|
|
917
|
+
.map(|value| value.to_str().unwrap())
|
|
918
|
+
.collect::<Vec<_>>();
|
|
919
|
+
assert_eq!(values, ["B=B3", "A=A3", "B=B2", "A=A2", "B=B1", "A=A1"]);
|
|
920
|
+
}
|
|
921
|
+
other => panic!("expected uncompressed Cookie fields, got {other:?}"),
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
#[test]
|
|
927
|
+
fn jar_inherits_creation_order_only_when_value_is_unchanged() {
|
|
928
|
+
let jar = Jar::default();
|
|
929
|
+
let origin = "https://example.com/foo";
|
|
930
|
+
let uri = Uri::from_static("https://example.com/foo/bar");
|
|
931
|
+
|
|
932
|
+
jar.add("B=old; Path=/foo", origin);
|
|
933
|
+
jar.add("A=value; Path=/foo", origin);
|
|
934
|
+
jar.add("B=old; Path=/foo; HttpOnly", origin);
|
|
935
|
+
|
|
936
|
+
match jar.cookies(&uri, Version::HTTP_11) {
|
|
937
|
+
Cookies::Compressed(value) => assert_eq!(value, "B=old; A=value"),
|
|
938
|
+
other => panic!("expected compressed Cookie field, got {other:?}"),
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
jar.add("B=new; Path=/foo", origin);
|
|
942
|
+
|
|
943
|
+
match jar.cookies(&uri, Version::HTTP_11) {
|
|
944
|
+
Cookies::Compressed(value) => assert_eq!(value, "A=value; B=new"),
|
|
945
|
+
other => panic!("expected compressed Cookie field, got {other:?}"),
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
#[test]
|
|
950
|
+
fn jar_sends_parent_domain_cookie_to_subdomain() {
|
|
951
|
+
let jar = Jar::default();
|
|
952
|
+
jar.add(
|
|
953
|
+
"session=abc; Domain=example.com; Path=/",
|
|
954
|
+
"http://example.com/login",
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
let should_receive = [
|
|
958
|
+
"http://example.com/dashboard",
|
|
959
|
+
"http://api.example.com/dashboard",
|
|
960
|
+
"http://sub.api.example.com/dashboard",
|
|
961
|
+
];
|
|
962
|
+
for uri_str in &should_receive {
|
|
963
|
+
let uri = Uri::from_static(uri_str);
|
|
964
|
+
match jar.cookies(&uri, Version::HTTP_11) {
|
|
965
|
+
Cookies::Compressed(v) => assert_eq!(
|
|
966
|
+
v.to_str().unwrap(),
|
|
967
|
+
"session=abc",
|
|
968
|
+
"expected cookie to be sent to {uri_str}"
|
|
969
|
+
),
|
|
970
|
+
other => panic!("expected Compressed cookie for {uri_str}, got {other:?}"),
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
let should_not_receive = [
|
|
975
|
+
"http://notexample.com/dashboard",
|
|
976
|
+
"http://fakeexample.com/dashboard",
|
|
977
|
+
];
|
|
978
|
+
for uri_str in &should_not_receive {
|
|
979
|
+
let uri = Uri::from_static(uri_str);
|
|
980
|
+
assert!(
|
|
981
|
+
matches!(jar.cookies(&uri, Version::HTTP_11), Cookies::Empty),
|
|
982
|
+
"cookie must NOT be sent to {uri_str}"
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
#[test]
|
|
988
|
+
fn jar_does_not_send_host_only_cookie_to_subdomain() {
|
|
989
|
+
let jar = Jar::default();
|
|
990
|
+
jar.add("session=abc; Path=/", "http://example.com/login");
|
|
991
|
+
|
|
992
|
+
let origin = Uri::from_static("http://example.com/dashboard");
|
|
993
|
+
match jar.cookies(&origin, Version::HTTP_11) {
|
|
994
|
+
Cookies::Compressed(value) => assert_eq!(value, "session=abc"),
|
|
995
|
+
other => panic!("expected host-only cookie for origin host, got {other:?}"),
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
let subdomain = Uri::from_static("http://api.example.com/dashboard");
|
|
999
|
+
assert!(
|
|
1000
|
+
matches!(jar.cookies(&subdomain, Version::HTTP_11), Cookies::Empty),
|
|
1001
|
+
"host-only cookie must not be sent to a subdomain"
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
#[test]
|
|
1006
|
+
fn jar_accepts_and_normalizes_mixed_case_domain() {
|
|
1007
|
+
let jar = Jar::default();
|
|
1008
|
+
jar.add(
|
|
1009
|
+
"session=abc; Domain=EXAMPLE.COM; Path=/",
|
|
1010
|
+
"https://example.com/login",
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
let cookies = jar.get_all().collect::<Vec<_>>();
|
|
1014
|
+
assert_eq!(cookies.len(), 1);
|
|
1015
|
+
assert_eq!(cookies[0].domain(), Some("example.com"));
|
|
1016
|
+
|
|
1017
|
+
let subdomain = Uri::from_static("https://api.example.com/dashboard");
|
|
1018
|
+
match jar.cookies(&subdomain, Version::HTTP_11) {
|
|
1019
|
+
Cookies::Compressed(value) => assert_eq!(value, "session=abc"),
|
|
1020
|
+
other => panic!("expected domain cookie for matching subdomain, got {other:?}"),
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
#[test]
|
|
1025
|
+
fn jar_ignores_leading_dot_in_domain() {
|
|
1026
|
+
let jar = Jar::default();
|
|
1027
|
+
jar.add(
|
|
1028
|
+
"session=abc; Domain=.example.com; Path=/",
|
|
1029
|
+
"https://example.com/login",
|
|
1030
|
+
);
|
|
1031
|
+
|
|
1032
|
+
let subdomain = Uri::from_static("https://api.example.com/dashboard");
|
|
1033
|
+
match jar.cookies(&subdomain, Version::HTTP_11) {
|
|
1034
|
+
Cookies::Compressed(value) => assert_eq!(value, "session=abc"),
|
|
1035
|
+
other => panic!("expected domain cookie for matching subdomain, got {other:?}"),
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
#[test]
|
|
1040
|
+
fn jar_enforces_positive_max_age_deadline() {
|
|
1041
|
+
let jar = Jar::default();
|
|
1042
|
+
let uri = Uri::from_static("http://example.com/");
|
|
1043
|
+
jar.add("short=lived; Max-Age=1; Path=/", &uri);
|
|
1044
|
+
|
|
1045
|
+
let cookie = jar.get("short", &uri).expect("cookie should be stored");
|
|
1046
|
+
assert_eq!(cookie.max_age(), None);
|
|
1047
|
+
assert!(cookie.expires().is_some());
|
|
1048
|
+
assert!(jar.contains("short", &uri));
|
|
1049
|
+
assert_eq!(jar.len(), 1);
|
|
1050
|
+
assert!(!jar.is_empty());
|
|
1051
|
+
assert_eq!(jar.matches(&uri).count(), 1);
|
|
1052
|
+
assert_eq!(jar.get_all().count(), 1);
|
|
1053
|
+
assert!(matches!(
|
|
1054
|
+
jar.cookies(&uri, Version::HTTP_11),
|
|
1055
|
+
Cookies::Compressed(_)
|
|
1056
|
+
));
|
|
1057
|
+
|
|
1058
|
+
thread::sleep(StdDuration::from_millis(1100));
|
|
1059
|
+
|
|
1060
|
+
assert!(jar.get("short", &uri).is_none());
|
|
1061
|
+
assert!(!jar.contains("short", &uri));
|
|
1062
|
+
assert_eq!(jar.len(), 0);
|
|
1063
|
+
assert!(jar.is_empty());
|
|
1064
|
+
assert_eq!(jar.matches(&uri).count(), 0);
|
|
1065
|
+
assert_eq!(jar.get_all().count(), 0);
|
|
1066
|
+
assert!(matches!(
|
|
1067
|
+
jar.cookies(&uri, Version::HTTP_11),
|
|
1068
|
+
Cookies::Empty
|
|
1069
|
+
));
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
#[test]
|
|
1073
|
+
fn jar_removes_non_positive_max_age() {
|
|
1074
|
+
let jar = Jar::default();
|
|
1075
|
+
let uri = "http://example.com/";
|
|
1076
|
+
|
|
1077
|
+
jar.add("zero=old; Path=/", uri);
|
|
1078
|
+
jar.add("zero=gone; Max-Age=0; Path=/", uri);
|
|
1079
|
+
assert!(jar.get("zero", uri).is_none());
|
|
1080
|
+
|
|
1081
|
+
jar.add("negative=old; Path=/", uri);
|
|
1082
|
+
jar.add("negative=gone; Max-Age=-10; Path=/", uri);
|
|
1083
|
+
assert!(jar.get("negative", uri).is_none());
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
#[test]
|
|
1087
|
+
fn jar_max_age_overrides_expires_in_either_order() {
|
|
1088
|
+
let jar = Jar::default();
|
|
1089
|
+
let uri = "http://example.com/";
|
|
1090
|
+
|
|
1091
|
+
jar.add(
|
|
1092
|
+
"first=kept; Max-Age=60; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/",
|
|
1093
|
+
uri,
|
|
1094
|
+
);
|
|
1095
|
+
jar.add(
|
|
1096
|
+
"last=kept; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=60; Path=/",
|
|
1097
|
+
uri,
|
|
1098
|
+
);
|
|
1099
|
+
assert!(jar.get("first", uri).is_some());
|
|
1100
|
+
assert!(jar.get("last", uri).is_some());
|
|
1101
|
+
|
|
1102
|
+
jar.add("remove_first=old; Path=/", uri);
|
|
1103
|
+
jar.add(
|
|
1104
|
+
"remove_first=gone; Max-Age=0; Expires=Fri, 31 Dec 9999 23:59:59 GMT; Path=/",
|
|
1105
|
+
uri,
|
|
1106
|
+
);
|
|
1107
|
+
jar.add("remove_last=old; Path=/", uri);
|
|
1108
|
+
jar.add(
|
|
1109
|
+
"remove_last=gone; Expires=Fri, 31 Dec 9999 23:59:59 GMT; Max-Age=0; Path=/",
|
|
1110
|
+
uri,
|
|
1111
|
+
);
|
|
1112
|
+
assert!(jar.get("remove_first", uri).is_none());
|
|
1113
|
+
assert!(jar.get("remove_last", uri).is_none());
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
#[test]
|
|
1117
|
+
fn jar_uses_last_valid_max_age() {
|
|
1118
|
+
let jar = Jar::default();
|
|
1119
|
+
let uri = "http://example.com/";
|
|
1120
|
+
|
|
1121
|
+
jar.add("kept=value; Max-Age=0; Max-Age=60; Path=/", uri);
|
|
1122
|
+
assert!(jar.get("kept", uri).is_some());
|
|
1123
|
+
|
|
1124
|
+
jar.add("removed=old; Path=/", uri);
|
|
1125
|
+
jar.add("removed=gone; Max-Age=60; Max-Age=0; Path=/", uri);
|
|
1126
|
+
assert!(jar.get("removed", uri).is_none());
|
|
1127
|
+
|
|
1128
|
+
jar.add(
|
|
1129
|
+
"malformed=kept; Max-Age=60; Max-Age=invalid; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/",
|
|
1130
|
+
uri,
|
|
1131
|
+
);
|
|
1132
|
+
assert!(jar.get("malformed", uri).is_some());
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
#[test]
|
|
1136
|
+
fn jar_ignores_malformed_max_age() {
|
|
1137
|
+
let jar = Jar::default();
|
|
1138
|
+
let uri = "http://example.com/";
|
|
1139
|
+
|
|
1140
|
+
jar.add(
|
|
1141
|
+
"persistent=value; Max-Age=invalid; Expires=Fri, 31 Dec 9999 23:59:59 GMT; Path=/",
|
|
1142
|
+
uri,
|
|
1143
|
+
);
|
|
1144
|
+
assert!(jar.get("persistent", uri).is_some());
|
|
1145
|
+
|
|
1146
|
+
jar.add(
|
|
1147
|
+
"expired=value; Max-Age=invalid; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/",
|
|
1148
|
+
uri,
|
|
1149
|
+
);
|
|
1150
|
+
assert!(jar.get("expired", uri).is_none());
|
|
1151
|
+
|
|
1152
|
+
jar.add(
|
|
1153
|
+
"plus=value; Max-Age=+60; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/",
|
|
1154
|
+
uri,
|
|
1155
|
+
);
|
|
1156
|
+
assert!(jar.get("plus", uri).is_none());
|
|
1157
|
+
|
|
1158
|
+
jar.add("session=value; Max-Age=invalid; Path=/", uri);
|
|
1159
|
+
let session = jar
|
|
1160
|
+
.get("session", uri)
|
|
1161
|
+
.expect("session cookie should be stored");
|
|
1162
|
+
assert_eq!(session.max_age(), None);
|
|
1163
|
+
assert_eq!(session.expires(), None);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
#[test]
|
|
1167
|
+
fn jar_does_not_suffix_match_ipv4_addresses() {
|
|
1168
|
+
let jar = Jar::default();
|
|
1169
|
+
jar.add("session=abc; Domain=0.1; Path=/", "http://192.168.0.1/");
|
|
1170
|
+
|
|
1171
|
+
assert_eq!(jar.get_all().count(), 0);
|
|
1172
|
+
|
|
1173
|
+
let unrelated = Uri::from_static("http://10.0.0.1/");
|
|
1174
|
+
assert!(matches!(
|
|
1175
|
+
jar.cookies(&unrelated, Version::HTTP_11),
|
|
1176
|
+
Cookies::Empty
|
|
1177
|
+
));
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
#[test]
|
|
1181
|
+
fn jar_preserves_ipv6_host_identity() {
|
|
1182
|
+
let jar = Jar::default();
|
|
1183
|
+
jar.add("session=abc; Path=/", "http://[2001:db8::1]/");
|
|
1184
|
+
|
|
1185
|
+
let equivalent = Uri::from_static("http://[2001:0db8:0:0:0:0:0:1]/");
|
|
1186
|
+
match jar.cookies(&equivalent, Version::HTTP_11) {
|
|
1187
|
+
Cookies::Compressed(value) => assert_eq!(value, "session=abc"),
|
|
1188
|
+
other => panic!("expected cookie for equivalent IPv6 host, got {other:?}"),
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
let unrelated = Uri::from_static("http://[2001:db8::2]/");
|
|
1192
|
+
assert!(matches!(
|
|
1193
|
+
jar.cookies(&unrelated, Version::HTTP_11),
|
|
1194
|
+
Cookies::Empty
|
|
1195
|
+
));
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
#[test]
|
|
1199
|
+
fn jar_subdomain_cookie_does_not_leak_to_parent_or_sibling() {
|
|
1200
|
+
let jar = Jar::default();
|
|
1201
|
+
jar.add(
|
|
1202
|
+
"token=xyz; Domain=api.example.com; Path=/",
|
|
1203
|
+
"http://api.example.com/",
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
let uri = Uri::from_static("http://api.example.com/");
|
|
1207
|
+
assert!(
|
|
1208
|
+
matches!(jar.cookies(&uri, Version::HTTP_11), Cookies::Compressed(_)),
|
|
1209
|
+
"cookie must be sent to api.example.com"
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
let must_not_receive = [
|
|
1213
|
+
"http://example.com/",
|
|
1214
|
+
"http://other.example.com/",
|
|
1215
|
+
"http://notapi.example.com/",
|
|
1216
|
+
];
|
|
1217
|
+
for uri_str in &must_not_receive {
|
|
1218
|
+
let uri = Uri::from_static(uri_str);
|
|
1219
|
+
assert!(
|
|
1220
|
+
matches!(jar.cookies(&uri, Version::HTTP_11), Cookies::Empty),
|
|
1221
|
+
"cookie must NOT leak to {uri_str}"
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|