wreq 1.2.11 → 1.2.13
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 +88 -146
- data/Cargo.toml +10 -7
- data/crates/wreq-util/src/emulate/profile/chrome/http2.rs +1 -1
- data/crates/wreq-util/src/emulate/profile/firefox/http2.rs +2 -2
- data/crates/wreq-util/src/emulate/profile/opera/http2.rs +1 -1
- data/docs/fork-safety.md +51 -0
- data/docs/interrupt-handling.md +134 -0
- data/examples/tls_info.rb +27 -0
- data/lib/wreq.rb +37 -0
- data/lib/wreq_ruby/body.rb +9 -0
- data/lib/wreq_ruby/client.rb +24 -1
- data/lib/wreq_ruby/cookie.rb +9 -0
- data/lib/wreq_ruby/error.rb +15 -0
- data/lib/wreq_ruby/response.rb +44 -0
- data/lib/wreq_ruby/tls.rb +73 -0
- data/src/arch.rs +174 -0
- data/src/client/body/stream.rs +28 -22
- data/src/client/req.rs +119 -122
- data/src/client/resp.rs +120 -47
- data/src/client.rs +103 -44
- data/src/cookie.rs +47 -11
- data/src/error.rs +28 -0
- data/src/lib.rs +3 -1
- data/src/macros.rs +2 -3
- data/src/rt.rs +46 -25
- data/src/tls.rs +51 -0
- data/test/fork_test.rb +106 -0
- data/test/scripts/fork_safety.rb +133 -0
- data/test/scripts/prefork_runtime.rb +95 -0
- data/test/support/tls_server.rb +95 -0
- data/test/tls_info_test.rb +59 -0
- metadata +11 -1
data/src/client.rs
CHANGED
|
@@ -11,7 +11,7 @@ use magnus::{Module, Object, RModule, Ruby, TryConvert, Value, function, method,
|
|
|
11
11
|
use wreq::Proxy;
|
|
12
12
|
|
|
13
13
|
use crate::{
|
|
14
|
-
arch::{SUPPORTS_INTERFACE, SUPPORTS_TCP_USER_TIMEOUT},
|
|
14
|
+
arch::{ProcessLocal, SUPPORTS_INTERFACE, SUPPORTS_TCP_USER_TIMEOUT},
|
|
15
15
|
client::{req::execute_request, resp::Response},
|
|
16
16
|
cookie::Jar,
|
|
17
17
|
emulate::Emulation,
|
|
@@ -50,7 +50,7 @@ struct Builder {
|
|
|
50
50
|
cookie_store: Option<bool>,
|
|
51
51
|
/// Whether to use cookie store provider.
|
|
52
52
|
#[serde(default)]
|
|
53
|
-
cookie_provider: NativeOption<Jar
|
|
53
|
+
cookie_provider: NativeOption<Obj<Jar>>,
|
|
54
54
|
|
|
55
55
|
// ========= Timeout options =========
|
|
56
56
|
/// The timeout to use for the client. (in seconds)
|
|
@@ -94,6 +94,8 @@ struct Builder {
|
|
|
94
94
|
// ========= TLS options =========
|
|
95
95
|
/// Whether to verify TLS certificates.
|
|
96
96
|
verify: Option<bool>,
|
|
97
|
+
/// Whether to retain peer certificate data on responses.
|
|
98
|
+
tls_info: Option<bool>,
|
|
97
99
|
|
|
98
100
|
// ========= Network options =========
|
|
99
101
|
/// Whether to disable the proxy for the client.
|
|
@@ -118,9 +120,8 @@ struct Builder {
|
|
|
118
120
|
zstd: Option<bool>,
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
#[derive(Clone)]
|
|
122
123
|
#[magnus::wrap(class = "Wreq::Client", free_immediately, size)]
|
|
123
|
-
pub struct Client(wreq::Client);
|
|
124
|
+
pub struct Client(ProcessLocal<wreq::Client>);
|
|
124
125
|
|
|
125
126
|
// ===== impl Builder =====
|
|
126
127
|
|
|
@@ -168,12 +169,7 @@ impl Builder {
|
|
|
168
169
|
extract_native_option!(options, builder, user_agent);
|
|
169
170
|
extract_native_option!(options, builder, headers);
|
|
170
171
|
extract_native_option!(options, builder, orig_headers);
|
|
171
|
-
extract_native_option!(
|
|
172
|
-
options,
|
|
173
|
-
builder,
|
|
174
|
-
cookie_provider,
|
|
175
|
-
Obj<Jar> => |value| (*value).clone()
|
|
176
|
-
);
|
|
172
|
+
extract_native_option!(options, builder, cookie_provider);
|
|
177
173
|
builder
|
|
178
174
|
.proxy
|
|
179
175
|
.set(Extractor::<Proxy>::try_convert(options.as_value())?.into_inner());
|
|
@@ -190,14 +186,16 @@ impl Client {
|
|
|
190
186
|
/// # Errors
|
|
191
187
|
///
|
|
192
188
|
/// Returns Ruby configuration errors from [`Builder::from_options`] or the
|
|
193
|
-
/// native fallible client builder.
|
|
194
|
-
/// `ArgumentError`.
|
|
189
|
+
/// native fallible client builder. An inherited cookie provider returns
|
|
190
|
+
/// `Wreq::ForkError`, and extra positional arguments return `ArgumentError`.
|
|
195
191
|
pub fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, magnus::Error> {
|
|
196
192
|
Options::from_args(ruby, args, "client")?
|
|
197
193
|
.map(Builder::from_options)
|
|
198
194
|
.transpose()
|
|
199
195
|
.map(Option::unwrap_or_default)
|
|
200
196
|
.and_then(|params| Self::build(ruby, params))
|
|
197
|
+
.map(ProcessLocal::new)
|
|
198
|
+
.map(Self)
|
|
201
199
|
}
|
|
202
200
|
|
|
203
201
|
/// Build the default client through the same fallible path as `new`.
|
|
@@ -206,7 +204,7 @@ impl Client {
|
|
|
206
204
|
///
|
|
207
205
|
/// Returns `Wreq::BuilderError`, `Wreq::TlsError`, or another mapped native
|
|
208
206
|
/// initialization error without unwinding through Ruby.
|
|
209
|
-
pub(crate) fn default_client(ruby: &Ruby) -> Result<
|
|
207
|
+
pub(crate) fn default_client(ruby: &Ruby) -> Result<wreq::Client, magnus::Error> {
|
|
210
208
|
Self::build(ruby, Builder::default())
|
|
211
209
|
}
|
|
212
210
|
|
|
@@ -214,8 +212,15 @@ impl Client {
|
|
|
214
212
|
///
|
|
215
213
|
/// # Errors
|
|
216
214
|
///
|
|
217
|
-
///
|
|
218
|
-
|
|
215
|
+
/// Returns `Wreq::ForkError` if the cookie provider belongs to a parent
|
|
216
|
+
/// process. Native build failures are mapped only after the GVL has been
|
|
217
|
+
/// reacquired.
|
|
218
|
+
fn build(ruby: &Ruby, mut params: Builder) -> Result<wreq::Client, magnus::Error> {
|
|
219
|
+
let mut cookie_provider = params
|
|
220
|
+
.cookie_provider
|
|
221
|
+
.take()
|
|
222
|
+
.map(|jar| jar.clone_store(ruby))
|
|
223
|
+
.transpose()?;
|
|
219
224
|
let result = gvl::nogvl(|| {
|
|
220
225
|
let mut builder = wreq::Client::builder();
|
|
221
226
|
|
|
@@ -259,12 +264,7 @@ impl Client {
|
|
|
259
264
|
|
|
260
265
|
// Cookie options.
|
|
261
266
|
apply_option!(set_if_some, builder, params.cookie_store, cookie_store);
|
|
262
|
-
apply_option!(
|
|
263
|
-
set_if_some_inner,
|
|
264
|
-
builder,
|
|
265
|
-
params.cookie_provider,
|
|
266
|
-
cookie_provider
|
|
267
|
-
);
|
|
267
|
+
apply_option!(set_if_some, builder, cookie_provider, cookie_provider);
|
|
268
268
|
|
|
269
269
|
// TCP options.
|
|
270
270
|
apply_option!(
|
|
@@ -349,6 +349,7 @@ impl Client {
|
|
|
349
349
|
|
|
350
350
|
// TLS options.
|
|
351
351
|
apply_option!(set_if_some, builder, params.verify, tls_cert_verification);
|
|
352
|
+
apply_option!(set_if_some, builder, params.tls_info, tls_info);
|
|
352
353
|
|
|
353
354
|
// Network options.
|
|
354
355
|
apply_option!(set_if_some, builder, params.proxy, proxy);
|
|
@@ -374,12 +375,22 @@ impl Client {
|
|
|
374
375
|
apply_option!(set_if_some, builder, params.deflate, deflate);
|
|
375
376
|
apply_option!(set_if_some, builder, params.zstd, zstd);
|
|
376
377
|
|
|
377
|
-
builder.build()
|
|
378
|
+
builder.build()
|
|
378
379
|
});
|
|
379
380
|
|
|
380
381
|
// Ruby exceptions must be created after the GVL has been reacquired.
|
|
381
382
|
result.map_err(|err| wreq_error(ruby, err))
|
|
382
383
|
}
|
|
384
|
+
|
|
385
|
+
/// Clone the native client handle in the process that created it.
|
|
386
|
+
///
|
|
387
|
+
/// # Errors
|
|
388
|
+
///
|
|
389
|
+
/// Returns `Wreq::ForkError` when the client was inherited from a parent
|
|
390
|
+
/// process.
|
|
391
|
+
fn native_client(&self, ruby: &Ruby) -> Result<wreq::Client, magnus::Error> {
|
|
392
|
+
self.0.get(ruby).cloned()
|
|
393
|
+
}
|
|
383
394
|
}
|
|
384
395
|
|
|
385
396
|
impl Client {
|
|
@@ -391,9 +402,9 @@ impl Client {
|
|
|
391
402
|
ruby: &Ruby,
|
|
392
403
|
args: &[Value],
|
|
393
404
|
) -> Result<Response, magnus::Error> {
|
|
394
|
-
let ((method, url), request) = extract_request!(args, (Obj<Method>, String));
|
|
405
|
+
let ((method, url), request) = extract_request!(ruby, args, (Obj<Method>, String));
|
|
395
406
|
let client = Self::default_client(ruby)?;
|
|
396
|
-
execute_request(ruby, client
|
|
407
|
+
execute_request(ruby, client, *method, url, request)
|
|
397
408
|
}
|
|
398
409
|
|
|
399
410
|
/// Send a request with `method` through a newly built default client.
|
|
@@ -405,72 +416,120 @@ impl Client {
|
|
|
405
416
|
method: Method,
|
|
406
417
|
args: &[Value],
|
|
407
418
|
) -> Result<Response, magnus::Error> {
|
|
408
|
-
let ((url,), request) = extract_request!(args, (String,));
|
|
419
|
+
let ((url,), request) = extract_request!(ruby, args, (String,));
|
|
409
420
|
let client = Self::default_client(ruby)?;
|
|
410
|
-
execute_request(ruby, client
|
|
421
|
+
execute_request(ruby, client, method, url, request)
|
|
411
422
|
}
|
|
412
423
|
|
|
413
424
|
/// Send a HTTP request.
|
|
414
425
|
#[inline]
|
|
415
426
|
pub fn request(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
416
|
-
let ((method, url), request) = extract_request!(args, (Obj<Method>, String));
|
|
417
|
-
execute_request(ruby, rb_self.
|
|
427
|
+
let ((method, url), request) = extract_request!(ruby, args, (Obj<Method>, String));
|
|
428
|
+
execute_request(ruby, rb_self.native_client(ruby)?, *method, url, request)
|
|
418
429
|
}
|
|
419
430
|
|
|
420
431
|
/// Send a GET request.
|
|
421
432
|
#[inline]
|
|
422
433
|
pub fn get(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
423
|
-
let ((url,), request) = extract_request!(args, (String,));
|
|
424
|
-
execute_request(
|
|
434
|
+
let ((url,), request) = extract_request!(ruby, args, (String,));
|
|
435
|
+
execute_request(
|
|
436
|
+
ruby,
|
|
437
|
+
rb_self.native_client(ruby)?,
|
|
438
|
+
Method::GET,
|
|
439
|
+
url,
|
|
440
|
+
request,
|
|
441
|
+
)
|
|
425
442
|
}
|
|
426
443
|
|
|
427
444
|
/// Send a POST request.
|
|
428
445
|
#[inline]
|
|
429
446
|
pub fn post(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
430
|
-
let ((url,), request) = extract_request!(args, (String,));
|
|
431
|
-
execute_request(
|
|
447
|
+
let ((url,), request) = extract_request!(ruby, args, (String,));
|
|
448
|
+
execute_request(
|
|
449
|
+
ruby,
|
|
450
|
+
rb_self.native_client(ruby)?,
|
|
451
|
+
Method::POST,
|
|
452
|
+
url,
|
|
453
|
+
request,
|
|
454
|
+
)
|
|
432
455
|
}
|
|
433
456
|
|
|
434
457
|
/// Send a PUT request.
|
|
435
458
|
#[inline]
|
|
436
459
|
pub fn put(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
437
|
-
let ((url,), request) = extract_request!(args, (String,));
|
|
438
|
-
execute_request(
|
|
460
|
+
let ((url,), request) = extract_request!(ruby, args, (String,));
|
|
461
|
+
execute_request(
|
|
462
|
+
ruby,
|
|
463
|
+
rb_self.native_client(ruby)?,
|
|
464
|
+
Method::PUT,
|
|
465
|
+
url,
|
|
466
|
+
request,
|
|
467
|
+
)
|
|
439
468
|
}
|
|
440
469
|
|
|
441
470
|
/// Send a DELETE request.
|
|
442
471
|
#[inline]
|
|
443
472
|
pub fn delete(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
444
|
-
let ((url,), request) = extract_request!(args, (String,));
|
|
445
|
-
execute_request(
|
|
473
|
+
let ((url,), request) = extract_request!(ruby, args, (String,));
|
|
474
|
+
execute_request(
|
|
475
|
+
ruby,
|
|
476
|
+
rb_self.native_client(ruby)?,
|
|
477
|
+
Method::DELETE,
|
|
478
|
+
url,
|
|
479
|
+
request,
|
|
480
|
+
)
|
|
446
481
|
}
|
|
447
482
|
|
|
448
483
|
/// Send a HEAD request.
|
|
449
484
|
#[inline]
|
|
450
485
|
pub fn head(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
451
|
-
let ((url,), request) = extract_request!(args, (String,));
|
|
452
|
-
execute_request(
|
|
486
|
+
let ((url,), request) = extract_request!(ruby, args, (String,));
|
|
487
|
+
execute_request(
|
|
488
|
+
ruby,
|
|
489
|
+
rb_self.native_client(ruby)?,
|
|
490
|
+
Method::HEAD,
|
|
491
|
+
url,
|
|
492
|
+
request,
|
|
493
|
+
)
|
|
453
494
|
}
|
|
454
495
|
|
|
455
496
|
/// Send an OPTIONS request.
|
|
456
497
|
#[inline]
|
|
457
498
|
pub fn options(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
458
|
-
let ((url,), request) = extract_request!(args, (String,));
|
|
459
|
-
execute_request(
|
|
499
|
+
let ((url,), request) = extract_request!(ruby, args, (String,));
|
|
500
|
+
execute_request(
|
|
501
|
+
ruby,
|
|
502
|
+
rb_self.native_client(ruby)?,
|
|
503
|
+
Method::OPTIONS,
|
|
504
|
+
url,
|
|
505
|
+
request,
|
|
506
|
+
)
|
|
460
507
|
}
|
|
461
508
|
|
|
462
509
|
/// Send a TRACE request.
|
|
463
510
|
#[inline]
|
|
464
511
|
pub fn trace(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
465
|
-
let ((url,), request) = extract_request!(args, (String,));
|
|
466
|
-
execute_request(
|
|
512
|
+
let ((url,), request) = extract_request!(ruby, args, (String,));
|
|
513
|
+
execute_request(
|
|
514
|
+
ruby,
|
|
515
|
+
rb_self.native_client(ruby)?,
|
|
516
|
+
Method::TRACE,
|
|
517
|
+
url,
|
|
518
|
+
request,
|
|
519
|
+
)
|
|
467
520
|
}
|
|
468
521
|
|
|
469
522
|
/// Send a PATCH request.
|
|
470
523
|
#[inline]
|
|
471
524
|
pub fn patch(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
472
|
-
let ((url,), request) = extract_request!(args, (String,));
|
|
473
|
-
execute_request(
|
|
525
|
+
let ((url,), request) = extract_request!(ruby, args, (String,));
|
|
526
|
+
execute_request(
|
|
527
|
+
ruby,
|
|
528
|
+
rb_self.native_client(ruby)?,
|
|
529
|
+
Method::PATCH,
|
|
530
|
+
url,
|
|
531
|
+
request,
|
|
532
|
+
)
|
|
474
533
|
}
|
|
475
534
|
}
|
|
476
535
|
|
data/src/cookie.rs
CHANGED
|
@@ -18,6 +18,7 @@ use magnus::{
|
|
|
18
18
|
use wreq::header::{self, HeaderMap, HeaderValue};
|
|
19
19
|
|
|
20
20
|
use crate::{
|
|
21
|
+
arch::ProcessLocal,
|
|
21
22
|
error::{header_value_error, type_error},
|
|
22
23
|
gvl,
|
|
23
24
|
options::{NativeOption, Options},
|
|
@@ -78,9 +79,14 @@ struct Builder {
|
|
|
78
79
|
/// A cookie jar that can be shared with a Ruby `Wreq::Client`.
|
|
79
80
|
///
|
|
80
81
|
/// Pass a populated jar as the client's `cookie_provider` option.
|
|
81
|
-
#[derive(Clone, Default)]
|
|
82
82
|
#[magnus::wrap(class = "Wreq::Jar", free_immediately, size)]
|
|
83
|
-
pub struct Jar(
|
|
83
|
+
pub struct Jar(ProcessLocal<Arc<wreq::cookie::Jar>>);
|
|
84
|
+
|
|
85
|
+
impl Default for Jar {
|
|
86
|
+
fn default() -> Self {
|
|
87
|
+
Self(ProcessLocal::new(Arc::new(wreq::cookie::Jar::default())))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
84
90
|
|
|
85
91
|
// ===== impl Builder =====
|
|
86
92
|
|
|
@@ -309,13 +315,18 @@ impl TryConvert for Cookies {
|
|
|
309
315
|
impl Jar {
|
|
310
316
|
/// Create a new [`Jar`] with an empty cookie store.
|
|
311
317
|
pub fn new() -> Self {
|
|
312
|
-
Self
|
|
318
|
+
Self::default()
|
|
313
319
|
}
|
|
314
320
|
|
|
315
321
|
/// Get all cookies.
|
|
322
|
+
///
|
|
323
|
+
/// # Errors
|
|
324
|
+
///
|
|
325
|
+
/// Returns `Wreq::ForkError` when the jar belongs to a parent process.
|
|
316
326
|
pub fn get_all(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
317
327
|
let cookies: Vec<Cookie> = rb_self
|
|
318
328
|
.0
|
|
329
|
+
.get(ruby)?
|
|
319
330
|
.get_all()
|
|
320
331
|
.map(RawCookie::from)
|
|
321
332
|
.map(Cookie)
|
|
@@ -331,28 +342,53 @@ impl Jar {
|
|
|
331
342
|
///
|
|
332
343
|
/// # Errors
|
|
333
344
|
///
|
|
334
|
-
/// Returns `
|
|
345
|
+
/// Returns `Wreq::ForkError` when the jar belongs to a parent process, or
|
|
346
|
+
/// `TypeError` when `cookie` is neither a [`Cookie`] nor a String.
|
|
335
347
|
pub fn add(&self, cookie: Value, url: String) -> Result<(), Error> {
|
|
348
|
+
let ruby = Ruby::get_with(cookie);
|
|
349
|
+
let jar = self.0.get(&ruby)?;
|
|
350
|
+
|
|
336
351
|
if let Ok(cookie) = Obj::<Cookie>::try_convert(cookie) {
|
|
337
|
-
gvl::nogvl(||
|
|
352
|
+
gvl::nogvl(|| jar.add(cookie.clone_for_jar(), &url));
|
|
338
353
|
return Ok(());
|
|
339
354
|
}
|
|
340
355
|
|
|
341
|
-
let ruby = Ruby::get_with(cookie);
|
|
342
356
|
let cookie = String::try_convert(cookie)
|
|
343
357
|
.map_err(|_| type_error(&ruby, "cookie must be a Wreq::Cookie or String"))?;
|
|
344
|
-
gvl::nogvl(||
|
|
358
|
+
gvl::nogvl(|| jar.add(cookie.as_ref(), &url));
|
|
345
359
|
Ok(())
|
|
346
360
|
}
|
|
347
361
|
|
|
348
362
|
/// Remove a cookie from this jar by name and URL.
|
|
349
|
-
|
|
350
|
-
|
|
363
|
+
///
|
|
364
|
+
/// # Errors
|
|
365
|
+
///
|
|
366
|
+
/// Returns `Wreq::ForkError` when the jar belongs to a parent process.
|
|
367
|
+
pub fn remove(ruby: &Ruby, rb_self: &Self, name: String, url: String) -> Result<(), Error> {
|
|
368
|
+
let jar = rb_self.0.get(ruby)?;
|
|
369
|
+
gvl::nogvl(|| jar.remove(name, &url));
|
|
370
|
+
Ok(())
|
|
351
371
|
}
|
|
352
372
|
|
|
353
373
|
/// Clear all cookies in this jar.
|
|
354
|
-
|
|
355
|
-
|
|
374
|
+
///
|
|
375
|
+
/// # Errors
|
|
376
|
+
///
|
|
377
|
+
/// Returns `Wreq::ForkError` when the jar belongs to a parent process.
|
|
378
|
+
pub fn clear(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
|
|
379
|
+
let jar = rb_self.0.get(ruby)?;
|
|
380
|
+
gvl::nogvl(|| jar.clear());
|
|
381
|
+
Ok(())
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/// Clone the shared native store in the process that created this jar.
|
|
385
|
+
///
|
|
386
|
+
/// # Errors
|
|
387
|
+
///
|
|
388
|
+
/// Returns `Wreq::ForkError` when the jar was inherited from a parent
|
|
389
|
+
/// process.
|
|
390
|
+
pub(crate) fn clone_store(&self, ruby: &Ruby) -> Result<Arc<wreq::cookie::Jar>, Error> {
|
|
391
|
+
self.0.get(ruby).cloned()
|
|
356
392
|
}
|
|
357
393
|
}
|
|
358
394
|
|
data/src/error.rs
CHANGED
|
@@ -56,6 +56,7 @@ macro_rules! map_wreq_error {
|
|
|
56
56
|
|
|
57
57
|
// System-level and runtime errors
|
|
58
58
|
define_exception!(MEMORY, "MemoryError", exception_runtime_error);
|
|
59
|
+
define_exception!(FORK_ERROR, "ForkError", exception_runtime_error);
|
|
59
60
|
|
|
60
61
|
// Network connection errors
|
|
61
62
|
define_exception!(CONNECTION_ERROR, "ConnectionError", exception_runtime_error);
|
|
@@ -98,6 +99,26 @@ pub fn interrupt_error(ruby: &Ruby) -> MagnusError {
|
|
|
98
99
|
MagnusError::new(ruby.get_inner(&INTERRUPT_ERROR), "request interrupted")
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/// Build `Wreq::ForkError` without touching inherited native state.
|
|
103
|
+
#[cfg(unix)]
|
|
104
|
+
pub fn fork_error(ruby: &Ruby, owner_pid: u32, current_pid: u32) -> MagnusError {
|
|
105
|
+
MagnusError::new(
|
|
106
|
+
ruby.get_inner(&FORK_ERROR),
|
|
107
|
+
format!(
|
|
108
|
+
"wreq-ruby native state was created in process {owner_pid} and cannot be used after fork in process {current_pid}"
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Map a failed process-fork handler registration to `Wreq::ForkError`.
|
|
114
|
+
#[cfg(unix)]
|
|
115
|
+
pub fn fork_handler_error(ruby: &Ruby, err: &std::io::Error) -> MagnusError {
|
|
116
|
+
MagnusError::new(
|
|
117
|
+
ruby.get_inner(&FORK_ERROR),
|
|
118
|
+
format!("failed to initialize process fork tracking: {err}"),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
/// Map a Tokio runtime initialization failure to `Wreq::BuilderError`.
|
|
102
123
|
pub fn runtime_initialization_error(ruby: &Ruby, err: &std::io::Error) -> MagnusError {
|
|
103
124
|
MagnusError::new(
|
|
@@ -239,6 +260,13 @@ pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), MagnusError> {
|
|
|
239
260
|
"MemoryError",
|
|
240
261
|
exception_runtime_error
|
|
241
262
|
);
|
|
263
|
+
initialize_exception!(
|
|
264
|
+
ruby,
|
|
265
|
+
gem_module,
|
|
266
|
+
FORK_ERROR,
|
|
267
|
+
"ForkError",
|
|
268
|
+
exception_runtime_error
|
|
269
|
+
);
|
|
242
270
|
initialize_exception!(
|
|
243
271
|
ruby,
|
|
244
272
|
gem_module,
|
data/src/lib.rs
CHANGED
|
@@ -15,6 +15,7 @@ mod http;
|
|
|
15
15
|
mod options;
|
|
16
16
|
mod rt;
|
|
17
17
|
mod serde;
|
|
18
|
+
mod tls;
|
|
18
19
|
|
|
19
20
|
use magnus::{Error, Module, Ruby, Value};
|
|
20
21
|
|
|
@@ -85,6 +86,7 @@ pub fn patch(ruby: &Ruby, args: &[Value]) -> Result<Response, magnus::Error> {
|
|
|
85
86
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
86
87
|
let gem_module = ruby.define_module(RUBY_MODULE_NAME)?;
|
|
87
88
|
gem_module.const_set("VERSION", VERSION)?;
|
|
89
|
+
error::include(ruby, &gem_module)?;
|
|
88
90
|
gem_module.define_module_function("request", magnus::function!(request, -1))?;
|
|
89
91
|
gem_module.define_module_function("get", magnus::function!(get, -1))?;
|
|
90
92
|
gem_module.define_module_function("post", magnus::function!(post, -1))?;
|
|
@@ -97,8 +99,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
97
99
|
http::include(ruby, &gem_module)?;
|
|
98
100
|
header::include(ruby, &gem_module)?;
|
|
99
101
|
cookie::include(ruby, &gem_module)?;
|
|
102
|
+
tls::include(ruby, &gem_module)?;
|
|
100
103
|
client::include(ruby, &gem_module)?;
|
|
101
104
|
emulate::include(ruby, &gem_module)?;
|
|
102
|
-
error::include(ruby, &gem_module)?;
|
|
103
105
|
Ok(())
|
|
104
106
|
}
|
data/src/macros.rs
CHANGED
|
@@ -148,11 +148,10 @@ macro_rules! define_ruby_enum {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
macro_rules! extract_request {
|
|
151
|
-
($args:expr, $required:ty) => {{
|
|
151
|
+
($ruby:expr, $args:expr, $required:ty) => {{
|
|
152
152
|
let args = magnus::scan_args::scan_args::<$required, (), (), (), magnus::RHash, ()>($args)?;
|
|
153
153
|
let required = args.required;
|
|
154
|
-
let
|
|
155
|
-
let request = crate::client::req::Request::new(&ruby, args.keywords)?;
|
|
154
|
+
let request = crate::client::req::Request::new($ruby, args.keywords)?;
|
|
156
155
|
(required, request)
|
|
157
156
|
}};
|
|
158
157
|
}
|
data/src/rt.rs
CHANGED
|
@@ -1,57 +1,78 @@
|
|
|
1
|
-
use std::sync::
|
|
1
|
+
use std::{io, sync::OnceLock};
|
|
2
2
|
|
|
3
3
|
use magnus::Ruby;
|
|
4
|
-
use tokio::runtime::{Builder, Runtime};
|
|
4
|
+
use tokio::runtime::{Builder, Runtime as TokioRuntime};
|
|
5
5
|
|
|
6
6
|
use crate::{
|
|
7
7
|
error::{interrupt_error, runtime_initialization_error},
|
|
8
8
|
gvl,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
#[cfg(unix)]
|
|
12
|
+
use crate::{
|
|
13
|
+
arch,
|
|
14
|
+
error::{fork_error, fork_handler_error},
|
|
15
|
+
};
|
|
16
|
+
|
|
11
17
|
/// Initialize the global runtime lazily and preserve failures for Ruby.
|
|
12
|
-
static RUNTIME:
|
|
13
|
-
let mut builder = Builder::new_multi_thread();
|
|
18
|
+
static RUNTIME: OnceLock<Result<TokioRuntime, io::Error>> = OnceLock::new();
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
/// Reject a child process that inherited an initialized native runtime.
|
|
21
|
+
///
|
|
22
|
+
/// # Errors
|
|
23
|
+
///
|
|
24
|
+
/// Returns `Wreq::ForkError` when the global runtime belongs to the parent
|
|
25
|
+
/// process.
|
|
26
|
+
fn ensure_runtime_owner(ruby: &Ruby) -> Result<(), magnus::Error> {
|
|
27
|
+
#[cfg(unix)]
|
|
28
|
+
if let Some((owner_pid, current_pid)) = arch::forked_process_ids() {
|
|
29
|
+
return Err(fork_error(ruby, owner_pid, current_pid));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[cfg(not(unix))]
|
|
33
|
+
let _ = ruby;
|
|
17
34
|
|
|
18
|
-
|
|
19
|
-
Interrupted,
|
|
20
|
-
Future(E),
|
|
35
|
+
Ok(())
|
|
21
36
|
}
|
|
22
37
|
|
|
23
|
-
/// Block on a future to completion on the global Tokio runtime.
|
|
38
|
+
/// Block on a future to completion on the current process's global Tokio runtime.
|
|
24
39
|
///
|
|
25
40
|
/// The future runs without Ruby's GVL, so it must not construct Ruby objects or
|
|
26
|
-
/// Ruby exceptions.
|
|
27
|
-
///
|
|
41
|
+
/// Ruby exceptions. Its output is returned unchanged. If that output is a
|
|
42
|
+
/// `Result`, convert its error after this function returns and reacquires the
|
|
43
|
+
/// GVL.
|
|
28
44
|
///
|
|
29
45
|
/// # Errors
|
|
30
46
|
///
|
|
31
|
-
/// Returns `Wreq::
|
|
32
|
-
/// `Wreq::
|
|
33
|
-
///
|
|
34
|
-
pub fn
|
|
47
|
+
/// Returns `Wreq::ForkError` if the runtime belongs to a parent process,
|
|
48
|
+
/// `Wreq::BuilderError` if the Tokio runtime cannot be initialized,
|
|
49
|
+
/// or `Wreq::InterruptError` if Ruby interrupts the operation.
|
|
50
|
+
pub(crate) fn block_on<F>(ruby: &Ruby, future: F) -> Result<F::Output, magnus::Error>
|
|
35
51
|
where
|
|
36
|
-
F: Future
|
|
37
|
-
M: FnOnce(&Ruby, E) -> magnus::Error,
|
|
52
|
+
F: Future,
|
|
38
53
|
{
|
|
54
|
+
// Install fork tracking at the same point as the lazy runtime. Loading the
|
|
55
|
+
// extension alone must not claim the runtime for the parent process.
|
|
56
|
+
#[cfg(unix)]
|
|
57
|
+
arch::initialize_fork_tracking().map_err(|err| fork_handler_error(ruby, &err))?;
|
|
58
|
+
|
|
59
|
+
ensure_runtime_owner(ruby)?;
|
|
39
60
|
let runtime = RUNTIME
|
|
61
|
+
.get_or_init(|| {
|
|
62
|
+
let mut builder = Builder::new_multi_thread();
|
|
63
|
+
builder.enable_all().build()
|
|
64
|
+
})
|
|
40
65
|
.as_ref()
|
|
41
66
|
.map_err(|err| runtime_initialization_error(ruby, err))?;
|
|
42
67
|
let result = gvl::nogvl_cancellable(|flag| {
|
|
43
68
|
runtime.block_on(async move {
|
|
44
69
|
tokio::select! {
|
|
45
70
|
biased;
|
|
46
|
-
_ = flag.cancelled() =>
|
|
47
|
-
result = future => result
|
|
71
|
+
_ = flag.cancelled() => None,
|
|
72
|
+
result = future => Some(result),
|
|
48
73
|
}
|
|
49
74
|
})
|
|
50
75
|
});
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
Ok(value) => Ok(value),
|
|
54
|
-
Err(BlockOnError::Interrupted) => Err(interrupt_error(ruby)),
|
|
55
|
-
Err(BlockOnError::Future(err)) => Err(map_err(ruby, err)),
|
|
56
|
-
}
|
|
77
|
+
result.ok_or_else(|| interrupt_error(ruby))
|
|
57
78
|
}
|
data/src/tls.rs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//! Ruby wrappers for TLS metadata attached to a response.
|
|
2
|
+
//!
|
|
3
|
+
//! Certificates use the DER encoding described by the X.509 profile in
|
|
4
|
+
//! [RFC 5280 section 4.1](https://www.rfc-editor.org/rfc/rfc5280#section-4.1).
|
|
5
|
+
|
|
6
|
+
use magnus::{Error, Module, RArray, RModule, RString, Ruby, value::ReprValue};
|
|
7
|
+
|
|
8
|
+
/// Read-only Ruby wrapper around [`wreq::tls::TlsInfo`].
|
|
9
|
+
///
|
|
10
|
+
/// The native value keeps certificate bytes alive independently of the response
|
|
11
|
+
/// body. Its `Bytes` buffers are cheap to clone, while accessors copy the data
|
|
12
|
+
/// into Ruby-owned Strings so callers cannot mutate the stored metadata.
|
|
13
|
+
#[derive(Clone)]
|
|
14
|
+
#[magnus::wrap(class = "Wreq::TlsInfo", free_immediately, size)]
|
|
15
|
+
pub(crate) struct TlsInfo(pub(crate) wreq::tls::TlsInfo);
|
|
16
|
+
|
|
17
|
+
impl TlsInfo {
|
|
18
|
+
/// Copy the DER-encoded leaf certificate into a binary Ruby String.
|
|
19
|
+
fn peer_certificate(ruby: &Ruby, rb_self: &Self) -> Option<RString> {
|
|
20
|
+
rb_self
|
|
21
|
+
.0
|
|
22
|
+
.peer_certificate()
|
|
23
|
+
.map(|der| ruby.str_from_slice(der))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Copy the certificate chain into a frozen Array of binary Ruby Strings.
|
|
27
|
+
///
|
|
28
|
+
/// Only the Array is frozen. Its Strings are independent copies and remain
|
|
29
|
+
/// mutable in Ruby.
|
|
30
|
+
fn peer_certificate_chain(ruby: &Ruby, rb_self: &Self) -> Option<RArray> {
|
|
31
|
+
rb_self.0.peer_certificate_chain().map(|chain| {
|
|
32
|
+
let certificates = ruby.ary_from_iter(chain.map(|cert| ruby.str_from_slice(cert)));
|
|
33
|
+
certificates.freeze();
|
|
34
|
+
certificates
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Define the `Wreq::TlsInfo` Ruby class and its readers.
|
|
40
|
+
pub(crate) fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
|
|
41
|
+
let tls_info_class = gem_module.define_class("TlsInfo", ruby.class_object())?;
|
|
42
|
+
tls_info_class.define_method(
|
|
43
|
+
"peer_certificate",
|
|
44
|
+
magnus::method!(TlsInfo::peer_certificate, 0),
|
|
45
|
+
)?;
|
|
46
|
+
tls_info_class.define_method(
|
|
47
|
+
"peer_certificate_chain",
|
|
48
|
+
magnus::method!(TlsInfo::peer_certificate_chain, 0),
|
|
49
|
+
)?;
|
|
50
|
+
Ok(())
|
|
51
|
+
}
|