thin_http 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +28 -0
- data/Rakefile +82 -0
- data/lib/cookie_collection.rb +85 -0
- data/lib/multipart_form_data.rb +109 -0
- data/lib/thin_http.rb +254 -0
- data/test/cookie_collection_test.rb +14 -0
- data/test/multipart_form_data_test.rb +62 -0
- data/test/scaffold/ascii.txt +1 -0
- data/test/scaffold/form_data.msg +0 -0
- data/test/scaffold/form_data_unknown.msg +8 -0
- data/test/scaffold/htdocs/cgi/echo_get_params.cgi +5 -0
- data/test/scaffold/htdocs/cgi/echo_post_params.cgi +5 -0
- data/test/scaffold/htdocs/cgi/print_env.cgi +4 -0
- data/test/scaffold/htdocs/cgi/redirect_loop.cgi +9 -0
- data/test/scaffold/htdocs/cgi/redirect_to_external.cgi +7 -0
- data/test/scaffold/htdocs/cgi/redirect_to_print_env.cgi +10 -0
- data/test/scaffold/htdocs/cgi/set_cookie.cgi +7 -0
- data/test/scaffold/htdocs/index.html +7 -0
- data/test/scaffold/image.jpg +0 -0
- data/test/scaffold/unknown.yyy +1 -0
- data/test/test_helper.rb +31 -0
- data/test/thin_http_test.rb +163 -0
- metadata +88 -0
data/README
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
== ThinHTTP Library
|
2
|
+
|
3
|
+
[ThinHTTP] A light-weight and user friendly HTTP library.
|
4
|
+
[MultipartFormData] Facilitates in building multipart/form-data encoded messages.
|
5
|
+
[CookieCollection] Handles the storing, parsing, and managing of HTTP cookies.
|
6
|
+
|
7
|
+
== Applications
|
8
|
+
|
9
|
+
* Testing environments
|
10
|
+
* Embedded in full-blown HTTP clients
|
11
|
+
* Automating HTTP transactions
|
12
|
+
|
13
|
+
== Usage Examples (see "Examples" sections)
|
14
|
+
|
15
|
+
* ThinHTTP
|
16
|
+
* MultipartFormData
|
17
|
+
* MIME Library (http://mime.rubyforge.org/)
|
18
|
+
|
19
|
+
== Contact
|
20
|
+
|
21
|
+
Please email inquiries to pachl at ecentryx dot com.
|
22
|
+
|
23
|
+
[Home Page] http://thinhttp.rubyforge.org/
|
24
|
+
[RubyForge] http://rubyforge.org/projects/thinhttp/
|
25
|
+
|
26
|
+
== License
|
27
|
+
|
28
|
+
The entire ThinHTTP library is free to use under the terms of the Ruby license.
|
data/Rakefile
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'rake/contrib/rubyforgepublisher'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'webrick'
|
6
|
+
|
7
|
+
WEBRICK_PID_FILE = 'webrick/pid'
|
8
|
+
WEBRICK_LOG_FILE = 'webrick/log'
|
9
|
+
|
10
|
+
gem_spec = Gem::Specification.new do |s|
|
11
|
+
s.add_dependency 'mime', '>=0.1.0'
|
12
|
+
s.name = 'thin_http'
|
13
|
+
s.version = '0.1.0'
|
14
|
+
s.summary = 'Lightweight and user-friendly HTTP client library'
|
15
|
+
s.test_files = FileList['test/*_test.rb']
|
16
|
+
s.files = FileList['README', 'Rakefile', 'lib/**/*.rb', 'test/**/*']
|
17
|
+
s.author = 'Clint Pachl'
|
18
|
+
s.email = 'pachl@ecentryx.com'
|
19
|
+
s.homepage = 'http://thinhttp.rubyforge.org/'
|
20
|
+
s.rubyforge_project = 'thinhttp'
|
21
|
+
s.has_rdoc = true
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
Rake::GemPackageTask.new(gem_spec) do |pkg|
|
26
|
+
pkg.need_tar_gz = true
|
27
|
+
end
|
28
|
+
|
29
|
+
Rake::RDocTask.new do |rd|
|
30
|
+
rd.rdoc_files.include('README', 'lib/')
|
31
|
+
rd.rdoc_dir = 'html' # :publish task looks here
|
32
|
+
rd.main = 'README'
|
33
|
+
rd.options << '--all' << '--inline-source'
|
34
|
+
end
|
35
|
+
|
36
|
+
Rake::TestTask.new do |t|
|
37
|
+
t.libs << 'lib' << 'test'
|
38
|
+
t.pattern = 'test/*_test.rb'
|
39
|
+
t.warning = true
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'Publish docs to RubyForge'
|
43
|
+
task :publish => [:rerdoc] do
|
44
|
+
Rake::RubyForgePublisher.new('thinhttp', 'pachl').upload
|
45
|
+
end
|
46
|
+
|
47
|
+
namespace :webrick do
|
48
|
+
|
49
|
+
desc 'Start WEBrick (run before test task)'
|
50
|
+
task :start do
|
51
|
+
|
52
|
+
if File.exist?(WEBRICK_PID_FILE)
|
53
|
+
puts "Already running (if not, remove '#{WEBRICK_PID_FILE}')."
|
54
|
+
else
|
55
|
+
webrick_pid = fork do
|
56
|
+
STDERR.reopen(WEBRICK_LOG_FILE, "w")
|
57
|
+
|
58
|
+
s = WEBrick::HTTPServer.new(
|
59
|
+
:DocumentRoot => Dir::pwd + '/test/scaffold/htdocs',
|
60
|
+
:Port => ENV['WEBRICK_PORT'] || 2000
|
61
|
+
)
|
62
|
+
|
63
|
+
trap('INT') {s.shutdown}
|
64
|
+
s.logger.info "Root=#{s.config[:DocumentRoot]}"
|
65
|
+
s.start
|
66
|
+
end
|
67
|
+
|
68
|
+
open(WEBRICK_PID_FILE, 'w') {|f| f << webrick_pid}
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
desc 'Stop WEBrick (run after test task)'
|
74
|
+
task :stop do
|
75
|
+
if File.exist?(WEBRICK_PID_FILE)
|
76
|
+
webrick_pid = IO.read(WEBRICK_PID_FILE).to_i
|
77
|
+
Process.kill('INT', webrick_pid)
|
78
|
+
FileUtils.rm(WEBRICK_PID_FILE)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
#
|
4
|
+
# == Synopsis
|
5
|
+
#
|
6
|
+
# CookieCollection should be utilized by clients that need HTTP cookie
|
7
|
+
# management. This class assists in storing and parsing raw cookies and
|
8
|
+
# returning non-expired cookies for the cookie's originating server.
|
9
|
+
#
|
10
|
+
# *NOTE* This class is largely unimplemented and only stores and returns a
|
11
|
+
# single raw cookie header without any processing or domain discretion.
|
12
|
+
#
|
13
|
+
# == Implementation Notes
|
14
|
+
#
|
15
|
+
# * RFC 2109, version 1 cookies (http://tools.ietf.org/html/rfc2109)
|
16
|
+
# * Uses Netscapes' "Expires" instead of RFC's "Max-Age" attribute. Expires is
|
17
|
+
# an RFC 2822 date.
|
18
|
+
#
|
19
|
+
#--
|
20
|
+
# TODO Fully implement this class.
|
21
|
+
#++
|
22
|
+
#
|
23
|
+
class CookieCollection
|
24
|
+
|
25
|
+
#--
|
26
|
+
# XXX do we want to accept CGI::Cookie or raw cookie strings and convert to CGI::Cookie?
|
27
|
+
#++
|
28
|
+
def initialize
|
29
|
+
@cookies = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
#--
|
33
|
+
# Need to parse set-cookie response string and instantiate a CGI::Cookie for
|
34
|
+
# each and store in +@cookies+.
|
35
|
+
#
|
36
|
+
# Example of 2 cookies returned by WEBrick:
|
37
|
+
#
|
38
|
+
# "VAL1=Data1; Version=1; Expires=Sat, 11 Oct 2008 11:17:54 GMT, VAL2=Data2; Max-Age=3600"
|
39
|
+
#++
|
40
|
+
def store cookie
|
41
|
+
@cookies = cookie
|
42
|
+
end
|
43
|
+
|
44
|
+
def clear
|
45
|
+
@cookies = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
#--
|
49
|
+
# Don't return expired cookies.
|
50
|
+
# Commas seperate cookie values.
|
51
|
+
# Only compile cookies that have applicable domain and path.
|
52
|
+
# Cookie data needs to be ordered by path.
|
53
|
+
#
|
54
|
+
# Cookie header to send in an HTTP request to the originating server.
|
55
|
+
#
|
56
|
+
# Cookie Header Syntax:
|
57
|
+
# cookie = "Cookie:" cookie-version
|
58
|
+
# 1*((";" | ",") cookie-value)
|
59
|
+
# cookie-value = NAME "=" VALUE [";" path] [";" domain]
|
60
|
+
# cookie-version = "$Version" "=" value
|
61
|
+
# NAME = attr
|
62
|
+
# VALUE = value
|
63
|
+
# path = "$Path" "=" value
|
64
|
+
# domain = "$Domain" "=" value
|
65
|
+
#
|
66
|
+
# Cookie Header Example:
|
67
|
+
#
|
68
|
+
# $Version=1; Val1="data1"; $Path="/dir"; Val2="data2"; $Path="/"
|
69
|
+
#++
|
70
|
+
def cookie_header_data domain, path
|
71
|
+
@cookies
|
72
|
+
#@cookies.collect do |cookie|
|
73
|
+
# next if Time.rfc2822(cookie.expires) < Time.now
|
74
|
+
# next if cookie.domain != domain
|
75
|
+
# next if cookie.path !~ /^#{path}/
|
76
|
+
|
77
|
+
# "Cookie: #{cookie}" if cookie.domain == domain
|
78
|
+
#end
|
79
|
+
end
|
80
|
+
|
81
|
+
def cookie_header domain, path
|
82
|
+
"Cookie: " + cookie_header_data(domain, path)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'mime'
|
3
|
+
|
4
|
+
#
|
5
|
+
# == Synopsis
|
6
|
+
#
|
7
|
+
# MultipartFormData facilitates in building multipart/form-data encoded
|
8
|
+
# messages that can be POSTed using ThinHTTP. This class is a simple wrapper
|
9
|
+
# around the MIME::MultipartMedia::FormData class.
|
10
|
+
#
|
11
|
+
# == Caveats
|
12
|
+
#
|
13
|
+
# According to RFC 1867 and 2388, each field of the form is to be sent in the
|
14
|
+
# order in which it occurs in the form. However, the Hash containing the form
|
15
|
+
# fields inherently does not maintain order. This is probably not a problem.
|
16
|
+
#
|
17
|
+
# == Examples
|
18
|
+
#
|
19
|
+
# === Simple usage of the class
|
20
|
+
#
|
21
|
+
# The following +params+ Hash:
|
22
|
+
#
|
23
|
+
# params = {
|
24
|
+
# 'text_field' => 'this is some text',
|
25
|
+
# 'image_field' => Pathname.new('/tmp/pic.jpg')
|
26
|
+
# }
|
27
|
+
#
|
28
|
+
# simulates the following HTML form:
|
29
|
+
#
|
30
|
+
# <form>
|
31
|
+
# <input type="text" name="text_field"/>
|
32
|
+
# <input type="file" name="image_field"/>
|
33
|
+
# </form>
|
34
|
+
#
|
35
|
+
# Creating and using a MultipartFormData instance initialized with +params+:
|
36
|
+
#
|
37
|
+
# fd = MultipartFormData.new(params)
|
38
|
+
# fd.content_type # outputs the content type header including boundary
|
39
|
+
# fd.content # outputs the content (multipart/form-data encoded entities)
|
40
|
+
#
|
41
|
+
#
|
42
|
+
# === Constructing a multipart/form-data message and POSTing it via ThinHTTP
|
43
|
+
#
|
44
|
+
# form_data = MultipartFormData.new(
|
45
|
+
# :dog_owner => 'Mary Jane',
|
46
|
+
# :pic_comment => 'These are my two black lab/pit mix puppies.',
|
47
|
+
# :dog_pic1 => Pathname.new('/tmp/lexi.jpg'),
|
48
|
+
# :dog_pic2 => Pathname.new('/tmp/simone.jpg')
|
49
|
+
# )
|
50
|
+
#
|
51
|
+
# th = ThinHTTP.new('petfotoz.com', 80)
|
52
|
+
# th.post('/photo_album.cgi', form_data, form_data.content_type)
|
53
|
+
#
|
54
|
+
class MultipartFormData
|
55
|
+
|
56
|
+
attr_reader :content_type
|
57
|
+
|
58
|
+
#
|
59
|
+
# Returns new MultipartFormData instance initialized with +params+. +params+
|
60
|
+
# is a Hash of key/value pairs representing HTML input variable names and
|
61
|
+
# values. For HTML file type inputs, use a Pathname object.
|
62
|
+
#
|
63
|
+
def initialize params
|
64
|
+
@form_data = MIME::MultipartMedia::FormData.new
|
65
|
+
@content_type = @form_data.content_type
|
66
|
+
initialize_form_data(params)
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Return the multipart/form-data content.
|
71
|
+
#
|
72
|
+
def to_s
|
73
|
+
@form_data.body
|
74
|
+
end
|
75
|
+
alias :content :to_s
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
#
|
80
|
+
# Add the +params+ to the form data.
|
81
|
+
#
|
82
|
+
def initialize_form_data params
|
83
|
+
params.each do |name, value|
|
84
|
+
@form_data.add_entity(media_type(value), name.to_s)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# Convert +param+, which can be a String or Pathname, to a MediaType.
|
90
|
+
#
|
91
|
+
def media_type param
|
92
|
+
case param
|
93
|
+
when String
|
94
|
+
MIME::TextMedia.new(param)
|
95
|
+
when Pathname
|
96
|
+
path = param.to_s
|
97
|
+
|
98
|
+
# return application/octet-stream if file content is unknown
|
99
|
+
begin
|
100
|
+
MIME::DiscreteMediaFactory.create(path)
|
101
|
+
rescue MIME::UnknownContentError
|
102
|
+
MIME::DiscreteMediaFactory.create(path, 'application/octet-stream')
|
103
|
+
end
|
104
|
+
else
|
105
|
+
raise 'invalid parameter'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
data/lib/thin_http.rb
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'net/http'
|
3
|
+
require 'cookie_collection'
|
4
|
+
|
5
|
+
#
|
6
|
+
# == Synopsis
|
7
|
+
#
|
8
|
+
# ThinHTTP is a light-weight and user friendly HTTP library. This class is
|
9
|
+
# useful for sending simple GET requests or uploading files via POST.
|
10
|
+
#
|
11
|
+
# By default, it sends URL encoded data (application/x-www-form-urlencoded).
|
12
|
+
# MIME multipart encoded data (multipart/form-data) can be sent by utilizing
|
13
|
+
# the MIME library or the MultipartFormData class.
|
14
|
+
#
|
15
|
+
# == Features
|
16
|
+
#
|
17
|
+
# * Implements GET and POST requests.
|
18
|
+
# * Follows redirects using GET requests and set instance's host, port, and
|
19
|
+
# path attributes using the final destination URI.
|
20
|
+
# * Accepts either a params hash or an encoded string for POST and GET
|
21
|
+
# requests.
|
22
|
+
# * User-defined headers are sent with each request.
|
23
|
+
# * Sends and receives cookies automatically.
|
24
|
+
# * HTTP support only (HTTPS will be implemented when needed).
|
25
|
+
#
|
26
|
+
# == Design Goals
|
27
|
+
#
|
28
|
+
# * Extremely simple implementation and easy to use interface.
|
29
|
+
# * Only implement what is absolutely necessary.
|
30
|
+
# * Adhere to the RFCs.
|
31
|
+
# * Utilize third party libraries for added functionality when appropriate
|
32
|
+
# (i.e. MIME library for constructing multipart/form-data POSTs).
|
33
|
+
# * Build lightly on top of the standard Net::HTTP library.
|
34
|
+
# * Useful as a test tool in unit and integration testing (original design
|
35
|
+
# goal).
|
36
|
+
#
|
37
|
+
# == Examples
|
38
|
+
#
|
39
|
+
# === GET Request
|
40
|
+
#
|
41
|
+
# th = ThinHTTP.new('rubyforge.org', 80)
|
42
|
+
# th.set_header('User-Agent', th.class.to_s)
|
43
|
+
# response = th.get('/')
|
44
|
+
#
|
45
|
+
#
|
46
|
+
# === POST Request (URL encoding)
|
47
|
+
#
|
48
|
+
# th = ThinHTTP.new('example.com')
|
49
|
+
# response = th.post('/submit_form', :param1 => 'val1', :param2 => 'val2')
|
50
|
+
#
|
51
|
+
#
|
52
|
+
# === POST Request (multipart/form-data encoding using MIME library)
|
53
|
+
#
|
54
|
+
# fd = MIME::MultipartMedia::FormData.new
|
55
|
+
# fd.add_entity(MIME::TextMedia.new('Clint'), 'first_name')
|
56
|
+
# fd.add_entity(MIME::DiscreteMediaFactory.create('/tmp/pic.jpg'), 'portrait')
|
57
|
+
#
|
58
|
+
# th = ThinHTTP.new('example.com')
|
59
|
+
# response = th.post('/upload_file', fd.body, fd.content_type)
|
60
|
+
#
|
61
|
+
#
|
62
|
+
# == More Examples
|
63
|
+
#
|
64
|
+
# * Check the MultipartFormData class examples
|
65
|
+
# * Check the ThinHTTPTest class source code
|
66
|
+
#
|
67
|
+
#--
|
68
|
+
# TODO
|
69
|
+
# * May want to create a Response object to encapsulate the HTTPResponse
|
70
|
+
# returned from the request methods. See SOAP::Response.
|
71
|
+
# * May want to decompose the HTTP headers.
|
72
|
+
#++
|
73
|
+
#
|
74
|
+
class ThinHTTP
|
75
|
+
|
76
|
+
attr_reader :host, :port
|
77
|
+
attr_accessor :path
|
78
|
+
attr_accessor :cookies
|
79
|
+
attr_accessor :request_headers
|
80
|
+
attr_accessor :response_headers
|
81
|
+
|
82
|
+
#
|
83
|
+
# Assign initial connection params for +host+, +port+, and +http_headers+. No
|
84
|
+
# connection is established until an HTTP method is invoked.
|
85
|
+
#
|
86
|
+
def initialize host = 'localhost', port = 2000, http_headers = {}
|
87
|
+
@host = host
|
88
|
+
@port = port
|
89
|
+
@path = ''
|
90
|
+
self.cookies = CookieCollection.new
|
91
|
+
self.request_headers = http_headers
|
92
|
+
self.response_headers = Hash.new
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# Change the remote connection host.
|
97
|
+
#
|
98
|
+
def host= host
|
99
|
+
if @host != host
|
100
|
+
@host = host
|
101
|
+
reset_connection
|
102
|
+
end
|
103
|
+
@host
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# Change the remote connection port.
|
108
|
+
#
|
109
|
+
def port= port
|
110
|
+
if @port != port
|
111
|
+
@port = port
|
112
|
+
reset_connection
|
113
|
+
end
|
114
|
+
@port
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Set the +name+ header and its +value+ in each request.
|
119
|
+
#
|
120
|
+
def set_header name, value
|
121
|
+
request_headers.store(name, value)
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Delete the +name+ header, thus not setting +name+ in subsequent requests.
|
126
|
+
#
|
127
|
+
def unset_header name
|
128
|
+
request_headers.delete(name)
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
# Send +params+ to +path+ as a GET request and return the response body.
|
133
|
+
#
|
134
|
+
# +params+ may be a URL encoded String or a Hash.
|
135
|
+
#
|
136
|
+
def get path, params = nil
|
137
|
+
url_path =
|
138
|
+
case params
|
139
|
+
when Hash ; path + '?' + url_encode(params)
|
140
|
+
when String ; path + '?' + params
|
141
|
+
when NilClass; path
|
142
|
+
else raise 'cannot process params'
|
143
|
+
end
|
144
|
+
|
145
|
+
send_request Net::HTTP::Get.new(url_path)
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# Send +params+ to +path+ as a POST request and return the response body.
|
150
|
+
#
|
151
|
+
# +params+ may be a String or a Hash. If +params+ is a String, it is
|
152
|
+
# considered encoded data and +content_type+ must be set accordingly.
|
153
|
+
# Otherwise, the +params+ Hash is URL encoded.
|
154
|
+
#
|
155
|
+
def post path, params, content_type = 'application/x-www-form-urlencoded'
|
156
|
+
post_request = Net::HTTP::Post.new(path)
|
157
|
+
post_request.content_type = content_type
|
158
|
+
post_request.body = params.is_a?(Hash) ? url_encode(params) : params.to_s
|
159
|
+
|
160
|
+
send_request post_request
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
#
|
167
|
+
# Return an existing connection, otherwise create a new one.
|
168
|
+
#
|
169
|
+
def connection
|
170
|
+
@connection ||= Net::HTTP.new(host, port)
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
# Force a new HTTP connection on next connection attempt.
|
175
|
+
#
|
176
|
+
def reset_connection
|
177
|
+
@connection = nil
|
178
|
+
end
|
179
|
+
|
180
|
+
#
|
181
|
+
# Send +request+ to <tt>self.host:self.port</tt>, following redirection
|
182
|
+
# responses if necessary. Applicable cookies and user-defined headers are
|
183
|
+
# attached to the +request+ before sending.
|
184
|
+
#
|
185
|
+
# NOTE Sends and receives cookies in unverifiable transactions (i.e. 3rd
|
186
|
+
# party cookies).
|
187
|
+
#
|
188
|
+
# http://tools.ietf.org/html/rfc2965#section-3.3.6
|
189
|
+
#
|
190
|
+
def send_request request
|
191
|
+
self.path = request.path
|
192
|
+
request['cookie'] = cookies.cookie_header_data(host, request.path)
|
193
|
+
request_headers.each {|name, value| request[name] = value}
|
194
|
+
|
195
|
+
response = connection.request(request)
|
196
|
+
|
197
|
+
case response
|
198
|
+
when Net::HTTPSuccess
|
199
|
+
save_response_headers response
|
200
|
+
response.body
|
201
|
+
when Net::HTTPRedirection
|
202
|
+
uri = URI.parse(response['location'])
|
203
|
+
self.host = uri.host
|
204
|
+
self.port = uri.port
|
205
|
+
send_request redirection_request(request, uri.path)
|
206
|
+
else
|
207
|
+
response.error!
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
#
|
212
|
+
# Create a request that is to be utilized in following a redirection to
|
213
|
+
# +path+. The new request will inherit +prev_request+'s state tracking
|
214
|
+
# redirection information if available. A maximum of five sequential
|
215
|
+
# redirections will be followed.
|
216
|
+
#--
|
217
|
+
# TODO Adhere to RFC2616 redirection behavior when implementing new request
|
218
|
+
# methods. See http://tools.ietf.org/html/rfc2616#section-10.3 for details.
|
219
|
+
#++
|
220
|
+
#
|
221
|
+
def redirection_request prev_request, path
|
222
|
+
max_redirects = 5
|
223
|
+
|
224
|
+
class << (request = Net::HTTP::Get.new(path))
|
225
|
+
attr_accessor :num_redirects
|
226
|
+
end
|
227
|
+
|
228
|
+
if prev_request.respond_to? :num_redirects
|
229
|
+
raise 'exceeded redirection limit' if prev_request.num_redirects >= max_redirects
|
230
|
+
request.num_redirects = prev_request.num_redirects + 1
|
231
|
+
else
|
232
|
+
request.num_redirects = 1
|
233
|
+
end
|
234
|
+
request
|
235
|
+
end
|
236
|
+
|
237
|
+
#
|
238
|
+
# Save +response+'s cookies and headers.
|
239
|
+
#
|
240
|
+
def save_response_headers response
|
241
|
+
cookies.store response['set-cookie']
|
242
|
+
self.response_headers = response.to_hash
|
243
|
+
end
|
244
|
+
|
245
|
+
#
|
246
|
+
# URL encodes +params+ hash.
|
247
|
+
#
|
248
|
+
def url_encode params
|
249
|
+
params.collect do |key,value|
|
250
|
+
CGI::escape(key.to_s) + "=" + CGI::escape(value)
|
251
|
+
end.join('&')
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'cookie_collection'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Test the CookieCollection class functionality.
|
6
|
+
#
|
7
|
+
class CookieCollectionTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def test_cookie_initialization
|
10
|
+
cookies = CookieCollection.new
|
11
|
+
assert_kind_of CookieCollection, cookies
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'test_helper'
|
3
|
+
require 'multipart_form_data'
|
4
|
+
|
5
|
+
|
6
|
+
#
|
7
|
+
# Test the MultipartFormData class functionality.
|
8
|
+
#
|
9
|
+
class MultipartFormDataTest < Test::Unit::TestCase
|
10
|
+
|
11
|
+
include TestHelper
|
12
|
+
|
13
|
+
def setup
|
14
|
+
@generalized_content_type = 'multipart/form-data; boundary=Boundary_00.00'
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_initialization_with_text_and_file_params
|
18
|
+
params = { # this will fail if params reorders the key/value pairs.
|
19
|
+
# symbol and string keys acceptable
|
20
|
+
:name => 'Betty Crocker',
|
21
|
+
'address' => '1234 Doughboy Lane',
|
22
|
+
'pie_pic' => Pathname.new(sd('/image.jpg'))
|
23
|
+
}
|
24
|
+
fd = MultipartFormData.new(params)
|
25
|
+
expected_content = IO.read(sd('/form_data.msg'))
|
26
|
+
|
27
|
+
assert_equal_content_type @generalized_content_type, fd.content_type
|
28
|
+
assert_equal_mime_message expected_content, fd.content
|
29
|
+
assert_equal fd.to_s, fd.content # check alias
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_initialization_with_unknown_file
|
33
|
+
params = { 'file_to_upload' => Pathname.new(sd('/unknown.yyy')) }
|
34
|
+
fd = MultipartFormData.new(params)
|
35
|
+
expected_content = IO.read(sd('/form_data_unknown.msg'))
|
36
|
+
|
37
|
+
assert_equal_content_type @generalized_content_type, fd.content_type
|
38
|
+
assert_equal_mime_message expected_content, fd.content
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_initialization_with_valid_parameter_values
|
42
|
+
params = Hash.new
|
43
|
+
valid_params = ['some string', Pathname.new(sd('/image.jpg'))]
|
44
|
+
|
45
|
+
valid_params.each do |valid_param|
|
46
|
+
params['field'] = valid_param
|
47
|
+
assert_nothing_raised {MultipartFormData.new(params)}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_initialization_with_invalid_parameter_values
|
52
|
+
params = Hash.new
|
53
|
+
invalid_params = [1, [1,2,3], {:key=>'value'}]
|
54
|
+
|
55
|
+
invalid_params.each do |invalid_param|
|
56
|
+
params['field'] = invalid_param
|
57
|
+
err = assert_raise(RuntimeError) {MultipartFormData.new(params)}
|
58
|
+
assert_equal 'invalid parameter', err.message
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
this is an ASCII file
|
Binary file
|
@@ -0,0 +1,8 @@
|
|
1
|
+
--Boundary_10425688400.695421641501527
|
2
|
+
Content-ID: <10425676800.215758180614315>
|
3
|
+
Content-Disposition: form-data; name="file_to_upload"; filename="unknown.yyy"
|
4
|
+
Content-Type: application/octet-stream
|
5
|
+
|
6
|
+
this is an unknown file type
|
7
|
+
|
8
|
+
--Boundary_10425688400.695421641501527--
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
this is an unknown file type
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module TestHelper
|
2
|
+
|
3
|
+
private
|
4
|
+
|
5
|
+
MATCH_BOUNDARY = /Boundary_\d+\.\d+/
|
6
|
+
MATCH_ID_HEADER = /-ID: <[^>]+>\r\n/
|
7
|
+
|
8
|
+
def assert_equal_mime_message expected, actual
|
9
|
+
assert_equal normalize_message(expected), normalize_message(actual)
|
10
|
+
end
|
11
|
+
|
12
|
+
def assert_equal_content_type expected, actual
|
13
|
+
assert_equal expected.sub(MATCH_BOUNDARY, ''), actual.sub(MATCH_BOUNDARY, '')
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Make messages comparable by removing *-ID header values and boundaries.
|
19
|
+
#
|
20
|
+
def normalize_message message
|
21
|
+
message.
|
22
|
+
gsub(MATCH_ID_HEADER, "-ID:\r\n").
|
23
|
+
gsub(MATCH_BOUNDARY, "Boundary_")
|
24
|
+
end
|
25
|
+
|
26
|
+
def sd filename
|
27
|
+
@scaffold_dir ||= File.join(File.dirname(__FILE__), 'scaffold')
|
28
|
+
@scaffold_dir + filename
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'test_helper'
|
3
|
+
require 'thin_http'
|
4
|
+
require 'multipart_form_data'
|
5
|
+
|
6
|
+
|
7
|
+
#
|
8
|
+
# Test the ThinHTTP class functionality by interacting with a local WEBrick
|
9
|
+
# server.
|
10
|
+
#
|
11
|
+
class ThinHTTPTest < Test::Unit::TestCase
|
12
|
+
|
13
|
+
include TestHelper
|
14
|
+
|
15
|
+
def setup
|
16
|
+
@th = ThinHTTP.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_thin_http_object_defaults
|
20
|
+
assert_equal @th.host, 'localhost'
|
21
|
+
assert_equal @th.port, 2000
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_get_index
|
25
|
+
response = @th.get('/index.html')
|
26
|
+
index_html = IO.read(sd('/htdocs/index.html'))
|
27
|
+
assert_equal index_html, response
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_get_hash_params
|
31
|
+
params = {:one => 'abc', 'two' => 'xyz'}
|
32
|
+
response = @th.get('/cgi/echo_get_params.cgi', params)
|
33
|
+
assert_equal_url_params params, response
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_get_string_params
|
37
|
+
params = 'foo=bar&dead=beef'
|
38
|
+
response = @th.get('/cgi/echo_get_params.cgi', params)
|
39
|
+
assert_equal_url_params params, response
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_post_url_encoded_params
|
43
|
+
params = {:one => 'abc', :two => 'xyz'}
|
44
|
+
response = @th.post('/cgi/echo_post_params.cgi', params)
|
45
|
+
assert_equal_url_params params, response
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_post_mime_encoded_params
|
49
|
+
form_data = MultipartFormData.new(
|
50
|
+
:name => 'John Schmo',
|
51
|
+
:address=>'1516 Nowhere Place'
|
52
|
+
)
|
53
|
+
response = @th.post('/cgi/echo_post_params.cgi', form_data, form_data.content_type)
|
54
|
+
assert_equal form_data.content, response
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_post_mime_encoded_params_and_files
|
58
|
+
form_data = MultipartFormData.new(
|
59
|
+
:comment => 'this is a cool test',
|
60
|
+
:txtfile => Pathname.new(sd('/ascii.txt')),
|
61
|
+
:imgfile => Pathname.new(sd('/image.jpg'))
|
62
|
+
)
|
63
|
+
response = @th.post('/cgi/echo_post_params.cgi', form_data, form_data.content_type)
|
64
|
+
assert_equal form_data.content, response
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_get_redirect_after_get_request
|
68
|
+
assert_equal '', @th.path
|
69
|
+
response = @th.get('/cgi/redirect_to_print_env.cgi')
|
70
|
+
assert_equal '/cgi/print_env.cgi', @th.path
|
71
|
+
assert_equal 'GET', print_env_var('METHOD', response)
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_get_redirect_after_post_request
|
75
|
+
assert_equal '', @th.path
|
76
|
+
response = @th.post('/cgi/redirect_to_print_env.cgi', :post => 'test')
|
77
|
+
assert_equal '/cgi/print_env.cgi', @th.path
|
78
|
+
assert_equal 'GET', print_env_var('METHOD', response)
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_exceed_maximum_redirects
|
82
|
+
err = assert_raise(RuntimeError) {@th.get('/cgi/redirect_loop.cgi')}
|
83
|
+
assert_equal 'exceeded redirection limit', err.message
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_redirect_to_external_domain
|
87
|
+
initial_host = @th.host
|
88
|
+
initial_port = @th.port
|
89
|
+
|
90
|
+
@th.get('/cgi/redirect_to_external.cgi')
|
91
|
+
|
92
|
+
assert_not_equal initial_host, @th.host
|
93
|
+
assert_not_equal initial_port, @th.port
|
94
|
+
end
|
95
|
+
|
96
|
+
# TODO more elaborate cookie tests.
|
97
|
+
def test_set_and_get_cookie
|
98
|
+
@th.get('/cgi/set_cookie.cgi')
|
99
|
+
response = @th.get('/cgi/print_env.cgi')
|
100
|
+
assert_equal 'cookie_key=cookie_value', print_env_var('COOKIE', response)
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_add_and_delete_custom_headers
|
104
|
+
user_agent_name = @th.class.to_s
|
105
|
+
referer_url = 'http://ecentryx.com/index.html'
|
106
|
+
|
107
|
+
@th.set_header('User-Agent', user_agent_name)
|
108
|
+
@th.set_header('Referer', referer_url)
|
109
|
+
response = @th.get('/cgi/print_env.cgi')
|
110
|
+
|
111
|
+
assert_equal user_agent_name, print_env_var('USER_AGENT', response)
|
112
|
+
assert_equal referer_url, print_env_var('REFERER', response)
|
113
|
+
|
114
|
+
@th.unset_header('User-Agent')
|
115
|
+
response = @th.get('/cgi/print_env.cgi')
|
116
|
+
|
117
|
+
assert_nil print_env_var('USER_AGENT', response)
|
118
|
+
assert_equal referer_url, print_env_var('REFERER', response)
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
#
|
125
|
+
# Test Helpers
|
126
|
+
#
|
127
|
+
|
128
|
+
#
|
129
|
+
# Easy comparison of URL params, which may be a Hash (i.e. {k1=>v1, k2=v2})
|
130
|
+
# or a String (i.e. "k1=v1&k2=v2").
|
131
|
+
#
|
132
|
+
def assert_equal_url_params expected, actual
|
133
|
+
assert_equal url_params_to_hash(expected), url_params_to_hash(actual)
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
# Converts +params+ to a Hash that contains only String keys. +params+
|
138
|
+
# represents URL parameters as a key/value Hash or a URL encoded String.
|
139
|
+
#
|
140
|
+
def url_params_to_hash params
|
141
|
+
if params.is_a? Hash
|
142
|
+
params.inject({}) do |hsh, (key,value)|
|
143
|
+
hsh.store(key.to_s, value)
|
144
|
+
hsh
|
145
|
+
end
|
146
|
+
else
|
147
|
+
params.split('&').inject({}) do |hsh, key_value|
|
148
|
+
key, value = key_value.split('=')
|
149
|
+
hsh.store(key, value)
|
150
|
+
hsh
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
#
|
156
|
+
# Return the value of the environment variable +var_name+, otherwise nil.
|
157
|
+
# Works in conjunction with the CGI script print_env.cgi.
|
158
|
+
#
|
159
|
+
def print_env_var var_name, response
|
160
|
+
response.match(/#{var_name}=(.*)$/)[1] rescue nil
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: thin_http
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Clint Pachl
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-11-06 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: mime
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.1.0
|
24
|
+
version:
|
25
|
+
description:
|
26
|
+
email: pachl@ecentryx.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- README
|
35
|
+
- Rakefile
|
36
|
+
- lib/thin_http.rb
|
37
|
+
- lib/cookie_collection.rb
|
38
|
+
- lib/multipart_form_data.rb
|
39
|
+
- test/scaffold
|
40
|
+
- test/scaffold/image.jpg
|
41
|
+
- test/scaffold/ascii.txt
|
42
|
+
- test/scaffold/htdocs
|
43
|
+
- test/scaffold/htdocs/cgi
|
44
|
+
- test/scaffold/htdocs/cgi/echo_get_params.cgi
|
45
|
+
- test/scaffold/htdocs/cgi/echo_post_params.cgi
|
46
|
+
- test/scaffold/htdocs/cgi/redirect_loop.cgi
|
47
|
+
- test/scaffold/htdocs/cgi/redirect_to_external.cgi
|
48
|
+
- test/scaffold/htdocs/cgi/print_env.cgi
|
49
|
+
- test/scaffold/htdocs/cgi/set_cookie.cgi
|
50
|
+
- test/scaffold/htdocs/cgi/redirect_to_print_env.cgi
|
51
|
+
- test/scaffold/htdocs/index.html
|
52
|
+
- test/scaffold/unknown.yyy
|
53
|
+
- test/scaffold/form_data.msg
|
54
|
+
- test/scaffold/form_data_unknown.msg
|
55
|
+
- test/thin_http_test.rb
|
56
|
+
- test/multipart_form_data_test.rb
|
57
|
+
- test/cookie_collection_test.rb
|
58
|
+
- test/test_helper.rb
|
59
|
+
has_rdoc: true
|
60
|
+
homepage: http://thinhttp.rubyforge.org/
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
version:
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: "0"
|
77
|
+
version:
|
78
|
+
requirements: []
|
79
|
+
|
80
|
+
rubyforge_project: thinhttp
|
81
|
+
rubygems_version: 1.3.0
|
82
|
+
signing_key:
|
83
|
+
specification_version: 2
|
84
|
+
summary: Lightweight and user-friendly HTTP client library
|
85
|
+
test_files:
|
86
|
+
- test/thin_http_test.rb
|
87
|
+
- test/multipart_form_data_test.rb
|
88
|
+
- test/cookie_collection_test.rb
|