curb 0.7.15 → 0.8.0

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.
data/lib/curl/multi.rb ADDED
@@ -0,0 +1,244 @@
1
+ module Curl
2
+
3
+ class Multi
4
+ class << self
5
+ # call-seq:
6
+ # Curl::Multi.get('url1','url2','url3','url4','url5', :follow_location => true) do|easy|
7
+ # easy
8
+ # end
9
+ #
10
+ # Blocking call to fetch multiple url's in parallel.
11
+ def get(urls, easy_options={}, multi_options={}, &blk)
12
+ url_confs = []
13
+ urls.each do|url|
14
+ url_confs << {:url => url, :method => :get}.merge(easy_options)
15
+ end
16
+ self.http(url_confs, multi_options) {|c,code,method| blk.call(c) if blk }
17
+ end
18
+
19
+ # call-seq:
20
+ #
21
+ # Curl::Multi.post([{:url => 'url1', :post_fields => {'field1' => 'value1', 'field2' => 'value2'}},
22
+ # {:url => 'url2', :post_fields => {'field1' => 'value1', 'field2' => 'value2'}},
23
+ # {:url => 'url3', :post_fields => {'field1' => 'value1', 'field2' => 'value2'}}],
24
+ # { :follow_location => true, :multipart_form_post => true },
25
+ # {:pipeline => true }) do|easy|
26
+ # easy_handle_on_request_complete
27
+ # end
28
+ #
29
+ # Blocking call to POST multiple form's in parallel.
30
+ #
31
+ # urls_with_config: is a hash of url's pointing to the postfields to send
32
+ # easy_options: are a set of common options to set on all easy handles
33
+ # multi_options: options to set on the Curl::Multi handle
34
+ #
35
+ def post(urls_with_config, easy_options={}, multi_options={}, &blk)
36
+ url_confs = []
37
+ urls_with_config.each do|uconf|
38
+ url_confs << uconf.merge(:method => :post).merge(easy_options)
39
+ end
40
+ self.http(url_confs, multi_options) {|c,code,method| blk.call(c) }
41
+ end
42
+
43
+ # call-seq:
44
+ #
45
+ # Curl::Multi.put([{:url => 'url1', :put_data => "some message"},
46
+ # {:url => 'url2', :put_data => IO.read('filepath')},
47
+ # {:url => 'url3', :put_data => "maybe another string or socket?"],
48
+ # {:follow_location => true},
49
+ # {:pipeline => true }) do|easy|
50
+ # easy_handle_on_request_complete
51
+ # end
52
+ #
53
+ # Blocking call to POST multiple form's in parallel.
54
+ #
55
+ # urls_with_config: is a hash of url's pointing to the postfields to send
56
+ # easy_options: are a set of common options to set on all easy handles
57
+ # multi_options: options to set on the Curl::Multi handle
58
+ #
59
+ def put(urls_with_config, easy_options={}, multi_options={}, &blk)
60
+ url_confs = []
61
+ urls_with_config.each do|uconf|
62
+ url_confs << uconf.merge(:method => :put).merge(easy_options)
63
+ end
64
+ self.http(url_confs, multi_options) {|c,code,method| blk.call(c) }
65
+ end
66
+
67
+
68
+ # call-seq:
69
+ #
70
+ # Curl::Multi.http( [
71
+ # { :url => 'url1', :method => :post,
72
+ # :post_fields => {'field1' => 'value1', 'field2' => 'value2'} },
73
+ # { :url => 'url2', :method => :get,
74
+ # :follow_location => true, :max_redirects => 3 },
75
+ # { :url => 'url3', :method => :put, :put_data => File.open('file.txt','rb') },
76
+ # { :url => 'url4', :method => :head }
77
+ # ], {:pipeline => true})
78
+ #
79
+ # Blocking call to issue multiple HTTP requests with varying verb's.
80
+ #
81
+ # urls_with_config: is a hash of url's pointing to the easy handle options as well as the special option :method, that can by one of [:get, :post, :put, :delete, :head], when no verb is provided e.g. :method => nil -> GET is used
82
+ # multi_options: options for the multi handle
83
+ # blk: a callback, that yeilds when a handle is completed
84
+ #
85
+ def http(urls_with_config, multi_options={}, &blk)
86
+ m = Curl::Multi.new
87
+
88
+ # maintain a sane number of easy handles
89
+ multi_options[:max_connects] = max_connects = multi_options.key?(:max_connects) ? multi_options[:max_connects] : 10
90
+
91
+ free_handles = [] # keep a list of free easy handles
92
+
93
+ # configure the multi handle
94
+ multi_options.each { |k,v| m.send("#{k}=", v) }
95
+ callbacks = [:on_progress,:on_debug,:on_failure,:on_success,:on_body,:on_header]
96
+
97
+ add_free_handle = proc do|conf, easy|
98
+ c = conf.dup # avoid being destructive to input
99
+ url = c.delete(:url)
100
+ method = c.delete(:method)
101
+ headers = c.delete(:headers)
102
+
103
+ easy = Curl::Easy.new if easy.nil?
104
+
105
+ easy.url = url
106
+
107
+ # assign callbacks
108
+ callbacks.each do |cb|
109
+ cbproc = c.delete(cb)
110
+ easy.send(cb,&cbproc) if cbproc
111
+ end
112
+
113
+ case method
114
+ when :post
115
+ fields = c.delete(:post_fields)
116
+ # set the post post using the url fields
117
+ easy.post_body = fields.map{|f,k| "#{easy.escape(f)}=#{easy.escape(k)}"}.join('&')
118
+ when :put
119
+ easy.put_data = c.delete(:put_data)
120
+ when :head
121
+ easy.head = true
122
+ when :delete
123
+ easy.delete = true
124
+ when :get
125
+ else
126
+ # XXX: nil is treated like a GET
127
+ end
128
+
129
+ # headers is a special key
130
+ headers.each {|k,v| easy.headers[k] = v } if headers
131
+
132
+ #
133
+ # use the remaining options as specific configuration to the easy handle
134
+ # bad options should raise an undefined method error
135
+ #
136
+ c.each { |k,v| easy.send("#{k}=",v) }
137
+
138
+ easy.on_complete {|curl,code|
139
+ free_handles << curl
140
+ blk.call(curl,code,method) if blk
141
+ }
142
+ m.add(easy)
143
+ end
144
+
145
+ max_connects.times do
146
+ conf = urls_with_config.pop
147
+ add_free_handle.call conf, nil
148
+ break if urls_with_config.empty?
149
+ end
150
+
151
+ consume_free_handles = proc do
152
+ # as we idle consume free handles
153
+ if urls_with_config.size > 0 && free_handles.size > 0
154
+ easy = free_handles.pop
155
+ conf = urls_with_config.pop
156
+ add_free_handle.call conf, easy
157
+ end
158
+ end
159
+
160
+ until urls_with_config.empty?
161
+ m.perform do
162
+ consume_free_handles.call
163
+ end
164
+ consume_free_handles.call
165
+ end
166
+ free_handles = nil
167
+ end
168
+
169
+ # call-seq:
170
+ #
171
+ # Curl::Multi.download(['http://example.com/p/a/t/h/file1.txt','http://example.com/p/a/t/h/file2.txt']){|c|}
172
+ #
173
+ # will create 2 new files file1.txt and file2.txt
174
+ #
175
+ # 2 files will be opened, and remain open until the call completes
176
+ #
177
+ # when using the :post or :put method, urls should be a hash, including the individual post fields per post
178
+ #
179
+ def download(urls,easy_options={},multi_options={},download_paths=nil,&blk)
180
+ errors = []
181
+ procs = []
182
+ files = []
183
+ urls_with_config = []
184
+ url_to_download_paths = {}
185
+
186
+ urls.each_with_index do|urlcfg,i|
187
+ if urlcfg.is_a?(Hash)
188
+ url = url[:url]
189
+ else
190
+ url = urlcfg
191
+ end
192
+
193
+ if download_paths and download_paths[i]
194
+ download_path = download_paths[i]
195
+ else
196
+ download_path = File.basename(url)
197
+ end
198
+
199
+ file = lambda do|dp|
200
+ file = File.open(dp,"wb")
201
+ procs << (lambda {|data| file.write data; data.size })
202
+ files << file
203
+ file
204
+ end.call(download_path)
205
+
206
+ if urlcfg.is_a?(Hash)
207
+ urls_with_config << urlcfg.merge({:on_body => procs.last}.merge(easy_options))
208
+ else
209
+ urls_with_config << {:url => url, :on_body => procs.last, :method => :get}.merge(easy_options)
210
+ end
211
+ url_to_download_paths[url] = {:path => download_path, :file => file} # store for later
212
+ end
213
+
214
+ if blk
215
+ # when injecting the block, ensure file is closed before yielding
216
+ Curl::Multi.http(urls_with_config, multi_options) do |c,code,method|
217
+ info = url_to_download_paths[c.url]
218
+ begin
219
+ file = info[:file]
220
+ files.reject!{|f| f == file }
221
+ file.close
222
+ rescue => e
223
+ errors << e
224
+ end
225
+ blk.call(c,info[:path])
226
+ end
227
+ else
228
+ Curl::Multi.http(urls_with_config, multi_options)
229
+ end
230
+
231
+ ensure
232
+ files.each {|f|
233
+ begin
234
+ f.close
235
+ rescue => e
236
+ errors << e
237
+ end
238
+ }
239
+ raise errors unless errors.empty?
240
+ end
241
+
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+
3
+ require 'webrick'
4
+ class ::WEBrick::HTTPServer ; def access_log(config, req, res) ; end ; end
5
+ class ::WEBrick::BasicLog ; def log(level, data) ; end ; end
6
+
7
+ require 'curl'
8
+
9
+ class BugCrashOnDebug < Test::Unit::TestCase
10
+
11
+ def test_on_debug
12
+ server = WEBrick::HTTPServer.new( :Port => 9999 )
13
+ server.mount_proc("/test") do|req,res|
14
+ res.body = "hi"
15
+ res['Content-Type'] = "text/html"
16
+ end
17
+ puts 'a'
18
+ thread = Thread.new(server) do|srv|
19
+ srv.start
20
+ end
21
+ puts 'b'
22
+ c = Curl::Easy.new('http://localhost:9999/test')
23
+ c.on_debug do|x|
24
+ puts x.inspect
25
+ raise "error" # this will get swallowed
26
+ end
27
+ c.perform
28
+ puts 'c'
29
+ ensure
30
+ puts 'd'
31
+ server.shutdown
32
+ puts 'e'
33
+ puts thread.exit
34
+ puts 'f'
35
+ end
36
+
37
+ end
38
+
39
+ #test_on_debug
@@ -0,0 +1,33 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+ require 'webrick'
3
+ class ::WEBrick::HTTPServer ; def access_log(config, req, res) ; end ; end
4
+ class ::WEBrick::BasicLog ; def log(level, data) ; end ; end
5
+
6
+ class BugCrashOnDebug < Test::Unit::TestCase
7
+
8
+ def test_on_debug
9
+ server = WEBrick::HTTPServer.new( :Port => 9999 )
10
+ server.mount_proc("/test") do|req,res|
11
+ res.body = "hi"
12
+ res['Content-Type'] = "text/html"
13
+ end
14
+
15
+ thread = Thread.new(server) do|srv|
16
+ srv.start
17
+ end
18
+
19
+ c = Curl::Easy.new('http://localhost:9999/test')
20
+ c.on_progress do|x|
21
+ raise "error"
22
+ end
23
+ c.perform
24
+
25
+ assert false, "should not reach this point"
26
+
27
+ rescue => e
28
+ assert_equal 'Curl::Err::AbortedByCallbackError', e.class.to_s
29
+ c.close
30
+ ensure
31
+ server.shutdown
32
+ end
33
+ end
data/tests/helper.rb CHANGED
@@ -65,6 +65,14 @@ class TestServlet < WEBrick::HTTPServlet::AbstractServlet
65
65
  end
66
66
 
67
67
  def do_GET(req,res)
68
+ if req.path.match /redirect$/
69
+ res.status = 302
70
+ res['Location'] = '/foo'
71
+ elsif req.path.match /not_here$/
72
+ res.status = 404
73
+ elsif req.path.match /error$/
74
+ res.status = 500
75
+ end
68
76
  respond_with(:GET,req,res)
69
77
  end
70
78
 
@@ -8,7 +8,7 @@ class TestCurbCurlDownload < Test::Unit::TestCase
8
8
  end
9
9
 
10
10
  def test_download_url_to_file_via_string
11
- dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
11
+ dl_url = "http://localhost:9129/ext/curb_easy.c"
12
12
  dl_path = File.join(Dir::tmpdir, "dl_url_test.file")
13
13
 
14
14
  curb = Curl::Easy.download(dl_url, dl_path)
@@ -19,7 +19,7 @@ class TestCurbCurlDownload < Test::Unit::TestCase
19
19
  end
20
20
 
21
21
  def test_download_url_to_file_via_file_io
22
- dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
22
+ dl_url = "http://localhost:9129/ext/curb_easy.c"
23
23
  dl_path = File.join(Dir::tmpdir, "dl_url_test.file")
24
24
  io = File.open(dl_path, 'wb')
25
25
 
@@ -32,7 +32,7 @@ class TestCurbCurlDownload < Test::Unit::TestCase
32
32
  end
33
33
 
34
34
  def test_download_url_to_file_via_io
35
- dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
35
+ dl_url = "http://localhost:9129/ext/curb_easy.c"
36
36
  dl_path = File.join(Dir::tmpdir, "dl_url_test.file")
37
37
  reader, writer = IO.pipe
38
38
 
@@ -62,7 +62,7 @@ class TestCurbCurlDownload < Test::Unit::TestCase
62
62
  end
63
63
 
64
64
  def test_download_bad_url_gives_404
65
- dl_url = "http://127.0.0.1:9129/this_file_does_not_exist.html"
65
+ dl_url = "http://localhost:9129/this_file_does_not_exist.html"
66
66
  dl_path = File.join(Dir::tmpdir, "dl_url_test.file")
67
67
 
68
68
  curb = Curl::Easy.download(dl_url, dl_path)
@@ -461,7 +461,8 @@ class TestCurbCurlEasy < Test::Unit::TestCase
461
461
  def test_ssl_verify_host
462
462
  c = Curl::Easy.new
463
463
  assert c.ssl_verify_host?
464
- assert !c.ssl_verify_host = false
464
+ c.ssl_verify_host = 0
465
+ c.ssl_verify_host = false
465
466
  assert !c.ssl_verify_host?
466
467
  end
467
468
 
@@ -507,6 +508,17 @@ class TestCurbCurlEasy < Test::Unit::TestCase
507
508
  assert c.ignore_content_length?
508
509
  end
509
510
 
511
+ def test_resolve_mode
512
+ c = Curl::Easy.new
513
+ assert_equal :auto, c.resolve_mode
514
+ c.resolve_mode = :ipv4
515
+ assert_equal :ipv4, c.resolve_mode
516
+ c.resolve_mode = :ipv6
517
+ assert_equal :ipv6, c.resolve_mode
518
+
519
+ assert_raises(ArgumentError) { c.resolve_mode = :bad }
520
+ end
521
+
510
522
  def test_enable_cookies
511
523
  c = Curl::Easy.new
512
524
  assert !c.enable_cookies?
@@ -549,13 +561,34 @@ class TestCurbCurlEasy < Test::Unit::TestCase
549
561
  end
550
562
 
551
563
  def test_on_success_with_on_failure
552
- curl = Curl::Easy.new("#{$TEST_URL.gsub(/file:\/\//,'')}/not_here")
564
+ curl = Curl::Easy.new(TestServlet.url + '/error')
553
565
  on_failure_called = false
554
566
  curl.on_success {|c| } # make sure we get the failure call even though this handler is defined
555
567
  curl.on_failure {|c,code| on_failure_called = true }
556
568
  curl.perform
569
+ assert_equal 500, curl.response_code
557
570
  assert on_failure_called, "Failure handler not called"
558
571
  end
572
+
573
+ def test_on_success_with_on_missing
574
+ curl = Curl::Easy.new(TestServlet.url + '/not_here')
575
+ on_missing_called = false
576
+ curl.on_success {|c| } # make sure we get the missing call even though this handler is defined
577
+ curl.on_missing {|c,code| on_missing_called = true }
578
+ curl.perform
579
+ assert_equal 404, curl.response_code
580
+ assert on_missing_called, "Missing handler not called"
581
+ end
582
+
583
+ def test_on_success_with_on_redirect
584
+ curl = Curl::Easy.new(TestServlet.url + '/redirect')
585
+ on_redirect_called = false
586
+ curl.on_success {|c| } # make sure we get the redirect call even though this handler is defined
587
+ curl.on_redirect {|c,code| on_redirect_called = true }
588
+ curl.perform
589
+ assert_equal 302, curl.response_code
590
+ assert on_redirect_called, "Redirect handler not called"
591
+ end
559
592
 
560
593
  def test_get_remote
561
594
  curl = Curl::Easy.new(TestServlet.url)
@@ -572,15 +605,35 @@ class TestCurbCurlEasy < Test::Unit::TestCase
572
605
  def test_post_remote_is_easy_handle
573
606
  # see: http://pastie.org/560852 and
574
607
  # http://groups.google.com/group/curb---ruby-libcurl-bindings/browse_thread/thread/216bb2d9b037f347?hl=en
575
- [:post, :get,:head,:delete].each do |method|
576
- count = 0
577
- curl = Curl::Easy.send("http_#{method}", TestServlet.url) do|c|
578
- count += 1
579
- assert_equal Curl::Easy, c.class
608
+ [:post, :get, :head, :delete].each do |method|
609
+ retries = 0
610
+ begin
611
+ count = 0
612
+ curl = Curl::Easy.send("http_#{method}", TestServlet.url) do|c|
613
+ count += 1
614
+ assert_equal Curl::Easy, c.class
615
+ end
616
+ assert_equal 1, count, "For request method: #{method.to_s.upcase}"
617
+ rescue Curl::Err::HostResolutionError => e # travis-ci.org fails to resolve... try again?
618
+ retries+=1
619
+ retry if retries < 3
620
+ raise e
580
621
  end
581
- assert_equal 1, count, "For request method: #{method.to_s.upcase}"
582
622
  end
583
623
  end
624
+
625
+ # see: https://github.com/rvanlieshout/curb/commit/8bcdefddc0162484681ebd1a92d52a642666a445
626
+ def test_post_multipart_array_remote
627
+ curl = Curl::Easy.new(TestServlet.url)
628
+ curl.multipart_form_post = true
629
+ fields = [
630
+ Curl::PostField.file('foo', File.expand_path(File.join(File.dirname(__FILE__),'..','README'))),
631
+ Curl::PostField.file('bar', File.expand_path(File.join(File.dirname(__FILE__),'..','README')))
632
+ ]
633
+ curl.http_post(fields)
634
+ assert_match /HTTP POST file upload/, curl.body_str
635
+ assert_match /Content-Disposition: form-data/, curl.body_str
636
+ end
584
637
 
585
638
  def test_post_with_body_remote
586
639
  curl = Curl::Easy.new(TestServlet.url)
@@ -664,6 +717,17 @@ class TestCurbCurlEasy < Test::Unit::TestCase
664
717
  assert_match /message$/, curl.body_str
665
718
  end
666
719
 
720
+ # https://github.com/taf2/curb/issues/101
721
+ def test_put_data_null_bytes
722
+ curl = Curl::Easy.new(TestServlet.url)
723
+ curl.put_data = "a\0b"
724
+
725
+ curl.perform
726
+
727
+ assert_match /^PUT/, curl.body_str
728
+ assert_match "a\0b", curl.body_str
729
+ end
730
+
667
731
  def test_put_nil_data_no_crash
668
732
  curl = Curl::Easy.new(TestServlet.url)
669
733
  curl.put_data = nil
@@ -673,7 +737,7 @@ class TestCurbCurlEasy < Test::Unit::TestCase
673
737
 
674
738
  def test_put_remote_file
675
739
  curl = Curl::Easy.new(TestServlet.url)
676
- File.open(__FILE__,'r') do|f|
740
+ File.open(__FILE__,'rb') do|f|
677
741
  assert curl.http_put(f)
678
742
  end
679
743
  assert_equal "PUT\n#{File.read(__FILE__)}", curl.body_str
@@ -700,7 +764,9 @@ class TestCurbCurlEasy < Test::Unit::TestCase
700
764
 
701
765
  def test_cert_with_password
702
766
  curl = Curl::Easy.new(TestServlet.url)
703
- curl.cert= File.join(File.dirname(__FILE__),"cert.pem:password")
767
+ path = File.join(File.dirname(__FILE__),"cert.pem")
768
+ curl.certpassword = 'password'
769
+ curl.cert = path
704
770
  assert_match /cert.pem$/,curl.cert
705
771
  end
706
772
 
@@ -888,6 +954,40 @@ class TestCurbCurlEasy < Test::Unit::TestCase
888
954
 
889
955
  end
890
956
 
957
+ def test_get_set_multi_on_easy
958
+ easy = Curl::Easy.new
959
+ assert_nil easy.multi
960
+ multi = Curl::Multi.new
961
+ easy.multi = multi
962
+ assert_not_nil easy.multi
963
+ assert_equal multi, easy.multi
964
+ end
965
+
966
+ def test_raise_on_progress
967
+ c = Curl::Easy.new($TEST_URL)
968
+ c.on_progress {|w,x,y,z| raise "error" }
969
+ c.perform
970
+ rescue => e
971
+ assert_equal 'Curl::Err::AbortedByCallbackError', e.class.to_s
972
+ c.close
973
+ end
974
+
975
+ def test_raise_on_success
976
+ c = Curl::Easy.new($TEST_URL)
977
+ c.on_success {|x| raise "error" }
978
+ c.perform
979
+ rescue => e
980
+ assert_equal 'Curl::Err::AbortedByCallbackError', e.class.to_s
981
+ c.close
982
+ end
983
+
984
+ def test_raise_on_debug
985
+ c = Curl::Easy.new($TEST_URL)
986
+ c.on_debug { raise "error" }
987
+ c.perform
988
+ assert true, "raise in on debug has no effect"
989
+ end
990
+
891
991
  include TestServerMethods
892
992
 
893
993
  def setup
@@ -0,0 +1,31 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+
3
+ class TestCurbCurlEasySetOpt < Test::Unit::TestCase
4
+ def setup
5
+ @easy = Curl::Easy.new
6
+ end
7
+
8
+ def test_opt_verbose
9
+ @easy.set :verbose, true
10
+ assert @easy.verbose?
11
+ end
12
+
13
+ def test_opt_header
14
+ @easy.set :header, true
15
+ end
16
+
17
+ def test_opt_noprogress
18
+ @easy.set :noprogress, true
19
+ end
20
+
21
+ def test_opt_nosignal
22
+ @easy.set :nosignal, true
23
+ end
24
+
25
+ def test_opt_url
26
+ url = "http://google.com/"
27
+ @easy.set :url, url
28
+ assert_equal url, @easy.url
29
+ end
30
+
31
+ end