wreq 1.0.0
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 +7 -0
- data/Cargo.toml +54 -0
- data/Gemfile +17 -0
- data/LICENSE +201 -0
- data/README.md +150 -0
- data/Rakefile +90 -0
- data/build.rs +9 -0
- data/examples/body.rb +42 -0
- data/examples/client.rb +33 -0
- data/examples/emulation_request.rb +37 -0
- data/examples/headers.rb +27 -0
- data/examples/proxy.rb +113 -0
- data/examples/send_stream.rb +85 -0
- data/examples/stream.rb +14 -0
- data/examples/thread_interrupt.rb +83 -0
- data/extconf.rb +7 -0
- data/lib/wreq.rb +313 -0
- data/lib/wreq_ruby/body.rb +36 -0
- data/lib/wreq_ruby/client.rb +516 -0
- data/lib/wreq_ruby/cookie.rb +144 -0
- data/lib/wreq_ruby/emulation.rb +186 -0
- data/lib/wreq_ruby/error.rb +159 -0
- data/lib/wreq_ruby/header.rb +197 -0
- data/lib/wreq_ruby/http.rb +132 -0
- data/lib/wreq_ruby/response.rb +208 -0
- data/script/build_platform_gem.rb +34 -0
- data/src/client/body/form.rs +2 -0
- data/src/client/body/json.rs +16 -0
- data/src/client/body/stream.rs +148 -0
- data/src/client/body.rs +57 -0
- data/src/client/param.rs +19 -0
- data/src/client/query.rs +2 -0
- data/src/client/req.rs +251 -0
- data/src/client/resp.rs +250 -0
- data/src/client.rs +392 -0
- data/src/cookie.rs +277 -0
- data/src/emulation.rs +317 -0
- data/src/error.rs +147 -0
- data/src/extractor.rs +199 -0
- data/src/gvl.rs +154 -0
- data/src/header.rs +177 -0
- data/src/http.rs +127 -0
- data/src/lib.rs +97 -0
- data/src/macros.rs +118 -0
- data/src/rt.rs +47 -0
- data/test/client_cookie_test.rb +46 -0
- data/test/client_test.rb +136 -0
- data/test/cookie_test.rb +166 -0
- data/test/emulation_test.rb +21 -0
- data/test/error_handling_test.rb +89 -0
- data/test/header_test.rb +290 -0
- data/test/module_methods_test.rb +75 -0
- data/test/request_parameters_test.rb +175 -0
- data/test/request_test.rb +234 -0
- data/test/response_test.rb +69 -0
- data/test/stream_test.rb +81 -0
- data/test/test_helper.rb +9 -0
- data/wreq.gemspec +68 -0
- metadata +112 -0
data/src/client.rs
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
mod body;
|
|
2
|
+
mod param;
|
|
3
|
+
mod query;
|
|
4
|
+
mod req;
|
|
5
|
+
pub mod resp;
|
|
6
|
+
|
|
7
|
+
use std::{net::IpAddr, time::Duration};
|
|
8
|
+
|
|
9
|
+
use magnus::{
|
|
10
|
+
Module, Object, RHash, RModule, Ruby, TryConvert, Value, function, method, typed_data::Obj,
|
|
11
|
+
};
|
|
12
|
+
use serde::Deserialize;
|
|
13
|
+
use wreq::{
|
|
14
|
+
Proxy,
|
|
15
|
+
header::{HeaderMap, HeaderValue, OrigHeaderMap},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
use crate::{
|
|
19
|
+
client::{req::execute_request, resp::Response},
|
|
20
|
+
cookie::Jar,
|
|
21
|
+
emulation::Emulation,
|
|
22
|
+
error::wreq_error_to_magnus,
|
|
23
|
+
extractor::Extractor,
|
|
24
|
+
gvl,
|
|
25
|
+
http::Method,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/// A builder for `Client`.
|
|
29
|
+
#[derive(Default, Deserialize)]
|
|
30
|
+
struct Builder {
|
|
31
|
+
// The emulation option for the client.
|
|
32
|
+
#[serde(skip)]
|
|
33
|
+
emulation: Option<Emulation>,
|
|
34
|
+
/// The user agent to use for the client.
|
|
35
|
+
#[serde(skip)]
|
|
36
|
+
user_agent: Option<HeaderValue>,
|
|
37
|
+
/// The headers to use for the client.
|
|
38
|
+
#[serde(skip)]
|
|
39
|
+
headers: Option<HeaderMap>,
|
|
40
|
+
/// The original headers to use for the client.
|
|
41
|
+
#[serde(skip)]
|
|
42
|
+
orig_headers: Option<OrigHeaderMap>,
|
|
43
|
+
/// Whether to use referer.
|
|
44
|
+
referer: Option<bool>,
|
|
45
|
+
/// Whether to allow redirects.
|
|
46
|
+
allow_redirects: Option<bool>,
|
|
47
|
+
/// The maximum number of redirects to follow.
|
|
48
|
+
max_redirects: Option<usize>,
|
|
49
|
+
|
|
50
|
+
// ========= Cookie options =========
|
|
51
|
+
/// Whether to use cookie store.
|
|
52
|
+
cookie_store: Option<bool>,
|
|
53
|
+
/// Whether to use cookie store provider.
|
|
54
|
+
#[serde(skip)]
|
|
55
|
+
cookie_provider: Option<Jar>,
|
|
56
|
+
|
|
57
|
+
// ========= Timeout options =========
|
|
58
|
+
/// The timeout to use for the client. (in seconds)
|
|
59
|
+
timeout: Option<u64>,
|
|
60
|
+
/// The connect timeout to use for the client. (in seconds)
|
|
61
|
+
connect_timeout: Option<u64>,
|
|
62
|
+
/// The read timeout to use for the client. (in seconds)
|
|
63
|
+
read_timeout: Option<u64>,
|
|
64
|
+
|
|
65
|
+
// ========= TCP options =========
|
|
66
|
+
/// Set that all sockets have `SO_KEEPALIVE` set with the supplied duration. (in seconds)
|
|
67
|
+
tcp_keepalive: Option<u64>,
|
|
68
|
+
/// Set the interval between TCP keepalive probes. (in seconds)
|
|
69
|
+
tcp_keepalive_interval: Option<u64>,
|
|
70
|
+
/// Set the number of retries for TCP keepalive.
|
|
71
|
+
tcp_keepalive_retries: Option<u32>,
|
|
72
|
+
/// Set an optional user timeout for TCP sockets. (in seconds)
|
|
73
|
+
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
|
74
|
+
tcp_user_timeout: Option<u64>,
|
|
75
|
+
/// Set that all sockets have `NO_DELAY` set.
|
|
76
|
+
tcp_nodelay: Option<bool>,
|
|
77
|
+
/// Set that all sockets have `SO_REUSEADDR` set.
|
|
78
|
+
tcp_reuse_address: Option<bool>,
|
|
79
|
+
|
|
80
|
+
// ========= Connection pool options =========
|
|
81
|
+
/// Set an optional timeout for idle sockets being kept-alive. (in seconds)
|
|
82
|
+
pool_idle_timeout: Option<u64>,
|
|
83
|
+
/// Sets the maximum idle connection per host allowed in the pool.
|
|
84
|
+
pool_max_idle_per_host: Option<usize>,
|
|
85
|
+
/// Sets the maximum number of connections in the pool.
|
|
86
|
+
pool_max_size: Option<u32>,
|
|
87
|
+
|
|
88
|
+
// ========= Protocol options =========
|
|
89
|
+
/// Whether to use the HTTP/1 protocol only.
|
|
90
|
+
http1_only: Option<bool>,
|
|
91
|
+
/// Whether to use the HTTP/2 protocol only.
|
|
92
|
+
http2_only: Option<bool>,
|
|
93
|
+
/// Whether to use HTTPS only.
|
|
94
|
+
https_only: Option<bool>,
|
|
95
|
+
|
|
96
|
+
// ========= TLS options =========
|
|
97
|
+
/// Whether to verify the SSL certificate or root certificate file path.
|
|
98
|
+
verify: Option<bool>,
|
|
99
|
+
|
|
100
|
+
// ========= Network options =========
|
|
101
|
+
/// Whether to disable the proxy for the client.
|
|
102
|
+
no_proxy: Option<bool>,
|
|
103
|
+
/// The proxy to use for the client.
|
|
104
|
+
#[serde(skip)]
|
|
105
|
+
proxy: Option<Proxy>,
|
|
106
|
+
/// Bind to a local IP Address.
|
|
107
|
+
local_address: Option<IpAddr>,
|
|
108
|
+
/// Bind to an interface by `SO_BINDTODEVICE`.
|
|
109
|
+
interface: Option<String>,
|
|
110
|
+
|
|
111
|
+
// ========= Compression options =========
|
|
112
|
+
/// Sets gzip as an accepted encoding.
|
|
113
|
+
gzip: Option<bool>,
|
|
114
|
+
/// Sets brotli as an accepted encoding.
|
|
115
|
+
brotli: Option<bool>,
|
|
116
|
+
/// Sets deflate as an accepted encoding.
|
|
117
|
+
deflate: Option<bool>,
|
|
118
|
+
/// Sets zstd as an accepted encoding.
|
|
119
|
+
zstd: Option<bool>,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#[derive(Clone, Default)]
|
|
123
|
+
#[magnus::wrap(class = "Wreq::Client", free_immediately, size)]
|
|
124
|
+
pub struct Client(wreq::Client);
|
|
125
|
+
|
|
126
|
+
// ===== impl Builder =====
|
|
127
|
+
|
|
128
|
+
impl Builder {
|
|
129
|
+
/// Create a new [`Builder`] from Ruby keyword arguments.
|
|
130
|
+
fn new(ruby: &magnus::Ruby, keyword: &Value) -> Result<Self, magnus::Error> {
|
|
131
|
+
if let Ok(hash) = RHash::try_convert(*keyword) {
|
|
132
|
+
let mut builder: Self = serde_magnus::deserialize(ruby, hash)?;
|
|
133
|
+
// extra emulation handling
|
|
134
|
+
if let Some(v) = hash.get(ruby.to_symbol("emulation")) {
|
|
135
|
+
let emulation_obj = Obj::<Emulation>::try_convert(v)?;
|
|
136
|
+
builder.emulation = Some((*emulation_obj).clone());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// extra user agent handling
|
|
140
|
+
builder.user_agent = Extractor::<HeaderValue>::try_convert(*keyword)?.into_inner();
|
|
141
|
+
|
|
142
|
+
// extra headers handling
|
|
143
|
+
builder.headers = Extractor::<HeaderMap>::try_convert(*keyword)?.into_inner();
|
|
144
|
+
|
|
145
|
+
// extra original headers handling
|
|
146
|
+
builder.orig_headers = Extractor::<OrigHeaderMap>::try_convert(*keyword)?.into_inner();
|
|
147
|
+
|
|
148
|
+
// extra proxy handling
|
|
149
|
+
builder.proxy = Extractor::<Proxy>::try_convert(*keyword)?.into_inner();
|
|
150
|
+
|
|
151
|
+
// extra cookie store handling
|
|
152
|
+
if let Some(jar) = hash.get(ruby.to_symbol("cookie_provider")) {
|
|
153
|
+
builder.cookie_provider = Some((*Obj::<Jar>::try_convert(jar)?).clone());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return Ok(builder);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
Ok(Default::default())
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ===== impl Client =====
|
|
164
|
+
|
|
165
|
+
impl Client {
|
|
166
|
+
/// Create a new [`Client`] with the given keyword arguments.
|
|
167
|
+
pub fn new(ruby: &Ruby, kwargs: &[Value]) -> Result<Self, magnus::Error> {
|
|
168
|
+
if let Some(kwargs) = kwargs.first() {
|
|
169
|
+
let mut params = Builder::new(ruby, kwargs)?;
|
|
170
|
+
gvl::nogvl(|| {
|
|
171
|
+
let mut builder = wreq::Client::builder();
|
|
172
|
+
|
|
173
|
+
// Emulation options.
|
|
174
|
+
apply_option!(set_if_some_inner, builder, params.emulation, emulation);
|
|
175
|
+
|
|
176
|
+
// User agent options.
|
|
177
|
+
apply_option!(set_if_some, builder, params.user_agent, user_agent);
|
|
178
|
+
|
|
179
|
+
// Default headers options.
|
|
180
|
+
apply_option!(set_if_some, builder, params.headers, default_headers);
|
|
181
|
+
apply_option!(set_if_some, builder, params.orig_headers, orig_headers);
|
|
182
|
+
|
|
183
|
+
// Allow redirects options.
|
|
184
|
+
apply_option!(set_if_some, builder, params.referer, referer);
|
|
185
|
+
apply_option!(
|
|
186
|
+
set_if_true_with,
|
|
187
|
+
builder,
|
|
188
|
+
params.allow_redirects,
|
|
189
|
+
redirect,
|
|
190
|
+
false,
|
|
191
|
+
params
|
|
192
|
+
.max_redirects
|
|
193
|
+
.take()
|
|
194
|
+
.map(wreq::redirect::Policy::limited)
|
|
195
|
+
.unwrap_or_default()
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Cookie options.
|
|
199
|
+
apply_option!(set_if_some, builder, params.cookie_store, cookie_store);
|
|
200
|
+
apply_option!(
|
|
201
|
+
set_if_some_inner,
|
|
202
|
+
builder,
|
|
203
|
+
params.cookie_provider,
|
|
204
|
+
cookie_provider
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// TCP options.
|
|
208
|
+
apply_option!(
|
|
209
|
+
set_if_some_map,
|
|
210
|
+
builder,
|
|
211
|
+
params.tcp_keepalive,
|
|
212
|
+
tcp_keepalive,
|
|
213
|
+
Duration::from_secs
|
|
214
|
+
);
|
|
215
|
+
apply_option!(
|
|
216
|
+
set_if_some_map,
|
|
217
|
+
builder,
|
|
218
|
+
params.tcp_keepalive_interval,
|
|
219
|
+
tcp_keepalive_interval,
|
|
220
|
+
Duration::from_secs
|
|
221
|
+
);
|
|
222
|
+
apply_option!(
|
|
223
|
+
set_if_some,
|
|
224
|
+
builder,
|
|
225
|
+
params.tcp_keepalive_retries,
|
|
226
|
+
tcp_keepalive_retries
|
|
227
|
+
);
|
|
228
|
+
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
|
229
|
+
apply_option!(
|
|
230
|
+
set_if_some_map,
|
|
231
|
+
builder,
|
|
232
|
+
params.tcp_user_timeout,
|
|
233
|
+
tcp_user_timeout,
|
|
234
|
+
Duration::from_secs
|
|
235
|
+
);
|
|
236
|
+
apply_option!(set_if_some, builder, params.tcp_nodelay, tcp_nodelay);
|
|
237
|
+
apply_option!(
|
|
238
|
+
set_if_some,
|
|
239
|
+
builder,
|
|
240
|
+
params.tcp_reuse_address,
|
|
241
|
+
tcp_reuse_address
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Timeout options.
|
|
245
|
+
apply_option!(
|
|
246
|
+
set_if_some_map,
|
|
247
|
+
builder,
|
|
248
|
+
params.timeout,
|
|
249
|
+
timeout,
|
|
250
|
+
Duration::from_secs
|
|
251
|
+
);
|
|
252
|
+
apply_option!(
|
|
253
|
+
set_if_some_map,
|
|
254
|
+
builder,
|
|
255
|
+
params.connect_timeout,
|
|
256
|
+
connect_timeout,
|
|
257
|
+
Duration::from_secs
|
|
258
|
+
);
|
|
259
|
+
apply_option!(
|
|
260
|
+
set_if_some_map,
|
|
261
|
+
builder,
|
|
262
|
+
params.read_timeout,
|
|
263
|
+
read_timeout,
|
|
264
|
+
Duration::from_secs
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Pool options.
|
|
268
|
+
apply_option!(
|
|
269
|
+
set_if_some_map,
|
|
270
|
+
builder,
|
|
271
|
+
params.pool_idle_timeout,
|
|
272
|
+
pool_idle_timeout,
|
|
273
|
+
Duration::from_secs
|
|
274
|
+
);
|
|
275
|
+
apply_option!(
|
|
276
|
+
set_if_some,
|
|
277
|
+
builder,
|
|
278
|
+
params.pool_max_idle_per_host,
|
|
279
|
+
pool_max_idle_per_host
|
|
280
|
+
);
|
|
281
|
+
apply_option!(set_if_some, builder, params.pool_max_size, pool_max_size);
|
|
282
|
+
|
|
283
|
+
// Protocol options.
|
|
284
|
+
apply_option!(set_if_true, builder, params.http1_only, http1_only, false);
|
|
285
|
+
apply_option!(set_if_true, builder, params.http2_only, http2_only, false);
|
|
286
|
+
apply_option!(set_if_some, builder, params.https_only, https_only);
|
|
287
|
+
|
|
288
|
+
// TLS options.
|
|
289
|
+
apply_option!(set_if_some, builder, params.verify, cert_verification);
|
|
290
|
+
|
|
291
|
+
// Network options.
|
|
292
|
+
apply_option!(set_if_some, builder, params.proxy, proxy);
|
|
293
|
+
apply_option!(set_if_true, builder, params.no_proxy, no_proxy, false);
|
|
294
|
+
apply_option!(set_if_some, builder, params.local_address, local_address);
|
|
295
|
+
apply_option!(set_if_some, builder, params.interface, interface);
|
|
296
|
+
|
|
297
|
+
// Compression options.
|
|
298
|
+
apply_option!(set_if_some, builder, params.gzip, gzip);
|
|
299
|
+
apply_option!(set_if_some, builder, params.brotli, brotli);
|
|
300
|
+
apply_option!(set_if_some, builder, params.deflate, deflate);
|
|
301
|
+
apply_option!(set_if_some, builder, params.zstd, zstd);
|
|
302
|
+
|
|
303
|
+
builder.build().map(Client).map_err(wreq_error_to_magnus)
|
|
304
|
+
})
|
|
305
|
+
} else {
|
|
306
|
+
gvl::nogvl(|| Ok(Self(wreq::Client::new())))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
impl Client {
|
|
312
|
+
/// Send a HTTP request.
|
|
313
|
+
#[inline]
|
|
314
|
+
pub fn request(rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
315
|
+
let ((method, url), request) = extract_request!(args, (Obj<Method>, String));
|
|
316
|
+
execute_request(rb_self.0.clone(), *method, url, request)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// Send a GET request.
|
|
320
|
+
#[inline]
|
|
321
|
+
pub fn get(rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
322
|
+
let ((url,), request) = extract_request!(args, (String,));
|
|
323
|
+
execute_request(rb_self.0.clone(), Method::GET, url, request)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/// Send a POST request.
|
|
327
|
+
#[inline]
|
|
328
|
+
pub fn post(rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
329
|
+
let ((url,), request) = extract_request!(args, (String,));
|
|
330
|
+
execute_request(rb_self.0.clone(), Method::POST, url, request)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/// Send a PUT request.
|
|
334
|
+
#[inline]
|
|
335
|
+
pub fn put(rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
336
|
+
let ((url,), request) = extract_request!(args, (String,));
|
|
337
|
+
execute_request(rb_self.0.clone(), Method::PUT, url, request)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// Send a DELETE request.
|
|
341
|
+
#[inline]
|
|
342
|
+
pub fn delete(rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
343
|
+
let ((url,), request) = extract_request!(args, (String,));
|
|
344
|
+
execute_request(rb_self.0.clone(), Method::DELETE, url, request)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// Send a HEAD request.
|
|
348
|
+
#[inline]
|
|
349
|
+
pub fn head(rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
350
|
+
let ((url,), request) = extract_request!(args, (String,));
|
|
351
|
+
execute_request(rb_self.0.clone(), Method::HEAD, url, request)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// Send an OPTIONS request.
|
|
355
|
+
#[inline]
|
|
356
|
+
pub fn options(rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
357
|
+
let ((url,), request) = extract_request!(args, (String,));
|
|
358
|
+
execute_request(rb_self.0.clone(), Method::OPTIONS, url, request)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// Send a TRACE request.
|
|
362
|
+
#[inline]
|
|
363
|
+
pub fn trace(rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
364
|
+
let ((url,), request) = extract_request!(args, (String,));
|
|
365
|
+
execute_request(rb_self.0.clone(), Method::TRACE, url, request)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/// Send a PATCH request.
|
|
369
|
+
#[inline]
|
|
370
|
+
pub fn patch(rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
371
|
+
let ((url,), request) = extract_request!(args, (String,));
|
|
372
|
+
execute_request(rb_self.0.clone(), Method::PATCH, url, request)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), magnus::Error> {
|
|
377
|
+
let client_class = gem_module.define_class("Client", ruby.class_object())?;
|
|
378
|
+
client_class.define_singleton_method("new", function!(Client::new, -1))?;
|
|
379
|
+
client_class.define_method("request", method!(Client::request, -1))?;
|
|
380
|
+
client_class.define_method("get", method!(Client::get, -1))?;
|
|
381
|
+
client_class.define_method("post", method!(Client::post, -1))?;
|
|
382
|
+
client_class.define_method("put", method!(Client::put, -1))?;
|
|
383
|
+
client_class.define_method("delete", method!(Client::delete, -1))?;
|
|
384
|
+
client_class.define_method("head", method!(Client::head, -1))?;
|
|
385
|
+
client_class.define_method("options", method!(Client::options, -1))?;
|
|
386
|
+
client_class.define_method("trace", method!(Client::trace, -1))?;
|
|
387
|
+
client_class.define_method("patch", method!(Client::patch, -1))?;
|
|
388
|
+
|
|
389
|
+
resp::include(ruby, gem_module)?;
|
|
390
|
+
body::include(ruby, gem_module)?;
|
|
391
|
+
Ok(())
|
|
392
|
+
}
|
data/src/cookie.rs
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
use std::{fmt, sync::Arc, time::SystemTime};
|
|
2
|
+
|
|
3
|
+
use cookie::{Cookie as RawCookie, Expiration, ParseError, time::Duration};
|
|
4
|
+
use magnus::{
|
|
5
|
+
Error, Module, Object, RModule, Ruby, Value, function, method, typed_data::Obj,
|
|
6
|
+
value::ReprValue,
|
|
7
|
+
};
|
|
8
|
+
use wreq::header::{self, HeaderMap, HeaderValue};
|
|
9
|
+
|
|
10
|
+
use crate::gvl;
|
|
11
|
+
|
|
12
|
+
define_ruby_enum!(
|
|
13
|
+
/// The Cookie SameSite attribute.
|
|
14
|
+
const,
|
|
15
|
+
SameSite,
|
|
16
|
+
"Wreq::SameSite",
|
|
17
|
+
cookie::SameSite,
|
|
18
|
+
Strict,
|
|
19
|
+
Lax,
|
|
20
|
+
None,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/// A single HTTP cookie.
|
|
24
|
+
#[derive(Clone)]
|
|
25
|
+
#[magnus::wrap(class = "Wreq::Cookie", free_immediately, size)]
|
|
26
|
+
pub struct Cookie(RawCookie<'static>);
|
|
27
|
+
|
|
28
|
+
/// A good default `CookieStore` implementation.
|
|
29
|
+
///
|
|
30
|
+
/// This is the implementation used when simply calling `cookie_store(true)`.
|
|
31
|
+
/// This type is exposed to allow creating one and filling it with some
|
|
32
|
+
/// existing cookies more easily, before creating a `Client`.
|
|
33
|
+
#[derive(Clone, Default)]
|
|
34
|
+
#[magnus::wrap(class = "Wreq::Jar", free_immediately, size)]
|
|
35
|
+
pub struct Jar(pub Arc<wreq::cookie::Jar>);
|
|
36
|
+
|
|
37
|
+
// ===== impl Cookie =====
|
|
38
|
+
|
|
39
|
+
impl Cookie {
|
|
40
|
+
/// Create a new [`Cookie`].
|
|
41
|
+
pub fn new(args: &[Value]) -> Result<Self, Error> {
|
|
42
|
+
let args =
|
|
43
|
+
magnus::scan_args::scan_args::<(String, String), (), (), (), magnus::RHash, ()>(args)?;
|
|
44
|
+
#[allow(clippy::type_complexity)]
|
|
45
|
+
let keywords: magnus::scan_args::KwArgs<
|
|
46
|
+
(),
|
|
47
|
+
(
|
|
48
|
+
Option<String>,
|
|
49
|
+
Option<String>,
|
|
50
|
+
Option<u64>,
|
|
51
|
+
Option<f64>,
|
|
52
|
+
Option<bool>,
|
|
53
|
+
Option<bool>,
|
|
54
|
+
Option<Obj<SameSite>>,
|
|
55
|
+
),
|
|
56
|
+
(),
|
|
57
|
+
> = magnus::scan_args::get_kwargs(
|
|
58
|
+
args.keywords,
|
|
59
|
+
&[],
|
|
60
|
+
&[
|
|
61
|
+
"domain",
|
|
62
|
+
"path",
|
|
63
|
+
"max_age",
|
|
64
|
+
"expires",
|
|
65
|
+
"http_only",
|
|
66
|
+
"secure",
|
|
67
|
+
"same_site",
|
|
68
|
+
],
|
|
69
|
+
)?;
|
|
70
|
+
|
|
71
|
+
let (name, value) = args.required;
|
|
72
|
+
|
|
73
|
+
let mut cookie = RawCookie::new(name, value);
|
|
74
|
+
|
|
75
|
+
if let Some(domain) = keywords.optional.0 {
|
|
76
|
+
cookie.set_domain(domain);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if let Some(path) = keywords.optional.1 {
|
|
80
|
+
cookie.set_path(path);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if let Some(max_age) = keywords.optional.2 {
|
|
84
|
+
cookie.set_max_age(Duration::seconds(max_age as i64));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if let Some(expires) = keywords.optional.3 {
|
|
88
|
+
let duration = std::time::Duration::from_secs_f64(expires);
|
|
89
|
+
if let Some(system_time) = SystemTime::UNIX_EPOCH.checked_add(duration) {
|
|
90
|
+
cookie.set_expires(Expiration::DateTime(system_time.into()));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
cookie.set_http_only(keywords.optional.4);
|
|
95
|
+
cookie.set_secure(keywords.optional.5);
|
|
96
|
+
|
|
97
|
+
if let Some(same_site) = keywords.optional.6 {
|
|
98
|
+
cookie.set_same_site(same_site.into_ffi());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Ok(Self(cookie))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// The name of the cookie.
|
|
105
|
+
#[inline]
|
|
106
|
+
pub fn name(&self) -> &str {
|
|
107
|
+
self.0.name()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// The value of the cookie.
|
|
111
|
+
#[inline]
|
|
112
|
+
pub fn value(&self) -> &str {
|
|
113
|
+
self.0.value()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Returns true if the 'HttpOnly' directive is enabled.
|
|
117
|
+
#[inline]
|
|
118
|
+
pub fn http_only(&self) -> bool {
|
|
119
|
+
self.0.http_only().unwrap_or(false)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Returns true if the 'Secure' directive is enabled.
|
|
123
|
+
#[inline]
|
|
124
|
+
pub fn secure(&self) -> bool {
|
|
125
|
+
self.0.secure().unwrap_or(false)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Returns true if 'SameSite' directive is 'Lax'.
|
|
129
|
+
#[inline]
|
|
130
|
+
pub fn same_site_lax(&self) -> bool {
|
|
131
|
+
self.0.same_site() == Some(cookie::SameSite::Lax)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Returns true if 'SameSite' directive is 'Strict'.
|
|
135
|
+
#[inline]
|
|
136
|
+
pub fn same_site_strict(&self) -> bool {
|
|
137
|
+
self.0.same_site() == Some(cookie::SameSite::Strict)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Returns the path directive of the cookie, if set.
|
|
141
|
+
#[inline]
|
|
142
|
+
pub fn path(&self) -> Option<&str> {
|
|
143
|
+
self.0.path()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Returns the domain directive of the cookie, if set.
|
|
147
|
+
#[inline]
|
|
148
|
+
pub fn domain(&self) -> Option<&str> {
|
|
149
|
+
self.0.domain()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Get the Max-Age information.
|
|
153
|
+
#[inline]
|
|
154
|
+
pub fn max_age(&self) -> Option<i64> {
|
|
155
|
+
self.0.max_age().map(|d| d.whole_seconds())
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// The cookie expiration time.
|
|
159
|
+
#[inline]
|
|
160
|
+
pub fn expires(&self) -> Option<f64> {
|
|
161
|
+
match self.0.expires() {
|
|
162
|
+
Some(Expiration::DateTime(offset)) => {
|
|
163
|
+
let system_time = SystemTime::from(offset);
|
|
164
|
+
system_time
|
|
165
|
+
.duration_since(SystemTime::UNIX_EPOCH)
|
|
166
|
+
.ok()
|
|
167
|
+
.map(|d| d.as_secs_f64())
|
|
168
|
+
}
|
|
169
|
+
None | Some(Expiration::Session) => None,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
impl Cookie {
|
|
175
|
+
/// Parse cookies from a `HeaderMap`.
|
|
176
|
+
pub fn extract_headers_cookies(headers: &HeaderMap) -> Vec<Cookie> {
|
|
177
|
+
headers
|
|
178
|
+
.get_all(header::SET_COOKIE)
|
|
179
|
+
.iter()
|
|
180
|
+
.map(Cookie::parse)
|
|
181
|
+
.flat_map(Result::ok)
|
|
182
|
+
.map(RawCookie::into_owned)
|
|
183
|
+
.map(Cookie)
|
|
184
|
+
.collect()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fn parse<'a>(value: &'a HeaderValue) -> Result<RawCookie<'a>, ParseError> {
|
|
188
|
+
std::str::from_utf8(value.as_bytes())
|
|
189
|
+
.map_err(cookie::ParseError::from)
|
|
190
|
+
.and_then(RawCookie::parse)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
impl fmt::Display for Cookie {
|
|
195
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
196
|
+
write!(f, "{}", self.0)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ===== impl Jar =====
|
|
201
|
+
|
|
202
|
+
impl Jar {
|
|
203
|
+
/// Create a new [`Jar`] with an empty cookie store.
|
|
204
|
+
pub fn new() -> Self {
|
|
205
|
+
Jar(Arc::new(wreq::cookie::Jar::default()))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// Get all cookies.
|
|
209
|
+
pub fn get_all(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
210
|
+
let cookies: Vec<Cookie> = rb_self
|
|
211
|
+
.0
|
|
212
|
+
.get_all()
|
|
213
|
+
.map(RawCookie::from)
|
|
214
|
+
.map(Cookie)
|
|
215
|
+
.collect();
|
|
216
|
+
let ary = ruby.ary_new_capa(cookies.len());
|
|
217
|
+
for cookie in cookies {
|
|
218
|
+
ary.push(cookie)?;
|
|
219
|
+
}
|
|
220
|
+
Ok(ary.as_value())
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// Add a cookie to this jar.
|
|
224
|
+
pub fn add_cookie(&self, cookie: &Cookie, url: String) {
|
|
225
|
+
gvl::nogvl(|| self.0.add(cookie.0.clone(), &url))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// Add a cookie str to this jar.
|
|
229
|
+
pub fn add_cookie_str(&self, cookie: String, url: String) {
|
|
230
|
+
gvl::nogvl(|| self.0.add(cookie.as_ref(), &url))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Remove a cookie from this jar by name and URL.
|
|
234
|
+
pub fn remove(&self, name: String, url: String) {
|
|
235
|
+
gvl::nogvl(|| self.0.remove(name, &url))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Clear all cookies in this jar.
|
|
239
|
+
pub fn clear(&self) {
|
|
240
|
+
gvl::nogvl(|| self.0.clear())
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
|
|
245
|
+
// SameSite enum
|
|
246
|
+
let same_site_class = gem_module.define_class("SameSite", ruby.class_object())?;
|
|
247
|
+
same_site_class.const_set("Strict", SameSite::Strict)?;
|
|
248
|
+
same_site_class.const_set("Lax", SameSite::Lax)?;
|
|
249
|
+
same_site_class.const_set("None", SameSite::None)?;
|
|
250
|
+
|
|
251
|
+
// Cookie class
|
|
252
|
+
let cookie_class = gem_module.define_class("Cookie", ruby.class_object())?;
|
|
253
|
+
cookie_class.define_singleton_method("new", function!(Cookie::new, -1))?;
|
|
254
|
+
cookie_class.define_method("name", method!(Cookie::name, 0))?;
|
|
255
|
+
cookie_class.define_method("value", method!(Cookie::value, 0))?;
|
|
256
|
+
cookie_class.define_method("http_only", method!(Cookie::http_only, 0))?;
|
|
257
|
+
cookie_class.define_method("http_only?", method!(Cookie::http_only, 0))?;
|
|
258
|
+
cookie_class.define_method("secure", method!(Cookie::secure, 0))?;
|
|
259
|
+
cookie_class.define_method("secure?", method!(Cookie::secure, 0))?;
|
|
260
|
+
cookie_class.define_method("same_site_lax?", method!(Cookie::same_site_lax, 0))?;
|
|
261
|
+
cookie_class.define_method("same_site_strict?", method!(Cookie::same_site_strict, 0))?;
|
|
262
|
+
cookie_class.define_method("path", method!(Cookie::path, 0))?;
|
|
263
|
+
cookie_class.define_method("domain", method!(Cookie::domain, 0))?;
|
|
264
|
+
cookie_class.define_method("max_age", method!(Cookie::max_age, 0))?;
|
|
265
|
+
cookie_class.define_method("expires", method!(Cookie::expires, 0))?;
|
|
266
|
+
|
|
267
|
+
// Jar class
|
|
268
|
+
let jar_class = gem_module.define_class("Jar", ruby.class_object())?;
|
|
269
|
+
jar_class.define_singleton_method("new", function!(Jar::new, 0))?;
|
|
270
|
+
jar_class.define_method("get_all", method!(Jar::get_all, 0))?;
|
|
271
|
+
jar_class.define_method("add_cookie", method!(Jar::add_cookie, 2))?;
|
|
272
|
+
jar_class.define_method("add_cookie_str", method!(Jar::add_cookie_str, 2))?;
|
|
273
|
+
jar_class.define_method("remove", method!(Jar::remove, 2))?;
|
|
274
|
+
jar_class.define_method("clear", method!(Jar::clear, 0))?;
|
|
275
|
+
|
|
276
|
+
Ok(())
|
|
277
|
+
}
|