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