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