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 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
@@ -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
- # Constructs and submits a request to the url using the headers and parameters.
34
- # Returns the response from the submission.
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
- # res = submit_request("http://www.google.com/search", {:request_method => 'get'}, {:q => 'tap rubyforge'})
37
- # # => <Net::HTTPOK 200 OK readbody=true>
42
+ # Returns the response from the submission.
38
43
  #
39
- # Notes:
40
- # - A request method must be specified in the headers; currently only get and
41
- # post are supported. See construct_post for supported post content types.
42
- # - A url or a url (as would result from URI.parse(url)) can be provided to
43
- # submit request. The Net::HTTP object performing the submission is passed
44
- # to the block, if given, before the request is made.
45
- def submit_request(config) # :yields: http
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
- params = config[:params] || {}
48
- headers = headerize_keys( config[:headers] || {})
49
- request_method = (config[:request_method] || 'GET').to_s
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.new("Missing or unsupported request_method: #{request_method}")
66
+ raise ArgumentError, "unsupported request method: #{request_method}"
60
67
  end
61
68
 
62
69
  # set the http version
63
- # if version = config[:http_version]
64
- # version_method = "version_#{version.to_s.gsub(".", "_")}".to_sym
65
- # if Object::Net::HTTP.respond_to?(version_method)
66
- # Object::Net::HTTP.send(version_method)
67
- # else
68
- # raise ArgumentError.new("unsupported http_version: #{version}")
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 = Object::Net::HTTP.new(uri.host, uri.port).start do |http|
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 post query. The 'Content-Type' header determines the format of the body
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
- # Headers for the request are set in this method. If uri contains query parameters,
89
- # they will be included in the request URI.
90
+ # ==== Supported Content Types:
90
91
  #
91
- # Supported content-types:
92
- # - application/x-www-form-urlencoded (the default)
93
- # - multipart/form-data
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 = Object::Net::HTTP::Post.new( URI.encode("#{uri.path}#{construct_query(uri)}") )
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 /multipart\/form-data/i
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
- content_type =~ /boundary=(.*)/i
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(boundary, params)
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
- req.body = format_www_form_urlencoded(params)
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 get query. All parameters in uri and params are added to the
120
- # request URI. Headers for the request are set in this method.
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 = Object::Net::HTTP::Get.new( URI.encode("#{uri.path}#{construct_query(uri, params)}") )
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, get the redirection,
128
- # otherwise return the response.
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
- # - The redirections will only recurse for the input redirection limit (default 10)
132
- # - Responses that are not Net::HTTPRedirection or Net::HTTPSuccess raise an error.
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 ArgumentError, 'Could not follow all the redirections.' if limit == 0
165
+ raise 'exceeded the redirection limit' if limit < 1
135
166
 
136
167
  case res
137
- when Object::Net::HTTPRedirection
138
- redirect = Object::Net::HTTP.get_response( URI.parse(res['location']) )
168
+ when ::Net::HTTPRedirection
169
+ redirect = ::Net::HTTP.get_response( URI.parse(res['location']) )
139
170
  fetch_redirection(redirect, limit - 1)
140
- when Object::Net::HTTPSuccess then res
141
- else raise StandardError, res.error!
171
+ when ::Net::HTTPSuccess
172
+ res
173
+ else
174
+ raise StandardError, res.error!
142
175
  end
143
176
  end
144
177
 
145
- # Normalizes the header keys to a titleized, dasherized string.
146
- # 'some_header' => 'Some-Header'
147
- # :some_header => 'Some-Header'
148
- # 'some header' => 'Some-Header'
149
- def headerize_keys(headers)
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
- headers.each_pair do |key, value|
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
- # Returns a URI query constructed from the query in uri and the input parameters.
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
- def construct_query(uri, params={})
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) ? values : [values]
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) ? values : [values]
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
- # Returns a post body formatted as 'multipart/form-data'. Special formatting occures
179
- # if value in one of the key-value parameter pairs is an Array or Hash.
180
- #
181
- # Array values are treated as a multiple inputs for a single key; each array value
182
- # is assigned to the key. Hash values are treated as files, with all file-related
183
- # headers specified in the hash.
184
- #
185
- #--
186
- # Example:
187
- # "--1234", {:key => 'value'} =>
188
- # --1234
189
- # Content-Disposition: form-data; name="key"
190
- #
191
- # value
192
- # --1234--
193
- def format_multipart_form_data(boundary, params)
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) ? values : [values]
256
+ values = [values] unless values.kind_of?(Array)
197
257
 
198
258
  values.each do |value|
199
259
  body << case value
@@ -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
- # Notes:
13
- # - socket can be any kind of IO (ex File, StringIO)
14
- # - the socket should be in a position such that the next line
15
- # is the start of an HTTP request, and the request should
16
- # correctly formatted (see below)
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
- # == WEBrick parsing of HTTP format
19
- # WEBrick will parse headers then the body of a request, and
20
- # currently (1.8.6) considers an empty line as a break between
21
- # them. In general header parsing is forgiving with end-line
22
- # characters (ie "\r\n" and "\n" are both acceptable) but parsing
23
- # of multipart/form data IS NOT.
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
- # are "\r\n". The boundary is always started with "--" and the last
27
- # boundary completed with "--". As always, the content-length
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
- # req = Tap::Net.parse_http_request(socket)
52
- # req.header # => {"content-type" => ["multipart/form-data; boundary=1234567890"], "content-length" => ["158"]}
53
- # req.query # => {"one" => "value one", "two" => "value two"}
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
- def parse_http_request(socket, parse_yaml=false)
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, parse_yaml)
93
+ parse_webrick_request(req, splat_values)
62
94
  end
63
95
 
64
- #
65
- # TODO -- test with cookies, HTTPS?
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)] = collect(values) do |value|
72
- objectify(value, false)
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, values|
78
- params[key] = collect(values.to_ary) do |value|
79
- # TODO - special for multipart file data?
80
- objectify(value, parse_yaml)
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
- end
83
-
84
- url = File.join("http://", headers['Host'], req.path_info)
120
+
121
+ params[key] = splat_values ? splat(values) : values
122
+ end if req.query
85
123
 
86
- { :url => url,
87
- :http_version => req.http_version.to_s,
88
- :request_method => req.request_method,
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
- def parse_cgi_request(cgi, parse_yaml=false)
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)] = collect(values) do |value|
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
- params[key] = collect(values) do |value|
110
- if value.respond_to?(:read)
111
- value = if value.original_filename.empty?
112
- value.read
113
- else
114
- {'Filename' => value.original_filename, 'Content-Type' => value.content_type}
115
- end
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 = File.join("http://", headers['Host'], ENV['PATH_INFO'])
123
-
124
- { :url => url,
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
- HELP_URL = "http://"
144
- COOKIES_HELP_URL = "http://"
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
- # Performs a deep merge of two hashes where hash values for
197
- # corresponding keys are merged.
198
- def deep_merge(a,b)
199
- result = {}
200
- a.each_pair do |key, value|
201
- result[key] = case value
202
- when Hash then value.dup
203
- # when Array then value.dup
204
- else value
205
- end
206
- end
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
- b.each_pair do |key, value|
209
- case value
210
- when Hash then (result[key.to_sym] ||= {}).merge!(value)
211
- #when Array then (result[key.to_sym] ||= []).concat(value)
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
- result
217
- end
218
-
219
- # Inflates (ie unzips) a gzip string, as may be returned by requests
220
- # that accept 'gzip' and 'deflate' content encoding.
221
- #
222
- #--
223
- # Helpers.inflate(res.body) if res['content-encoding'] == 'gzip'
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
@@ -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
@@ -11,8 +11,7 @@ module Tap
11
11
  #
12
12
  module HttpTest
13
13
 
14
- # The test-specific web server. All request to the server
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.1
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-18 00:00:00 -07:00
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.0
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.