thin_http 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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--
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ # Echo URL encoded parameters.
3
+
4
+ printf "Content-type: text/plain\n\n"
5
+ printf "$QUERY_STRING"
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ # Echo POST parameters.
3
+
4
+ echo "Content-type: text/plain\n"
5
+ cat /dev/stdin
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+
3
+ echo "Content-type: text/plain\n"
4
+ printenv
@@ -0,0 +1,9 @@
1
+ #!/bin/sh
2
+
3
+ echo redirect >> /tmp/REDIRECT
4
+
5
+ cat << EOF
6
+ Status: 307 Temporary Redirect
7
+ Location: $REQUEST_URI
8
+
9
+ EOF
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ cat << EOF
4
+ Status: 307 Temporary Redirect
5
+ Location: http://ecentryx.com:80/
6
+
7
+ EOF
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+
3
+ # Flush STDIN in case of POST or PUT request.
4
+ cat /dev/stdin >/dev/null
5
+
6
+ cat << EOF
7
+ Status: 307 Temporary Redirect
8
+ Location: http://$HTTP_HOST/cgi/print_env.cgi
9
+
10
+ EOF
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ cat << EOF
4
+ Status: 200 OK
5
+ Set-Cookie: cookie_key=cookie_value
6
+
7
+ EOF
@@ -0,0 +1,7 @@
1
+ <html>
2
+ <head><title>index.html</title></head>
3
+ <body>
4
+ <h1>index.html</h1>
5
+ <p>ThinHTTP test</p>
6
+ </body>
7
+ </html>
Binary file
@@ -0,0 +1 @@
1
+ this is an unknown file type
@@ -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