tap-http 0.2.1 → 0.3.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.
@@ -1,411 +0,0 @@
1
- require 'tap/http/utils'
2
- require 'net/http'
3
- require 'thread'
4
-
5
- module Tap
6
- module Http
7
-
8
- # :startdoc::manifest submits an http request
9
- #
10
- # Dispatch is a base class for submitting HTTP requests from a request
11
- # hash. Multiple requests may be submitted on individual threads, up
12
- # to a configurable limit.
13
- #
14
- # Request hashes are like the following:
15
- #
16
- # request_method: GET
17
- # url: http://tap.rubyforge.org/
18
- # headers: {}
19
- # params: {}
20
- # version: 1.1
21
- #
22
- # Missing fields are added from the task configuration. Note that since
23
- # Dispatch takes hash inputs, it is often convenient to save requests in
24
- # a .yml file and sequence dispatch with load:
25
- #
26
- # [requests.yml]
27
- # - url: http://tap.rubyforge.org/
28
- # - url: http://tap.rubyforge.org/about.html
29
- #
30
- # % rap load requests.yml --:i dispatch --+ dump
31
- #
32
- # :startdoc::manifest-end
33
- # === Dispatch Methods
34
- #
35
- # Dispatch itself provides methods for constructing and submitting get and
36
- # post HTTP requests from a request hash.
37
- #
38
- # res = Tap::Http::Dispatch.submit(
39
- # :url => "http://tap.rubyforge.org",
40
- # :version => '1.1',
41
- # :request_method => 'GET',
42
- # :headers => {},
43
- # :params => {}
44
- # )
45
- # res.inspect # => "#<Net::HTTPOK 200 OK readbody=true>"
46
- # res.body =~ /Tap/ # => true
47
- #
48
- # Headers and parameters take the form:
49
- #
50
- # {
51
- # 'single' => 'value',
52
- # 'multiple' => ['value one', 'value two']
53
- # }
54
- #
55
- # To capture request hashes from web forms using Firefox, see the README.
56
- class Dispatch < Tap::Task
57
- class << self
58
- def intern(*args, &block)
59
- instance = new(*args)
60
- instance.extend Support::Intern(:process_response)
61
- instance.process_response_block = block
62
- instance
63
- end
64
-
65
- # Constructs and submits an http request to the url using the request hash.
66
- # Request hashes are like this:
67
- #
68
- # {
69
- # :url => "http://tap.rubyforge.org",
70
- # :version => '1.1',
71
- # :request_method => 'GET',
72
- # :headers => {},
73
- # :params => {}
74
- # }
75
- #
76
- # If left unspecified, the default configuration values will be used (but
77
- # note that since the default url is nil, a url MUST be specified).
78
- # Headers and parameters can use array values to specifiy multiple values
79
- # for the same key.
80
- #
81
- # Submit only support get and post request methods; see construct_get and
82
- # construct_post for more details. A block may be given to receive the
83
- # Net::HTTP and request just prior to submission.
84
- #
85
- # Returns the Net::HTTP response.
86
- #
87
- def submit(request_hash)
88
- url_or_uri = request_hash[:url] || configurations[:url].default
89
- headers = request_hash[:headers] || configurations[:headers].default
90
- params = request_hash[:params] || configurations[:params].default
91
- request_method = request_hash[:request_method] || configurations[:request_method].default
92
- version = request_hash[:version] || configurations[:version].default
93
-
94
- raise ArgumentError, "no url specified" unless url_or_uri
95
- uri = url_or_uri.kind_of?(URI) ? url_or_uri : URI.parse(url_or_uri)
96
- uri.path = "/" if uri.path.empty?
97
-
98
- # construct the request based on the method
99
- request = case request_method.to_s
100
- when /^get$/i then construct_get(uri, headers, params)
101
- when /^post$/i then construct_post(uri, headers, params)
102
- else
103
- raise ArgumentError, "unsupported request method: #{request_method}"
104
- end
105
-
106
- # set the http version
107
- version_method = "version_#{version.to_s.gsub(".", "_")}".to_sym
108
- if ::Net::HTTP.respond_to?(version_method)
109
- ::Net::HTTP.send(version_method)
110
- else
111
- raise ArgumentError, "unsupported HTTP version: #{version}"
112
- end
113
-
114
- # submit the request
115
- res = ::Net::HTTP.new(uri.host, uri.port).start do |http|
116
- yield(http, request) if block_given?
117
- http.request(request)
118
- end
119
-
120
- # fetch redirections
121
- redirection_limit = request_hash[:redirection_limit]
122
- redirection_limit ? fetch_redirection(res, redirection_limit) : res
123
- end
124
-
125
- # Constructs a Net::HTTP::Post query, setting headers and parameters.
126
- #
127
- # ==== Supported Content Types:
128
- #
129
- # - application/x-www-form-urlencoded (the default)
130
- # - multipart/form-data
131
- #
132
- # The multipart/form-data content type may specify a boundary. If no
133
- # boundary is specified, a randomly generated boundary will be used
134
- # to delimit the parameters.
135
- #
136
- # post = construct_post(
137
- # URI.parse('http://some.url/'),
138
- # {:content_type => 'multipart/form-data; boundary=1234'},
139
- # {:key => 'value'})
140
- #
141
- # post.body
142
- # # => %Q{--1234\r
143
- # # Content-Disposition: form-data; name="key"\r
144
- # # \r
145
- # # value\r
146
- # # --1234--\r
147
- # # }
148
- #
149
- # (Note the carriage returns are required in multipart content)
150
- #
151
- # The content-length header is determined automatically from the
152
- # formatted request body; manually specified content-length headers
153
- # will be overridden.
154
- #
155
- def construct_post(uri, headers, params)
156
- req = ::Net::HTTP::Post.new( URI.encode("#{uri.path}#{format_query(uri)}") )
157
- headers = headerize_keys(headers)
158
- content_type = headers['Content-Type']
159
-
160
- case content_type
161
- when nil, /^application\/x-www-form-urlencoded$/i
162
- req.body = format_www_form_urlencoded(params)
163
- headers['Content-Type'] ||= "application/x-www-form-urlencoded"
164
- headers['Content-Length'] = req.body.length
165
-
166
- when /^multipart\/form-data(;\s*boundary=(.*))?$/i
167
- # extract the boundary if it exists
168
- boundary = $2 || rand.to_s[2..20]
169
-
170
- req.body = format_multipart_form_data(params, boundary)
171
- headers['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
172
- headers['Content-Length'] = req.body.length
173
-
174
- else
175
- raise ArgumentError, "unsupported Content-Type for POST: #{content_type}"
176
- end
177
-
178
- headers.each_pair { |key, value| req[key] = value }
179
- req
180
- end
181
-
182
- # Constructs a Net::HTTP::Get query. All parameters in uri and params are
183
- # encoded and added to the request URI.
184
- #
185
- # get = construct_get(URI.parse('http://some.url/path'), {}, {:key => 'value'})
186
- # get.path # => "/path?key=value"
187
- #
188
- def construct_get(uri, headers, params)
189
- req = ::Net::HTTP::Get.new( URI.encode("#{uri.path}#{format_query(uri, params)}") )
190
- headerize_keys(headers).each_pair { |key, value| req[key] = value }
191
- req
192
- end
193
-
194
- # Checks the type of the response; if it is a redirection, fetches the
195
- # redirection. Otherwise return the response.
196
- #
197
- # Notes:
198
- # - Fetch will recurse up to the input redirection limit (default 10)
199
- # - Responses that are not Net::HTTPRedirection or Net::HTTPSuccess
200
- # raise an error.
201
- def fetch_redirection(res, limit=10)
202
- raise 'exceeded the redirection limit' if limit < 1
203
-
204
- case res
205
- when ::Net::HTTPRedirection
206
- redirect = ::Net::HTTP.get_response( URI.parse(res['location']) )
207
- fetch_redirection(redirect, limit - 1)
208
- when ::Net::HTTPSuccess
209
- res
210
- else
211
- raise StandardError, res.error!
212
- end
213
- end
214
-
215
- # Constructs a URI query string from the uri and the input parameters.
216
- # Multiple values for a parameter may be specified using an array.
217
- # The query is not encoded, so you may need to URI.encode it later.
218
- #
219
- # format_query(URI.parse('http://some.url/path'), {:key => 'value'})
220
- # # => "?key=value"
221
- #
222
- # format_query(URI.parse('http://some.url/path?one=1'), {:two => '2'})
223
- # # => "?one=1&two=2"
224
- #
225
- def format_query(uri, params={})
226
- query = []
227
- query << uri.query if uri.query
228
- params.each_pair do |key, values|
229
- values = [values] unless values.kind_of?(Array)
230
- values.each { |value| query << "#{key}=#{value}" }
231
- end
232
- "#{query.empty? ? '' : '?'}#{query.join('&')}"
233
- end
234
-
235
- # Formats params as 'application/x-www-form-urlencoded' for use as the
236
- # body of a post request. Multiple values for a parameter may be
237
- # specified using an array. The result is obviously URI encoded.
238
- #
239
- # format_www_form_urlencoded(:key => 'value with spaces')
240
- # # => "key=value%20with%20spaces"
241
- #
242
- def format_www_form_urlencoded(params={})
243
- query = []
244
- params.each_pair do |key, values|
245
- values = [values] unless values.kind_of?(Array)
246
- values.each { |value| query << "#{key}=#{value}" }
247
- end
248
- URI.encode( query.join('&') )
249
- end
250
-
251
- # Formats params as 'multipart/form-data' using the specified boundary,
252
- # for use as the body of a post request. Multiple values for a parameter
253
- # may be specified using an array. All newlines include a carriage
254
- # return for proper formatting.
255
- #
256
- # format_multipart_form_data(:key => 'value')
257
- # # => %Q{--1234567890\r
258
- # # Content-Disposition: form-data; name="key"\r
259
- # # \r
260
- # # value\r
261
- # # --1234567890--\r
262
- # # }
263
- #
264
- # To specify a file, use a hash of file-related headers.
265
- #
266
- # format_multipart_form_data(:key => {
267
- # 'Content-Type' => 'text/plain',
268
- # 'Filename' => "path/to/file.txt"}
269
- # )
270
- # # => %Q{--1234567890\r
271
- # # Content-Disposition: form-data; name="key"; filename="path/to/file.txt"\r
272
- # # Content-Type: text/plain\r
273
- # # \r
274
- # # \r
275
- # # --1234567890--\r
276
- # # }
277
- #
278
- def format_multipart_form_data(params, boundary="1234567890")
279
- body = []
280
- params.each_pair do |key, values|
281
- values = [values] unless values.kind_of?(Array)
282
-
283
- values.each do |value|
284
- body << case value
285
- when Hash
286
- hash = headerize_keys(value)
287
- filename = hash.delete('Filename') || ""
288
- content = File.exists?(filename) ? File.read(filename) : ""
289
-
290
- header = "Content-Disposition: form-data; name=\"#{key.to_s}\"; filename=\"#{filename}\"\r\n"
291
- hash.each_pair { |key, value| header << "#{key}: #{value}\r\n" }
292
- "#{header}\r\n#{content}\r\n"
293
- else
294
- %Q{Content-Disposition: form-data; name="#{key.to_s}"\r\n\r\n#{value.to_s}\r\n}
295
- end
296
- end
297
- end
298
-
299
- body.collect {|p| "--#{boundary}\r\n#{p}" }.join('') + "--#{boundary}--\r\n"
300
- end
301
-
302
- protected
303
-
304
- # Helper to headerize the keys of a hash to headers.
305
- # See Utils#headerize.
306
- def headerize_keys(hash) # :nodoc:
307
- result = {}
308
- hash.each_pair do |key, value|
309
- result[Utils.headerize(key)] = value
310
- end
311
- result
312
- end
313
- end
314
-
315
- config :url, nil # the target url
316
- config :headers, {}, &c.hash # a hash of request headers
317
- config :params, {}, &c.hash # a hash of query parameters
318
- config :request_method, 'GET' # the request method (get or post)
319
- config :version, 1.1 # the HTTP version
320
- config :redirection_limit, nil, &c.integer_or_nil # the redirection limit for the request
321
-
322
- config :max_threads, 10, &c.integer # the maximum number of request threads
323
-
324
- # Prepares the request_hash by symbolizing keys and adding missing
325
- # parameters using the current configuration values.
326
- def prepare(request_hash)
327
- request_hash.inject(
328
- :url => url,
329
- :headers => headers,
330
- :params => params,
331
- :request_method => request_method,
332
- :version => version,
333
- :redirection_limit => redirection_limit
334
- ) do |options, (key, value)|
335
- options[(key.to_sym rescue key) || key] = value
336
- options
337
- end
338
- end
339
-
340
- def process(*requests)
341
- # build a queue of all the requests to be handled
342
- queue = Queue.new
343
- requests.each_with_index do |request, index|
344
- queue.enq [prepare(request), index]
345
- index += 1
346
- end
347
-
348
- # submit and retrieve all requests before processing
349
- # responses. this assures responses are processed
350
- # in order, in case it matters.
351
- lock = Mutex.new
352
- responses = []
353
- request_threads = Array.new(max_threads) do
354
- Thread.new do
355
- begin
356
- while !queue.empty?
357
- request, index = queue.deq(true)
358
- log(request[:request_method], request[:url])
359
-
360
- res = Dispatch.submit(request)
361
- lock.synchronize { responses[index] = res }
362
- end
363
- rescue(ThreadError)
364
- # Catch errors due to the queue being empty.
365
- # (this should not occur as the queue is checked)
366
- raise $! unless $!.message == 'queue empty'
367
- end
368
- end
369
- end
370
- request_threads.each {|thread| thread.join }
371
-
372
- # process responses and collect results
373
- errors = []
374
- responses = responses.collect do |res|
375
- begin
376
- process_response(res)
377
- rescue(ResponseError)
378
- errors << [$!, responses.index(res)]
379
- nil
380
- end
381
- end
382
-
383
- unless errors.empty?
384
- handle_response_errors(responses, errors)
385
- end
386
-
387
- responses
388
- end
389
-
390
- # Hook for processing a response. By default process_response
391
- # simply logs the response message and returns the response.
392
- def process_response(res)
393
- log(nil, res.message)
394
- res
395
- end
396
-
397
- # A hook for handling a batch of response errors, perhaps
398
- # doing something meaningful with the successful responses.
399
- # By default, concatenates the error messages and raises
400
- # a new ResponseError.
401
- def handle_response_errors(responses, errors)
402
- errors.collect! {|error, n| "request #{n}: #{error.message}"}
403
- errors.unshift("Error processing responses:")
404
- raise ResponseError, errors.join("\n")
405
- end
406
-
407
- class ResponseError < StandardError
408
- end
409
- end
410
- end
411
- end
@@ -1,194 +0,0 @@
1
- module Tap
2
- module Test
3
- module HttpTest
4
-
5
- # A collection of sample requests used in testing.
6
- module Requests
7
- GET_REQUESTS = {}
8
- POST_REQUESTS = {}
9
-
10
- def self.add(type, name, request, expected)
11
- collection = case type
12
- when :get then GET_REQUESTS
13
- when :post then POST_REQUESTS
14
- end
15
-
16
- collection["#{type}_#{name}"] = [request.lstrip.gsub(/\n/, "\r\n"), expected]
17
- end
18
- end
19
- end
20
- end
21
- end
22
-
23
- #
24
- # get requests
25
- #
26
-
27
- Tap::Test::HttpTest::Requests.add :get, :basic, %q{
28
- GET /path HTTP/1.1
29
- Host: www.example.com
30
- Keep-Alive: 300
31
- Connection: keep-alive
32
- }, {
33
- :url => "http://www.example.com/path",
34
- :version => '1.1',
35
- :request_method => 'GET',
36
- :headers => {
37
- "Host" => "www.example.com",
38
- "Keep-Alive" => "300",
39
- "Connection" => 'keep-alive'},
40
- :params => {}
41
- }
42
-
43
- Tap::Test::HttpTest::Requests.add :get, :header_less, %q{
44
- GET /path HTTP/1.1
45
- }, {
46
- :url => "/path",
47
- :version => '1.1',
48
- :request_method => 'GET',
49
- :headers => {},
50
- :params => {}
51
- }
52
-
53
- Tap::Test::HttpTest::Requests.add :get, :version_less, %q{
54
- GET /path
55
- }, {
56
- :url => "/path",
57
- :version => '0.9',
58
- :request_method => 'GET',
59
- :headers => {},
60
- :params => {}
61
- }
62
-
63
- Tap::Test::HttpTest::Requests.add :get, :with_query, %q{
64
- GET /path?one=value%20one&two=value%20two HTTP/1.1
65
- }, {
66
- :url => "/path",
67
- :version => '1.1',
68
- :request_method => 'GET',
69
- :headers => {},
70
- :params => {
71
- 'one' => 'value one',
72
- 'two' => 'value two'}
73
- }
74
-
75
- #
76
- # post requests
77
- #
78
-
79
- Tap::Test::HttpTest::Requests.add :post, :with_multipart_form_data, %q{
80
- POST /path HTTP/1.1
81
- Host: www.example.com
82
- Content-Type: multipart/form-data; boundary=1234567890
83
- Content-Length: 158
84
-
85
- --1234567890
86
- Content-Disposition: form-data; name="one"
87
-
88
- value one
89
- --1234567890
90
- Content-Disposition: form-data; name="two"
91
-
92
- value two
93
- --1234567890--
94
- }, {
95
- :url => "http://www.example.com/path",
96
- :version => '1.1',
97
- :request_method => 'POST',
98
- :headers => {
99
- "Host" => 'www.example.com',
100
- "Content-Type" => "multipart/form-data; boundary=1234567890",
101
- "Content-Length" => "158"},
102
- :params => {
103
- 'one' => 'value one',
104
- 'two' => 'value two'}
105
- }
106
-
107
- Tap::Test::HttpTest::Requests.add :post, :with_multipart_data_and_multiple_values, %q{
108
- POST /path HTTP/1.1
109
- Host: www.example.com
110
- Content-Type: multipart/form-data; boundary=1234567890
111
- Content-Length: 158
112
-
113
- --1234567890
114
- Content-Disposition: form-data; name="key"
115
-
116
- value one
117
- --1234567890
118
- Content-Disposition: form-data; name="key"
119
-
120
- value two
121
- --1234567890--
122
- }, {
123
- :url => "http://www.example.com/path",
124
- :version => '1.1',
125
- :request_method => 'POST',
126
- :headers => {
127
- "Host" => 'www.example.com',
128
- "Content-Type" => "multipart/form-data; boundary=1234567890",
129
- "Content-Length" => "158"},
130
- :params => {
131
- 'key' => ["value one", "value two"]}
132
- }
133
-
134
- Tap::Test::HttpTest::Requests.add :post, :with_file_data, %q{
135
- POST /path HTTP/1.1
136
- Host: www.example.com
137
- Content-Type: multipart/form-data; boundary=1234567890
138
- Content-Length: 148
139
-
140
- --1234567890
141
- Content-Disposition: form-data; name="key"; filename="file.txt"
142
- Content-Type: application/octet-stream
143
-
144
- value one
145
- --1234567890--
146
- }, {
147
- :url => "http://www.example.com/path",
148
- :version => '1.1',
149
- :request_method => 'POST',
150
- :headers => {
151
- "Host" => 'www.example.com',
152
- "Content-Type" => "multipart/form-data; boundary=1234567890",
153
- "Content-Length" => "148"},
154
- :params => {
155
- 'key' => {'Filename' => 'file.txt', 'Content-Type' => 'application/octet-stream'}}
156
- }
157
-
158
- Tap::Test::HttpTest::Requests.add :post, :with_mixed_multi_value_file_data, %q{
159
- POST /path HTTP/1.1
160
- Host: www.example.com
161
- Content-Type: multipart/form-data; boundary=1234567890
162
- Content-Length: 329
163
-
164
- --1234567890
165
- Content-Disposition: form-data; name="key"
166
-
167
- one
168
- --1234567890
169
- Content-Disposition: form-data; name="key"; filename="one.txt"
170
- Content-Type: application/octet-stream
171
-
172
- value one
173
- --1234567890
174
- Content-Disposition: form-data; name="key"; filename="two.txt"
175
- Content-Type: text/plain
176
-
177
- value two
178
- --1234567890--
179
- }, {
180
- :url => "http://www.example.com/path",
181
- :version => '1.1',
182
- :request_method => 'POST',
183
- :headers => {
184
- "Host" => 'www.example.com',
185
- "Content-Type" => "multipart/form-data; boundary=1234567890",
186
- "Content-Length" => "329"},
187
- :params => {
188
- 'key' => [
189
- "one",
190
- {'Filename' => 'one.txt', 'Content-Type' => 'application/octet-stream'},
191
- {'Filename' => 'two.txt', 'Content-Type' => 'text/plain'}]}
192
- }
193
-
194
-