tap-http 0.0.1 → 0.1.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 +97 -1
- data/cgi/echo.rb +3 -3
- data/cgi/http_to_yaml.rb +4 -4
- data/lib/tap/http/dispatch.rb +166 -106
- data/lib/tap/http/helpers.rb +128 -131
- data/lib/tap/http/request.rb +1 -1
- data/lib/tap/test/http_test.rb +1 -146
- metadata +4 -4
data/README
CHANGED
@@ -1,15 +1,111 @@
|
|
1
|
-
= {TapHttp}[http://tap.rubyforge.org/tap-http]
|
1
|
+
= {TapHttp}[http://tap.rubyforge.org/projects/tap-http]
|
2
2
|
|
3
3
|
A task library for submitting http requests using {Tap}[http://tap.rubyforge.org].
|
4
4
|
|
5
5
|
== Description
|
6
6
|
|
7
|
+
TapHttp provides modules to construct and submit HTTP requests from a hash
|
8
|
+
that specifies the target url, headers, parameters, etc. TapHttp is
|
9
|
+
designed to work with a {Ubiquity}[http://labs.mozilla.com/2008/08/introducing-ubiquity/]
|
10
|
+
command called {redirect-http}[http://gist.github.com/25932]; together
|
11
|
+
they allow the capture and resubmission of web forms.
|
12
|
+
|
7
13
|
* Lighthouse[http://bahuvrihi.lighthouseapp.com/projects/9908-tap-task-application/tickets]
|
8
14
|
* Github[http://github.com/bahuvrihi/tap-http/tree/master]
|
9
15
|
* {Google Group}[http://groups.google.com/group/ruby-on-tap]
|
10
16
|
|
11
17
|
=== Usage
|
12
18
|
|
19
|
+
TapHttp submits http requests using the Tap::Http::Dispatch module. Headers,
|
20
|
+
parameters, and other configurations may be specified, but the only required
|
21
|
+
field is :url.
|
22
|
+
|
23
|
+
include Tap::Http
|
24
|
+
|
25
|
+
res = Dispatch.submit_request(
|
26
|
+
:params => {'q' => 'tap-http'},
|
27
|
+
:url => 'http://www.google.com/search')
|
28
|
+
|
29
|
+
res.body[0,80] # => "<!doctype html><head><title>tap-http - Google Search</title><style>body{backgrou"
|
30
|
+
|
31
|
+
=== Getting Http Configurations
|
32
|
+
|
33
|
+
More complicated http requests may be captured and resubmitted using a
|
34
|
+
combination of tools that redirects web forms to a tap server and reformats
|
35
|
+
the request as YAML. To do so:
|
36
|
+
|
37
|
+
* Install {Firefox}[http://www.mozilla.com/en-US/firefox/]
|
38
|
+
* Install {Ubiquity}[http://labs.mozilla.com/2008/08/introducing-ubiquity/]
|
39
|
+
* Install {redirect-http}[http://gist.github.com/25932]
|
40
|
+
|
41
|
+
Start a tap server from the command line (of course tap-http must be installed):
|
42
|
+
|
43
|
+
% tap server
|
44
|
+
|
45
|
+
Now in the browser, go to a web form like {google}[http://www.google.com/] and
|
46
|
+
invoke the redirection.
|
47
|
+
|
48
|
+
* Bring up Ubiquity in Firefox by pressing 'option+space'
|
49
|
+
* Enter the command: 'redirect-http http://localhost:8080/http_to_yaml'
|
50
|
+
|
51
|
+
You should see a notice that the form is being redirected. Fill out the form
|
52
|
+
and submit as normal; the redirect command will send the form to the tap server
|
53
|
+
instead of performing the original action. The tap server returns a yaml file
|
54
|
+
with the http configuration.
|
55
|
+
|
56
|
+
# Copy and paste into a configuration file. Multiple configs
|
57
|
+
# can be added to a single file to perform batch submission.
|
58
|
+
- headers:
|
59
|
+
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
60
|
+
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
|
61
|
+
Accept-Encoding: gzip,deflate
|
62
|
+
Accept-Language: en-us,en;q=0.5
|
63
|
+
Connection: keep-alive
|
64
|
+
Host: www.google.com
|
65
|
+
Keep-Alive: "300"
|
66
|
+
Referer: http://www.google.com/
|
67
|
+
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.4) Gecko/2008102920 Firefox/3.0.4
|
68
|
+
params:
|
69
|
+
aq: f
|
70
|
+
btnG: Google Search
|
71
|
+
hl: en
|
72
|
+
oq: ""
|
73
|
+
q: tap-http
|
74
|
+
request_method: GET
|
75
|
+
url: http://www.google.com/search
|
76
|
+
version: "1.1"
|
77
|
+
|
78
|
+
Save the file as 'request.yml' and resubmit the form using the Tap::Http::Request
|
79
|
+
task.
|
80
|
+
|
81
|
+
% rap load requests.yml --:i request --+ dump --no-audit
|
82
|
+
I[10:51:40] load request.yml
|
83
|
+
I[10:51:40] GET http://www.google.com/search
|
84
|
+
I[10:51:41] OK
|
85
|
+
# date: 2008-11-25 10:51:41
|
86
|
+
---
|
87
|
+
tap/http/request (2772040):
|
88
|
+
- - !ruby/object:Net::HTTPOK
|
89
|
+
body: !binary |
|
90
|
+
H4sIAAAAAAAC/6xabXPbNhL+3l/B0BeN1FAUJfktoihf07pppmkm06TXu0lz
|
91
|
+
HZAEScQgQZOQZVfhf79dgBRJS4ndmRvPSAC42F3sPvsCyssnoQjkXU6NRKZ8
|
92
|
+
tUwoCVdLySSnK0nycSJlboyNl0LEnBrvKCmCZDnRz5elvIMvX4R3W58EV3Eh
|
93
|
+
1lm4OIqiyA0EF8XiyHEc...
|
94
|
+
|
95
|
+
Note the result is encoded as gzip, as per the parameters. As with all tasks,
|
96
|
+
the request results could be passed into a workflow. Alternatively, the
|
97
|
+
configuration could be used to submit the request using Dispatch.
|
98
|
+
|
99
|
+
=== Bugs/Known Issues
|
100
|
+
|
101
|
+
The Tap::Http::Helpers#parse_cgi_request (used in parsing redirected requests
|
102
|
+
into a YAML file) is currently untested because I can't figure a way to setup
|
103
|
+
the ENV variables in a standard way. Of course I could set them up myself, but
|
104
|
+
I can't be sure I'm setting up a realistic test environment.
|
105
|
+
|
106
|
+
The capture procedure seems to work in practice, but please report bugs and let
|
107
|
+
me know if you know a way to setup a CGI environment for testing!
|
108
|
+
|
13
109
|
== Installation
|
14
110
|
|
15
111
|
TapHttp is available as a gem on RubyForge[http://rubyforge.org/projects/tap]. Use:
|
data/cgi/echo.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/local/bin/ruby
|
2
2
|
|
3
3
|
####################################
|
4
|
-
# Echos back the HTTP header and parameters as YAML.
|
4
|
+
# Echos back the HTTP header and parameters as YAML.
|
5
5
|
#
|
6
6
|
# Copyright (c) 2008, Regents of the University of Colorado
|
7
7
|
# Developer: Simon Chiang, Biomolecular Structure Program
|
@@ -14,8 +14,8 @@ require 'tap/http/helpers'
|
|
14
14
|
|
15
15
|
cgi = CGI.new
|
16
16
|
cgi.out("text/plain") do
|
17
|
-
begin
|
18
|
-
request = Tap::Http::Helpers.parse_cgi_request(cgi)
|
17
|
+
begin
|
18
|
+
request = Tap::Http::Helpers.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
@@ -88,14 +88,14 @@ begin
|
|
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::Helpers::HELP_URL) { "help" }
|
92
|
+
# how_to_get_cookies = cgi.a(Tap::Http::Helpers::COOKIES_HELP_URL) { "how to get cookies" }
|
93
|
+
#
|
94
|
+
# If you need cookies, see #{how_to_get_cookies} or the #{help}.
|
93
95
|
|
94
96
|
cgi.out('text/plain') do
|
95
97
|
%Q{# Copy and paste into a configuration file. Multiple configs
|
96
98
|
# can be added to a single file to perform batch submission.
|
97
|
-
#
|
98
|
-
# If you need cookies, see #{how_to_get_cookies} or the #{help}.
|
99
99
|
- #{config.to_yaml[5..-1].gsub(/\n/, "\n ")}
|
100
100
|
}
|
101
101
|
end
|
data/lib/tap/http/dispatch.rb
CHANGED
@@ -1,53 +1,60 @@
|
|
1
1
|
require 'tap/http/helpers'
|
2
2
|
require 'net/http'
|
3
3
|
|
4
|
-
#module Net
|
5
|
-
# class HTTP
|
6
|
-
# attr_reader :socket
|
7
|
-
# end
|
8
|
-
|
9
|
-
# class BufferedIO
|
10
|
-
#include Prosperity::Acts::Monitorable
|
11
|
-
|
12
|
-
#private
|
13
|
-
|
14
|
-
#def rbuf_fill
|
15
|
-
# tick_monitor
|
16
|
-
# timeout(@read_timeout) {
|
17
|
-
# @rbuf << @io.sysread(1024)
|
18
|
-
# }
|
19
|
-
#end
|
20
|
-
#end
|
21
|
-
#end
|
22
|
-
|
23
4
|
module Tap
|
24
5
|
module Http
|
25
6
|
|
26
7
|
# Dispatch provides methods for constructing and submitting get and post
|
27
|
-
# HTTP requests.
|
8
|
+
# HTTP requests from a configuration hash.
|
9
|
+
#
|
10
|
+
# res = Tap::Http::Dispatch.submit_request(
|
11
|
+
# :url => "http://tap.rubyforge.org",
|
12
|
+
# :version => '1.1',
|
13
|
+
# :request_method => 'GET',
|
14
|
+
# :headers => {},
|
15
|
+
# :params => {}
|
16
|
+
# )
|
17
|
+
# res.inspect # => "#<Net::HTTPOK 200 OK readbody=true>"
|
18
|
+
# res.body =~ /Tap/ # => true
|
19
|
+
#
|
20
|
+
# Headers and parameters take the form:
|
21
|
+
#
|
22
|
+
# { 'single' => 'value',
|
23
|
+
# 'multiple' => ['value one', 'value two']}
|
24
|
+
#
|
28
25
|
module Dispatch
|
29
|
-
REQUEST_KEYS = [:url, :request_method, :headers, :params, :redirection_limit]
|
30
|
-
|
31
26
|
module_function
|
32
27
|
|
33
|
-
|
34
|
-
|
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.
|
35
41
|
#
|
36
|
-
#
|
37
|
-
# # => <Net::HTTPOK 200 OK readbody=true>
|
42
|
+
# Returns the response from the submission.
|
38
43
|
#
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
def submit_request(config)
|
45
|
+
symbolized = DEFAULT_CONFIG.dup
|
46
|
+
config.each_pair do |key, value|
|
47
|
+
symbolized[key.to_sym] = value
|
48
|
+
end
|
49
|
+
config = symbolized
|
50
|
+
|
51
|
+
request_method = (config[:request_method]).to_s
|
46
52
|
url_or_uri = config[:url]
|
47
|
-
|
48
|
-
|
49
|
-
|
53
|
+
version = config[:version]
|
54
|
+
params = config[:params]
|
55
|
+
headers = headerize_keys(config[:headers])
|
50
56
|
|
57
|
+
raise ArgumentError, "no url specified" unless url_or_uri
|
51
58
|
uri = url_or_uri.kind_of?(URI) ? url_or_uri : URI.parse(url_or_uri)
|
52
59
|
uri.path = "/" if uri.path.empty?
|
53
60
|
|
@@ -56,22 +63,20 @@ module Tap
|
|
56
63
|
when /^get$/i then construct_get(uri, headers, params)
|
57
64
|
when /^post$/i then construct_post(uri, headers, params)
|
58
65
|
else
|
59
|
-
raise ArgumentError
|
66
|
+
raise ArgumentError, "unsupported request method: #{request_method}"
|
60
67
|
end
|
61
68
|
|
62
69
|
# set the http version
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
# end
|
70
|
-
# end
|
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
|
71
76
|
|
72
77
|
# submit the request
|
73
|
-
res =
|
74
|
-
yield(http) if block_given?
|
78
|
+
res = ::Net::HTTP.new(uri.host, uri.port).start do |http|
|
79
|
+
yield(http, request) if block_given?
|
75
80
|
http.request(request)
|
76
81
|
end
|
77
82
|
|
@@ -80,120 +85,175 @@ module Tap
|
|
80
85
|
redirection_limit ? fetch_redirection(res, redirection_limit) : res
|
81
86
|
end
|
82
87
|
|
83
|
-
# Constructs a
|
84
|
-
# content. If the content type is 'multipart/form-data', then the parameters will be
|
85
|
-
# formatted using the boundary in the content-type header, if provided, or a randomly
|
86
|
-
# generated boundary.
|
88
|
+
# Constructs a Net::HTTP::Post query, setting headers and parameters.
|
87
89
|
#
|
88
|
-
#
|
89
|
-
# they will be included in the request URI.
|
90
|
+
# ==== Supported Content Types:
|
90
91
|
#
|
91
|
-
#
|
92
|
-
# -
|
93
|
-
#
|
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.
|
94
117
|
#
|
95
118
|
def construct_post(uri, headers, params)
|
96
|
-
req =
|
119
|
+
req = ::Net::HTTP::Post.new( URI.encode("#{uri.path}#{format_query(uri)}") )
|
97
120
|
headers = headerize_keys(headers)
|
98
121
|
content_type = headers['Content-Type']
|
99
122
|
|
100
123
|
case content_type
|
101
|
-
when
|
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
|
102
130
|
# extract the boundary if it exists
|
103
|
-
|
104
|
-
boundary = $1 || rand.to_s[2..20]
|
131
|
+
boundary = $2 || rand.to_s[2..20]
|
105
132
|
|
106
|
-
req.body = format_multipart_form_data(
|
133
|
+
req.body = format_multipart_form_data(params, boundary)
|
107
134
|
headers['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
|
108
135
|
headers['Content-Length'] = req.body.length
|
136
|
+
|
109
137
|
else
|
110
|
-
|
111
|
-
headers['Content-Type'] = "application/x-www-form-urlencoded"
|
112
|
-
headers['Content-Length'] = req.body.length
|
138
|
+
raise ArgumentError, "unsupported Content-Type for POST: #{content_type}"
|
113
139
|
end
|
114
|
-
|
140
|
+
|
115
141
|
headers.each_pair { |key, value| req[key] = value }
|
116
142
|
req
|
117
143
|
end
|
118
144
|
|
119
|
-
# Constructs a
|
120
|
-
#
|
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
|
+
#
|
121
151
|
def construct_get(uri, headers, params)
|
122
|
-
req =
|
152
|
+
req = ::Net::HTTP::Get.new( URI.encode("#{uri.path}#{format_query(uri, params)}") )
|
123
153
|
headerize_keys(headers).each_pair { |key, value| req[key] = value }
|
124
154
|
req
|
125
155
|
end
|
126
156
|
|
127
|
-
# Checks the type of the response; if it is a redirection,
|
128
|
-
#
|
157
|
+
# Checks the type of the response; if it is a redirection, fetches the
|
158
|
+
# redirection. Otherwise return the response.
|
129
159
|
#
|
130
160
|
# Notes:
|
131
|
-
# -
|
132
|
-
# - Responses that are not Net::HTTPRedirection or Net::HTTPSuccess
|
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.
|
133
164
|
def fetch_redirection(res, limit=10)
|
134
|
-
raise
|
165
|
+
raise 'exceeded the redirection limit' if limit < 1
|
135
166
|
|
136
167
|
case res
|
137
|
-
when
|
138
|
-
redirect =
|
168
|
+
when ::Net::HTTPRedirection
|
169
|
+
redirect = ::Net::HTTP.get_response( URI.parse(res['location']) )
|
139
170
|
fetch_redirection(redirect, limit - 1)
|
140
|
-
when
|
141
|
-
|
171
|
+
when ::Net::HTTPSuccess
|
172
|
+
res
|
173
|
+
else
|
174
|
+
raise StandardError, res.error!
|
142
175
|
end
|
143
176
|
end
|
144
177
|
|
145
|
-
#
|
146
|
-
#
|
147
|
-
#
|
148
|
-
#
|
149
|
-
def headerize_keys(
|
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)
|
150
183
|
result = {}
|
151
|
-
|
184
|
+
hash.each_pair do |key, value|
|
152
185
|
result[Helpers.headerize(key)] = value
|
153
186
|
end
|
154
187
|
result
|
155
188
|
end
|
156
189
|
|
157
|
-
#
|
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.
|
158
192
|
# The query is not encoded, so you may need to URI.encode it later.
|
159
|
-
|
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={})
|
160
201
|
query = []
|
202
|
+
query << uri.query if uri.query
|
161
203
|
params.each_pair do |key, values|
|
162
|
-
values = values.kind_of?(Array)
|
204
|
+
values = [values] unless values.kind_of?(Array)
|
163
205
|
values.each { |value| query << "#{key}=#{value}" }
|
164
206
|
end
|
165
|
-
query << uri.query if uri.query
|
166
207
|
"#{query.empty? ? '' : '?'}#{query.join('&')}"
|
167
208
|
end
|
168
|
-
|
209
|
+
|
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
|
+
#
|
169
217
|
def format_www_form_urlencoded(params={})
|
170
218
|
query = []
|
171
219
|
params.each_pair do |key, values|
|
172
|
-
values = values.kind_of?(Array)
|
220
|
+
values = [values] unless values.kind_of?(Array)
|
173
221
|
values.each { |value| query << "#{key}=#{value}" }
|
174
222
|
end
|
175
223
|
URI.encode( query.join('&') )
|
176
224
|
end
|
177
225
|
|
178
|
-
#
|
179
|
-
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
#
|
183
|
-
#
|
184
|
-
#
|
185
|
-
|
186
|
-
#
|
187
|
-
#
|
188
|
-
# --
|
189
|
-
#
|
190
|
-
#
|
191
|
-
#
|
192
|
-
#
|
193
|
-
|
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")
|
194
254
|
body = []
|
195
255
|
params.each_pair do |key, values|
|
196
|
-
values = values.kind_of?(Array)
|
256
|
+
values = [values] unless values.kind_of?(Array)
|
197
257
|
|
198
258
|
values.each do |value|
|
199
259
|
body << case value
|
data/lib/tap/http/helpers.rb
CHANGED
@@ -1,36 +1,55 @@
|
|
1
1
|
autoload(:WEBrick, 'webrick')
|
2
|
-
autoload(:Zlib, 'zlib')
|
3
|
-
autoload(:StringIO, 'stringio')
|
2
|
+
autoload(:Zlib, 'zlib')
|
3
|
+
autoload(:StringIO, 'stringio')
|
4
4
|
|
5
5
|
module Tap
|
6
6
|
module Http
|
7
7
|
module Helpers
|
8
8
|
module_function
|
9
9
|
|
10
|
-
# Parses a WEBrick::HTTPRequest from the input socket
|
10
|
+
# Parses a WEBrick::HTTPRequest from the input socket into a hash that
|
11
|
+
# may be resubmitted by Dispatch. Sockets can be any kind of IO (File,
|
12
|
+
# StringIO, etc..) and should be positioned such that the next line is
|
13
|
+
# the start of an HTTP request. Strings used as sockets are converted
|
14
|
+
# into StringIO objects.
|
11
15
|
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
16
|
+
# parse_http_request("GET /path HTTP/1.1\n")
|
17
|
+
# # => {
|
18
|
+
# # :request_method => "GET",
|
19
|
+
# # :url => "/path",
|
20
|
+
# # :version => "1.1",
|
21
|
+
# # :headers => {},
|
22
|
+
# # :params => {},
|
23
|
+
# # }
|
17
24
|
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
25
|
+
# If splat_values is specified, single-value headers and parameters
|
26
|
+
# will be hashed as single values. Otherwise, all header and parameter
|
27
|
+
# values will be arrays.
|
28
|
+
#
|
29
|
+
# str = "GET /path?one=a&one=b&two=c HTTP/1.1\n"
|
30
|
+
# req = parse_http_request(str)
|
31
|
+
# req[:params] # => {'one' => ['a', 'b'], 'two' => 'c'}
|
32
|
+
#
|
33
|
+
# req = parse_http_request(str, false)
|
34
|
+
# req[:params] # => {'one' => ['a', 'b'], 'two' => ['c']}
|
35
|
+
#
|
36
|
+
# ==== WEBrick parsing of HTTP format
|
37
|
+
#
|
38
|
+
# WEBrick will parse headers then the body of a request, and currently
|
39
|
+
# (1.8.6) considers an empty line as a break between the headers and
|
40
|
+
# body. In general header parsing is forgiving with end-line
|
41
|
+
# characters (ie "\r\n" and "\n" are both acceptable) but parsing of
|
42
|
+
# multipart/form data IS NOT.
|
24
43
|
#
|
25
|
-
# Multipart/form data REQUIRES that the end-line characters
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# must be correct.
|
44
|
+
# Multipart/form data REQUIRES that the end-line characters are "\r\n".
|
45
|
+
# A boundary is always started with "--" and the last boundary completed
|
46
|
+
# with "--". As always, the content-length must be correct.
|
29
47
|
#
|
30
48
|
# # Notice an empty line between the last header
|
31
49
|
# # (in this case 'Content-Length') and the body.
|
32
50
|
# msg = <<-_end_of_message_
|
33
51
|
# POST /path HTTP/1.1
|
52
|
+
# Host: localhost:8080
|
34
53
|
# Content-Type: multipart/form-data; boundary=1234567890
|
35
54
|
# Content-Length: 158
|
36
55
|
#
|
@@ -48,82 +67,103 @@ module Tap
|
|
48
67
|
# # ensure the end of line characters are correct...
|
49
68
|
# socket = StringIO.new msg.gsub(/\n/, "\r\n")
|
50
69
|
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
70
|
+
# Tap::Net.parse_http_request(socket)
|
71
|
+
# # => {
|
72
|
+
# # :request_method => "POST",
|
73
|
+
# # :url => "http://localhost:8080/path",
|
74
|
+
# # :version => "HTTP/1.1",
|
75
|
+
# # :headers => {
|
76
|
+
# # "Host" => "localhost:8080",
|
77
|
+
# # "Content-Type" => "multipart/form-data; boundary=1234567890",
|
78
|
+
# # "Content-Length" => "158"},
|
79
|
+
# # :params => {
|
80
|
+
# # "one" => "value one",
|
81
|
+
# # "two" => "value two"}}
|
54
82
|
#
|
55
|
-
|
83
|
+
#--
|
84
|
+
# TODO: check if there are other headers to capture from
|
85
|
+
# a multipart/form file. Currently only
|
86
|
+
# 'Filename' and 'Content-Type' are added
|
87
|
+
def parse_http_request(socket, splat_values=true)
|
56
88
|
socket = StringIO.new(socket) if socket.kind_of?(String)
|
57
89
|
|
58
90
|
req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
|
59
91
|
req.parse(socket)
|
60
92
|
|
61
|
-
parse_webrick_request(req,
|
93
|
+
parse_webrick_request(req, splat_values)
|
62
94
|
end
|
63
95
|
|
64
|
-
#
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
def parse_webrick_request(req, parse_yaml=false)
|
96
|
+
# Parses a WEBrick::HTTPRequest, with the same activity as
|
97
|
+
# parse_http_request.
|
98
|
+
def parse_webrick_request(req, splat_values=true)
|
69
99
|
headers = {}
|
70
100
|
req.header.each_pair do |key, values|
|
71
|
-
headers[headerize(key)] =
|
72
|
-
|
73
|
-
end
|
74
|
-
end
|
101
|
+
headers[headerize(key)] = splat_values ? splat(values) : values
|
102
|
+
end if req.header
|
75
103
|
|
76
104
|
params = {}
|
77
|
-
req.query.each_pair do |key,
|
78
|
-
|
79
|
-
|
80
|
-
|
105
|
+
req.query.each_pair do |key, value|
|
106
|
+
# no sense for how robust this is...
|
107
|
+
# In tests value is (always?) a WEBrick::HTTPUtils::FormData. Each
|
108
|
+
# data is likewise a FormData. If FormData is a file, it has a
|
109
|
+
# filename and you have to try [] to get the content-type.
|
110
|
+
# Senseless. No wonder WEBrick has no documentation, who could
|
111
|
+
# write it?
|
112
|
+
values = []
|
113
|
+
value.each_data do |data|
|
114
|
+
values << if data.filename
|
115
|
+
{'Filename' => data.filename, 'Content-Type' => data['Content-Type']}
|
116
|
+
else
|
117
|
+
data.to_s
|
118
|
+
end
|
81
119
|
end
|
82
|
-
|
83
|
-
|
84
|
-
|
120
|
+
|
121
|
+
params[key] = splat_values ? splat(values) : values
|
122
|
+
end if req.query
|
85
123
|
|
86
|
-
{ :url =>
|
87
|
-
:
|
88
|
-
:
|
124
|
+
{ :url => headers['Host'] ? File.join("http://", headers['Host'], req.path_info) : req.path_info,
|
125
|
+
:request_method => req.request_method,
|
126
|
+
:version => req.http_version.to_s,
|
89
127
|
:headers => headers,
|
90
128
|
:params => params}
|
91
129
|
end
|
92
130
|
|
93
|
-
|
131
|
+
# Parses the input CGI into a hash that may be resubmitted by Dispatch.
|
132
|
+
# To work properly, the standard CGI environmental variables must be
|
133
|
+
# set in ENV.
|
134
|
+
#
|
135
|
+
def parse_cgi_request(cgi, splat_values=true)
|
94
136
|
headers = {}
|
95
137
|
ENV.each_pair do |key, values|
|
96
138
|
key = case key
|
139
|
+
when "HTTP_VERSION" then next
|
97
140
|
when /^HTTP_(.*)/ then $1
|
98
141
|
when 'CONTENT_TYPE' then key
|
99
142
|
else next
|
100
143
|
end
|
101
144
|
|
102
|
-
headers[headerize(key)] =
|
103
|
-
objectify(value, false)
|
104
|
-
end
|
145
|
+
headers[headerize(key)] = splat_values ? splat(values) : values
|
105
146
|
end
|
106
147
|
|
107
148
|
params = {}
|
108
149
|
cgi.params.each_pair do |key, values|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
150
|
+
values = values.collect do |value|
|
151
|
+
case
|
152
|
+
when !value.respond_to?(:read)
|
153
|
+
value
|
154
|
+
when value.original_filename.empty?
|
155
|
+
value.read
|
156
|
+
else
|
157
|
+
{'Filename' => value.original_filename, 'Content-Type' => value.content_type}
|
116
158
|
end
|
117
|
-
|
118
|
-
objectify(value, parse_yaml)
|
119
159
|
end
|
160
|
+
|
161
|
+
params[key] = splat_values ? splat(values) : values
|
120
162
|
end
|
121
163
|
|
122
|
-
url
|
123
|
-
|
124
|
-
|
125
|
-
:http_version => ENV['SERVER_PROTOCOL'], # right or no?
|
126
|
-
:request_method => ENV['REQUEST_METHOD'],
|
164
|
+
{ :url => File.join("http://", headers['Host'], ENV['PATH_INFO']),
|
165
|
+
:request_method => ENV['REQUEST_METHOD'],
|
166
|
+
:version => ENV['HTTP_VERSION'] =~ /^HTTP\/(.*)$/ ? $1 : ENV['HTTP_VERSION'],
|
127
167
|
:headers => headers,
|
128
168
|
:params => params}
|
129
169
|
end
|
@@ -139,22 +179,9 @@ module Tap
|
|
139
179
|
else File.join(base, action)
|
140
180
|
end
|
141
181
|
end
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
CGI_VARIABLES = %w{
|
146
|
-
AUTH_TYPE HTTP_HOST REMOTE_IDENT
|
147
|
-
CONTENT_LENGTH HTTP_NEGOTIATE REMOTE_USER
|
148
|
-
CONTENT_TYPE HTTP_PRAGMA REQUEST_METHOD
|
149
|
-
GATEWAY_INTERFACE HTTP_REFERER SCRIPT_NAME
|
150
|
-
HTTP_ACCEPT HTTP_USER_AGENT SERVER_NAME
|
151
|
-
HTTP_ACCEPT_CHARSET PATH_INFO SERVER_PORT
|
152
|
-
HTTP_ACCEPT_ENCODING PATH_TRANSLATED SERVER_PROTOCOL
|
153
|
-
HTTP_ACCEPT_LANGUAGE QUERY_STRING SERVER_SOFTWARE
|
154
|
-
HTTP_CACHE_CONTROL REMOTE_ADDR
|
155
|
-
HTTP_FROM REMOTE_HOST}
|
156
|
-
|
157
|
-
# Headerizes an underscored string.
|
182
|
+
|
183
|
+
# Headerizes an underscored string. The input is be converted to
|
184
|
+
# a string using to_s.
|
158
185
|
#
|
159
186
|
# headerize('SOME_STRING') # => 'Some-String'
|
160
187
|
# headerize('some string') # => 'Some-String'
|
@@ -166,64 +193,34 @@ module Tap
|
|
166
193
|
$1.upcase + $2.downcase
|
167
194
|
end.join("-")
|
168
195
|
end
|
169
|
-
|
170
|
-
def collect(array)
|
171
|
-
array = [array] unless array.kind_of?(Array)
|
172
|
-
|
173
|
-
array.collect! do |value|
|
174
|
-
yield(value)
|
175
|
-
end
|
176
|
-
|
177
|
-
case array.length
|
178
|
-
when 0 then nil
|
179
|
-
when 1 then array.first
|
180
|
-
else array
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
def objectify(str, parse_yaml=false)
|
185
|
-
return str unless str.kind_of?(String)
|
186
|
-
|
187
|
-
case str
|
188
|
-
when /^\d+(\.\d+)?$/ then YAML.load(str)
|
189
|
-
when /^\s*$/ then nil
|
190
|
-
when /^---\s*\n/
|
191
|
-
parse_yaml ? YAML.load(str) : str
|
192
|
-
else str
|
193
|
-
end
|
194
|
-
end
|
195
196
|
|
196
|
-
#
|
197
|
-
#
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
197
|
+
# Returns the first member of arrays length <= 1, or the array in all
|
198
|
+
# other cases. Splat is useful to simplify hashes of http headers
|
199
|
+
# and parameters that may have multiple values, but typically only
|
200
|
+
# have one.
|
201
|
+
#
|
202
|
+
# splat([]) # => nil
|
203
|
+
# splat([:one]) # => :one
|
204
|
+
# splat([:one, :two]) # => [:one, :two]
|
205
|
+
#
|
206
|
+
def splat(array)
|
207
|
+
return array unless array.kind_of?(Array)
|
207
208
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
else result[key.to_sym] = value
|
213
|
-
end
|
209
|
+
case array.length
|
210
|
+
when 0 then nil
|
211
|
+
when 1 then array.first
|
212
|
+
else array
|
214
213
|
end
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
#
|
220
|
-
|
221
|
-
#
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
def inflate(str)
|
226
|
-
Zlib::GzipReader.new( StringIO.new( str ) ).read
|
214
|
+
end
|
215
|
+
|
216
|
+
# Inflates (ie unzips) a gzip string, as may be returned by requests
|
217
|
+
# that accept 'gzip' and 'deflate' content encoding.
|
218
|
+
#
|
219
|
+
#--
|
220
|
+
# Helpers.inflate(res.body) if res['content-encoding'] == 'gzip'
|
221
|
+
#
|
222
|
+
def inflate(str)
|
223
|
+
Zlib::GzipReader.new( StringIO.new( str ) ).read
|
227
224
|
end
|
228
225
|
end
|
229
226
|
end
|
data/lib/tap/http/request.rb
CHANGED
@@ -4,7 +4,7 @@ require 'thread'
|
|
4
4
|
module Tap
|
5
5
|
module Http
|
6
6
|
|
7
|
-
# ::manifest submits an http request
|
7
|
+
# :startdoc::manifest submits an http request
|
8
8
|
#
|
9
9
|
# Request is a base class for submitting HTTP requests from a request
|
10
10
|
# hash. Multiple requests may be submitted on individual threads, up
|
data/lib/tap/test/http_test.rb
CHANGED
@@ -11,8 +11,7 @@ module Tap
|
|
11
11
|
#
|
12
12
|
module HttpTest
|
13
13
|
|
14
|
-
#
|
15
|
-
# are echoed back.
|
14
|
+
# Server echos back all requests.
|
16
15
|
class Server
|
17
16
|
include Singleton
|
18
17
|
include WEBrick
|
@@ -144,150 +143,6 @@ module Tap
|
|
144
143
|
end
|
145
144
|
URI.encode(query.join('&'))
|
146
145
|
end
|
147
|
-
|
148
|
-
module RequestLibrary
|
149
|
-
def get_request
|
150
|
-
msg = <<-_end_of_message_
|
151
|
-
GET /path?str=value&int=123&yaml=---+%0A-+a%0A-+b%0A-+c%0A&float=1.23 HTTP/1.1
|
152
|
-
Host: www.example.com
|
153
|
-
Keep-Alive: 300
|
154
|
-
Connection: keep-alive
|
155
|
-
_end_of_message_
|
156
|
-
end
|
157
|
-
|
158
|
-
def _get_request
|
159
|
-
{ :url => "http://www.example.com/path",
|
160
|
-
:http_version => '1.1',
|
161
|
-
:request_method => 'GET',
|
162
|
-
:headers => {
|
163
|
-
"Host" => "www.example.com",
|
164
|
-
"Keep-Alive" => 300,
|
165
|
-
"Connection" => 'keep-alive'},
|
166
|
-
:params => {
|
167
|
-
'str' => 'value',
|
168
|
-
'int' => 123,
|
169
|
-
'float' => 1.23,
|
170
|
-
'yaml' => ['a', 'b', 'c']}
|
171
|
-
}
|
172
|
-
end
|
173
|
-
|
174
|
-
def get_request_with_multiple_values
|
175
|
-
msg = <<-_end_of_message_
|
176
|
-
GET /path?one=value&one=123&one=---+%0A-+a%0A-+b%0A-+c%0A&two=1.23 HTTP/1.1
|
177
|
-
Host: www.example.com
|
178
|
-
Keep-Alive: 300
|
179
|
-
Connection: keep-alive
|
180
|
-
_end_of_message_
|
181
|
-
end
|
182
|
-
|
183
|
-
def _get_request_with_multiple_values
|
184
|
-
{ :url => "http://www.example.com/path",
|
185
|
-
:http_version => '1.1',
|
186
|
-
:request_method => 'GET',
|
187
|
-
:headers => {
|
188
|
-
"Host" => "www.example.com",
|
189
|
-
"Keep-Alive" => 300,
|
190
|
-
"Connection" => 'keep-alive'},
|
191
|
-
:params => {
|
192
|
-
'one' => ['value', 123, ['a', 'b', 'c']],
|
193
|
-
'two' => 1.23}
|
194
|
-
}
|
195
|
-
end
|
196
|
-
|
197
|
-
def multipart_request
|
198
|
-
msg = <<-_end_of_message_
|
199
|
-
POST /path HTTP/1.1
|
200
|
-
Host: www.example.com
|
201
|
-
Content-Type: multipart/form-data; boundary=1234567890
|
202
|
-
Content-Length: 305
|
203
|
-
|
204
|
-
--1234567890
|
205
|
-
Content-Disposition: form-data; name="str"
|
206
|
-
|
207
|
-
string value
|
208
|
-
--1234567890
|
209
|
-
Content-Disposition: form-data; name="int"
|
210
|
-
|
211
|
-
123
|
212
|
-
--1234567890
|
213
|
-
Content-Disposition: form-data; name="float"
|
214
|
-
|
215
|
-
1.23
|
216
|
-
--1234567890
|
217
|
-
Content-Disposition: form-data; name="yaml"
|
218
|
-
|
219
|
-
---
|
220
|
-
- a
|
221
|
-
- b
|
222
|
-
- c
|
223
|
-
--1234567890--
|
224
|
-
_end_of_message_
|
225
|
-
|
226
|
-
msg.gsub(/\n/, "\r\n")
|
227
|
-
end
|
228
|
-
|
229
|
-
def _multipart_request
|
230
|
-
{ :url => "http://www.example.com/path",
|
231
|
-
:http_version => '1.1',
|
232
|
-
:request_method => 'POST',
|
233
|
-
:headers => {
|
234
|
-
"Host" => 'www.example.com',
|
235
|
-
"Content-Type" => "multipart/form-data; boundary=1234567890",
|
236
|
-
"Content-Length" => 305},
|
237
|
-
:params => {
|
238
|
-
'str' => 'string value',
|
239
|
-
'int' => 123,
|
240
|
-
'float' => 1.23,
|
241
|
-
'yaml' => ['a', 'b', 'c']}
|
242
|
-
}
|
243
|
-
end
|
244
|
-
|
245
|
-
def multipart_request_with_multiple_values
|
246
|
-
msg = <<-_end_of_message_
|
247
|
-
POST /path HTTP/1.1
|
248
|
-
Host: www.example.com
|
249
|
-
Content-Type: multipart/form-data; boundary=1234567890
|
250
|
-
Content-Length: 302
|
251
|
-
|
252
|
-
--1234567890
|
253
|
-
Content-Disposition: form-data; name="one"
|
254
|
-
|
255
|
-
string value
|
256
|
-
--1234567890
|
257
|
-
Content-Disposition: form-data; name="one"
|
258
|
-
|
259
|
-
123
|
260
|
-
--1234567890
|
261
|
-
Content-Disposition: form-data; name="two"
|
262
|
-
|
263
|
-
1.23
|
264
|
-
--1234567890
|
265
|
-
Content-Disposition: form-data; name="one"
|
266
|
-
|
267
|
-
---
|
268
|
-
- a
|
269
|
-
- b
|
270
|
-
- c
|
271
|
-
--1234567890--
|
272
|
-
_end_of_message_
|
273
|
-
|
274
|
-
msg.gsub(/\n/, "\r\n")
|
275
|
-
end
|
276
|
-
|
277
|
-
def _multipart_request_with_multiple_values
|
278
|
-
{ :url => "http://www.example.com/path",
|
279
|
-
:http_version => '1.1',
|
280
|
-
:request_method => 'POST',
|
281
|
-
:headers => {
|
282
|
-
"Host" => 'www.example.com',
|
283
|
-
"Content-Type" => "multipart/form-data; boundary=1234567890",
|
284
|
-
"Content-Length" => 302},
|
285
|
-
:params => {
|
286
|
-
'one' => ['string value', 123, ['a', 'b', 'c']],
|
287
|
-
'two' => 1.23}
|
288
|
-
}
|
289
|
-
end
|
290
|
-
end
|
291
146
|
end
|
292
147
|
end
|
293
148
|
end
|
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.0
|
4
|
+
version: 0.1.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-
|
12
|
+
date: 2008-11-25 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -43,7 +43,7 @@ files:
|
|
43
43
|
- README
|
44
44
|
- MIT-LICENSE
|
45
45
|
has_rdoc: true
|
46
|
-
homepage: http://rubyforge.org/projects/tap
|
46
|
+
homepage: http://tap.rubyforge.org/projects/tap-http
|
47
47
|
post_install_message:
|
48
48
|
rdoc_options: []
|
49
49
|
|
@@ -64,7 +64,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements: []
|
65
65
|
|
66
66
|
rubyforge_project: tap
|
67
|
-
rubygems_version: 1.3.
|
67
|
+
rubygems_version: 1.3.1
|
68
68
|
signing_key:
|
69
69
|
specification_version: 2
|
70
70
|
summary: A task library for submitting http requests using Tap.
|