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.
- data/LICENSE +51 -0
- data/README +194 -0
- data/Rakefile +320 -0
- data/doc.rb +42 -0
- data/ext/curb.c +977 -0
- data/ext/curb.h +52 -0
- data/ext/curb_easy.c +3404 -0
- data/ext/curb_easy.h +90 -0
- data/ext/curb_errors.c +647 -0
- data/ext/curb_errors.h +129 -0
- data/ext/curb_macros.h +159 -0
- data/ext/curb_multi.c +633 -0
- data/ext/curb_multi.h +26 -0
- data/ext/curb_postfield.c +523 -0
- data/ext/curb_postfield.h +40 -0
- data/ext/curb_upload.c +80 -0
- data/ext/curb_upload.h +30 -0
- data/ext/extconf.rb +399 -0
- data/lib/curb.rb +4 -0
- data/lib/curl.rb +57 -0
- data/lib/curl/easy.rb +468 -0
- data/lib/curl/multi.rb +248 -0
- data/tests/alltests.rb +3 -0
- data/tests/bug_crash_on_debug.rb +39 -0
- data/tests/bug_crash_on_progress.rb +33 -0
- data/tests/bug_curb_easy_blocks_ruby_threads.rb +52 -0
- data/tests/bug_curb_easy_post_with_string_no_content_length_header.rb +83 -0
- data/tests/bug_instance_post_differs_from_class_post.rb +53 -0
- data/tests/bug_multi_segfault.rb +14 -0
- data/tests/bug_postfields_crash.rb +26 -0
- data/tests/bug_postfields_crash2.rb +57 -0
- data/tests/bug_require_last_or_segfault.rb +40 -0
- data/tests/bugtests.rb +9 -0
- data/tests/helper.rb +199 -0
- data/tests/mem_check.rb +65 -0
- data/tests/require_last_or_segfault_script.rb +36 -0
- data/tests/tc_curl_download.rb +75 -0
- data/tests/tc_curl_easy.rb +1011 -0
- data/tests/tc_curl_easy_setopt.rb +31 -0
- data/tests/tc_curl_multi.rb +485 -0
- data/tests/tc_curl_postfield.rb +143 -0
- data/tests/timeout.rb +100 -0
- data/tests/timeout_server.rb +33 -0
- data/tests/unittests.rb +2 -0
- 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,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
|