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