tap-http 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
-