tap-http 0.1.0 → 0.2.0

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