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