tap-http 0.1.0 → 0.2.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/README CHANGED
@@ -22,7 +22,7 @@ field is :url.
22
22
 
23
23
  include Tap::Http
24
24
 
25
- res = Dispatch.submit_request(
25
+ res = Dispatch.submit(
26
26
  :params => {'q' => 'tap-http'},
27
27
  :url => 'http://www.google.com/search')
28
28
 
@@ -73,12 +73,12 @@ with the http configuration.
73
73
  q: tap-http
74
74
  request_method: GET
75
75
  url: http://www.google.com/search
76
- version: "1.1"
76
+ version: 1.1
77
77
 
78
- Save the file as 'request.yml' and resubmit the form using the Tap::Http::Request
78
+ Save the file as 'request.yml' and resubmit the form using the Tap::Http::Dispatch
79
79
  task.
80
80
 
81
- % rap load requests.yml --:i request --+ dump --no-audit
81
+ % rap load requests.yml --:i dispatch --+ dump --no-audit
82
82
  I[10:51:40] load request.yml
83
83
  I[10:51:40] GET http://www.google.com/search
84
84
  I[10:51:41] OK
@@ -98,7 +98,7 @@ configuration could be used to submit the request using Dispatch.
98
98
 
99
99
  === Bugs/Known Issues
100
100
 
101
- The Tap::Http::Helpers#parse_cgi_request (used in parsing redirected requests
101
+ The Tap::Http::Utils#parse_cgi_request (used in parsing redirected requests
102
102
  into a YAML file) is currently untested because I can't figure a way to setup
103
103
  the ENV variables in a standard way. Of course I could set them up myself, but
104
104
  I can't be sure I'm setting up a realistic test environment.
data/cgi/echo.rb CHANGED
@@ -10,12 +10,12 @@
10
10
  ####################################
11
11
 
12
12
  require 'cgi'
13
- require 'tap/http/helpers'
13
+ require 'tap/http/utils'
14
14
 
15
15
  cgi = CGI.new
16
16
  cgi.out("text/plain") do
17
17
  begin
18
- request = Tap::Http::Helpers.parse_cgi_request(cgi)
18
+ request = Tap::Http::Utils.parse_cgi_request(cgi)
19
19
  request[:headers].to_yaml + request[:params].to_yaml
20
20
  rescue
21
21
  "Error: #{$!.message}\n" +
data/cgi/http_to_yaml.rb CHANGED
@@ -45,7 +45,7 @@ require 'rubygems'
45
45
  require 'cgi'
46
46
  require 'yaml'
47
47
  require 'net/http'
48
- require 'tap/http/helpers'
48
+ require 'tap/http/utils'
49
49
 
50
50
  # included to sort the hash keys
51
51
  class Hash
@@ -75,21 +75,21 @@ begin
75
75
  #
76
76
 
77
77
  config = {}
78
- Tap::Http::Helpers.parse_cgi_request(cgi).each_pair do |key, value|
78
+ Tap::Http::Utils.parse_cgi_request(cgi).each_pair do |key, value|
79
79
  config[key.to_s] = value
80
80
  end
81
81
 
82
82
  original_action = config['params'].delete("__original_action").to_s
83
83
  referer = config['headers']['Referer'].to_s
84
- config['url'] = Tap::Http::Helpers.determine_url(original_action, referer)
84
+ config['url'] = Tap::Http::Utils.determine_url(original_action, referer)
85
85
  config['headers']['Host'] = URI.parse(config['url']).host
86
86
 
87
87
  #
88
88
  # format output
89
89
  #
90
90
 
91
- # help = cgi.a(Tap::Http::Helpers::HELP_URL) { "help" }
92
- # how_to_get_cookies = cgi.a(Tap::Http::Helpers::COOKIES_HELP_URL) { "how to get cookies" }
91
+ # help = cgi.a(Tap::Http::Utils::HELP_URL) { "help" }
92
+ # how_to_get_cookies = cgi.a(Tap::Http::Utils::COOKIES_HELP_URL) { "how to get cookies" }
93
93
  #
94
94
  # If you need cookies, see #{how_to_get_cookies} or the #{help}.
95
95
 
data/cgi/parse_http.rb CHANGED
@@ -30,7 +30,7 @@
30
30
 
31
31
  require 'rubygems'
32
32
  require 'cgi'
33
- require 'tap/http/helpers'
33
+ require 'tap/http/utils'
34
34
 
35
35
  # included to sort the hash keys
36
36
  class Hash
@@ -97,12 +97,12 @@ Connection: keep-alive
97
97
 
98
98
  else
99
99
  config = {}
100
- Tap::Http::Helpers.parse_http_request(http_request).each_pair do |key, value|
100
+ Tap::Http::Utils.parse_http_request(http_request).each_pair do |key, value|
101
101
  config[key.to_s] = value
102
102
  end
103
103
 
104
- help = cgi.a(Tap::Http::Helpers::HELP_URL) { "help" }
105
- how_to_get_cookies = cgi.a(Tap::Http::Helpers::COOKIES_HELP_URL) { "how to get cookies" }
104
+ help = cgi.a(Tap::Http::Utils::HELP_URL) { "help" }
105
+ how_to_get_cookies = cgi.a(Tap::Http::Utils::COOKIES_HELP_URL) { "how to get cookies" }
106
106
 
107
107
  cgi.out do
108
108
  cgi.html do
@@ -1,280 +1,411 @@
1
- require 'tap/http/helpers'
1
+ require 'tap/http/utils'
2
2
  require 'net/http'
3
+ require 'thread'
3
4
 
4
5
  module Tap
5
6
  module Http
6
-
7
- # Dispatch provides methods for constructing and submitting get and post
8
- # HTTP requests from a configuration hash.
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:
9
25
  #
10
- # res = Tap::Http::Dispatch.submit_request(
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(
11
39
  # :url => "http://tap.rubyforge.org",
12
40
  # :version => '1.1',
13
41
  # :request_method => 'GET',
14
42
  # :headers => {},
15
43
  # :params => {}
16
44
  # )
17
- # res.inspect # => "#<Net::HTTPOK 200 OK readbody=true>"
18
- # res.body =~ /Tap/ # => true
45
+ # res.inspect # => "#<Net::HTTPOK 200 OK readbody=true>"
46
+ # res.body =~ /Tap/ # => true
19
47
  #
20
48
  # Headers and parameters take the form:
21
49
  #
22
- # { 'single' => 'value',
23
- # 'multiple' => ['value one', 'value two']}
50
+ # {
51
+ # 'single' => 'value',
52
+ # 'multiple' => ['value one', 'value two']
53
+ # }
24
54
  #
25
- module Dispatch
26
- module_function
27
-
28
- DEFAULT_CONFIG = {
29
- :request_method => 'GET',
30
- :version => '1.1',
31
- :params => {},
32
- :headers => {},
33
- :redirection_limit => nil
34
- }
35
-
36
- # Constructs and submits a request to the url using the request configuration.
37
- # A url must be specified in the configuration, but other configurations are
38
- # optional; if unspecified, the values in DEFAULT_CONFIG will be used. A
39
- # block may be given to receive the Net::HTTP and request just prior to
40
- # submission.
41
- #
42
- # Returns the response from the submission.
43
- #
44
- def submit_request(config)
45
- symbolized = DEFAULT_CONFIG.dup
46
- config.each_pair do |key, value|
47
- symbolized[key.to_sym] = value
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
48
63
  end
49
- config = symbolized
50
-
51
- request_method = (config[:request_method]).to_s
52
- url_or_uri = config[:url]
53
- version = config[:version]
54
- params = config[:params]
55
- headers = headerize_keys(config[:headers])
56
-
57
- raise ArgumentError, "no url specified" unless url_or_uri
58
- uri = url_or_uri.kind_of?(URI) ? url_or_uri : URI.parse(url_or_uri)
59
- uri.path = "/" if uri.path.empty?
60
64
 
61
- # construct the request based on the method
62
- request = case request_method
63
- when /^get$/i then construct_get(uri, headers, params)
64
- when /^post$/i then construct_post(uri, headers, params)
65
- else
66
- raise ArgumentError, "unsupported request method: #{request_method}"
67
- end
68
-
69
- # set the http version
70
- version_method = "version_#{version.to_s.gsub(".", "_")}".to_sym
71
- if ::Net::HTTP.respond_to?(version_method)
72
- ::Net::HTTP.send(version_method)
73
- else
74
- raise ArgumentError, "unsupported HTTP version: #{version}"
75
- end
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
76
119
 
77
- # submit the request
78
- res = ::Net::HTTP.new(uri.host, uri.port).start do |http|
79
- yield(http, request) if block_given?
80
- http.request(request)
120
+ # fetch redirections
121
+ redirection_limit = request_hash[:redirection_limit]
122
+ redirection_limit ? fetch_redirection(res, redirection_limit) : res
81
123
  end
82
-
83
- # fetch redirections
84
- redirection_limit = config[:redirection_limit]
85
- redirection_limit ? fetch_redirection(res, redirection_limit) : res
86
- end
87
124
 
88
- # Constructs a Net::HTTP::Post query, setting headers and parameters.
89
- #
90
- # ==== Supported Content Types:
91
- #
92
- # - application/x-www-form-urlencoded (the default)
93
- # - multipart/form-data
94
- #
95
- # The multipart/form-data content type may specify a boundary. If no
96
- # boundary is specified, a randomly generated boundary will be used
97
- # to delimit the parameters.
98
- #
99
- # post = construct_post(
100
- # URI.parse('http://some.url/'),
101
- # {:content_type => 'multipart/form-data; boundary=1234'},
102
- # {:key => 'value'})
103
- #
104
- # post.body
105
- # # => %Q{--1234\r
106
- # # Content-Disposition: form-data; name="key"\r
107
- # # \r
108
- # # value\r
109
- # # --1234--\r
110
- # # }
111
- #
112
- # (Note the carriage returns are required in multipart content)
113
- #
114
- # The content-length header is determined automatically from the
115
- # formatted request body; manually specified content-length headers
116
- # will be overridden.
117
- #
118
- def construct_post(uri, headers, params)
119
- req = ::Net::HTTP::Post.new( URI.encode("#{uri.path}#{format_query(uri)}") )
120
- headers = headerize_keys(headers)
121
- content_type = headers['Content-Type']
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']
122
159
 
123
- case content_type
124
- when nil, /^application\/x-www-form-urlencoded$/i
125
- req.body = format_www_form_urlencoded(params)
126
- headers['Content-Type'] ||= "application/x-www-form-urlencoded"
127
- headers['Content-Length'] = req.body.length
128
-
129
- when /^multipart\/form-data(;\s*boundary=(.*))?$/i
130
- # extract the boundary if it exists
131
- boundary = $2 || rand.to_s[2..20]
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
132
177
 
133
- req.body = format_multipart_form_data(params, boundary)
134
- headers['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
135
- headers['Content-Length'] = req.body.length
178
+ headers.each_pair { |key, value| req[key] = value }
179
+ req
180
+ end
136
181
 
137
- else
138
- raise ArgumentError, "unsupported Content-Type for POST: #{content_type}"
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
139
192
  end
140
-
141
- headers.each_pair { |key, value| req[key] = value }
142
- req
143
- end
144
193
 
145
- # Constructs a Net::HTTP::Get query. All parameters in uri and params are
146
- # encoded and added to the request URI.
147
- #
148
- # get = construct_get(URI.parse('http://some.url/path'), {}, {:key => 'value'})
149
- # get.path # => "/path?key=value"
150
- #
151
- def construct_get(uri, headers, params)
152
- req = ::Net::HTTP::Get.new( URI.encode("#{uri.path}#{format_query(uri, params)}") )
153
- headerize_keys(headers).each_pair { |key, value| req[key] = value }
154
- req
155
- end
156
-
157
- # Checks the type of the response; if it is a redirection, fetches the
158
- # redirection. Otherwise return the response.
159
- #
160
- # Notes:
161
- # - Fetch will recurse up to the input redirection limit (default 10)
162
- # - Responses that are not Net::HTTPRedirection or Net::HTTPSuccess
163
- # raise an error.
164
- def fetch_redirection(res, limit=10)
165
- raise 'exceeded the redirection limit' if limit < 1
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
166
203
 
167
- case res
168
- when ::Net::HTTPRedirection
169
- redirect = ::Net::HTTP.get_response( URI.parse(res['location']) )
170
- fetch_redirection(redirect, limit - 1)
171
- when ::Net::HTTPSuccess
172
- res
173
- else
174
- raise StandardError, res.error!
175
- end
176
- end
177
-
178
- # Converts the keys of a hash to headers. See Helpers#headerize.
179
- #
180
- # headerize_keys('some_header' => 'value') # => {'Some-Header' => 'value'}
181
- #
182
- def headerize_keys(hash)
183
- result = {}
184
- hash.each_pair do |key, value|
185
- result[Helpers.headerize(key)] = value
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
186
213
  end
187
- result
188
- end
189
214
 
190
- # Constructs a URI query string from the uri and the input parameters.
191
- # Multiple values for a parameter may be specified using an array.
192
- # The query is not encoded, so you may need to URI.encode it later.
193
- #
194
- # format_query(URI.parse('http://some.url/path'), {:key => 'value'})
195
- # # => "?key=value"
196
- #
197
- # format_query(URI.parse('http://some.url/path?one=1'), {:two => '2'})
198
- # # => "?one=1&two=2"
199
- #
200
- def format_query(uri, params={})
201
- query = []
202
- query << uri.query if uri.query
203
- params.each_pair do |key, values|
204
- values = [values] unless values.kind_of?(Array)
205
- values.each { |value| query << "#{key}=#{value}" }
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
206
312
  end
207
- "#{query.empty? ? '' : '?'}#{query.join('&')}"
208
313
  end
209
314
 
210
- # Formats params as 'application/x-www-form-urlencoded' for use as the
211
- # body of a post request. Multiple values for a parameter may be
212
- # specified using an array. The result is obviously URI encoded.
213
- #
214
- # format_www_form_urlencoded(:key => 'value with spaces')
215
- # # => "key=value%20with%20spaces"
216
- #
217
- def format_www_form_urlencoded(params={})
218
- query = []
219
- params.each_pair do |key, values|
220
- values = [values] unless values.kind_of?(Array)
221
- values.each { |value| query << "#{key}=#{value}" }
222
- end
223
- URI.encode( query.join('&') )
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
224
338
  end
225
-
226
- # Formats params as 'multipart/form-data' using the specified boundary,
227
- # for use as the body of a post request. Multiple values for a parameter
228
- # may be specified using an array. All newlines include a carriage
229
- # return for proper formatting.
230
- #
231
- # format_multipart_form_data(:key => 'value')
232
- # # => %Q{--1234567890\r
233
- # # Content-Disposition: form-data; name="key"\r
234
- # # \r
235
- # # value\r
236
- # # --1234567890--\r
237
- # # }
238
- #
239
- # To specify a file, use a hash of file-related headers.
240
- #
241
- # format_multipart_form_data(:key => {
242
- # 'Content-Type' => 'text/plain',
243
- # 'Filename' => "path/to/file.txt"}
244
- # )
245
- # # => %Q{--1234567890\r
246
- # # Content-Disposition: form-data; name="key"; filename="path/to/file.txt"\r
247
- # # Content-Type: text/plain\r
248
- # # \r
249
- # # \r
250
- # # --1234567890--\r
251
- # # }
252
- #
253
- def format_multipart_form_data(params, boundary="1234567890")
254
- body = []
255
- params.each_pair do |key, values|
256
- values = [values] unless values.kind_of?(Array)
257
-
258
- values.each do |value|
259
- body << case value
260
- when Hash
261
- hash = headerize_keys(value)
262
- filename = hash.delete('Filename') || ""
263
- content = File.exists?(filename) ? File.read(filename) : ""
264
-
265
- header = "Content-Disposition: form-data; name=\"#{key.to_s}\"; filename=\"#{filename}\"\r\n"
266
- hash.each_pair { |key, value| header << "#{key}: #{value}\r\n" }
267
- "#{header}\r\n#{content}\r\n"
268
- else
269
- %Q{Content-Disposition: form-data; name="#{key.to_s}"\r\n\r\n#{value.to_s}\r\n}
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'
270
367
  end
271
368
  end
272
369
  end
370
+ request_threads.each {|thread| thread.join }
273
371
 
274
- body.collect {|p| "--#{boundary}\r\n#{p}" }.join('') + "--#{boundary}--\r\n"
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
275
408
  end
276
-
277
409
  end
278
410
  end
279
- end
280
-
411
+ end
@@ -4,7 +4,7 @@ autoload(:StringIO, 'stringio')
4
4
 
5
5
  module Tap
6
6
  module Http
7
- module Helpers
7
+ module Utils
8
8
  module_function
9
9
 
10
10
  # Parses a WEBrick::HTTPRequest from the input socket into a hash that
@@ -163,7 +163,7 @@ module Tap
163
163
 
164
164
  { :url => File.join("http://", headers['Host'], ENV['PATH_INFO']),
165
165
  :request_method => ENV['REQUEST_METHOD'],
166
- :version => ENV['HTTP_VERSION'] =~ /^HTTP\/(.*)$/ ? $1 : ENV['HTTP_VERSION'],
166
+ :version => ENV['HTTP_VERSION'] =~ /^HTTP\/(.*)$/ ? $1.to_f : ENV['HTTP_VERSION'],
167
167
  :headers => headers,
168
168
  :params => params}
169
169
  end
@@ -217,7 +217,7 @@ module Tap
217
217
  # that accept 'gzip' and 'deflate' content encoding.
218
218
  #
219
219
  #--
220
- # Helpers.inflate(res.body) if res['content-encoding'] == 'gzip'
220
+ # Utils.inflate(res.body) if res['content-encoding'] == 'gzip'
221
221
  #
222
222
  def inflate(str)
223
223
  Zlib::GzipReader.new( StringIO.new( str ) ).read
@@ -0,0 +1,194 @@
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
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tap-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Chiang
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-11-25 00:00:00 -07:00
12
+ date: 2008-12-03 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -36,14 +36,14 @@ files:
36
36
  - cgi/http_to_yaml.rb
37
37
  - cgi/parse_http.rb
38
38
  - lib/tap/http/dispatch.rb
39
- - lib/tap/http/helpers.rb
40
- - lib/tap/http/request.rb
39
+ - lib/tap/http/utils.rb
41
40
  - lib/tap/test/http_test.rb
41
+ - lib/tap/test/http_test/requests.rb
42
42
  - tap.yml
43
43
  - README
44
44
  - MIT-LICENSE
45
45
  has_rdoc: true
46
- homepage: http://tap.rubyforge.org/projects/tap-http
46
+ homepage: http://tap.rubyforge.org/tap-http
47
47
  post_install_message:
48
48
  rdoc_options: []
49
49
 
@@ -1,117 +0,0 @@
1
- require 'tap/http/dispatch'
2
- require 'thread'
3
-
4
- module Tap
5
- module Http
6
-
7
- # :startdoc::manifest submits an http request
8
- #
9
- # Request is a base class for submitting HTTP requests from a request
10
- # hash. Multiple requests may be submitted on individual threads, up
11
- # to a configurable limit.
12
- #
13
- # Configuration hashes are like the following:
14
- #
15
- # url: http://tap.rubyforge.org/
16
- # request_method: GET
17
- # headers: {}
18
- # params: {}
19
- #
20
- # The only required field is the url (by default no headers or params
21
- # are specified, and the request method is GET). Request requires
22
- # hash inputs, which can be inconvenient from the command line. A
23
- # good workaround is to save requests in a .yml file and use a simple
24
- # workflow:
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 request --+ dump
31
- #
32
- #--
33
- # To generate configuration hashes from Firefox, see the redirect_http
34
- # ubiquity command.
35
- #++
36
- class Request < Tap::Task
37
- class << self
38
- def intern(*args, &block)
39
- instance = new(*args)
40
- instance.extend Support::Intern(:process_response)
41
- instance.process_response_block = block
42
- instance
43
- end
44
- end
45
-
46
- config :redirection_limit, 10, &c.integer # the redirection limit for the request
47
- config :max_threads, 10, &c.integer # the maximum number of request threads
48
-
49
- def process(*requests)
50
- # build a queue of all the requests to be handled
51
- queue = Queue.new
52
- requests.each_with_index do |request, index|
53
- request = symbolize_keys(request)
54
-
55
- if request[:url] == nil
56
- raise ArgumentError, "no url specified: #{request.inspect}"
57
- end
58
-
59
- queue.enq [request, index]
60
- index += 1
61
- end
62
-
63
- # submit and retrieve all requests before processing
64
- # responses. this assures responses are processed
65
- # in order, in case it matters.
66
- lock = Mutex.new
67
- responses = []
68
- request_threads = Array.new(max_threads) do
69
- Thread.new do
70
- begin
71
- while !queue.empty?
72
- request, index = queue.deq(true)
73
-
74
- log(request[:request_method], request[:url])
75
- if app.verbose
76
- log 'headers', config[:headers].inspect
77
- log 'params', config[:params].inspect
78
- end
79
-
80
- res = Dispatch.submit_request(request)
81
- lock.synchronize { responses[index] = res }
82
- end
83
- rescue(ThreadError)
84
- # Catch errors due to the queue being empty.
85
- # (this should not occur as the queue is checked)
86
- raise $! unless $!.message == 'queue empty'
87
- end
88
- end
89
- end
90
- request_threads.each {|thread| thread.join }
91
-
92
- # process responses and collect results
93
- responses.collect! do |res|
94
- process_response(res)
95
- end
96
- end
97
-
98
- # Hook for processing a response. By default process_response
99
- # simply logs the response message and returns the response.
100
- def process_response(res)
101
- log(nil, res.message)
102
- res
103
- end
104
-
105
- protected
106
-
107
- # Return a new hash with all keys converted to symbols.
108
- # Lifted from ActiveSupport
109
- def symbolize_keys(hash)
110
- hash.inject({}) do |options, (key, value)|
111
- options[(key.to_sym rescue key) || key] = value
112
- options
113
- end
114
- end
115
- end
116
- end
117
- end