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 +5 -5
- data/cgi/echo.rb +2 -2
- data/cgi/http_to_yaml.rb +5 -5
- data/cgi/parse_http.rb +4 -4
- data/lib/tap/http/dispatch.rb +372 -241
- data/lib/tap/http/{helpers.rb → utils.rb} +3 -3
- data/lib/tap/test/http_test/requests.rb +194 -0
- metadata +5 -5
- data/lib/tap/http/request.rb +0 -117
data/README
CHANGED
|
@@ -22,7 +22,7 @@ field is :url.
|
|
|
22
22
|
|
|
23
23
|
include Tap::Http
|
|
24
24
|
|
|
25
|
-
res = Dispatch.
|
|
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:
|
|
76
|
+
version: 1.1
|
|
77
77
|
|
|
78
|
-
Save the file as 'request.yml' and resubmit the form using the Tap::Http::
|
|
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
|
|
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::
|
|
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/
|
|
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::
|
|
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/
|
|
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::
|
|
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::
|
|
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::
|
|
92
|
-
# how_to_get_cookies = cgi.a(Tap::Http::
|
|
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/
|
|
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::
|
|
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::
|
|
105
|
-
how_to_get_cookies = cgi.a(Tap::Http::
|
|
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
|
data/lib/tap/http/dispatch.rb
CHANGED
|
@@ -1,280 +1,411 @@
|
|
|
1
|
-
require 'tap/http/
|
|
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
|
-
#
|
|
8
|
-
#
|
|
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
|
-
#
|
|
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
|
|
18
|
-
# res.body =~ /Tap/
|
|
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
|
-
# {
|
|
23
|
-
# '
|
|
50
|
+
# {
|
|
51
|
+
# 'single' => 'value',
|
|
52
|
+
# 'multiple' => ['value one', 'value two']
|
|
53
|
+
# }
|
|
24
54
|
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
178
|
+
headers.each_pair { |key, value| req[key] = value }
|
|
179
|
+
req
|
|
180
|
+
end
|
|
136
181
|
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
#
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
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-
|
|
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/
|
|
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/
|
|
46
|
+
homepage: http://tap.rubyforge.org/tap-http
|
|
47
47
|
post_install_message:
|
|
48
48
|
rdoc_options: []
|
|
49
49
|
|
data/lib/tap/http/request.rb
DELETED
|
@@ -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
|