wreq 1.2.10 → 1.2.12
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 -147
- data/Cargo.toml +10 -6
- 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 +34 -0
- data/docs/interrupt-handling.md +133 -0
- data/examples/tls_info.rb +27 -0
- data/lib/wreq.rb +13 -0
- data/lib/wreq_ruby/body.rb +6 -0
- data/lib/wreq_ruby/client.rb +19 -0
- data/lib/wreq_ruby/error.rb +13 -0
- data/lib/wreq_ruby/response.rb +28 -0
- data/lib/wreq_ruby/tls.rb +73 -0
- data/src/arch.rs +128 -0
- data/src/client/body/stream.rs +24 -9
- data/src/client/resp.rs +45 -18
- data/src/client.rs +45 -28
- data/src/error.rs +28 -0
- data/src/lib.rs +5 -1
- data/src/macros.rs +3 -3
- data/src/rt.rs +46 -9
- data/src/tls.rs +51 -0
- data/test/fork_test.rb +82 -0
- data/test/scripts/fork_safety.rb +110 -0
- data/test/support/tls_server.rb +95 -0
- data/test/tls_info_test.rb +59 -0
- metadata +10 -1
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,10 @@ 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
|
-
|
|
105
|
+
#[cfg(unix)]
|
|
106
|
+
rt::initialize(ruby)?;
|
|
103
107
|
Ok(())
|
|
104
108
|
}
|
data/src/macros.rs
CHANGED
|
@@ -148,11 +148,11 @@ 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
|
+
crate::rt::ensure_current($ruby)?;
|
|
152
153
|
let args = magnus::scan_args::scan_args::<$required, (), (), (), magnus::RHash, ()>($args)?;
|
|
153
154
|
let required = args.required;
|
|
154
|
-
let
|
|
155
|
-
let request = crate::client::req::Request::new(&ruby, args.keywords)?;
|
|
155
|
+
let request = crate::client::req::Request::new($ruby, args.keywords)?;
|
|
156
156
|
(required, request)
|
|
157
157
|
}};
|
|
158
158
|
}
|
data/src/rt.rs
CHANGED
|
@@ -1,26 +1,57 @@
|
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
#[cfg(unix)]
|
|
12
|
+
use crate::{
|
|
13
|
+
arch,
|
|
14
|
+
error::{fork_error, fork_handler_error},
|
|
15
|
+
};
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/// Initialize the global runtime lazily and preserve failures for Ruby.
|
|
18
|
+
static RUNTIME: OnceLock<Result<TokioRuntime, io::Error>> = OnceLock::new();
|
|
17
19
|
|
|
18
20
|
enum BlockOnError<E> {
|
|
19
21
|
Interrupted,
|
|
20
22
|
Future(E),
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
///
|
|
25
|
+
/// Register fork tracking while the native extension is being loaded.
|
|
26
|
+
///
|
|
27
|
+
/// # Errors
|
|
28
|
+
///
|
|
29
|
+
/// Returns `Wreq::ForkError` if the platform cannot install its child-process
|
|
30
|
+
/// callback.
|
|
31
|
+
#[cfg(unix)]
|
|
32
|
+
pub fn initialize(ruby: &Ruby) -> Result<(), magnus::Error> {
|
|
33
|
+
arch::initialize_fork_tracking().map_err(|err| fork_handler_error(ruby, &err))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Reject a child process that inherited the loaded native extension.
|
|
37
|
+
///
|
|
38
|
+
/// # Errors
|
|
39
|
+
///
|
|
40
|
+
/// Returns `Wreq::ForkError` when the extension was loaded before the current
|
|
41
|
+
/// process was forked.
|
|
42
|
+
pub fn ensure_current(ruby: &Ruby) -> Result<(), magnus::Error> {
|
|
43
|
+
#[cfg(unix)]
|
|
44
|
+
if let Some((owner_pid, current_pid)) = arch::forked_process_ids() {
|
|
45
|
+
return Err(fork_error(ruby, owner_pid, current_pid));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[cfg(not(unix))]
|
|
49
|
+
let _ = ruby;
|
|
50
|
+
|
|
51
|
+
Ok(())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Block on a future to completion on the current process's global Tokio runtime.
|
|
24
55
|
///
|
|
25
56
|
/// The future runs without Ruby's GVL, so it must not construct Ruby objects or
|
|
26
57
|
/// Ruby exceptions. Convert Rust errors back into Ruby errors after the GVL has
|
|
@@ -28,7 +59,8 @@ enum BlockOnError<E> {
|
|
|
28
59
|
///
|
|
29
60
|
/// # Errors
|
|
30
61
|
///
|
|
31
|
-
/// Returns `Wreq::
|
|
62
|
+
/// Returns `Wreq::ForkError` if the extension belongs to a parent process,
|
|
63
|
+
/// `Wreq::BuilderError` if the Tokio runtime cannot be initialized,
|
|
32
64
|
/// `Wreq::InterruptError` if Ruby interrupts the request, or the error produced
|
|
33
65
|
/// by `map_err` if the future fails.
|
|
34
66
|
pub fn try_block_on<F, T, E, M>(ruby: &Ruby, future: F, map_err: M) -> Result<T, magnus::Error>
|
|
@@ -36,7 +68,12 @@ where
|
|
|
36
68
|
F: Future<Output = Result<T, E>>,
|
|
37
69
|
M: FnOnce(&Ruby, E) -> magnus::Error,
|
|
38
70
|
{
|
|
71
|
+
ensure_current(ruby)?;
|
|
39
72
|
let runtime = RUNTIME
|
|
73
|
+
.get_or_init(|| {
|
|
74
|
+
let mut builder = Builder::new_multi_thread();
|
|
75
|
+
builder.enable_all().build()
|
|
76
|
+
})
|
|
40
77
|
.as_ref()
|
|
41
78
|
.map_err(|err| runtime_initialization_error(ruby, err))?;
|
|
42
79
|
let result = gvl::nogvl_cancellable(|flag| {
|
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
|
+
}
|
data/test/fork_test.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
require "timeout"
|
|
7
|
+
|
|
8
|
+
class ForkTest < Minitest::Test
|
|
9
|
+
FORK_ERROR_LABELS = %w[
|
|
10
|
+
before_runtime
|
|
11
|
+
invalid_client
|
|
12
|
+
invalid_request
|
|
13
|
+
fresh_body_sender
|
|
14
|
+
inherited_body_sender_push
|
|
15
|
+
inherited_body_sender_close
|
|
16
|
+
inherited_body_sender_closed
|
|
17
|
+
fresh_client
|
|
18
|
+
inherited_client
|
|
19
|
+
inherited_response
|
|
20
|
+
inherited_response_text
|
|
21
|
+
inherited_response_chunks
|
|
22
|
+
inherited_response_close
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def test_fork_error_is_a_runtime_error
|
|
26
|
+
assert_operator Wreq::ForkError, :<, RuntimeError
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_loaded_extension_is_rejected_after_fork
|
|
30
|
+
skip "fork is not supported on this platform" unless Process.respond_to?(:fork)
|
|
31
|
+
|
|
32
|
+
stdout, stderr, status = run_fork_script("fork_safety.rb")
|
|
33
|
+
|
|
34
|
+
assert status.success?, "subprocess failed with #{status.inspect}: #{stderr}"
|
|
35
|
+
assert_equal "ok\n", stdout
|
|
36
|
+
FORK_ERROR_LABELS.each do |label|
|
|
37
|
+
assert_match(/#{label}=Wreq::ForkError:.*cannot be used after fork/, stderr)
|
|
38
|
+
assert_match(/#{label}_retry=Wreq::ForkError:.*cannot be used after fork/, stderr)
|
|
39
|
+
end
|
|
40
|
+
assert_match(/inherited_gc=ok/, stderr)
|
|
41
|
+
refute_match(/\[BUG\]|segmentation fault|panicked/i, stderr)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def run_fork_script(name)
|
|
47
|
+
lib_dir = File.expand_path("../lib", __dir__)
|
|
48
|
+
script = File.expand_path("scripts/#{name}", __dir__)
|
|
49
|
+
|
|
50
|
+
Tempfile.create("wreq-fork-stdout") do |stdout|
|
|
51
|
+
Tempfile.create("wreq-fork-stderr") do |stderr|
|
|
52
|
+
pid = Process.spawn(
|
|
53
|
+
RbConfig.ruby,
|
|
54
|
+
"-I",
|
|
55
|
+
lib_dir,
|
|
56
|
+
script,
|
|
57
|
+
out: stdout,
|
|
58
|
+
err: stderr,
|
|
59
|
+
pgroup: true
|
|
60
|
+
)
|
|
61
|
+
status = Timeout.timeout(30) { Process.wait2(pid).last }
|
|
62
|
+
stdout.rewind
|
|
63
|
+
stderr.rewind
|
|
64
|
+
return [stdout.read, stderr.read, status]
|
|
65
|
+
rescue Timeout::Error
|
|
66
|
+
begin
|
|
67
|
+
Process.kill("KILL", -pid)
|
|
68
|
+
rescue Errno::ESRCH
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
Process.wait(pid)
|
|
74
|
+
rescue Errno::ECHILD
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
flunk "#{name} timed out"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "timeout"
|
|
5
|
+
require "weakref"
|
|
6
|
+
require "wreq"
|
|
7
|
+
|
|
8
|
+
$stdout.sync = true
|
|
9
|
+
$stderr.sync = true
|
|
10
|
+
|
|
11
|
+
def expect_fork_error(label)
|
|
12
|
+
child_pid = fork do
|
|
13
|
+
2.times do |attempt|
|
|
14
|
+
attempt_label = attempt.zero? ? label : "#{label}_retry"
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
Timeout.timeout(5) { yield }
|
|
18
|
+
rescue Wreq::ForkError => error
|
|
19
|
+
warn "#{attempt_label}=#{error.class}: #{error.message}"
|
|
20
|
+
next
|
|
21
|
+
rescue => error
|
|
22
|
+
warn "#{attempt_label}=unexpected #{error.class}: #{error.message}"
|
|
23
|
+
exit! 2
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
warn "#{attempt_label}=missing Wreq::ForkError"
|
|
27
|
+
exit! 3
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
exit! 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
_, status = Process.wait2(child_pid)
|
|
34
|
+
abort "#{label} child failed with #{status.inspect}" unless status.success?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
expect_fork_error("before_runtime") { Wreq::Client.new }
|
|
38
|
+
expect_fork_error("invalid_client") { Wreq::Client.new(unknown: true) }
|
|
39
|
+
expect_fork_error("invalid_request") { Wreq.get(1) }
|
|
40
|
+
expect_fork_error("fresh_body_sender") { Wreq::BodySender.new(0) }
|
|
41
|
+
|
|
42
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
43
|
+
port = server.addr[1]
|
|
44
|
+
server_pid = fork do
|
|
45
|
+
3.times do
|
|
46
|
+
ready = IO.select([server], nil, nil, 10)
|
|
47
|
+
exit! 4 unless ready
|
|
48
|
+
|
|
49
|
+
socket = server.accept
|
|
50
|
+
begin
|
|
51
|
+
while (line = socket.gets)
|
|
52
|
+
break if line == "\r\n"
|
|
53
|
+
end
|
|
54
|
+
socket.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
|
|
55
|
+
ensure
|
|
56
|
+
socket.close
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
exit! 0
|
|
60
|
+
ensure
|
|
61
|
+
server.close
|
|
62
|
+
end
|
|
63
|
+
server.close
|
|
64
|
+
|
|
65
|
+
url = "http://127.0.0.1:#{port}/"
|
|
66
|
+
client = Wreq::Client.new
|
|
67
|
+
abort "parent warm-up failed" unless client.get(url).bytes == "ok"
|
|
68
|
+
|
|
69
|
+
def build_inherited_objects(client, url)
|
|
70
|
+
[Wreq::Client.new, Wreq::BodySender.new, client.get(url)]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
inherited_objects = build_inherited_objects(client, url)
|
|
74
|
+
inherited_weak_refs = inherited_objects.map { |object| WeakRef.new(object) }
|
|
75
|
+
|
|
76
|
+
expect_fork_error("inherited_body_sender_push") do
|
|
77
|
+
inherited_objects[1].push("chunk")
|
|
78
|
+
end
|
|
79
|
+
expect_fork_error("inherited_body_sender_close") { inherited_objects[1].close }
|
|
80
|
+
expect_fork_error("inherited_body_sender_closed") { inherited_objects[1].closed? }
|
|
81
|
+
expect_fork_error("fresh_client") { Wreq::Client.new }
|
|
82
|
+
expect_fork_error("inherited_client") { client.get(url) }
|
|
83
|
+
expect_fork_error("inherited_response") { inherited_objects[2].bytes }
|
|
84
|
+
expect_fork_error("inherited_response_text") { inherited_objects[2].text(1) }
|
|
85
|
+
expect_fork_error("inherited_response_chunks") { inherited_objects[2].chunks }
|
|
86
|
+
expect_fork_error("inherited_response_close") { inherited_objects[2].close }
|
|
87
|
+
|
|
88
|
+
# Release the earlier test blocks so this array is the only strong reference.
|
|
89
|
+
GC.start(full_mark: true, immediate_sweep: true)
|
|
90
|
+
gc_pid = fork do
|
|
91
|
+
inherited_objects = nil
|
|
92
|
+
3.times { GC.start(full_mark: true, immediate_sweep: true) }
|
|
93
|
+
alive = inherited_weak_refs.each_index.select do |index|
|
|
94
|
+
inherited_weak_refs[index].weakref_alive?
|
|
95
|
+
end
|
|
96
|
+
abort "inherited objects were not collected: #{alive.join(", ")}" unless alive.empty?
|
|
97
|
+
warn "inherited_gc=ok"
|
|
98
|
+
exit! 0
|
|
99
|
+
rescue => error
|
|
100
|
+
warn "inherited_gc=unexpected #{error.class}: #{error.message}"
|
|
101
|
+
exit! 5
|
|
102
|
+
end
|
|
103
|
+
_, gc_status = Process.wait2(gc_pid)
|
|
104
|
+
abort "inherited GC child failed with #{gc_status.inspect}" unless gc_status.success?
|
|
105
|
+
|
|
106
|
+
abort "parent request after fork failed" unless client.get(url).bytes == "ok"
|
|
107
|
+
_, server_status = Process.wait2(server_pid)
|
|
108
|
+
abort "server failed with #{server_status.inspect}" unless server_status.success?
|
|
109
|
+
|
|
110
|
+
puts "ok"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "socket"
|
|
5
|
+
require "timeout"
|
|
6
|
+
|
|
7
|
+
# A small HTTPS server that serves every expected request on one TLS connection.
|
|
8
|
+
module TlsTestServer
|
|
9
|
+
RESPONSE_BODY = "ok"
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def with_connection(request_count:)
|
|
14
|
+
tcp_server = TCPServer.new("127.0.0.1", 0)
|
|
15
|
+
context, certificate_der = server_context
|
|
16
|
+
ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, context)
|
|
17
|
+
outcome = Queue.new
|
|
18
|
+
server_thread = Thread.new do
|
|
19
|
+
socket = ssl_server.accept
|
|
20
|
+
request_lines = []
|
|
21
|
+
|
|
22
|
+
request_count.times do |index|
|
|
23
|
+
request_lines << read_request(socket)
|
|
24
|
+
connection = (index == request_count - 1) ? "close" : "keep-alive"
|
|
25
|
+
socket.write(response(connection))
|
|
26
|
+
socket.flush
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
outcome << {connections: 1, requests: request_lines}
|
|
30
|
+
rescue => error
|
|
31
|
+
outcome << error
|
|
32
|
+
ensure
|
|
33
|
+
socket&.close
|
|
34
|
+
end
|
|
35
|
+
server_thread.report_on_exception = false
|
|
36
|
+
|
|
37
|
+
yield "https://127.0.0.1:#{tcp_server.addr[1]}/", certificate_der
|
|
38
|
+
|
|
39
|
+
result = Timeout.timeout(5) { outcome.pop }
|
|
40
|
+
raise result if result.is_a?(StandardError)
|
|
41
|
+
|
|
42
|
+
result
|
|
43
|
+
ensure
|
|
44
|
+
tcp_server&.close
|
|
45
|
+
server_thread&.join(5)
|
|
46
|
+
if server_thread&.alive?
|
|
47
|
+
server_thread.kill
|
|
48
|
+
server_thread.join
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def read_request(socket)
|
|
53
|
+
request_line = socket.gets
|
|
54
|
+
raise EOFError, "client closed before sending a request" unless request_line
|
|
55
|
+
|
|
56
|
+
loop do
|
|
57
|
+
line = socket.gets
|
|
58
|
+
raise EOFError, "client closed while sending headers" unless line
|
|
59
|
+
break if line == "\r\n"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
request_line
|
|
63
|
+
end
|
|
64
|
+
private_class_method :read_request
|
|
65
|
+
|
|
66
|
+
def response(connection)
|
|
67
|
+
[
|
|
68
|
+
"HTTP/1.1 200 OK",
|
|
69
|
+
"Content-Length: #{RESPONSE_BODY.bytesize}",
|
|
70
|
+
"Connection: #{connection}",
|
|
71
|
+
"",
|
|
72
|
+
RESPONSE_BODY
|
|
73
|
+
].join("\r\n")
|
|
74
|
+
end
|
|
75
|
+
private_class_method :response
|
|
76
|
+
|
|
77
|
+
def server_context
|
|
78
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
79
|
+
certificate = OpenSSL::X509::Certificate.new
|
|
80
|
+
certificate.version = 2
|
|
81
|
+
certificate.serial = 1
|
|
82
|
+
certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse("/CN=127.0.0.1")
|
|
83
|
+
certificate.public_key = key.public_key
|
|
84
|
+
certificate.not_before = Time.now - 60
|
|
85
|
+
certificate.not_after = Time.now + 3600
|
|
86
|
+
certificate.sign(key, OpenSSL::Digest.new("SHA256"))
|
|
87
|
+
|
|
88
|
+
context = OpenSSL::SSL::SSLContext.new.tap do |ssl_context|
|
|
89
|
+
ssl_context.cert = certificate
|
|
90
|
+
ssl_context.key = key
|
|
91
|
+
end
|
|
92
|
+
[context, certificate.to_der]
|
|
93
|
+
end
|
|
94
|
+
private_class_method :server_context
|
|
95
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require_relative "support/tls_server"
|
|
5
|
+
|
|
6
|
+
class TlsInfoTest < Minitest::Test
|
|
7
|
+
HTTPBIN_HTTP_URL = ENV.fetch("HTTPBIN_HTTP_URL", HTTPBIN_URL.sub(/\Ahttps:/, "http:"))
|
|
8
|
+
|
|
9
|
+
def test_tls_info_is_nil_when_disabled_or_request_is_plain_http
|
|
10
|
+
default_response = Wreq::Client.new.get("#{HTTPBIN_URL}/get")
|
|
11
|
+
plain_response = Wreq::Client.new(tls_info: true).get("#{HTTPBIN_HTTP_URL}/get")
|
|
12
|
+
|
|
13
|
+
assert_nil default_response.tls_info
|
|
14
|
+
assert_nil plain_response.tls_info
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_certificate_data_survives_body_lifecycle_on_a_reused_connection
|
|
18
|
+
fixture = TlsTestServer.with_connection(request_count: 2) do |base_url, certificate_der|
|
|
19
|
+
client = Wreq::Client.new(
|
|
20
|
+
tls_info: true,
|
|
21
|
+
verify: false,
|
|
22
|
+
http1_only: true,
|
|
23
|
+
no_proxy: true,
|
|
24
|
+
timeout: 5
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
read_response = client.get("#{base_url}read")
|
|
28
|
+
assert_equal "ok", read_response.text
|
|
29
|
+
read_tls = read_response.tls_info
|
|
30
|
+
|
|
31
|
+
closed_response = client.get("#{base_url}close")
|
|
32
|
+
closed_response.close
|
|
33
|
+
closed_tls = closed_response.tls_info
|
|
34
|
+
|
|
35
|
+
assert_instance_of Wreq::TlsInfo, read_tls
|
|
36
|
+
certificate = read_tls.peer_certificate
|
|
37
|
+
chain = read_tls.peer_certificate_chain
|
|
38
|
+
assert_equal certificate_der, certificate
|
|
39
|
+
assert_equal Encoding::BINARY, certificate.encoding
|
|
40
|
+
assert_equal [certificate_der], chain
|
|
41
|
+
assert_equal Encoding::BINARY, chain.first.encoding
|
|
42
|
+
assert_predicate chain, :frozen?
|
|
43
|
+
assert_equal(
|
|
44
|
+
"#<Wreq::TlsInfo peer_certificate=#{certificate_der.bytesize}B peer_certificate_chain=1>",
|
|
45
|
+
read_tls.inspect
|
|
46
|
+
)
|
|
47
|
+
assert_empty Wreq::TlsInfo.instance_methods(false) & %i[to_h to_s]
|
|
48
|
+
|
|
49
|
+
certificate.clear
|
|
50
|
+
assert_equal certificate_der, read_tls.peer_certificate
|
|
51
|
+
assert_equal certificate_der, closed_tls.peer_certificate
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
assert_equal(
|
|
55
|
+
{connections: 1, requests: ["GET /read HTTP/1.1\r\n", "GET /close HTTP/1.1\r\n"]},
|
|
56
|
+
fixture
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wreq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SearchApi
|
|
@@ -80,6 +80,8 @@ files:
|
|
|
80
80
|
- crates/wreq-util/tests/emulate_safari.rs
|
|
81
81
|
- crates/wreq-util/tests/support/mod.rs
|
|
82
82
|
- crates/wreq-util/tests/support/server.rs
|
|
83
|
+
- docs/fork-safety.md
|
|
84
|
+
- docs/interrupt-handling.md
|
|
83
85
|
- docs/windows-gnu-tokio-crash.md
|
|
84
86
|
- examples/body.rb
|
|
85
87
|
- examples/client.rb
|
|
@@ -90,6 +92,7 @@ files:
|
|
|
90
92
|
- examples/send_stream.rb
|
|
91
93
|
- examples/stream.rb
|
|
92
94
|
- examples/thread_interrupt.rb
|
|
95
|
+
- examples/tls_info.rb
|
|
93
96
|
- extconf.rb
|
|
94
97
|
- lib/wreq.rb
|
|
95
98
|
- lib/wreq_ruby/body.rb
|
|
@@ -100,6 +103,7 @@ files:
|
|
|
100
103
|
- lib/wreq_ruby/header.rb
|
|
101
104
|
- lib/wreq_ruby/http.rb
|
|
102
105
|
- lib/wreq_ruby/response.rb
|
|
106
|
+
- lib/wreq_ruby/tls.rb
|
|
103
107
|
- script/build_platform_gem.rb
|
|
104
108
|
- script/rust_env.rb
|
|
105
109
|
- src/arch.rs
|
|
@@ -142,12 +146,14 @@ files:
|
|
|
142
146
|
- src/serde/ser/struct_variant_serializer.rs
|
|
143
147
|
- src/serde/ser/tuple_variant_serializer.rs
|
|
144
148
|
- src/serde/tests.rs
|
|
149
|
+
- src/tls.rs
|
|
145
150
|
- test/body_sender_test.rb
|
|
146
151
|
- test/client_cookie_test.rb
|
|
147
152
|
- test/client_test.rb
|
|
148
153
|
- test/cookie_test.rb
|
|
149
154
|
- test/emulation_test.rb
|
|
150
155
|
- test/error_handling_test.rb
|
|
156
|
+
- test/fork_test.rb
|
|
151
157
|
- test/header_test.rb
|
|
152
158
|
- test/inspect_test.rb
|
|
153
159
|
- test/json_precision_test.rb
|
|
@@ -157,8 +163,11 @@ files:
|
|
|
157
163
|
- test/request_parameters_test.rb
|
|
158
164
|
- test/request_test.rb
|
|
159
165
|
- test/response_test.rb
|
|
166
|
+
- test/scripts/fork_safety.rb
|
|
160
167
|
- test/stream_test.rb
|
|
168
|
+
- test/support/tls_server.rb
|
|
161
169
|
- test/test_helper.rb
|
|
170
|
+
- test/tls_info_test.rb
|
|
162
171
|
- test/value_semantics_test.rb
|
|
163
172
|
- wreq.gemspec
|
|
164
173
|
homepage: https://github.com/SearchApi/wreq-ruby
|