curb 0.8.4 → 1.3.5

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +522 -0
  3. data/Rakefile +79 -26
  4. data/ext/banned.h +32 -0
  5. data/ext/curb.c +549 -230
  6. data/ext/curb.h +19 -10
  7. data/ext/curb_easy.c +1935 -366
  8. data/ext/curb_easy.h +16 -0
  9. data/ext/curb_errors.c +115 -18
  10. data/ext/curb_errors.h +8 -5
  11. data/ext/curb_macros.h +33 -21
  12. data/ext/curb_multi.c +1858 -272
  13. data/ext/curb_multi.h +10 -3
  14. data/ext/curb_postfield.c +149 -77
  15. data/ext/curb_postfield.h +1 -0
  16. data/ext/curb_upload.c +38 -11
  17. data/ext/curb_upload.h +2 -0
  18. data/ext/extconf.rb +353 -35
  19. data/lib/curb.rb +1 -0
  20. data/lib/curl/easy.rb +314 -78
  21. data/lib/curl/multi.rb +134 -28
  22. data/lib/curl.rb +217 -11
  23. data/tests/bug_crash_on_debug.rb +14 -28
  24. data/tests/bug_crash_on_progress.rb +32 -16
  25. data/tests/bug_curb_easy_blocks_ruby_threads.rb +10 -15
  26. data/tests/bug_curb_easy_post_with_string_no_content_length_header.rb +6 -30
  27. data/tests/bug_follow_redirect_288.rb +83 -0
  28. data/tests/bug_instance_post_differs_from_class_post.rb +3 -5
  29. data/tests/bug_issue102.rb +1 -1
  30. data/tests/bug_issue_noproxy.rb +56 -0
  31. data/tests/bug_issue_post_redirect.rb +93 -0
  32. data/tests/bug_issue_spnego.rb +41 -0
  33. data/tests/bug_multi_segfault.rb +1 -0
  34. data/tests/bug_raise_on_callback.rb +30 -0
  35. data/tests/helper.rb +400 -44
  36. data/tests/leak_trace.rb +237 -0
  37. data/tests/mem_check.rb +3 -0
  38. data/tests/tc_curl.rb +31 -1
  39. data/tests/tc_curl_download.rb +12 -7
  40. data/tests/tc_curl_easy.rb +911 -63
  41. data/tests/tc_curl_easy_cookielist.rb +277 -0
  42. data/tests/tc_curl_easy_request_target.rb +41 -0
  43. data/tests/tc_curl_easy_resolve.rb +48 -0
  44. data/tests/tc_curl_maxfilesize.rb +12 -0
  45. data/tests/tc_curl_multi.rb +855 -53
  46. data/tests/tc_curl_native_coverage.rb +145 -0
  47. data/tests/tc_curl_postfield.rb +207 -30
  48. data/tests/tc_curl_protocols.rb +37 -0
  49. data/tests/tc_fiber_scheduler.rb +543 -0
  50. data/tests/tc_ftp_options.rb +39 -0
  51. data/tests/tc_gc_compact.rb +223 -0
  52. data/tests/tc_test_server_methods.rb +110 -0
  53. data/tests/timeout.rb +30 -6
  54. metadata +59 -36
  55. data/README +0 -194
data/tests/helper.rb CHANGED
@@ -2,6 +2,10 @@
2
2
  # Copyright (c)2006 Ross Bamford. See LICENSE.
3
3
  $CURB_TESTING = true
4
4
  require 'uri'
5
+ require 'stringio'
6
+ require 'digest/md5'
7
+ require 'rbconfig'
8
+ require File.join(RbConfig::CONFIG['rubylibdir'], 'timeout')
5
9
 
6
10
  $TOPDIR = File.expand_path(File.join(File.dirname(__FILE__), '..'))
7
11
  $EXTDIR = File.join($TOPDIR, 'ext')
@@ -9,14 +13,53 @@ $LIBDIR = File.join($TOPDIR, 'lib')
9
13
  $:.unshift($LIBDIR)
10
14
  $:.unshift($EXTDIR)
11
15
 
16
+ # Setup SimpleCov for Ruby code coverage if COVERAGE env var is set
17
+ if ENV['COVERAGE']
18
+ begin
19
+ require 'simplecov'
20
+ require 'simplecov-lcov'
21
+
22
+ SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
23
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
24
+ SimpleCov::Formatter::HTMLFormatter,
25
+ SimpleCov::Formatter::LcovFormatter
26
+ ])
27
+
28
+ SimpleCov.start do
29
+ add_filter '/tests/'
30
+ add_filter '/spec/'
31
+ add_filter '/vendor/'
32
+ add_filter '/.bundle/'
33
+ add_group 'Library', 'lib'
34
+ add_group 'Extensions', 'ext'
35
+
36
+ # Track branch coverage if available
37
+ enable_coverage :branch if respond_to?(:enable_coverage)
38
+ end
39
+ rescue LoadError
40
+ puts "SimpleCov not available. Install it with: gem install simplecov simplecov-lcov"
41
+ end
42
+ end
43
+
12
44
  require 'curb'
13
- require 'test/unit'
45
+ begin
46
+ require 'test/unit'
47
+ rescue LoadError
48
+ gem 'test/unit'
49
+ require 'test/unit'
50
+ end
14
51
  require 'fileutils'
52
+ require 'rbconfig'
53
+
54
+ # Platform helpers
55
+ WINDOWS = /mswin|msys|mingw|cygwin|bccwin|wince|emc|windows/i.match?(RbConfig::CONFIG['host_os'])
56
+ NO_FORK = !Process.respond_to?(:fork)
15
57
 
16
- $TEST_URL = "file://#{URI.escape(File.expand_path(__FILE__).tr('\\','/').tr(':','|'))}"
58
+ $TEST_URL = "file://#{'/' if RUBY_DESCRIPTION =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/}#{File.expand_path(__FILE__).tr('\\','/')}"
17
59
 
18
60
  require 'thread'
19
61
  require 'webrick'
62
+ require 'socket'
20
63
 
21
64
  # set this to true to avoid testing with multiple threads
22
65
  # or to test with multiple threads set it to false
@@ -24,20 +67,41 @@ require 'webrick'
24
67
  # on the presence of multiple threads
25
68
  TEST_SINGLE_THREADED=false
26
69
 
27
- # keep webrick quiet
28
- class ::WEBrick::HTTPServer
29
- def access_log(config, req, res)
30
- # nop
31
- end
32
- end
33
- class ::WEBrick::BasicLog
34
- def log(level, data)
35
- # nop
70
+ WEBRICK_TEST_LOG = WEBrick::Log.new(File.open(File::NULL, 'w'), WEBrick::BasicLog::ERROR)
71
+
72
+ module CurbTestResourceCleanup
73
+ def teardown
74
+ super
75
+ ensure
76
+ begin
77
+ if Curl::Easy.respond_to?(:flush_deferred_multi_closes)
78
+ Curl::Easy.flush_deferred_multi_closes(all_threads: true)
79
+ end
80
+ rescue StandardError
81
+ nil
82
+ end
83
+
84
+ begin
85
+ ObjectSpace.each_object(Curl::Multi) do |multi|
86
+ begin
87
+ next if multi.instance_variable_defined?(:@deferred_close) && multi.instance_variable_get(:@deferred_close)
88
+ multi.instance_variable_set(:@requests, {})
89
+ multi._close
90
+ rescue StandardError
91
+ nil
92
+ end
93
+ end
94
+ rescue StandardError
95
+ nil
96
+ end
97
+
36
98
  end
37
99
  end
38
100
 
101
+ Test::Unit::TestCase.prepend(CurbTestResourceCleanup)
102
+
39
103
  #
40
- # Simple test server to record number of times a request is sent/recieved of a specific
104
+ # Simple test server to record number of times a request is sent/recieved of a specific
41
105
  # request type, e.g. GET,POST,PUT,DELETE
42
106
  #
43
107
  class TestServlet < WEBrick::HTTPServlet::AbstractServlet
@@ -47,13 +111,12 @@ class TestServlet < WEBrick::HTTPServlet::AbstractServlet
47
111
  end
48
112
 
49
113
  def self.port
50
- (@port or 9129)
114
+ @port ||= 9129
51
115
  end
52
116
 
53
117
  def self.path
54
118
  '/methods'
55
119
  end
56
-
57
120
  def self.url
58
121
  "http://127.0.0.1:#{port}#{path}"
59
122
  end
@@ -65,13 +128,17 @@ class TestServlet < WEBrick::HTTPServlet::AbstractServlet
65
128
  end
66
129
 
67
130
  def do_GET(req,res)
68
- if req.path.match /redirect$/
131
+ if req.path.match(/redirect$/)
69
132
  res.status = 302
70
133
  res['Location'] = '/foo'
71
- elsif req.path.match /not_here$/
134
+ elsif req.path.match(/not_here$/)
72
135
  res.status = 404
73
- elsif req.path.match /error$/
136
+ elsif req.path.match(/error$/)
74
137
  res.status = 500
138
+ elsif req.path.match(/get_cookies$/)
139
+ res['Content-Type'] = "text/plain"
140
+ res.body = req['Cookie']
141
+ return
75
142
  end
76
143
  respond_with("GET#{req.query_string}",req,res)
77
144
  end
@@ -82,13 +149,27 @@ class TestServlet < WEBrick::HTTPServlet::AbstractServlet
82
149
  end
83
150
 
84
151
  def do_POST(req,res)
85
- if req.query['filename'].nil?
152
+ if req.path.match(/set_cookies$/)
153
+ JSON.parse(req.body || '[]', symbolize_names: true).each do |hash|
154
+ cookie = WEBrick::Cookie.new(hash.fetch(:name), hash.fetch(:value))
155
+ cookie.domain = hash[:domain] if hash.key?(:domain)
156
+ cookie.expires = hash[:expires] if hash.key?(:expires)
157
+ cookie.path = hash[:path] if hash.key?(:path)
158
+ cookie.secure = hash[:secure] if hash.key?(:secure)
159
+ cookie.max_age = hash[:max_age] if hash.key?(:max_age)
160
+ res.cookies.push(cookie)
161
+ end
162
+ respond_with('OK', req, res)
163
+ elsif req.query['filename'].nil?
86
164
  if req.body
87
165
  params = {}
88
166
  req.body.split('&').map{|s| k,v=s.split('='); params[k] = v }
89
167
  end
90
168
  if params and params['s'] == '500'
91
169
  res.status = 500
170
+ elsif params and params['c']
171
+ cookie = URI.decode_www_form_component(params['c']).split('=')
172
+ res.cookies << WEBrick::Cookie.new(*cookie)
92
173
  else
93
174
  respond_with("POST\n#{req.body}",req,res)
94
175
  end
@@ -124,50 +205,255 @@ class TestServlet < WEBrick::HTTPServlet::AbstractServlet
124
205
 
125
206
  end
126
207
 
208
+ module BugTestServerSetupTeardown
209
+ def unused_local_port
210
+ socket = TCPServer.new('127.0.0.1', 0)
211
+ socket.addr[1]
212
+ ensure
213
+ socket.close if socket
214
+ end
215
+
216
+ def setup
217
+ @port ||= 9992
218
+ @server = WEBrick::HTTPServer.new(:Port => @port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
219
+ @server.mount_proc("/test") do|req,res|
220
+ if @response_proc
221
+ @response_proc.call(res)
222
+ else
223
+ res.body = "hi"
224
+ res['Content-Type'] = "text/html"
225
+ end
226
+ end
227
+
228
+ @thread = Thread.new(@server) do|srv|
229
+ srv.start
230
+ end
231
+ end
232
+
233
+ def teardown
234
+ while @server.status != :Shutdown
235
+ @server.shutdown
236
+ end
237
+ @thread.join
238
+ end
239
+ end
240
+
127
241
  module TestServerMethods
242
+ def server_responding?(port)
243
+ socket = TCPSocket.new('127.0.0.1', port)
244
+ socket.close
245
+ true
246
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, IOError
247
+ false
248
+ end
249
+
250
+ def server_startup_timeout
251
+ ENV['RUBY_MEMCHECK_RUNNING'] ? 30 : 5
252
+ end
253
+
254
+ def wait_for_server_ready(port, thread: nil)
255
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + server_startup_timeout
256
+
257
+ loop do
258
+ if thread && !thread.alive?
259
+ return false
260
+ end
261
+
262
+ return true if server_responding?(port)
263
+
264
+ raise "Failed to startup test server on port #{port}" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
265
+
266
+ sleep 0.01
267
+ end
268
+ end
269
+
270
+ def server_shutdown_timeout
271
+ [server_startup_timeout, 0.25].max
272
+ end
273
+
274
+ def wait_for_server_stopped(port, thread: nil)
275
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + server_shutdown_timeout
276
+
277
+ loop do
278
+ return true unless server_responding?(port)
279
+
280
+ if thread && !thread.alive?
281
+ return !server_responding?(port)
282
+ end
283
+
284
+ return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
285
+
286
+ sleep 0.01
287
+ end
288
+ end
289
+
128
290
  def locked_file
129
291
  File.join(File.dirname(__FILE__),"server_lock-#{@__port}")
130
292
  end
131
293
 
294
+ def read_server_lock_pid
295
+ Integer(File.read(locked_file).strip, 10)
296
+ rescue Errno::ENOENT, ArgumentError, TypeError
297
+ nil
298
+ end
299
+
300
+ def write_server_lock(pid = Process.pid)
301
+ File.open(locked_file, 'w') { |f| f << "#{pid}\n" }
302
+ end
303
+
304
+ def process_alive?(pid)
305
+ return false unless pid && pid.positive?
306
+
307
+ Process.kill(0, pid)
308
+ true
309
+ rescue Errno::ESRCH
310
+ false
311
+ rescue Errno::EPERM
312
+ true
313
+ end
314
+
315
+ def server_lock_fresh?
316
+ (Time.now - File.mtime(locked_file)) < server_startup_timeout
317
+ rescue Errno::ENOENT
318
+ false
319
+ end
320
+
321
+ def stale_server_lock?(port)
322
+ return false unless File.exist?(locked_file)
323
+
324
+ pid = read_server_lock_pid
325
+ return false if pid && process_alive?(pid) && server_lock_fresh?
326
+ return false if server_responding?(port)
327
+
328
+ true
329
+ end
330
+
331
+ def clear_stale_server_lock(port)
332
+ return unless stale_server_lock?(port)
333
+
334
+ File.unlink(locked_file)
335
+ rescue Errno::ENOENT
336
+ nil
337
+ end
338
+
339
+ def stop_test_server
340
+ server = instance_variable_defined?(:@server) ? @server : nil
341
+ pid = instance_variable_defined?(:@__pid) ? @__pid : nil
342
+ return unless server || pid
343
+
344
+ if TEST_SINGLE_THREADED
345
+ @__pid = nil
346
+
347
+ if pid
348
+ begin
349
+ Process.kill('INT', pid)
350
+ rescue Errno::ESRCH
351
+ nil
352
+ end
353
+
354
+ begin
355
+ Process.wait(pid)
356
+ rescue Errno::ECHILD, Errno::ESRCH
357
+ nil
358
+ end
359
+ end
360
+ else
361
+ thread = instance_variable_defined?(:@test_thread) ? @test_thread : nil
362
+ port = instance_variable_defined?(:@__port) ? @__port : nil
363
+ @server = nil
364
+ @test_thread = nil
365
+
366
+ server.shutdown if server
367
+ wait_for_server_stopped(port, thread: thread) if port
368
+ thread.join(server_shutdown_timeout) if thread
369
+
370
+ if thread&.alive?
371
+ thread.kill
372
+ thread.join(server_shutdown_timeout)
373
+ wait_for_server_stopped(port) if port
374
+ end
375
+ end
376
+
377
+ File.unlink(locked_file) if File.exist?(locked_file)
378
+ rescue Errno::ENOENT
379
+ nil
380
+ end
381
+
382
+ def teardown
383
+ super
384
+ ensure
385
+ stop_test_server
386
+ end
387
+
132
388
  def server_setup(port=9129,servlet=TestServlet)
133
389
  @__port = port
134
- if @server.nil? and !File.exist?(locked_file)
390
+ @server = nil unless instance_variable_defined?(:@server)
391
+ @__pid = nil unless instance_variable_defined?(:@__pid)
392
+ @test_thread = nil unless instance_variable_defined?(:@test_thread)
393
+ clear_stale_server_lock(port)
394
+
395
+ if @server.nil? && File.exist?(locked_file)
396
+ begin
397
+ wait_for_server_ready(port)
398
+ return
399
+ rescue RuntimeError
400
+ clear_stale_server_lock(port)
401
+ end
402
+ end
135
403
 
136
- File.open(locked_file,'w') {|f| f << 'locked' }
404
+ if @server.nil? and !File.exist?(locked_file)
405
+ write_server_lock
137
406
  if TEST_SINGLE_THREADED
138
407
  rd, wr = IO.pipe
139
408
  @__pid = fork do
140
409
  rd.close
141
410
  rd = nil
142
411
 
143
- # start up a webrick server for testing delete
144
- server = WEBrick::HTTPServer.new :Port => port, :DocumentRoot => File.expand_path(File.dirname(__FILE__))
412
+ # start up a webrick server for testing delete
413
+ server = WEBrick::HTTPServer.new(:Port => port, :DocumentRoot => File.expand_path(File.dirname(__FILE__)), :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
145
414
 
146
415
  server.mount(servlet.path, servlet)
147
416
  server.mount("/ext", WEBrick::HTTPServlet::FileHandler, File.join(File.dirname(__FILE__),'..','ext'))
148
417
 
149
418
  trap("INT") { server.shutdown }
150
419
  GC.start
151
- wr.flush
152
- wr.close
153
- server.start
420
+ server_thread = Thread.new { server.start }
421
+
422
+ begin
423
+ if wait_for_server_ready(port, thread: server_thread)
424
+ wr.write('1')
425
+ else
426
+ wr.write('0')
427
+ end
428
+ rescue StandardError
429
+ wr.write('0')
430
+ ensure
431
+ wr.flush
432
+ wr.close
433
+ end
434
+
435
+ server_thread.join
154
436
  end
155
437
  wr.close
156
- rd.read
438
+ ready = rd.read
157
439
  rd.close
440
+ if ready != '1'
441
+ STDERR.puts "Failed to startup test server!"
442
+ exit(1)
443
+ end
158
444
  else
159
- # start up a webrick server for testing delete
160
- @server = WEBrick::HTTPServer.new :Port => port, :DocumentRoot => File.expand_path(File.dirname(__FILE__))
445
+ # start up a webrick server for testing delete
446
+ server = WEBrick::HTTPServer.new(:Port => port, :DocumentRoot => File.expand_path(File.dirname(__FILE__)), :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
161
447
 
162
- @server.mount(servlet.path, servlet)
163
- @server.mount("/ext", WEBrick::HTTPServlet::FileHandler, File.join(File.dirname(__FILE__),'..','ext'))
164
- queue = Queue.new # synchronize the thread startup to the main thread
448
+ server.mount(servlet.path, servlet)
449
+ server.mount("/ext", WEBrick::HTTPServlet::FileHandler, File.join(File.dirname(__FILE__),'..','ext'))
165
450
 
166
- @test_thread = Thread.new { queue << 1; @server.start }
451
+ @server = server
452
+ # Keep a stable reference inside the thread so helper shutdown can clear
453
+ # @server without racing the server startup path.
454
+ @test_thread = Thread.new(server) { |srv| srv.start }
167
455
 
168
- # wait for the queue
169
- value = queue.pop
170
- if !value
456
+ if !wait_for_server_ready(port, thread: @test_thread)
171
457
  STDERR.puts "Failed to startup test server!"
172
458
  exit(1)
173
459
  end
@@ -176,15 +462,7 @@ module TestServerMethods
176
462
 
177
463
  exit_code = lambda do
178
464
  begin
179
- if File.exist?(locked_file)
180
- File.unlink locked_file
181
- if TEST_SINGLE_THREADED
182
- Process.kill 'INT', @__pid
183
- else
184
- @server.shutdown unless @server.nil?
185
- end
186
- end
187
- #@server.shutdown unless @server.nil?
465
+ stop_test_server
188
466
  rescue Object => e
189
467
  puts "Error #{__FILE__}:#{__LINE__}\n#{e.message}"
190
468
  end
@@ -197,3 +475,81 @@ module TestServerMethods
197
475
  rescue Errno::EADDRINUSE
198
476
  end
199
477
  end
478
+
479
+
480
+
481
+ # Backport for Ruby 1.8
482
+ module Backports
483
+ module Ruby18
484
+ module URIFormEncoding
485
+ TBLENCWWWCOMP_ = {}
486
+ TBLDECWWWCOMP_ = {}
487
+
488
+ def encode_www_form_component(str)
489
+ if TBLENCWWWCOMP_.empty?
490
+ 256.times do |i|
491
+ TBLENCWWWCOMP_[i.chr] = '%%%02X' % i
492
+ end
493
+ TBLENCWWWCOMP_[' '] = '+'
494
+ TBLENCWWWCOMP_.freeze
495
+ end
496
+ str.to_s.gsub( /([^*\-.0-9A-Z_a-z])/ ) {|*| TBLENCWWWCOMP_[$1] }
497
+ end
498
+
499
+ def decode_www_form_component(str)
500
+ if TBLDECWWWCOMP_.empty?
501
+ 256.times do |i|
502
+ h, l = i>>4, i&15
503
+ TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
504
+ TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
505
+ TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
506
+ TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
507
+ end
508
+ TBLDECWWWCOMP_['+'] = ' '
509
+ TBLDECWWWCOMP_.freeze
510
+ end
511
+
512
+ raise ArgumentError, "invalid %-encoding (#{str.dump})" unless /\A(?:%[[:xdigit:]]{2}|[^%]+)*\z/ =~ str
513
+ str.gsub( /(\+|%[[:xdigit:]]{2})/ ) {|*| TBLDECWWWCOMP_[$1] }
514
+ end
515
+
516
+ def encode_www_form( enum )
517
+ enum.map do |k,v|
518
+ if v.nil?
519
+ encode_www_form_component(k)
520
+ elsif v.respond_to?(:to_ary)
521
+ v.to_ary.map do |w|
522
+ str = encode_www_form_component(k)
523
+ unless w.nil?
524
+ str << '='
525
+ str << encode_www_form_component(w)
526
+ end
527
+ end.join('&')
528
+ else
529
+ str = encode_www_form_component(k)
530
+ str << '='
531
+ str << encode_www_form_component(v)
532
+ end
533
+ end.join('&')
534
+ end
535
+
536
+ WFKV_ = '(?:%\h\h|[^%#=;&])'
537
+ def decode_www_form(str, _)
538
+ return [] if str.to_s == ''
539
+
540
+ unless /\A#{WFKV_}=#{WFKV_}(?:[;&]#{WFKV_}=#{WFKV_})*\z/ =~ str
541
+ raise ArgumentError, "invalid data of application/x-www-form-urlencoded (#{str})"
542
+ end
543
+ ary = []
544
+ $&.scan(/([^=;&]+)=([^;&]*)/) do
545
+ ary << [decode_www_form_component($1, enc), decode_www_form_component($2, enc)]
546
+ end
547
+ ary
548
+ end
549
+ end
550
+ end
551
+ end
552
+
553
+ unless URI.methods.include?(:encode_www_form)
554
+ URI.extend(Backports::Ruby18::URIFormEncoding)
555
+ end