curb 0.8.5 → 1.3.6

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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +579 -0
  3. data/Rakefile +85 -27
  4. data/doc.rb +48 -8
  5. data/ext/banned.h +32 -0
  6. data/ext/curb.c +563 -233
  7. data/ext/curb.h +19 -10
  8. data/ext/curb_easy.c +3229 -368
  9. data/ext/curb_easy.h +42 -0
  10. data/ext/curb_errors.c +117 -18
  11. data/ext/curb_errors.h +9 -5
  12. data/ext/curb_macros.h +33 -21
  13. data/ext/curb_multi.c +1904 -272
  14. data/ext/curb_multi.h +11 -3
  15. data/ext/curb_postfield.c +149 -77
  16. data/ext/curb_postfield.h +1 -0
  17. data/ext/curb_upload.c +38 -11
  18. data/ext/curb_upload.h +2 -0
  19. data/ext/extconf.rb +355 -35
  20. data/lib/curb.rb +1 -0
  21. data/lib/curl/download.rb +160 -0
  22. data/lib/curl/easy.rb +422 -88
  23. data/lib/curl/multi.rb +295 -56
  24. data/lib/curl.rb +676 -11
  25. data/tests/bug_crash_on_debug.rb +14 -28
  26. data/tests/bug_crash_on_progress.rb +32 -16
  27. data/tests/bug_curb_easy_blocks_ruby_threads.rb +10 -15
  28. data/tests/bug_curb_easy_post_with_string_no_content_length_header.rb +6 -30
  29. data/tests/bug_follow_redirect_288.rb +83 -0
  30. data/tests/bug_instance_post_differs_from_class_post.rb +3 -5
  31. data/tests/bug_issue102.rb +17 -0
  32. data/tests/bug_issue_noproxy.rb +56 -0
  33. data/tests/bug_issue_post_redirect.rb +93 -0
  34. data/tests/bug_issue_spnego.rb +41 -0
  35. data/tests/bug_multi_segfault.rb +1 -0
  36. data/tests/bug_poison.rb +29 -0
  37. data/tests/bug_raise_on_callback.rb +30 -0
  38. data/tests/helper.rb +400 -44
  39. data/tests/leak_trace.rb +237 -0
  40. data/tests/mem_check.rb +3 -0
  41. data/tests/tc_curl.rb +31 -1
  42. data/tests/tc_curl_download.rb +98 -7
  43. data/tests/tc_curl_easy.rb +985 -66
  44. data/tests/tc_curl_easy_cookielist.rb +277 -0
  45. data/tests/tc_curl_easy_request_target.rb +41 -0
  46. data/tests/tc_curl_easy_resolve.rb +48 -0
  47. data/tests/tc_curl_maxfilesize.rb +212 -0
  48. data/tests/tc_curl_multi.rb +1111 -51
  49. data/tests/tc_curl_native_coverage.rb +145 -0
  50. data/tests/tc_curl_network_policy.rb +1475 -0
  51. data/tests/tc_curl_postfield.rb +207 -30
  52. data/tests/tc_curl_protocols.rb +388 -0
  53. data/tests/tc_fiber_scheduler.rb +584 -0
  54. data/tests/tc_ftp_options.rb +39 -0
  55. data/tests/tc_gc_compact.rb +223 -0
  56. data/tests/tc_test_server_methods.rb +110 -0
  57. data/tests/timeout.rb +30 -6
  58. metadata +66 -31
  59. data/README +0 -194
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'objspace'
6
+ require 'optparse'
7
+ require 'rbconfig'
8
+ require 'tmpdir'
9
+
10
+ TOPDIR = File.expand_path('..', __dir__)
11
+ LIBDIR = File.join(TOPDIR, 'lib')
12
+ EXTDIR = File.join(TOPDIR, 'ext')
13
+ $LOAD_PATH.unshift(LIBDIR)
14
+ $LOAD_PATH.unshift(EXTDIR)
15
+
16
+ require 'curb'
17
+
18
+ module LeakTrace
19
+ Record = Struct.new(:identifier, :created_location, :closed, keyword_init: true)
20
+
21
+ module_function
22
+
23
+ def install_multi_trace!
24
+ return if @multi_trace_installed
25
+
26
+ @multi_trace_installed = true
27
+ @multi_records = {}
28
+
29
+ multi_singleton = class << Curl::Multi
30
+ self
31
+ end
32
+
33
+ multi_singleton.alias_method(:__leak_trace_new, :new)
34
+ multi_singleton.define_method(:new) do |*args, **kwargs, &block|
35
+ multi = if kwargs.empty?
36
+ __leak_trace_new(*args, &block)
37
+ else
38
+ __leak_trace_new(*args, **kwargs, &block)
39
+ end
40
+
41
+ LeakTrace.multi_records[multi.object_id] ||= Record.new(
42
+ identifier: multi.object_id,
43
+ created_location: caller_locations(1, 6).map(&:to_s),
44
+ closed: false
45
+ )
46
+ multi
47
+ end
48
+
49
+ Curl::Multi.class_eval do
50
+ alias_method :__leak_trace__close, :_close
51
+
52
+ def _close(*args, **kwargs, &block)
53
+ record = LeakTrace.multi_records[object_id]
54
+ record.closed = true if record
55
+
56
+ if kwargs.empty?
57
+ __leak_trace__close(*args, &block)
58
+ else
59
+ __leak_trace__close(*args, **kwargs, &block)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def multi_records
66
+ @multi_records ||= {}
67
+ end
68
+
69
+ def live_multis
70
+ ObjectSpace.each_object(Curl::Multi).to_a
71
+ end
72
+
73
+ def fixture_path
74
+ File.expand_path('helper.rb', __dir__)
75
+ end
76
+
77
+ def fixture_url
78
+ path = fixture_path.tr('\\', '/')
79
+ "file://#{path.start_with?('/') ? '' : '/'}#{path}"
80
+ end
81
+
82
+ def full_gc(compact: false)
83
+ GC.start(full_mark: true, immediate_sweep: true)
84
+ GC.compact if compact && GC.respond_to?(:compact)
85
+ GC.start(full_mark: true, immediate_sweep: true)
86
+ end
87
+
88
+ def close_multi(multi)
89
+ return unless multi
90
+
91
+ multi.instance_variable_set(:@requests, {}) if multi.respond_to?(:instance_variable_set)
92
+ multi._close
93
+ rescue StandardError
94
+ multi.close rescue nil
95
+ end
96
+
97
+ def multi_perform(iterations:, handles:, close:, compact:)
98
+ iterations.times do
99
+ multi = Curl::Multi.new
100
+ handles.times { multi.add(Curl::Easy.new(fixture_url)) }
101
+ multi.perform
102
+ ensure
103
+ close_multi(multi) if close
104
+ multi = nil
105
+ full_gc(compact: compact)
106
+ end
107
+ end
108
+
109
+ def multi_gc_cleanup(iterations:, handles:, compact:)
110
+ iterations.times do
111
+ multi = Curl::Multi.new
112
+ handles.times { multi.add(Curl::Easy.new(fixture_url)) }
113
+ multi.perform
114
+ multi = nil
115
+ full_gc(compact: compact)
116
+ end
117
+ end
118
+
119
+ def easy_perform(iterations:, compact:)
120
+ iterations.times do
121
+ easy = Curl::Easy.new(fixture_url)
122
+ easy.perform
123
+ easy.close
124
+ easy = nil
125
+ full_gc(compact: compact)
126
+ end
127
+ end
128
+
129
+ def download(iterations:, compact:)
130
+ iterations.times do |i|
131
+ Dir.mktmpdir("curb-leak-trace-#{i}") do |dir|
132
+ Curl::Easy.download(fixture_url, File.join(dir, 'helper-copy.rb'))
133
+ end
134
+ full_gc(compact: compact)
135
+ end
136
+ end
137
+
138
+ def report(io:, verbose:)
139
+ live = live_multis
140
+ tracked_live = live.filter_map { |multi| multi_records[multi.object_id] }
141
+
142
+ io.puts "scanned live Curl::Multi objects: #{live.size}"
143
+ io.puts "tracked Curl::Multi allocations: #{multi_records.size}"
144
+ io.puts "tracked Curl::Multi closes: #{multi_records.count { |_id, record| record.closed }}"
145
+
146
+ grouped = tracked_live.group_by { |record| record.created_location.first || '<unknown>' }
147
+ grouped.sort_by { |location, records| [-records.size, location] }.each do |location, records|
148
+ io.puts "live #{records.size}: #{location}"
149
+ next unless verbose
150
+
151
+ records.each do |record|
152
+ io.puts " object_id=#{record.identifier}"
153
+ record.created_location.drop(1).each do |line|
154
+ io.puts " #{line}"
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ options = {
162
+ scenario: 'multi_perform',
163
+ iterations: 25,
164
+ handles: 3,
165
+ close: true,
166
+ compact: false,
167
+ verbose: false,
168
+ fail_on_live: false
169
+ }
170
+
171
+ OptionParser.new do |opts|
172
+ opts.banner = 'Usage: ruby tests/leak_trace.rb [options]'
173
+
174
+ opts.on('--scenario NAME', 'multi_perform, multi_gc_cleanup, easy_perform, download') do |value|
175
+ options[:scenario] = value
176
+ end
177
+
178
+ opts.on('--iterations N', Integer, 'number of iterations to run') do |value|
179
+ options[:iterations] = value
180
+ end
181
+
182
+ opts.on('--handles N', Integer, 'number of easy handles per multi iteration') do |value|
183
+ options[:handles] = value
184
+ end
185
+
186
+ opts.on('--[no-]close', 'explicitly close multi handles when the scenario finishes') do |value|
187
+ options[:close] = value
188
+ end
189
+
190
+ opts.on('--compact', 'run GC.compact between iterations when available') do
191
+ options[:compact] = true
192
+ end
193
+
194
+ opts.on('--verbose', 'print full creation stacks for live multi handles') do
195
+ options[:verbose] = true
196
+ end
197
+
198
+ opts.on('--fail-on-live', 'exit non-zero when any live Curl::Multi objects remain') do
199
+ options[:fail_on_live] = true
200
+ end
201
+ end.parse!
202
+
203
+ LeakTrace.install_multi_trace!
204
+
205
+ case options[:scenario]
206
+ when 'multi_perform'
207
+ LeakTrace.multi_perform(
208
+ iterations: options[:iterations],
209
+ handles: options[:handles],
210
+ close: options[:close],
211
+ compact: options[:compact]
212
+ )
213
+ when 'multi_gc_cleanup'
214
+ LeakTrace.multi_gc_cleanup(
215
+ iterations: options[:iterations],
216
+ handles: options[:handles],
217
+ compact: options[:compact]
218
+ )
219
+ when 'easy_perform'
220
+ LeakTrace.easy_perform(
221
+ iterations: options[:iterations],
222
+ compact: options[:compact]
223
+ )
224
+ when 'download'
225
+ LeakTrace.download(
226
+ iterations: options[:iterations],
227
+ compact: options[:compact]
228
+ )
229
+ else
230
+ warn "unknown scenario: #{options[:scenario]}"
231
+ exit 64
232
+ end
233
+
234
+ LeakTrace.full_gc(compact: options[:compact])
235
+ LeakTrace.report(io: $stdout, verbose: options[:verbose])
236
+
237
+ exit 1 if options[:fail_on_live] && LeakTrace.live_multis.any?
data/tests/mem_check.rb CHANGED
@@ -6,6 +6,9 @@ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
6
6
  # Run some tests to measure the memory usage of curb, these tests require fork and ps
7
7
  #
8
8
  class TestCurbMemory < Test::Unit::TestCase
9
+ def setup
10
+ omit('Memory fork/ps tests not supported on Windows') if WINDOWS || NO_FORK
11
+ end
9
12
 
10
13
  def test_easy_memory
11
14
  easy_avg, easy_std = measure_object_memory(Curl::Easy)
data/tests/tc_curl.rb CHANGED
@@ -31,7 +31,37 @@ class TestCurl < Test::Unit::TestCase
31
31
  assert_equal "OPTIONSfoo=bar", curl.body_str
32
32
  end
33
33
 
34
- include TestServerMethods
34
+ def test_urlalize_without_extra_params
35
+ url_no_params = 'http://localhost/test'
36
+ url_with_params = 'http://localhost/test?a=1'
37
+
38
+ assert_equal(url_no_params, Curl.urlalize(url_no_params))
39
+ assert_equal(url_with_params, Curl.urlalize(url_with_params))
40
+ end
41
+
42
+ def test_urlalize_with_nil_as_params
43
+ url = 'http://localhost/test'
44
+ assert_equal(url, Curl.urlalize(url, nil))
45
+ end
46
+
47
+ def test_urlalize_with_extra_params
48
+ url_no_params = 'http://localhost/test'
49
+ url_with_params = 'http://localhost/test?a=1'
50
+ extra_params = { :b => 2 }
51
+
52
+ expected_url_no_params = 'http://localhost/test?b=2'
53
+ expected_url_with_params = 'http://localhost/test?a=1&b=2'
54
+
55
+ assert_equal(expected_url_no_params, Curl.urlalize(url_no_params, extra_params))
56
+ assert_equal(expected_url_with_params, Curl.urlalize(url_with_params, extra_params))
57
+ end
58
+
59
+ def test_urlalize_does_not_strip_trailing_?
60
+ url_empty_params = 'http://localhost/test?'
61
+ assert_equal(url_empty_params, Curl.urlalize(url_empty_params))
62
+ end
63
+
64
+ include TestServerMethods
35
65
 
36
66
  def setup
37
67
  server_setup
@@ -1,4 +1,5 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+ require 'tmpdir'
2
3
 
3
4
  class TestCurbCurlDownload < Test::Unit::TestCase
4
5
  include TestServerMethods
@@ -6,24 +7,109 @@ class TestCurbCurlDownload < Test::Unit::TestCase
6
7
  def setup
7
8
  server_setup
8
9
  end
9
-
10
+
10
11
  def test_download_url_to_file_via_string
11
12
  dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
12
13
  dl_path = File.join(Dir::tmpdir, "dl_url_test.file")
13
14
 
14
- curb = Curl::Easy.download(dl_url, dl_path)
15
+ Curl::Easy.download(dl_url, dl_path)
15
16
  assert File.exist?(dl_path)
16
17
  assert_equal File.read(File.join(File.dirname(__FILE__), '..','ext','curb_easy.c')), File.read(dl_path)
17
18
  ensure
18
19
  File.unlink(dl_path) if File.exist?(dl_path)
19
20
  end
20
21
 
22
+ def test_download_default_path_does_not_overwrite_existing_file
23
+ dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
24
+
25
+ Dir.mktmpdir('curb-download-') do |dir|
26
+ Dir.chdir(dir) do
27
+ File.write('curb_easy.c', 'sentinel')
28
+
29
+ assert_raise(Curl::DownloadTargetExistsError) do
30
+ Curl::Easy.download(dl_url)
31
+ end
32
+
33
+ assert_equal 'sentinel', File.read('curb_easy.c')
34
+ end
35
+ end
36
+ end
37
+
38
+ def test_download_default_path_strips_query_from_url_basename
39
+ dl_url = "http://127.0.0.1:9129/ext/curb_easy.c?cache=1"
40
+ source_path = File.expand_path(File.join(File.dirname(__FILE__), '..','ext','curb_easy.c'))
41
+
42
+ Dir.mktmpdir('curb-download-') do |dir|
43
+ Dir.chdir(dir) do
44
+ Curl::Easy.download(dl_url)
45
+
46
+ assert File.exist?('curb_easy.c')
47
+ assert !File.exist?('curb_easy.c?cache=1')
48
+ assert_equal File.read(source_path), File.read('curb_easy.c')
49
+ end
50
+ end
51
+ end
52
+
53
+ def test_download_safe_directory_rejects_unsafe_names
54
+ dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
55
+
56
+ Dir.mktmpdir('curb-download-') do |dir|
57
+ ['../escape.c', '.env', 'nested/file', '/tmp/escape.c', "bad\0name"].each do |name|
58
+ assert_raise(ArgumentError, "expected #{name.inspect} to be rejected") do
59
+ Curl::Easy.download(dl_url, name, :download_dir => dir)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def test_download_safe_directory_rejects_dotfile_from_url
66
+ dl_url = "http://127.0.0.1:9129/ext/.env"
67
+
68
+ Dir.mktmpdir('curb-download-') do |dir|
69
+ assert_raise(ArgumentError) do
70
+ Curl::Easy.download(dl_url, :download_dir => dir)
71
+ end
72
+
73
+ assert !File.exist?(File.join(dir, '.env'))
74
+ end
75
+ end
76
+
77
+ def test_download_overwrite_true_replaces_existing_file
78
+ dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
79
+
80
+ Dir.mktmpdir('curb-download-') do |dir|
81
+ dl_path = File.join(dir, 'curb_easy.c')
82
+ File.write(dl_path, 'sentinel')
83
+
84
+ Curl::Easy.download(dl_url, dl_path, :overwrite => true)
85
+
86
+ assert_equal File.read(File.join(File.dirname(__FILE__), '..','ext','curb_easy.c')), File.read(dl_path)
87
+ end
88
+ end
89
+
90
+ def test_download_callback_abort_does_not_replace_existing_file
91
+ dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
92
+
93
+ Dir.mktmpdir('curb-download-') do |dir|
94
+ dl_path = File.join(dir, 'curb_easy.c')
95
+ File.write(dl_path, 'sentinel')
96
+
97
+ assert_raise(Curl::Err::WriteError, Curl::Err::RecvError) do
98
+ Curl::Easy.download(dl_url, dl_path, :overwrite => true) do |curl|
99
+ curl.on_body { |_data| 0 }
100
+ end
101
+ end
102
+
103
+ assert_equal 'sentinel', File.read(dl_path)
104
+ end
105
+ end
106
+
21
107
  def test_download_url_to_file_via_file_io
22
108
  dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
23
109
  dl_path = File.join(Dir::tmpdir, "dl_url_test.file")
24
110
  io = File.open(dl_path, 'wb')
25
111
 
26
- curb = Curl::Easy.download(dl_url, io)
112
+ Curl::Easy.download(dl_url, io)
27
113
  assert io.closed?
28
114
  assert File.exist?(dl_path)
29
115
  assert_equal File.read(File.join(File.dirname(__FILE__), '..','ext','curb_easy.c')), File.read(dl_path)
@@ -32,15 +118,19 @@ class TestCurbCurlDownload < Test::Unit::TestCase
32
118
  end
33
119
 
34
120
  def test_download_url_to_file_via_io
121
+ omit('fork not available on this platform') if NO_FORK || WINDOWS
35
122
  dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
36
123
  dl_path = File.join(Dir::tmpdir, "dl_url_test.file")
37
124
  reader, writer = IO.pipe
38
125
 
39
126
  # Write to local file
40
- fork do
127
+ child_pid = fork do
41
128
  begin
42
129
  writer.close
43
130
  File.open(dl_path, 'wb') { |file| file << reader.read }
131
+ exit! 0
132
+ rescue StandardError
133
+ exit! 1
44
134
  ensure
45
135
  reader.close rescue IOError # if the stream has already been closed
46
136
  end
@@ -49,8 +139,9 @@ class TestCurbCurlDownload < Test::Unit::TestCase
49
139
  # Download remote source
50
140
  begin
51
141
  reader.close
52
- curb = Curl::Easy.download(dl_url, writer)
53
- Process.wait
142
+ Curl::Easy.download(dl_url, writer)
143
+ _pid, status = Process.wait2(child_pid)
144
+ assert_predicate status, :success?
54
145
  ensure
55
146
  writer.close rescue IOError # if the stream has already been closed, which occurs in Easy::download
56
147
  end
@@ -58,7 +149,7 @@ class TestCurbCurlDownload < Test::Unit::TestCase
58
149
  assert File.exist?(dl_path)
59
150
  assert_equal File.read(File.join(File.dirname(__FILE__), '..','ext','curb_easy.c')), File.read(dl_path)
60
151
  ensure
61
- File.unlink(dl_path) if File.exist?(dl_path)
152
+ File.unlink(dl_path) if dl_path && File.exist?(dl_path)
62
153
  end
63
154
 
64
155
  def test_download_bad_url_gives_404