codders-curb 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.
Files changed (45) hide show
  1. data/LICENSE +51 -0
  2. data/README +194 -0
  3. data/Rakefile +320 -0
  4. data/doc.rb +42 -0
  5. data/ext/curb.c +977 -0
  6. data/ext/curb.h +52 -0
  7. data/ext/curb_easy.c +3404 -0
  8. data/ext/curb_easy.h +90 -0
  9. data/ext/curb_errors.c +647 -0
  10. data/ext/curb_errors.h +129 -0
  11. data/ext/curb_macros.h +159 -0
  12. data/ext/curb_multi.c +633 -0
  13. data/ext/curb_multi.h +26 -0
  14. data/ext/curb_postfield.c +523 -0
  15. data/ext/curb_postfield.h +40 -0
  16. data/ext/curb_upload.c +80 -0
  17. data/ext/curb_upload.h +30 -0
  18. data/ext/extconf.rb +399 -0
  19. data/lib/curb.rb +4 -0
  20. data/lib/curl.rb +57 -0
  21. data/lib/curl/easy.rb +468 -0
  22. data/lib/curl/multi.rb +248 -0
  23. data/tests/alltests.rb +3 -0
  24. data/tests/bug_crash_on_debug.rb +39 -0
  25. data/tests/bug_crash_on_progress.rb +33 -0
  26. data/tests/bug_curb_easy_blocks_ruby_threads.rb +52 -0
  27. data/tests/bug_curb_easy_post_with_string_no_content_length_header.rb +83 -0
  28. data/tests/bug_instance_post_differs_from_class_post.rb +53 -0
  29. data/tests/bug_multi_segfault.rb +14 -0
  30. data/tests/bug_postfields_crash.rb +26 -0
  31. data/tests/bug_postfields_crash2.rb +57 -0
  32. data/tests/bug_require_last_or_segfault.rb +40 -0
  33. data/tests/bugtests.rb +9 -0
  34. data/tests/helper.rb +199 -0
  35. data/tests/mem_check.rb +65 -0
  36. data/tests/require_last_or_segfault_script.rb +36 -0
  37. data/tests/tc_curl_download.rb +75 -0
  38. data/tests/tc_curl_easy.rb +1011 -0
  39. data/tests/tc_curl_easy_setopt.rb +31 -0
  40. data/tests/tc_curl_multi.rb +485 -0
  41. data/tests/tc_curl_postfield.rb +143 -0
  42. data/tests/timeout.rb +100 -0
  43. data/tests/timeout_server.rb +33 -0
  44. data/tests/unittests.rb +2 -0
  45. metadata +133 -0
data/lib/curl/multi.rb ADDED
@@ -0,0 +1,248 @@
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
+ if urls_with_config.empty?
161
+ m.perform
162
+ else
163
+ until urls_with_config.empty?
164
+ m.perform do
165
+ consume_free_handles.call
166
+ end
167
+ consume_free_handles.call
168
+ end
169
+ free_handles = nil
170
+ end
171
+ end
172
+
173
+ # call-seq:
174
+ #
175
+ # Curl::Multi.download(['http://example.com/p/a/t/h/file1.txt','http://example.com/p/a/t/h/file2.txt']){|c|}
176
+ #
177
+ # will create 2 new files file1.txt and file2.txt
178
+ #
179
+ # 2 files will be opened, and remain open until the call completes
180
+ #
181
+ # when using the :post or :put method, urls should be a hash, including the individual post fields per post
182
+ #
183
+ def download(urls,easy_options={},multi_options={},download_paths=nil,&blk)
184
+ errors = []
185
+ procs = []
186
+ files = []
187
+ urls_with_config = []
188
+ url_to_download_paths = {}
189
+
190
+ urls.each_with_index do|urlcfg,i|
191
+ if urlcfg.is_a?(Hash)
192
+ url = url[:url]
193
+ else
194
+ url = urlcfg
195
+ end
196
+
197
+ if download_paths and download_paths[i]
198
+ download_path = download_paths[i]
199
+ else
200
+ download_path = File.basename(url)
201
+ end
202
+
203
+ file = lambda do|dp|
204
+ file = File.open(dp,"wb")
205
+ procs << (lambda {|data| file.write data; data.size })
206
+ files << file
207
+ file
208
+ end.call(download_path)
209
+
210
+ if urlcfg.is_a?(Hash)
211
+ urls_with_config << urlcfg.merge({:on_body => procs.last}.merge(easy_options))
212
+ else
213
+ urls_with_config << {:url => url, :on_body => procs.last, :method => :get}.merge(easy_options)
214
+ end
215
+ url_to_download_paths[url] = {:path => download_path, :file => file} # store for later
216
+ end
217
+
218
+ if blk
219
+ # when injecting the block, ensure file is closed before yielding
220
+ Curl::Multi.http(urls_with_config, multi_options) do |c,code,method|
221
+ info = url_to_download_paths[c.url]
222
+ begin
223
+ file = info[:file]
224
+ files.reject!{|f| f == file }
225
+ file.close
226
+ rescue => e
227
+ errors << e
228
+ end
229
+ blk.call(c,info[:path])
230
+ end
231
+ else
232
+ Curl::Multi.http(urls_with_config, multi_options)
233
+ end
234
+
235
+ ensure
236
+ files.each {|f|
237
+ begin
238
+ f.close
239
+ rescue => e
240
+ errors << e
241
+ end
242
+ }
243
+ raise errors unless errors.empty?
244
+ end
245
+
246
+ end
247
+ end
248
+ end
data/tests/alltests.rb ADDED
@@ -0,0 +1,3 @@
1
+ $: << $TESTDIR = File.expand_path(File.dirname(__FILE__))
2
+ require 'unittests'
3
+ Dir[File.join($TESTDIR, 'bug_*.rb')].each { |lib| require lib }
@@ -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
@@ -0,0 +1,52 @@
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 BugTestInstancePostDiffersFromClassPost < Test::Unit::TestCase
7
+ def test_bug
8
+ server = WEBrick::HTTPServer.new( :Port => 9999 )
9
+ server.mount_proc("/test") do|req,res|
10
+ sleep 0.5
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
+ threads = []
20
+ timer = Time.now
21
+
22
+ 5.times do |i|
23
+ t = Thread.new do
24
+ c = Curl::Easy.perform('http://localhost:9999/test')
25
+ c.header_str
26
+ end
27
+ threads << t
28
+ end
29
+
30
+ multi_responses = threads.collect do|t|
31
+ t.value
32
+ end
33
+
34
+ multi_time = (Time.now - timer)
35
+ puts "requested in #{multi_time}"
36
+
37
+ timer = Time.now
38
+ single_responses = []
39
+ 5.times do |i|
40
+ c = Curl::Easy.perform('http://localhost:9999/test')
41
+ single_responses << c.header_str
42
+ end
43
+
44
+ single_time = (Time.now - timer)
45
+ puts "requested in #{single_time}"
46
+
47
+ assert single_time > multi_time
48
+
49
+ server.shutdown
50
+ thread.join
51
+ end
52
+ end
@@ -0,0 +1,83 @@
1
+ =begin
2
+ From jwhitmire
3
+ Todd, I'm trying to use curb to post data to a REST url. We're using it to post support questions from our iphone app directly to tender. The post looks good to me, but curl is not adding the content-length header so I get a 411 length required response from the server.
4
+
5
+ Here's my post block, do you see anything obvious? Do I need to manually add the Content-Length header?
6
+
7
+ c = Curl::Easy.http_post(url) do |curl|
8
+ curl.headers["User-Agent"] = "Curl/Ruby"
9
+ if user
10
+ curl.headers["X-Multipass"] = user.multipass
11
+ else
12
+ curl.headers["X-Tender-Auth"] = TOKEN
13
+ end
14
+ curl.headers["Accept"] = "application/vnd.tender-v1+json"
15
+
16
+ curl.post_body = params.map{|f,k| "#{curl.escape(f)}=#{curl.escape(k)}"}.join('&')
17
+
18
+ curl.verbose = true
19
+ curl.follow_location = true
20
+ curl.enable_cookies = true
21
+ end
22
+ Any insight you care to share would be helpful. Thanks.
23
+ =end
24
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
25
+ require 'webrick'
26
+ class ::WEBrick::HTTPServer ; def access_log(config, req, res) ; end ; end
27
+ class ::WEBrick::BasicLog ; def log(level, data) ; end ; end
28
+
29
+ class BugCurbEasyPostWithStringNoContentLengthHeader < Test::Unit::TestCase
30
+ def test_bug_workaround
31
+ server = WEBrick::HTTPServer.new( :Port => 9999 )
32
+ server.mount_proc("/test") do|req,res|
33
+ assert_equal '15', req['Content-Length']
34
+ res.body = "hi"
35
+ res['Content-Type'] = "text/html"
36
+ end
37
+
38
+ thread = Thread.new(server) do|srv|
39
+ srv.start
40
+ end
41
+ params = {:cat => "hat", :foo => "bar"}
42
+
43
+ post_body = params.map{|f,k| "#{Curl::Easy.new.escape(f)}=#{Curl::Easy.new.escape(k)}"}.join('&')
44
+ c = Curl::Easy.http_post("http://127.0.0.1:9999/test",post_body) do |curl|
45
+ curl.headers["User-Agent"] = "Curl/Ruby"
46
+ curl.headers["X-Tender-Auth"] = "A Token"
47
+ curl.headers["Accept"] = "application/vnd.tender-v1+json"
48
+
49
+ curl.follow_location = true
50
+ curl.enable_cookies = true
51
+ end
52
+
53
+ server.shutdown
54
+ thread.join
55
+ end
56
+ def test_bug
57
+ server = WEBrick::HTTPServer.new( :Port => 9999 )
58
+ server.mount_proc("/test") do|req,res|
59
+ assert_equal '15', req['Content-Length']
60
+ res.body = "hi"
61
+ res['Content-Type'] = "text/html"
62
+ end
63
+
64
+ thread = Thread.new(server) do|srv|
65
+ srv.start
66
+ end
67
+ params = {:cat => "hat", :foo => "bar"}
68
+
69
+ c = Curl::Easy.http_post("http://127.0.0.1:9999/test") do |curl|
70
+ curl.headers["User-Agent"] = "Curl/Ruby"
71
+ curl.headers["X-Tender-Auth"] = "A Token"
72
+ curl.headers["Accept"] = "application/vnd.tender-v1+json"
73
+
74
+ curl.post_body = params.map{|f,k| "#{curl.escape(f)}=#{curl.escape(k)}"}.join('&')
75
+
76
+ curl.follow_location = true
77
+ curl.enable_cookies = true
78
+ end
79
+
80
+ server.shutdown
81
+ thread.join
82
+ end
83
+ end