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 +19 -0
- data/README +25 -0
- data/cgi/echo.rb +24 -0
- data/cgi/http_to_yaml.rb +108 -0
- data/cgi/parse_http.rb +129 -0
- data/lib/tap/http/dispatch.rb +220 -0
- data/lib/tap/http/helpers.rb +230 -0
- data/lib/tap/http/request.rb +117 -0
- data/lib/tap/test/http_test.rb +296 -0
- data/tap.yml +0 -0
- metadata +72 -0
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
|
data/cgi/http_to_yaml.rb
ADDED
@@ -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
|
+
|