curb 1.3.7 → 1.4.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/ChangeLog.md +432 -0
- data/README.md +62 -5
- data/Rakefile +12 -1
- data/ext/curb.c +17 -4
- data/ext/curb.h +4 -4
- data/ext/curb_easy.c +344 -191
- data/ext/curb_easy.h +6 -0
- data/ext/curb_macros.h +16 -0
- data/ext/curb_multi.c +479 -103
- data/ext/curb_multi.h +8 -0
- data/ext/curb_postfield.c +134 -145
- data/ext/curb_postfield.h +10 -1
- data/ext/extconf.rb +6 -0
- data/lib/curl/easy.rb +28 -12
- data/lib/curl/multi.rb +1 -1
- data/lib/curl.rb +58 -19
- data/tests/helper.rb +122 -3
- data/tests/tc_curl_easy.rb +175 -4
- data/tests/tc_curl_easy_cookielist.rb +18 -12
- data/tests/tc_curl_maxfilesize.rb +109 -5
- data/tests/tc_curl_multi.rb +29 -0
- data/tests/tc_curl_multi_poll.rb +223 -0
- data/tests/tc_curl_multi_socket_poller.rb +374 -0
- data/tests/tc_curl_native_coverage.rb +57 -0
- data/tests/tc_curl_network_policy.rb +108 -1
- data/tests/tc_curl_postfield.rb +176 -0
- data/tests/tc_curl_protocols.rb +19 -7
- data/tests/tc_curl_transfer_guard.rb +112 -0
- data/tests/tc_fiber_scheduler.rb +101 -2
- data/tests/tc_ractor.rb +141 -0
- metadata +11 -2
data/tests/helper.rb
CHANGED
|
@@ -228,12 +228,17 @@ module BugTestServerSetupTeardown
|
|
|
228
228
|
@thread = Thread.new(@server) do|srv|
|
|
229
229
|
srv.start
|
|
230
230
|
end
|
|
231
|
+
|
|
232
|
+
# WEBrick#shutdown is a no-op until start has created its shutdown pipe
|
|
233
|
+
# and set :Running, so wait for that before the test (and its teardown)
|
|
234
|
+
# runs. Otherwise a teardown racing server startup could miss the brief
|
|
235
|
+
# :Shutdown status and spin forever once the server settled back on :Stop.
|
|
236
|
+
sleep 0.001 until @server.status == :Running || !@thread.alive?
|
|
237
|
+
@thread.value unless @thread.alive?
|
|
231
238
|
end
|
|
232
239
|
|
|
233
240
|
def teardown
|
|
234
|
-
|
|
235
|
-
@server.shutdown
|
|
236
|
-
end
|
|
241
|
+
@server.shutdown
|
|
237
242
|
@thread.join
|
|
238
243
|
end
|
|
239
244
|
end
|
|
@@ -730,3 +735,117 @@ end
|
|
|
730
735
|
unless URI.methods.include?(:encode_www_form)
|
|
731
736
|
URI.extend(Backports::Ruby18::URIFormEncoding)
|
|
732
737
|
end
|
|
738
|
+
|
|
739
|
+
# Valgrind slows every request enough that tight wall-clock bounds stop
|
|
740
|
+
# meaning anything; tests use this to relax or skip only those bounds.
|
|
741
|
+
def curb_under_valgrind?
|
|
742
|
+
ENV['LD_PRELOAD'].to_s.include?('vgpreload')
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Queue#pop(timeout:) needs Ruby 3.2. Older Rubies take the keyword hash as
|
|
746
|
+
# the positional non_block flag and raise ThreadError immediately.
|
|
747
|
+
def curb_pop_within(queue, seconds)
|
|
748
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
|
|
749
|
+
loop do
|
|
750
|
+
begin
|
|
751
|
+
return queue.pop(true)
|
|
752
|
+
rescue ThreadError
|
|
753
|
+
return nil if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
754
|
+
sleep 0.01
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# Minimal HTTP/1.1 server so a test controls exactly when and how a response
|
|
760
|
+
# is written. Every request gets its own connection (Connection: close).
|
|
761
|
+
#
|
|
762
|
+
# /fast "fast" immediately
|
|
763
|
+
# /delay/<seconds> "delayed" after sleeping
|
|
764
|
+
# /hang "released" once release_hung is called
|
|
765
|
+
# /stream/<chunks>/<n> <chunks> writes of <n> bytes (see stream_body)
|
|
766
|
+
# /truncate promises 100000 bytes, sends 10, then closes
|
|
767
|
+
# /reset sends part of a body, then aborts with a TCP RST
|
|
768
|
+
class CurbTinyHTTPServer
|
|
769
|
+
attr_reader :port, :requests
|
|
770
|
+
|
|
771
|
+
# Chunk i is filled with one letter so a test can verify reassembly.
|
|
772
|
+
def self.stream_body(chunks, bytes)
|
|
773
|
+
(0...chunks).map { |i| (97 + (i % 26)).chr * bytes }.join
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def initialize
|
|
777
|
+
@listener = TCPServer.new('127.0.0.1', 0)
|
|
778
|
+
@port = @listener.addr[1]
|
|
779
|
+
@requests = Queue.new
|
|
780
|
+
@release = Queue.new
|
|
781
|
+
@clients = []
|
|
782
|
+
@thread = Thread.new { accept_loop }
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def url(path)
|
|
786
|
+
"http://127.0.0.1:#{@port}#{path}"
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# Let pending /hang requests finish.
|
|
790
|
+
def release_hung(count = 64)
|
|
791
|
+
count.times { @release << true }
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def close
|
|
795
|
+
release_hung
|
|
796
|
+
@listener.close rescue nil
|
|
797
|
+
@thread.kill
|
|
798
|
+
@thread.join(2)
|
|
799
|
+
@clients.each { |t| t.kill; t.join(1) }
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
private
|
|
803
|
+
|
|
804
|
+
def accept_loop
|
|
805
|
+
loop do
|
|
806
|
+
sock = @listener.accept
|
|
807
|
+
@clients << Thread.new(sock) { |s| handle(s) }
|
|
808
|
+
end
|
|
809
|
+
rescue IOError, Errno::EBADF
|
|
810
|
+
nil
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def handle(sock)
|
|
814
|
+
request_line = sock.gets.to_s
|
|
815
|
+
while (line = sock.gets) && line != "\r\n"; end
|
|
816
|
+
path = request_line.split(' ')[1].to_s
|
|
817
|
+
@requests << path
|
|
818
|
+
|
|
819
|
+
case path
|
|
820
|
+
when %r{\A/delay/([\d.]+)}
|
|
821
|
+
sleep Float($1)
|
|
822
|
+
respond(sock, "delayed")
|
|
823
|
+
when '/hang'
|
|
824
|
+
curb_pop_within(@release, 10)
|
|
825
|
+
respond(sock, "released")
|
|
826
|
+
when %r{\A/stream/(\d+)/(\d+)}
|
|
827
|
+
chunks = Integer($1)
|
|
828
|
+
bytes = Integer($2)
|
|
829
|
+
sock.write("HTTP/1.1 200 OK\r\nContent-Length: #{chunks * bytes}\r\nConnection: close\r\n\r\n")
|
|
830
|
+
chunks.times do |i|
|
|
831
|
+
sock.write((97 + (i % 26)).chr * bytes)
|
|
832
|
+
Thread.pass
|
|
833
|
+
end
|
|
834
|
+
when '/truncate'
|
|
835
|
+
sock.write("HTTP/1.1 200 OK\r\nContent-Length: 100000\r\nConnection: close\r\n\r\n0123456789")
|
|
836
|
+
when '/reset'
|
|
837
|
+
sock.write("HTTP/1.1 200 OK\r\nContent-Length: 100000\r\nConnection: close\r\n\r\n0123456789")
|
|
838
|
+
sock.setsockopt(Socket::Option.linger(true, 0))
|
|
839
|
+
else
|
|
840
|
+
respond(sock, "fast")
|
|
841
|
+
end
|
|
842
|
+
rescue IOError, SystemCallError
|
|
843
|
+
nil
|
|
844
|
+
ensure
|
|
845
|
+
sock.close rescue nil
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
def respond(sock, body)
|
|
849
|
+
sock.write("HTTP/1.1 200 OK\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}")
|
|
850
|
+
end
|
|
851
|
+
end
|
data/tests/tc_curl_easy.rb
CHANGED
|
@@ -1,9 +1,131 @@
|
|
|
1
1
|
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
|
2
|
-
class FooNoToS
|
|
2
|
+
class FooNoToS
|
|
3
3
|
undef to_s
|
|
4
4
|
end
|
|
5
5
|
|
|
6
|
+
class FooBadToS
|
|
7
|
+
def to_s
|
|
8
|
+
42
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
class TestCurbCurlEasy < Test::Unit::TestCase
|
|
13
|
+
CONNECTION_INFO_METHODS = %i[
|
|
14
|
+
last_effective_url response_code code primary_ip http_connect_code file_time
|
|
15
|
+
total_time name_lookup_time connect_time app_connect_time pre_transfer_time
|
|
16
|
+
start_transfer_time redirect_time redirect_count redirect_url downloaded_bytes
|
|
17
|
+
uploaded_bytes download_speed upload_speed header_size request_size
|
|
18
|
+
ssl_verify_result downloaded_content_length uploaded_content_length content_type
|
|
19
|
+
os_errno num_connects cookielist ftp_entry_path
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
def test_uninitialized_handles_reject_native_operations
|
|
23
|
+
subclass = Class.new(Curl::Easy) { def initialize; end }
|
|
24
|
+
[Curl::Easy.allocate, subclass.new].each do |easy|
|
|
25
|
+
methods = CONNECTION_INFO_METHODS + %i[reset close clone dup perform]
|
|
26
|
+
methods.each do |method|
|
|
27
|
+
next unless easy.respond_to?(method)
|
|
28
|
+
error = assert_raise(Curl::Err::FailedInitError, method.to_s) { easy.public_send(method) }
|
|
29
|
+
assert_match(/not initialized/, error.message)
|
|
30
|
+
end
|
|
31
|
+
[[:escape, "x"], [:unescape, "x"], [:setopt, Curl::CURLOPT_VERBOSE, true],
|
|
32
|
+
[:http_post, "x"], [:http_put, "x"], [:http_patch, "x"]].each do |args|
|
|
33
|
+
assert_raise(Curl::Err::FailedInitError, args.first.to_s) { easy.public_send(*args) }
|
|
34
|
+
end
|
|
35
|
+
multi = Curl::Multi.new
|
|
36
|
+
begin
|
|
37
|
+
assert_raise(Curl::Err::FailedInitError) { multi.add(easy) }
|
|
38
|
+
assert_raise(Curl::Err::FailedInitError) { multi.send(:_remove, easy) }
|
|
39
|
+
ensure
|
|
40
|
+
multi.close
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_allocated_handle_can_be_initialized
|
|
46
|
+
easy = Curl::Easy.allocate
|
|
47
|
+
easy.send(:initialize, $TEST_URL)
|
|
48
|
+
easy.perform
|
|
49
|
+
# libcurl normalizes file:///C:/ to file://C:/ on Windows.
|
|
50
|
+
expected_url = WINDOWS ? $TEST_URL.sub(%r{\Afile:///}, "file://") : $TEST_URL
|
|
51
|
+
assert_equal expected_url, easy.last_effective_url
|
|
52
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_connection_info_before_perform
|
|
56
|
+
easy = Curl::Easy.new
|
|
57
|
+
CONNECTION_INFO_METHODS.each do |method|
|
|
58
|
+
next unless easy.respond_to?(method)
|
|
59
|
+
assert_nothing_raised(method.to_s) { easy.public_send(method) }
|
|
60
|
+
end
|
|
61
|
+
assert_nil easy.last_effective_url
|
|
62
|
+
assert_nil easy.content_type
|
|
63
|
+
assert_equal 0, easy.response_code
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_reinitialize_detaches_previous_handle
|
|
67
|
+
easy = Curl::Easy.new($TEST_URL)
|
|
68
|
+
easy.headers = { "X-Old" => "old" }
|
|
69
|
+
multi = Curl::Multi.new
|
|
70
|
+
multi.add(easy)
|
|
71
|
+
easy.send(:initialize, $TEST_URL)
|
|
72
|
+
assert_nil easy.multi
|
|
73
|
+
assert_equal({}, easy.headers)
|
|
74
|
+
multi.perform
|
|
75
|
+
easy.perform
|
|
76
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
77
|
+
multi.add(easy)
|
|
78
|
+
multi.perform
|
|
79
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
80
|
+
ensure
|
|
81
|
+
multi.close if multi
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
[:initialize, :close, :reset].each do |operation|
|
|
85
|
+
define_method("test_#{operation}_removes_multi_bookkeeping_before_readd") do
|
|
86
|
+
easy = Curl::Easy.new($TEST_URL)
|
|
87
|
+
multi = Curl::Multi.new
|
|
88
|
+
multi.add(easy)
|
|
89
|
+
easy.__send__(operation)
|
|
90
|
+
|
|
91
|
+
assert_nil easy.multi
|
|
92
|
+
assert_empty multi.requests
|
|
93
|
+
assert_empty multi.__send__(:__curb_native_safety_signatures)
|
|
94
|
+
|
|
95
|
+
easy.url = $TEST_URL
|
|
96
|
+
multi.add(easy)
|
|
97
|
+
assert_equal [easy], multi.requests.values
|
|
98
|
+
multi.perform
|
|
99
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
100
|
+
ensure
|
|
101
|
+
multi.close if multi
|
|
102
|
+
easy.close if easy
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_reinitialize_during_callback_is_blocked
|
|
107
|
+
easy = Curl::Easy.new($TEST_URL)
|
|
108
|
+
easy.on_body do |data|
|
|
109
|
+
error = assert_raise(RuntimeError) { easy.send(:initialize, $TEST_URL) }
|
|
110
|
+
assert_match(/Cannot initialize an active curl handle/, error.message)
|
|
111
|
+
data.bytesize
|
|
112
|
+
end
|
|
113
|
+
easy.perform
|
|
114
|
+
easy.send(:initialize, $TEST_URL)
|
|
115
|
+
easy.perform
|
|
116
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_reinitialize_during_argument_coercion_is_blocked
|
|
120
|
+
easy = Curl::Easy.new($TEST_URL)
|
|
121
|
+
field = Object.new
|
|
122
|
+
field.define_singleton_method(:to_s) { easy.send(:initialize, $TEST_URL); "a=b" }
|
|
123
|
+
error = assert_raise(RuntimeError) { easy.http_post(field) }
|
|
124
|
+
assert_match(/Cannot initialize an active curl handle/, error.message)
|
|
125
|
+
easy.perform
|
|
126
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
127
|
+
end
|
|
128
|
+
|
|
7
129
|
def test_global_reset
|
|
8
130
|
Curl.get($TEST_URL)
|
|
9
131
|
# in a Timeout block you should reset the thread current handle
|
|
@@ -560,7 +682,24 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
560
682
|
assert_equal "one\000two three", c.unescape("one%00two%20three")
|
|
561
683
|
end
|
|
562
684
|
end
|
|
563
|
-
|
|
685
|
+
|
|
686
|
+
def test_escape_with_non_string_to_s_result_does_not_crash
|
|
687
|
+
c = Curl::Easy.new
|
|
688
|
+
result = nil
|
|
689
|
+
# #to_s returning a non-String (42, a Fixnum) makes rb_obj_as_string
|
|
690
|
+
# fall back to the default Object#to_s representation rather than "42" -
|
|
691
|
+
# the point of this test is that it doesn't crash, not the exact value.
|
|
692
|
+
assert_nothing_raised { result = c.escape(FooBadToS.new) }
|
|
693
|
+
assert_kind_of String, result
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def test_unescape_with_non_string_to_s_result_raises_type_error
|
|
697
|
+
c = Curl::Easy.new
|
|
698
|
+
# unescape expects an actual String (or something #to_str-coercible);
|
|
699
|
+
# it must raise cleanly rather than crash on a bad #to_s.
|
|
700
|
+
assert_raise(TypeError) { c.unescape(FooBadToS.new) }
|
|
701
|
+
end
|
|
702
|
+
|
|
564
703
|
def test_headers
|
|
565
704
|
c = Curl::Easy.new($TEST_URL)
|
|
566
705
|
|
|
@@ -745,7 +884,23 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
745
884
|
c.http_auth_types = nil
|
|
746
885
|
assert_nil c.http_auth_types
|
|
747
886
|
end
|
|
748
|
-
|
|
887
|
+
|
|
888
|
+
def test_http_auth_types_with_non_string_to_s_result_does_not_crash
|
|
889
|
+
c = Curl::Easy.new
|
|
890
|
+
assert_nothing_raised { c.http_auth_types = FooBadToS.new }
|
|
891
|
+
assert_nothing_raised { c.http_auth_types = [FooBadToS.new, :basic] }
|
|
892
|
+
assert_equal Curl::CURLAUTH_BASIC, c.http_auth_types
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
def test_http_auth_types_array_of_symbols
|
|
896
|
+
c = Curl::Easy.new
|
|
897
|
+
c.http_auth_types = [:basic, :digest]
|
|
898
|
+
assert_equal Curl::CURLAUTH_BASIC | Curl::CURLAUTH_DIGEST, c.http_auth_types
|
|
899
|
+
|
|
900
|
+
c.send(:http_auth_types=, :basic, :ntlm)
|
|
901
|
+
assert_equal Curl::CURLAUTH_BASIC | Curl::CURLAUTH_NTLM, c.http_auth_types
|
|
902
|
+
end
|
|
903
|
+
|
|
749
904
|
def test_proxy_auth_types_01
|
|
750
905
|
c = Curl::Easy.new($TEST_URL)
|
|
751
906
|
|
|
@@ -757,7 +912,23 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
757
912
|
c.proxy_auth_types = nil
|
|
758
913
|
assert_nil c.proxy_auth_types
|
|
759
914
|
end
|
|
760
|
-
|
|
915
|
+
|
|
916
|
+
def test_proxy_auth_types_symbols
|
|
917
|
+
c = Curl::Easy.new
|
|
918
|
+
|
|
919
|
+
c.proxy_auth_types = :basic
|
|
920
|
+
assert_equal Curl::CURLAUTH_BASIC, c.proxy_auth_types
|
|
921
|
+
|
|
922
|
+
c.proxy_auth_types = [:basic, :ntlm]
|
|
923
|
+
assert_equal Curl::CURLAUTH_BASIC | Curl::CURLAUTH_NTLM, c.proxy_auth_types
|
|
924
|
+
|
|
925
|
+
c.send(:proxy_auth_types=, :basic, :digest)
|
|
926
|
+
assert_equal Curl::CURLAUTH_BASIC | Curl::CURLAUTH_DIGEST, c.proxy_auth_types
|
|
927
|
+
|
|
928
|
+
assert_nothing_raised { c.proxy_auth_types = [FooBadToS.new, :basic] }
|
|
929
|
+
assert_equal Curl::CURLAUTH_BASIC, c.proxy_auth_types
|
|
930
|
+
end
|
|
931
|
+
|
|
761
932
|
def test_max_redirects_01
|
|
762
933
|
c = Curl::Easy.new($TEST_URL)
|
|
763
934
|
|
|
@@ -46,14 +46,17 @@ class TestCurbCurlEasyCookielist < Test::Unit::TestCase
|
|
|
46
46
|
expires = (Date.today + 2).to_datetime
|
|
47
47
|
easy.url = "http://localhost:#{TestServlet.port}#{TestServlet.path}/set_cookies"
|
|
48
48
|
easy.setopt(Curl::CURLOPT_COOKIELIST, "Set-Cookie: c1=v1; domain=localhost; expires=#{expires.httpdate};")
|
|
49
|
-
|
|
49
|
+
# c2 has no Domain attribute so it is host-only on every libcurl version.
|
|
50
|
+
# With domain=localhost, curl >= 8.22 built with libpsl makes it host-only
|
|
51
|
+
# (localhost is a public suffix) while older curl tail-matches (#483).
|
|
52
|
+
easy.post_body = JSON.generate([{ name: 'c2', value: 'v2', expires: expires.httpdate, path: '/' }])
|
|
50
53
|
easy.perform
|
|
51
54
|
easy.url = "http://localhost:#{TestServlet.port}#{TestServlet.path}/get_cookies"
|
|
52
55
|
easy.post_body = nil
|
|
53
56
|
easy.perform
|
|
54
57
|
|
|
55
58
|
assert !easy.enable_cookies?
|
|
56
|
-
assert_equal [".localhost\tTRUE\t/\tFALSE\t#{expires.to_time.to_i}\tc1\tv1", "
|
|
59
|
+
assert_equal [".localhost\tTRUE\t/\tFALSE\t#{expires.to_time.to_i}\tc1\tv1", "localhost\tFALSE\t/\tFALSE\t#{expires.to_time.to_i}\tc2\tv2"], easy.cookielist
|
|
57
60
|
assert_equal 'c2=v2; c1=v1', easy.body_str
|
|
58
61
|
end
|
|
59
62
|
|
|
@@ -183,13 +186,15 @@ class TestCurbCurlEasyCookielist < Test::Unit::TestCase
|
|
|
183
186
|
cookiefile = File.join(Dir.tmpdir, 'curl_test_cookiefile')
|
|
184
187
|
assert !File.exist?(cookiefile)
|
|
185
188
|
begin
|
|
189
|
+
# Use host-only file cookies: libpsl builds normalize localhost domain
|
|
190
|
+
# cookies on file load, but COOKIELIST imports retain tail matching.
|
|
186
191
|
cookielist = [
|
|
187
192
|
# Won't be updated, added instead
|
|
188
|
-
"
|
|
189
|
-
"
|
|
193
|
+
"localhost\tFALSE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent\t84",
|
|
194
|
+
"localhost\tFALSE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent_file\t84",
|
|
190
195
|
# Won't be updated, added instead
|
|
191
|
-
"
|
|
192
|
-
"
|
|
196
|
+
"localhost\tFALSE\t/\tFALSE\t0\tsession\t840",
|
|
197
|
+
"localhost\tFALSE\t/\tFALSE\t0\tsession_file\t840",
|
|
193
198
|
'',
|
|
194
199
|
]
|
|
195
200
|
File.write(cookiefile, cookielist.join("\n"))
|
|
@@ -201,10 +206,10 @@ class TestCurbCurlEasyCookielist < Test::Unit::TestCase
|
|
|
201
206
|
expected_cookielist = [
|
|
202
207
|
".localhost\tTRUE\t/\tFALSE\t#{expires.to_time.to_i}\tpermanent\t42",
|
|
203
208
|
".localhost\tTRUE\t/\tFALSE\t0\tsession\t420",
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
"
|
|
207
|
-
"
|
|
209
|
+
"localhost\tFALSE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent\t84",
|
|
210
|
+
"localhost\tFALSE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent_file\t84",
|
|
211
|
+
"localhost\tFALSE\t/\tFALSE\t0\tsession\t840",
|
|
212
|
+
"localhost\tFALSE\t/\tFALSE\t0\tsession_file\t840",
|
|
208
213
|
]
|
|
209
214
|
assert_equal expected_cookielist, easy.cookielist
|
|
210
215
|
easy.perform
|
|
@@ -245,14 +250,15 @@ class TestCurbCurlEasyCookielist < Test::Unit::TestCase
|
|
|
245
250
|
expires = (Date.today + 2).to_datetime
|
|
246
251
|
easy.url = "http://localhost:#{TestServlet.port}#{TestServlet.path}/set_cookies"
|
|
247
252
|
easy.setopt(Curl::CURLOPT_COOKIELIST, command)
|
|
248
|
-
|
|
253
|
+
# host-only c2, see test_setopt_cookielist_enables_cookie_engine (#483)
|
|
254
|
+
easy.post_body = JSON.generate([{ name: 'c2', value: 'v2', expires: expires.httpdate, path: '/' }])
|
|
249
255
|
easy.perform
|
|
250
256
|
easy.url = "http://localhost:#{TestServlet.port}#{TestServlet.path}/get_cookies"
|
|
251
257
|
easy.post_body = nil
|
|
252
258
|
easy.perform
|
|
253
259
|
|
|
254
260
|
assert !easy.enable_cookies?
|
|
255
|
-
assert_equal ["
|
|
261
|
+
assert_equal ["localhost\tFALSE\t/\tFALSE\t#{expires.to_time.to_i}\tc2\tv2"], easy.cookielist
|
|
256
262
|
assert_equal 'c2=v2', easy.body_str
|
|
257
263
|
end
|
|
258
264
|
end
|
|
@@ -10,6 +10,94 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
10
10
|
super
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def test_safe_body_limit_defaults_and_raw_requests
|
|
14
|
+
assert_equal 10 * 1024 * 1024, Curl::SafetyConfig.new.max_body_bytes
|
|
15
|
+
assert_nil @easy.max_body_bytes
|
|
16
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
17
|
+
@easy.safe_http!
|
|
18
|
+
assert_equal Curl::SafetyConfig::DEFAULT_MAX_BODY_BYTES, @easy.max_body_bytes
|
|
19
|
+
@easy.max_body_bytes = nil
|
|
20
|
+
Curl.__send__(:apply_safety!, @easy)
|
|
21
|
+
assert_equal Curl::SafetyConfig::DEFAULT_MAX_BODY_BYTES, @easy.max_body_bytes
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
[:global, :helper, :handle].each do |mode|
|
|
25
|
+
define_method("test_#{mode}_default_body_limit_rejects_oversized_response") do
|
|
26
|
+
payload = 'x' * (Curl::SafetyConfig::DEFAULT_MAX_BODY_BYTES + 1)
|
|
27
|
+
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: #{payload.bytesize}\r\n\r\n#{payload}") do |url|
|
|
28
|
+
assert_body_limit_error do
|
|
29
|
+
if mode == :helper
|
|
30
|
+
Curl.safe_get(url) { |easy| stub_public_network_policy_for_local_fixture(easy) }
|
|
31
|
+
else
|
|
32
|
+
Curl.safe! if mode == :global
|
|
33
|
+
@easy.url = url
|
|
34
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
35
|
+
@easy.safe_http! if mode == :handle
|
|
36
|
+
@easy.perform
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_default_body_limit_bounds_callback_delivery_without_content_length
|
|
44
|
+
limit = Curl::SafetyConfig::DEFAULT_MAX_BODY_BYTES
|
|
45
|
+
payload = 'x' * (limit + 1)
|
|
46
|
+
received = 0
|
|
47
|
+
with_raw_http_response("HTTP/1.0 200 OK\r\n\r\n#{payload}") do |url|
|
|
48
|
+
@easy.url = url
|
|
49
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
50
|
+
@easy.safe_http!
|
|
51
|
+
@easy.on_body { |data| received += data.bytesize; data.bytesize }
|
|
52
|
+
assert_body_limit_error { @easy.perform }
|
|
53
|
+
end
|
|
54
|
+
assert_operator received, :<=, limit
|
|
55
|
+
assert_operator received, :>, 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_global_body_limit_can_be_explicitly_disabled
|
|
59
|
+
Curl.safe! { |config| config.max_body_bytes = nil }
|
|
60
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
61
|
+
@easy.safe_http!
|
|
62
|
+
assert_nil @easy.max_body_bytes
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_safe_helper_accepts_explicit_larger_limit_and_unlimited
|
|
66
|
+
[20 * 1024 * 1024, nil, 0].each do |limit|
|
|
67
|
+
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok") do |url|
|
|
68
|
+
easy = Curl.safe_get(url, :max_body_bytes => limit) do |handle|
|
|
69
|
+
stub_public_network_policy_for_local_fixture(handle)
|
|
70
|
+
end
|
|
71
|
+
assert_equal(limit == 0 ? nil : limit, easy.max_body_bytes)
|
|
72
|
+
assert_equal 'ok', easy.body_str
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_safe_helper_inherits_global_body_limit
|
|
78
|
+
Curl.safe! { |config| config.max_body_bytes = 20 * 1024 * 1024 }
|
|
79
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
80
|
+
@easy.safe_http!
|
|
81
|
+
assert_equal 20 * 1024 * 1024, @easy.max_body_bytes
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_safe_helper_cannot_weaken_global_body_limit
|
|
85
|
+
Curl.safe! { |config| config.max_body_bytes = 10 }
|
|
86
|
+
[100, nil, 0].each do |limit|
|
|
87
|
+
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 64\r\n\r\n#{'x' * 64}") do |url|
|
|
88
|
+
assert_body_limit_error do
|
|
89
|
+
Curl.safe_get(url, :max_body_bytes => limit) do |handle|
|
|
90
|
+
stub_public_network_policy_for_local_fixture(handle)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_safe_http_rejects_negative_body_limit
|
|
98
|
+
assert_raise(ArgumentError) { @easy.safe_http!(:max_body_bytes => -1) }
|
|
99
|
+
end
|
|
100
|
+
|
|
13
101
|
def test_maxfilesize_setopt
|
|
14
102
|
@easy.set(Curl::CURLOPT_MAXFILESIZE, 5000000)
|
|
15
103
|
@easy.set(Curl::CURLOPT_MAXFILESIZE_LARGE, 5000000) if Curl.const_defined?(:CURLOPT_MAXFILESIZE_LARGE)
|
|
@@ -41,7 +129,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
41
129
|
def test_safe_get_accepts_max_body_bytes_as_second_argument
|
|
42
130
|
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 64\r\n\r\n#{'x' * 64}") do |url|
|
|
43
131
|
assert_body_limit_error do
|
|
44
|
-
Curl.safe_get(url, :max_body_bytes => 10)
|
|
132
|
+
Curl.safe_get(url, :max_body_bytes => 10) { |easy| stub_public_network_policy_for_local_fixture(easy) }
|
|
45
133
|
end
|
|
46
134
|
end
|
|
47
135
|
end
|
|
@@ -50,6 +138,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
50
138
|
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 64\r\n\r\n#{'x' * 64}") do |url|
|
|
51
139
|
assert_body_limit_error do
|
|
52
140
|
Curl.safe_get(url, {}, :max_body_bytes => 10) do |easy|
|
|
141
|
+
stub_public_network_policy_for_local_fixture(easy)
|
|
53
142
|
easy.max_body_bytes = nil
|
|
54
143
|
end
|
|
55
144
|
end
|
|
@@ -63,6 +152,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
63
152
|
end
|
|
64
153
|
|
|
65
154
|
@easy.url = url
|
|
155
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
66
156
|
|
|
67
157
|
assert_body_limit_error do
|
|
68
158
|
@easy.perform
|
|
@@ -78,6 +168,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
78
168
|
|
|
79
169
|
assert_body_limit_error do
|
|
80
170
|
Curl.get(url) do |easy|
|
|
171
|
+
stub_public_network_policy_for_local_fixture(easy)
|
|
81
172
|
easy.max_body_bytes = nil
|
|
82
173
|
end
|
|
83
174
|
end
|
|
@@ -91,7 +182,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
91
182
|
end
|
|
92
183
|
|
|
93
184
|
assert_body_limit_error do
|
|
94
|
-
Curl.safe_get(url, :max_body_bytes => 10)
|
|
185
|
+
Curl.safe_get(url, :max_body_bytes => 10) { |easy| stub_public_network_policy_for_local_fixture(easy) }
|
|
95
186
|
end
|
|
96
187
|
end
|
|
97
188
|
end
|
|
@@ -106,7 +197,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
106
197
|
|
|
107
198
|
def test_safe_head_does_not_apply_body_cap_to_content_length
|
|
108
199
|
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 65536\r\n\r\n") do |url|
|
|
109
|
-
easy = Curl.safe_head(url, :max_body_bytes => 1)
|
|
200
|
+
easy = Curl.safe_head(url, :max_body_bytes => 1) { |handle| stub_public_network_policy_for_local_fixture(handle) }
|
|
110
201
|
|
|
111
202
|
assert_equal 200, easy.response_code
|
|
112
203
|
assert_equal '', easy.body_str
|
|
@@ -182,6 +273,14 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
182
273
|
|
|
183
274
|
private
|
|
184
275
|
|
|
276
|
+
# Keep body-limit integration tests local; real destination enforcement is
|
|
277
|
+
# covered separately in tc_curl_network_policy.rb.
|
|
278
|
+
def stub_public_network_policy_for_local_fixture(easy)
|
|
279
|
+
def easy.network_policy=(policy)
|
|
280
|
+
raise "unexpected policy: #{policy}" unless policy == :public
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
185
284
|
def assert_body_limit_error
|
|
186
285
|
assert_raise(Curl::Err::FileSizeExceededError) do
|
|
187
286
|
yield
|
|
@@ -197,8 +296,13 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
197
296
|
socket.readpartial(4096)
|
|
198
297
|
rescue EOFError
|
|
199
298
|
end
|
|
200
|
-
|
|
201
|
-
|
|
299
|
+
begin
|
|
300
|
+
socket.write(response)
|
|
301
|
+
rescue Errno::EPIPE, Errno::ECONNRESET
|
|
302
|
+
# Expected when the client aborts at the body limit.
|
|
303
|
+
ensure
|
|
304
|
+
socket.close
|
|
305
|
+
end
|
|
202
306
|
end
|
|
203
307
|
|
|
204
308
|
yield "http://127.0.0.1:#{port}/"
|
data/tests/tc_curl_multi.rb
CHANGED
|
@@ -586,6 +586,35 @@ class TestCurbCurlMulti < Test::Unit::TestCase
|
|
|
586
586
|
end
|
|
587
587
|
end
|
|
588
588
|
|
|
589
|
+
# Only easies attached by the raising callback itself are withdrawn. Work
|
|
590
|
+
# queued by an earlier, successful callback in the same perform was already
|
|
591
|
+
# attached when the failing callback started, so it keeps running and the
|
|
592
|
+
# deferred exception is raised once it drains.
|
|
593
|
+
def test_multi_perform_keeps_work_added_by_earlier_callback_when_later_callback_raises
|
|
594
|
+
server = CurbTinyHTTPServer.new
|
|
595
|
+
multi = Curl::Multi.new
|
|
596
|
+
first = Curl::Easy.new(server.url('/fast'))
|
|
597
|
+
failing = Curl::Easy.new(server.url('/delay/0.15'))
|
|
598
|
+
kept = Curl::Easy.new(server.url('/delay/0.4'))
|
|
599
|
+
|
|
600
|
+
first.on_complete { multi.add(kept) }
|
|
601
|
+
failing.on_complete { raise "complete blew up" }
|
|
602
|
+
|
|
603
|
+
error = assert_raise(Curl::Err::AbortedByCallbackError) do
|
|
604
|
+
multi.add(first)
|
|
605
|
+
multi.add(failing)
|
|
606
|
+
multi.perform
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
assert_equal "complete blew up", error.message
|
|
610
|
+
assert_equal 0, kept.last_result
|
|
611
|
+
assert_equal "delayed", kept.body_str,
|
|
612
|
+
"work added by an earlier successful callback should finish, not be withdrawn with the failing callback's additions"
|
|
613
|
+
ensure
|
|
614
|
+
multi.close if multi
|
|
615
|
+
server.close if server
|
|
616
|
+
end
|
|
617
|
+
|
|
589
618
|
def test_multi_perform_does_not_start_work_added_within_on_complete_after_on_body_exception
|
|
590
619
|
with_queue_refill_test_server do |port, hits|
|
|
591
620
|
multi = Curl::Multi.new
|