tap-http 0.0.1

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/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2008, Regents of the University of Colorado.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
4
+ software and associated documentation files (the "Software"), to deal in the Software
5
+ without restriction, including without limitation the rights to use, copy, modify, merge,
6
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7
+ to whom the Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or
10
+ substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
16
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
19
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,25 @@
1
+ = {TapHttp}[http://tap.rubyforge.org/tap-http]
2
+
3
+ A task library for submitting http requests using {Tap}[http://tap.rubyforge.org].
4
+
5
+ == Description
6
+
7
+ * Lighthouse[http://bahuvrihi.lighthouseapp.com/projects/9908-tap-task-application/tickets]
8
+ * Github[http://github.com/bahuvrihi/tap-http/tree/master]
9
+ * {Google Group}[http://groups.google.com/group/ruby-on-tap]
10
+
11
+ === Usage
12
+
13
+ == Installation
14
+
15
+ TapHttp is available as a gem on RubyForge[http://rubyforge.org/projects/tap]. Use:
16
+
17
+ % gem install tap-http
18
+
19
+ == Info
20
+
21
+ Copyright (c) 2006-2008, Regents of the University of Colorado.
22
+ Developer:: {Simon Chiang}[http://bahuvrihi.wordpress.com], {Biomolecular Structure Program}[http://biomol.uchsc.edu/], {Hansen Lab}[http://hsc-proteomics.uchsc.edu/hansenlab/]
23
+ Support:: CU Denver School of Medicine Deans Academic Enrichment Fund
24
+ Licence:: {MIT-Style}[link:files/MIT-LICENSE.html]
25
+
data/cgi/echo.rb ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ ####################################
4
+ # Echos back the HTTP header and parameters as YAML.
5
+ #
6
+ # Copyright (c) 2008, Regents of the University of Colorado
7
+ # Developer: Simon Chiang, Biomolecular Structure Program
8
+ # Homepage: http://hsc-proteomics.ucdenver.edu/hansen_lab
9
+ #
10
+ ####################################
11
+
12
+ require 'cgi'
13
+ require 'tap/http/helpers'
14
+
15
+ cgi = CGI.new
16
+ cgi.out("text/plain") do
17
+ begin
18
+ request = Tap::Http::Helpers.parse_cgi_request(cgi)
19
+ request[:headers].to_yaml + request[:params].to_yaml
20
+ rescue
21
+ "Error: #{$!.message}\n" +
22
+ $!.backtrace.join("\n")
23
+ end
24
+ end
@@ -0,0 +1,108 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ #################################################
4
+ #
5
+ # Echos back redirected HTTP requests as YAML, suitable for use with the Tap::Net::Submit
6
+ # task. All HTTP parameters and headers are echoed back directly, except for the
7
+ # '__original_action' parameter which is used in conjuction with the 'Referer' header to
8
+ # reconstruct the original url of the request. The '__original_action' parameter is not echoed.
9
+ #
10
+ # For example:
11
+ # __original_action Referer echoed url
12
+ # http://www.example.com?key=value (any) http://www.example.com?key=value
13
+ # /page http://www.example.com http://www.example.com/page
14
+ #
15
+ # Simply drop this script into a cgi directory, and send it a redirected request. See the
16
+ # RedirectHTTP Firefox extension for a simple way of redirecting requests to this script.
17
+ #
18
+ # Developer: Simon Chiang, Biomolecular Structure Program
19
+ # Homepage: http://tap.rubyforge.org
20
+ # Licence: MIT-STYLE
21
+ #
22
+ # Copyright (c) 2008, Regents of the University of Colorado
23
+ #
24
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this
25
+ # software and associated documentation files (the "Software"), to deal in the Software
26
+ # without restriction, including without limitation the rights to use, copy, modify, merge,
27
+ # publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
28
+ # to whom the Software is furnished to do so, subject to the following conditions:
29
+ #
30
+ # The above copyright notice and this permission notice shall be included in all copies or
31
+ # substantial portions of the Software.
32
+ #
33
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
34
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
35
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
36
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
37
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
38
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
39
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
40
+ # OTHER DEALINGS IN THE SOFTWARE.
41
+ #
42
+ #################################################
43
+
44
+ require 'rubygems'
45
+ require 'cgi'
46
+ require 'yaml'
47
+ require 'net/http'
48
+ require 'tap/http/helpers'
49
+
50
+ # included to sort the hash keys
51
+ class Hash
52
+ def to_yaml( opts = {} )
53
+ YAML::quick_emit( object_id, opts ) do |out|
54
+ out.map( taguri, to_yaml_style ) do |map|
55
+ sorted_keys = keys
56
+ sorted_keys = begin
57
+ sorted_keys.sort
58
+ rescue
59
+ sorted_keys.sort_by {|k| k.to_s} rescue sorted_keys
60
+ end
61
+
62
+ sorted_keys.each do |k|
63
+ map.add( k, fetch(k) )
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ cgi = CGI.new("html3")
71
+ begin
72
+
73
+ #
74
+ # gather configs
75
+ #
76
+
77
+ config = {}
78
+ Tap::Http::Helpers.parse_cgi_request(cgi).each_pair do |key, value|
79
+ config[key.to_s] = value
80
+ end
81
+
82
+ original_action = config['params'].delete("__original_action").to_s
83
+ referer = config['headers']['Referer'].to_s
84
+ config['url'] = Tap::Http::Helpers.determine_url(original_action, referer)
85
+ config['headers']['Host'] = URI.parse(config['url']).host
86
+
87
+ #
88
+ # format output
89
+ #
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" }
93
+
94
+ cgi.out('text/plain') do
95
+ %Q{# Copy and paste into a configuration file. Multiple configs
96
+ # 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
+ - #{config.to_yaml[5..-1].gsub(/\n/, "\n ")}
100
+ }
101
+ end
102
+
103
+ rescue
104
+ cgi.out("text/plain") do
105
+ "Error: #{$!.message}\n" +
106
+ $!.backtrace.join("\n")
107
+ end
108
+ end
data/cgi/parse_http.rb ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ #################################################
4
+ #
5
+ # Developer: Simon Chiang, Biomolecular Structure Program
6
+ # Homepage: http://tap.rubyforge.org
7
+ # Licence: MIT-STYLE
8
+ #
9
+ # Copyright (c) 2008, Regents of the University of Colorado
10
+ #
11
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this
12
+ # software and associated documentation files (the "Software"), to deal in the Software
13
+ # without restriction, including without limitation the rights to use, copy, modify, merge,
14
+ # publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
15
+ # to whom the Software is furnished to do so, subject to the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be included in all copies or
18
+ # substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27
+ # OTHER DEALINGS IN THE SOFTWARE.
28
+ #
29
+ #################################################
30
+
31
+ require 'rubygems'
32
+ require 'cgi'
33
+ require 'tap/http/helpers'
34
+
35
+ # included to sort the hash keys
36
+ class Hash
37
+ def to_yaml( opts = {} )
38
+ YAML::quick_emit( object_id, opts ) do |out|
39
+ out.map( taguri, to_yaml_style ) do |map|
40
+ sorted_keys = keys
41
+ sorted_keys = begin
42
+ sorted_keys.sort
43
+ rescue
44
+ sorted_keys.sort_by {|k| k.to_s} rescue sorted_keys
45
+ end
46
+
47
+ sorted_keys.each do |k|
48
+ map.add( k, fetch(k) )
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ cgi = CGI.new("html3")
56
+ begin
57
+
58
+ http_request_key = "http_request"
59
+ http_request = cgi[http_request_key]
60
+ http_request = http_request.respond_to?(:read) ? http_request.read : http_request.to_s
61
+
62
+ case
63
+ when http_request.strip.empty?
64
+
65
+ cgi.out do
66
+ cgi.html do
67
+ cgi.body do %Q{
68
+ <h1>Parse HTTP Parameters</h1>
69
+
70
+ <p>Enter an HTTP request, like the ones you can capture using the
71
+ <a href='https://addons.mozilla.org/en-US/firefox/addon/3829'>LiveHTTPHeaders</a> addon for
72
+ <a href='http://www.mozilla.com/en-US/firefox/'>Firefox</a>.
73
+ </p>
74
+
75
+ <form action='' method='post'>
76
+ <textarea rows='20' cols='60' name='#{http_request_key}'></textarea>
77
+ <br/>
78
+ <input type='submit' value='Parse'>
79
+ </form>
80
+
81
+ <p>Note the request must be properly formated. For example:</p>
82
+
83
+ <pre>
84
+ GET / HTTP/1.1
85
+ Host: tap.rubyforge.org
86
+ User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12
87
+ Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
88
+ Accept-Language: en-us,en;q=0.5
89
+ Accept-Encoding: gzip,deflate
90
+ Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
91
+ Keep-Alive: 300
92
+ Connection: keep-alive
93
+ </pre>}
94
+ end
95
+ end
96
+ end
97
+
98
+ else
99
+ config = {}
100
+ Tap::Http::Helpers.parse_http_request(http_request).each_pair do |key, value|
101
+ config[key.to_s] = value
102
+ end
103
+
104
+ help = cgi.a(Tap::Http::Helpers::HELP_URL) { "help" }
105
+ how_to_get_cookies = cgi.a(Tap::Http::Helpers::COOKIES_HELP_URL) { "how to get cookies" }
106
+
107
+ cgi.out do
108
+ cgi.html do
109
+ cgi.body do
110
+ cgi.pre do %Q{
111
+ # Copy and paste into a configuration file. Multiple configs
112
+ # can be added to a single file to perform batch submission.
113
+ #
114
+ # If you need cookies, see #{how_to_get_cookies} or the #{help}.
115
+ - #{config.to_yaml[5..-1].gsub(/\n/, "\n ")}
116
+ }
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ end
123
+
124
+ rescue
125
+ cgi.out("text/plain") do
126
+ "Error: #{$!.message}\n" +
127
+ $!.backtrace.join("\n")
128
+ end
129
+ end
@@ -0,0 +1,220 @@
1
+ require 'tap/http/helpers'
2
+ require 'net/http'
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
+ module Tap
24
+ module Http
25
+
26
+ # Dispatch provides methods for constructing and submitting get and post
27
+ # HTTP requests.
28
+ module Dispatch
29
+ REQUEST_KEYS = [:url, :request_method, :headers, :params, :redirection_limit]
30
+
31
+ module_function
32
+
33
+ # Constructs and submits a request to the url using the headers and parameters.
34
+ # Returns the response from the submission.
35
+ #
36
+ # res = submit_request("http://www.google.com/search", {:request_method => 'get'}, {:q => 'tap rubyforge'})
37
+ # # => <Net::HTTPOK 200 OK readbody=true>
38
+ #
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
46
+ url_or_uri = config[:url]
47
+ params = config[:params] || {}
48
+ headers = headerize_keys( config[:headers] || {})
49
+ request_method = (config[:request_method] || 'GET').to_s
50
+
51
+ uri = url_or_uri.kind_of?(URI) ? url_or_uri : URI.parse(url_or_uri)
52
+ uri.path = "/" if uri.path.empty?
53
+
54
+ # construct the request based on the method
55
+ request = case request_method
56
+ when /^get$/i then construct_get(uri, headers, params)
57
+ when /^post$/i then construct_post(uri, headers, params)
58
+ else
59
+ raise ArgumentError.new("Missing or unsupported request_method: #{request_method}")
60
+ end
61
+
62
+ # 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
71
+
72
+ # submit the request
73
+ res = Object::Net::HTTP.new(uri.host, uri.port).start do |http|
74
+ yield(http) if block_given?
75
+ http.request(request)
76
+ end
77
+
78
+ # fetch redirections
79
+ redirection_limit = config[:redirection_limit]
80
+ redirection_limit ? fetch_redirection(res, redirection_limit) : res
81
+ end
82
+
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.
87
+ #
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
+ #
91
+ # Supported content-types:
92
+ # - application/x-www-form-urlencoded (the default)
93
+ # - multipart/form-data
94
+ #
95
+ def construct_post(uri, headers, params)
96
+ req = Object::Net::HTTP::Post.new( URI.encode("#{uri.path}#{construct_query(uri)}") )
97
+ headers = headerize_keys(headers)
98
+ content_type = headers['Content-Type']
99
+
100
+ case content_type
101
+ when /multipart\/form-data/i
102
+ # extract the boundary if it exists
103
+ content_type =~ /boundary=(.*)/i
104
+ boundary = $1 || rand.to_s[2..20]
105
+
106
+ req.body = format_multipart_form_data(boundary, params)
107
+ headers['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
108
+ headers['Content-Length'] = req.body.length
109
+ 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
113
+ end
114
+
115
+ headers.each_pair { |key, value| req[key] = value }
116
+ req
117
+ end
118
+
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.
121
+ def construct_get(uri, headers, params)
122
+ req = Object::Net::HTTP::Get.new( URI.encode("#{uri.path}#{construct_query(uri, params)}") )
123
+ headerize_keys(headers).each_pair { |key, value| req[key] = value }
124
+ req
125
+ end
126
+
127
+ # Checks the type of the response; if it is a redirection, get the redirection,
128
+ # otherwise return the response.
129
+ #
130
+ # 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.
133
+ def fetch_redirection(res, limit=10)
134
+ raise ArgumentError, 'Could not follow all the redirections.' if limit == 0
135
+
136
+ case res
137
+ when Object::Net::HTTPRedirection
138
+ redirect = Object::Net::HTTP.get_response( URI.parse(res['location']) )
139
+ fetch_redirection(redirect, limit - 1)
140
+ when Object::Net::HTTPSuccess then res
141
+ else raise StandardError, res.error!
142
+ end
143
+ end
144
+
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)
150
+ result = {}
151
+ headers.each_pair do |key, value|
152
+ result[Helpers.headerize(key)] = value
153
+ end
154
+ result
155
+ end
156
+
157
+ # Returns a URI query constructed from the query in uri and the input parameters.
158
+ # The query is not encoded, so you may need to URI.encode it later.
159
+ def construct_query(uri, params={})
160
+ query = []
161
+ params.each_pair do |key, values|
162
+ values = values.kind_of?(Array) ? values : [values]
163
+ values.each { |value| query << "#{key}=#{value}" }
164
+ end
165
+ query << uri.query if uri.query
166
+ "#{query.empty? ? '' : '?'}#{query.join('&')}"
167
+ end
168
+
169
+ def format_www_form_urlencoded(params={})
170
+ query = []
171
+ params.each_pair do |key, values|
172
+ values = values.kind_of?(Array) ? values : [values]
173
+ values.each { |value| query << "#{key}=#{value}" }
174
+ end
175
+ URI.encode( query.join('&') )
176
+ end
177
+
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)
194
+ body = []
195
+ params.each_pair do |key, values|
196
+ values = values.kind_of?(Array) ? values : [values]
197
+
198
+ values.each do |value|
199
+ body << case value
200
+ when Hash
201
+ hash = headerize_keys(value)
202
+ filename = hash.delete('Filename') || ""
203
+ content = File.exists?(filename) ? File.read(filename) : ""
204
+
205
+ header = "Content-Disposition: form-data; name=\"#{key.to_s}\"; filename=\"#{filename}\"\r\n"
206
+ hash.each_pair { |key, value| header << "#{key}: #{value}\r\n" }
207
+ "#{header}\r\n#{content}\r\n"
208
+ else
209
+ %Q{Content-Disposition: form-data; name="#{key.to_s}"\r\n\r\n#{value.to_s}\r\n}
210
+ end
211
+ end
212
+ end
213
+
214
+ body.collect {|p| "--#{boundary}\r\n#{p}" }.join('') + "--#{boundary}--\r\n"
215
+ end
216
+
217
+ end
218
+ end
219
+ end
220
+
@@ -0,0 +1,230 @@
1
+ autoload(:WEBrick, 'webrick')
2
+ autoload(:Zlib, 'zlib')
3
+ autoload(:StringIO, 'stringio')
4
+
5
+ module Tap
6
+ module Http
7
+ module Helpers
8
+ module_function
9
+
10
+ # Parses a WEBrick::HTTPRequest from the input socket.
11
+ #
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)
17
+ #
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.
24
+ #
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.
29
+ #
30
+ # # Notice an empty line between the last header
31
+ # # (in this case 'Content-Length') and the body.
32
+ # msg = <<-_end_of_message_
33
+ # POST /path HTTP/1.1
34
+ # Content-Type: multipart/form-data; boundary=1234567890
35
+ # Content-Length: 158
36
+ #
37
+ # --1234567890
38
+ # Content-Disposition: form-data; name="one"
39
+ #
40
+ # value one
41
+ # --1234567890
42
+ # Content-Disposition: form-data; name="two"
43
+ #
44
+ # value two
45
+ # --1234567890--
46
+ # _end_of_message_
47
+ #
48
+ # # ensure the end of line characters are correct...
49
+ # socket = StringIO.new msg.gsub(/\n/, "\r\n")
50
+ #
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"}
54
+ #
55
+ def parse_http_request(socket, parse_yaml=false)
56
+ socket = StringIO.new(socket) if socket.kind_of?(String)
57
+
58
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
59
+ req.parse(socket)
60
+
61
+ parse_webrick_request(req, parse_yaml)
62
+ end
63
+
64
+ #
65
+ # TODO -- test with cookies, HTTPS?
66
+ #
67
+
68
+ def parse_webrick_request(req, parse_yaml=false)
69
+ headers = {}
70
+ req.header.each_pair do |key, values|
71
+ headers[headerize(key)] = collect(values) do |value|
72
+ objectify(value, false)
73
+ end
74
+ end
75
+
76
+ 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)
81
+ end
82
+ end
83
+
84
+ url = File.join("http://", headers['Host'], req.path_info)
85
+
86
+ { :url => url,
87
+ :http_version => req.http_version.to_s,
88
+ :request_method => req.request_method,
89
+ :headers => headers,
90
+ :params => params}
91
+ end
92
+
93
+ def parse_cgi_request(cgi, parse_yaml=false)
94
+ headers = {}
95
+ ENV.each_pair do |key, values|
96
+ key = case key
97
+ when /^HTTP_(.*)/ then $1
98
+ when 'CONTENT_TYPE' then key
99
+ else next
100
+ end
101
+
102
+ headers[headerize(key)] = collect(values) do |value|
103
+ objectify(value, false)
104
+ end
105
+ end
106
+
107
+ params = {}
108
+ 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
116
+ end
117
+
118
+ objectify(value, parse_yaml)
119
+ end
120
+ end
121
+
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'],
127
+ :headers => headers,
128
+ :params => params}
129
+ end
130
+
131
+ def determine_url(action, referer)
132
+ base = File.basename(referer)
133
+
134
+ case action
135
+ when /^https?:/ then action
136
+ when /\//
137
+ # only use host of page_url
138
+ File.join(base, action)
139
+ else File.join(base, action)
140
+ end
141
+ 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.
158
+ #
159
+ # headerize('SOME_STRING') # => 'Some-String'
160
+ # headerize('some string') # => 'Some-String'
161
+ # headerize('Some-String') # => 'Some-String'
162
+ #
163
+ def headerize(str)
164
+ str.to_s.gsub(/\s|-/, "_").split("_").collect do |s|
165
+ s =~ /^(.)(.*)/
166
+ $1.upcase + $2.downcase
167
+ end.join("-")
168
+ 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
+ # 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
207
+
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
214
+ 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
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,117 @@
1
+ require 'tap/http/dispatch'
2
+ require 'thread'
3
+
4
+ module Tap
5
+ module Http
6
+
7
+ # ::manifest submits an http request
8
+ #
9
+ # Request is a base class for submitting HTTP requests from a request
10
+ # hash. Multiple requests may be submitted on individual threads, up
11
+ # to a configurable limit.
12
+ #
13
+ # Configuration hashes are like the following:
14
+ #
15
+ # url: http://tap.rubyforge.org/
16
+ # request_method: GET
17
+ # headers: {}
18
+ # params: {}
19
+ #
20
+ # The only required field is the url (by default no headers or params
21
+ # are specified, and the request method is GET). Request requires
22
+ # hash inputs, which can be inconvenient from the command line. A
23
+ # good workaround is to save requests in a .yml file and use a simple
24
+ # workflow:
25
+ #
26
+ # [requests.yml]
27
+ # - url: http://tap.rubyforge.org/
28
+ # - url: http://tap.rubyforge.org/about.html
29
+ #
30
+ # % rap load requests.yml --:i request --+ dump
31
+ #
32
+ #--
33
+ # To generate configuration hashes from Firefox, see the redirect_http
34
+ # ubiquity command.
35
+ #++
36
+ class Request < Tap::Task
37
+ class << self
38
+ def intern(*args, &block)
39
+ instance = new(*args)
40
+ instance.extend Support::Intern(:process_response)
41
+ instance.process_response_block = block
42
+ instance
43
+ end
44
+ end
45
+
46
+ config :redirection_limit, 10, &c.integer # the redirection limit for the request
47
+ config :max_threads, 10, &c.integer # the maximum number of request threads
48
+
49
+ def process(*requests)
50
+ # build a queue of all the requests to be handled
51
+ queue = Queue.new
52
+ requests.each_with_index do |request, index|
53
+ request = symbolize_keys(request)
54
+
55
+ if request[:url] == nil
56
+ raise ArgumentError, "no url specified: #{request.inspect}"
57
+ end
58
+
59
+ queue.enq [request, index]
60
+ index += 1
61
+ end
62
+
63
+ # submit and retrieve all requests before processing
64
+ # responses. this assures responses are processed
65
+ # in order, in case it matters.
66
+ lock = Mutex.new
67
+ responses = []
68
+ request_threads = Array.new(max_threads) do
69
+ Thread.new do
70
+ begin
71
+ while !queue.empty?
72
+ request, index = queue.deq(true)
73
+
74
+ log(request[:request_method], request[:url])
75
+ if app.verbose
76
+ log 'headers', config[:headers].inspect
77
+ log 'params', config[:params].inspect
78
+ end
79
+
80
+ res = Dispatch.submit_request(request)
81
+ lock.synchronize { responses[index] = res }
82
+ end
83
+ rescue(ThreadError)
84
+ # Catch errors due to the queue being empty.
85
+ # (this should not occur as the queue is checked)
86
+ raise $! unless $!.message == 'queue empty'
87
+ end
88
+ end
89
+ end
90
+ request_threads.each {|thread| thread.join }
91
+
92
+ # process responses and collect results
93
+ responses.collect! do |res|
94
+ process_response(res)
95
+ end
96
+ end
97
+
98
+ # Hook for processing a response. By default process_response
99
+ # simply logs the response message and returns the response.
100
+ def process_response(res)
101
+ log(nil, res.message)
102
+ res
103
+ end
104
+
105
+ protected
106
+
107
+ # Return a new hash with all keys converted to symbols.
108
+ # Lifted from ActiveSupport
109
+ def symbolize_keys(hash)
110
+ hash.inject({}) do |options, (key, value)|
111
+ options[(key.to_sym rescue key) || key] = value
112
+ options
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,296 @@
1
+ require 'webrick'
2
+ require 'singleton'
3
+ require 'tap/test/subset_test'
4
+
5
+ module Tap
6
+ module Test
7
+
8
+ # HttpTest facilitates testing of HTTP requests by initializing a
9
+ # Webrick server that echos requests, and providing methods to
10
+ # validate echoed requests.
11
+ #
12
+ module HttpTest
13
+
14
+ # The test-specific web server. All request to the server
15
+ # are echoed back.
16
+ class Server
17
+ include Singleton
18
+ include WEBrick
19
+
20
+ attr_accessor :server_thread, :server
21
+
22
+ def initialize
23
+ # set the default log level to warn to prevent general access logs, unless otherwise specified
24
+ log_level = ENV["WEB_LOG_LEVEL"] ? ENV["WEB_LOG_LEVEL"].upcase : "WARN"
25
+ logger = Log.new($stderr, Log.const_get( log_level ) )
26
+
27
+ self.server = HTTPServer.new(:Port => 2000,
28
+ :Logger => logger,
29
+ :AccessLog => [
30
+ [ logger, AccessLog::COMMON_LOG_FORMAT ],
31
+ [ logger, AccessLog::REFERER_LOG_FORMAT ]])
32
+
33
+ server.mount_proc("/") do |req, res|
34
+ res.body << req.request_line
35
+ res.body << req.raw_header.join('')
36
+
37
+ # an extra line must be added to delimit the headers from the body.
38
+ if req.body
39
+ res.body << "\r\n"
40
+ res.body << req.body
41
+ end
42
+
43
+ res['Content-Type'] = "text/html"
44
+ end
45
+ end
46
+
47
+ # Starts the server on a new thread.
48
+ def start_web_server
49
+ self.server_thread ||= Thread.new { server.start }
50
+ end
51
+ end
52
+
53
+ def self.included(base)
54
+ base.send(:include, Tap::Test::SubsetTest)
55
+ end
56
+
57
+ # WEB subset of tests. Starts HTTPTest::Server if necessary
58
+ # and yields to the block.
59
+ def web_test
60
+ subset_test("WEB", "w") do
61
+ Server.instance.start_web_server
62
+ yield
63
+ end
64
+ end
65
+
66
+ REQUEST_ATTRIBUTES = %w{
67
+ request_method http_version
68
+
69
+ host port path
70
+ script_name path_info
71
+
72
+ header cookies query
73
+ accept accept_charset
74
+ accept_encoding accept_language
75
+
76
+ user
77
+ addr peeraddr
78
+ attributes
79
+ keep_alive}
80
+
81
+ UNCHECKED_REQUEST_ATTRIBUTES = %w{
82
+ request_line
83
+ unparsed_uri
84
+ request_uri
85
+ request_time
86
+ raw_header
87
+ query_string}
88
+
89
+ # Parses expected and actual as an http request (using Tap::Net.parse_http_request)
90
+ # and asserts that all of the REQUEST_ATTRIBUTES are equal. See the parse_http_request
91
+ # documentation for some important notes, particularly involving "\r\n" vs "\n" and
92
+ # post requests.
93
+ def assert_request_equal(expected, actual)
94
+ e = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
95
+ e.parse( StringIO.new(expected) )
96
+
97
+ a = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
98
+ a.parse( StringIO.new(actual) )
99
+
100
+ errors = []
101
+ REQUEST_ATTRIBUTES.each do |attribute|
102
+ exp = e.send(attribute)
103
+ act = a.send(attribute)
104
+ next if exp == act
105
+ errors << "<#{PP.singleline_pp(exp, '')}> expected for #{attribute} but was:\n<#{PP.singleline_pp(act, '')}>."
106
+ end
107
+
108
+ if errors.empty?
109
+ # this rather unecessary assertion is used simply to
110
+ # make assert_request_equal cause an assertion.
111
+ assert errors.empty?
112
+ else
113
+ flunk errors.join("\n")
114
+ end
115
+ end
116
+
117
+ # Convenience method that strips str, strips each line of str, and
118
+ # then rejoins them using "\r\n" as is typical of HTTP messages.
119
+ #
120
+ # strip_align %Q{
121
+ # GET /echo HTTP/1.1
122
+ # Accept: */*
123
+ # Host: localhost:2000}
124
+ #
125
+ # # => "GET /echo HTTP/1.1\r\nAccept: */*\r\nHost: localhost:2000\r\n"
126
+ def strip_align(str)
127
+ str.strip.split(/\r?\n/).collect do |line|
128
+ "#{line.strip}\r\n"
129
+ end.compact.join('')
130
+ end
131
+
132
+ # Turns a hash of parameters into an encoded HTTP query string.
133
+ # Multiple values for a given key can be specified by an array.
134
+ #
135
+ # to_query('key' => 'value', 'array' => ['one', 'two']) # => "array=one&array=two&key=value"
136
+ #
137
+ # Note: the order of the parameters in the result is determined
138
+ # by hash.each_pair and is thus fairly unpredicatable.
139
+ def to_query(hash)
140
+ query = []
141
+ hash.each_pair do |key,values|
142
+ values = values.kind_of?(Array) ? values : [values]
143
+ values.each { |value| query << "#{key}=#{value}" }
144
+ end
145
+ URI.encode(query.join('&'))
146
+ 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
+ end
292
+ end
293
+ end
294
+
295
+
296
+
data/tap.yml ADDED
File without changes
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tap-http
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Simon Chiang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-18 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: tap
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.11.1
24
+ version:
25
+ description:
26
+ email: simon.a.chiang@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README
33
+ - MIT-LICENSE
34
+ files:
35
+ - cgi/echo.rb
36
+ - cgi/http_to_yaml.rb
37
+ - cgi/parse_http.rb
38
+ - lib/tap/http/dispatch.rb
39
+ - lib/tap/http/helpers.rb
40
+ - lib/tap/http/request.rb
41
+ - lib/tap/test/http_test.rb
42
+ - tap.yml
43
+ - README
44
+ - MIT-LICENSE
45
+ has_rdoc: true
46
+ homepage: http://rubyforge.org/projects/tap
47
+ post_install_message:
48
+ rdoc_options: []
49
+
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ requirements: []
65
+
66
+ rubyforge_project: tap
67
+ rubygems_version: 1.3.0
68
+ signing_key:
69
+ specification_version: 2
70
+ summary: A task library for submitting http requests using Tap.
71
+ test_files: []
72
+