tap-http 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+