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 +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
|