thin_http 0.1.0
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/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
|