em-http-server 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/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ .idea
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in em-http-server.gemspec
4
+ gemspec
5
+
6
+ gem 'eventmachine'
7
+
8
+ # Add dependencies to develop your gem here.
9
+ # Include everything needed to run rake, tests, features, etc.
10
+ group :development do
11
+ gem "bundler", "> 1.0.0"
12
+ gem 'rake'
13
+ gem 'test-unit'
14
+ gem 'simplecov'
15
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 alor
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # Em::Http::Server
2
+
3
+ Simple http server to be used with Eventmachine.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'em-http-server'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install em-http-server
18
+
19
+ ## Usage
20
+
21
+ require 'eventmachine'
22
+ require 'em-http-server'
23
+
24
+ class HTTPHandler < EM::Http::Server
25
+
26
+ def process_http_request
27
+ puts @http_request_method
28
+ puts @http_request_uri
29
+ puts @http_query_string
30
+ puts @http_protocol
31
+ puts @http_content
32
+ puts @http[:cookie]
33
+ puts @http[:content_type]
34
+ # you have all the http headers in this hash
35
+ puts @http.inspect
36
+
37
+ response = EM::DelegatedHttpResponse.new(self)
38
+ response.status = 200
39
+ response.content_type 'text/html'
40
+ response.content = 'It works'
41
+ response.send_response
42
+ end
43
+
44
+ end
45
+
46
+ EM::run do
47
+ EM::start_server("0.0.0.0", 80, HTTPHandler)
48
+ end
49
+
50
+ ## Contributing
51
+
52
+ 1. Fork it
53
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
54
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
55
+ 4. Push to the branch (`git push origin my-new-feature`)
56
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.pattern = 'test/**/test_*.rb'
8
+ test.verbose = true
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ Gem::Specification.new do |gem|
3
+ gem.authors = ["alor"]
4
+ gem.email = ["alberto.ornaghi@gmail.com"]
5
+ gem.description = %q{Simple http server for eventmachine}
6
+ gem.summary = %q{Simple http server for eventmachine with the same interface as evma_httpserver}
7
+ gem.homepage = ""
8
+
9
+ gem.files = `git ls-files`.split($\)
10
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
11
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
12
+ gem.name = "em-http-server"
13
+ gem.require_paths = ["lib"]
14
+ gem.version = "0.1.0"
15
+
16
+ gem.add_runtime_dependency "eventmachine"
17
+ end
@@ -0,0 +1,2 @@
1
+ require "em-http-server/response"
2
+ require "em-http-server/server"
@@ -0,0 +1,314 @@
1
+ # EventMachine HTTP Server
2
+ # HTTP Response-support class
3
+ #
4
+ # Author:: blackhedd (gmail address: garbagecat10).
5
+ #
6
+ # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
7
+ #
8
+ # This program is made available under the terms of the GPL version 2.
9
+ #
10
+ #----------------------------------------------------------------------------
11
+ #
12
+ # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
13
+ #
14
+ # Gmail: garbagecat10
15
+ #
16
+ # This program is free software; you can redistribute it and/or modify
17
+ # it under the terms of the GNU General Public License as published by
18
+ # the Free Software Foundation; either version 2 of the License, or
19
+ # (at your option) any later version.
20
+ #
21
+ # This program is distributed in the hope that it will be useful,
22
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
23
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
+ # GNU General Public License for more details.
25
+ #
26
+ # You should have received a copy of the GNU General Public License
27
+ # along with this program; if not, write to the Free Software
28
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
29
+ #
30
+ #---------------------------------------------------------------------------
31
+ #
32
+
33
+ module EventMachine
34
+
35
+ # This class provides a wide variety of features for generating and
36
+ # dispatching HTTP responses. It allows you to conveniently generate
37
+ # headers and content (including chunks and multiparts), and dispatch
38
+ # responses (including deferred or partially-complete responses).
39
+ #
40
+ # Although HttpResponse is coded as a class, it's not complete as it
41
+ # stands. It assumes that it has certain of the behaviors of
42
+ # EventMachine::Connection. You must add these behaviors, either by
43
+ # subclassing HttpResponse, or using the alternate version of this
44
+ # class, DelegatedHttpResponse. See the test cases for current information
45
+ # on which behaviors you have to add.
46
+ #
47
+ # TODO, someday it would be nice to provide a version of this functionality
48
+ # that is coded as a Module, so it can simply be mixed into an instance of
49
+ # EventMachine::Connection.
50
+ #
51
+ class HttpResponse
52
+ attr_accessor :status, :status_string, :content, :headers, :chunks, :multiparts
53
+
54
+ def initialize
55
+ @headers = {}
56
+ end
57
+
58
+ def keep_connection_open arg=true
59
+ @keep_connection_open = arg
60
+ end
61
+
62
+ # sugarings for headers
63
+ def content_type *mime
64
+ if mime.length > 0
65
+ @headers["Content-type"] = mime.first.to_s
66
+ else
67
+ @headers["Content-type"]
68
+ end
69
+ end
70
+
71
+ # Sugaring for Set-cookie headers. These are a pain because there can easily and
72
+ # legitimately be more than one. So we use an ugly verb to signify that.
73
+ # #add_set_cookies does NOT disturb the set-cookie headers which may have been
74
+ # added on a prior call. #set_cookie clears them out first.
75
+ def add_set_cookie *ck
76
+ if ck.length > 0
77
+ h = (@headers["Set-cookie"] ||= [])
78
+ ck.each {|c| h << c}
79
+ end
80
+ end
81
+ def set_cookie *ck
82
+ h = (@headers["Set-cookie"] ||= [])
83
+ if ck.length > 0
84
+ h.clear
85
+ add_set_cookie *ck
86
+ else
87
+ h
88
+ end
89
+ end
90
+
91
+
92
+ # This is intended to send a complete HTTP response, including closing the connection
93
+ # if appropriate at the end of the transmission. Don't use this method to send partial
94
+ # or iterated responses. This method will send chunks and multiparts provided they
95
+ # are all available when we get here.
96
+ # Note that the default @status is 200 if the value doesn't exist.
97
+ def send_response
98
+ send_headers
99
+ send_body
100
+ send_trailer
101
+ close_connection_after_writing unless (@keep_connection_open and (@status || 200) < 500)
102
+ end
103
+
104
+ # Send the headers out in alpha-sorted order. This will degrade performance to some
105
+ # degree, and is intended only to simplify the construction of unit tests.
106
+ #
107
+ def send_headers
108
+ raise "sent headers already" if @sent_headers
109
+ @sent_headers = true
110
+
111
+ fixup_headers
112
+
113
+ ary = []
114
+ ary << "HTTP/1.1 #{@status || 200} #{@status_string || '...'}\r\n"
115
+ ary += generate_header_lines(@headers)
116
+ ary << "\r\n"
117
+
118
+ send_data ary.join
119
+ end
120
+
121
+
122
+ def generate_header_lines in_hash
123
+ out_ary = []
124
+ in_hash.keys.sort.each {|k|
125
+ v = in_hash[k]
126
+ if v.is_a?(Array)
127
+ v.each {|v1| out_ary << "#{k}: #{v1}\r\n" }
128
+ else
129
+ out_ary << "#{k}: #{v}\r\n"
130
+ end
131
+ }
132
+ out_ary
133
+ end
134
+ private :generate_header_lines
135
+
136
+
137
+ # Examine the content type and data and other things, and perform a final
138
+ # fixup of the header array. We expect this to be called just before sending
139
+ # headers to the remote peer.
140
+ # In the case of multiparts, we ASSUME we will get called before any content
141
+ # gets sent out, because the multipart boundary is created here.
142
+ #
143
+ def fixup_headers
144
+ if @content
145
+ @headers["Content-length"] = @content.to_s.length
146
+ elsif @chunks
147
+ @headers["Transfer-encoding"] = "chunked"
148
+ # Might be nice to ENSURE there is no content-length header,
149
+ # but how to detect all the possible permutations of upper/lower case?
150
+ elsif @multiparts
151
+ @multipart_boundary = self.class.concoct_multipart_boundary
152
+ @headers["Content-type"] = "multipart/x-mixed-replace; boundary=\"#{@multipart_boundary}\""
153
+ else
154
+ @headers["Content-length"] = 0
155
+ end
156
+ end
157
+
158
+ # we send either content, chunks, or multiparts. Content can only be sent once.
159
+ # Chunks and multiparts can be sent any number of times.
160
+ # DO NOT close the connection or send any goodbye kisses. This method can
161
+ # be called multiple times to send out chunks or multiparts.
162
+ def send_body
163
+ if @content
164
+ send_content
165
+ elsif @chunks
166
+ send_chunks
167
+ elsif @multiparts
168
+ send_multiparts
169
+ else
170
+ @content = ""
171
+ send_content
172
+ end
173
+ end
174
+
175
+ # send a trailer which depends on the type of body we're dealing with.
176
+ # The assumption is that we're about to end the transmission of this particular
177
+ # HTTP response. (A connection-close may or may not follow.)
178
+ #
179
+ def send_trailer
180
+ send_headers unless @sent_headers
181
+ if @content
182
+ # no-op
183
+ elsif @chunks
184
+ unless @last_chunk_sent
185
+ chunk ""
186
+ send_chunks
187
+ end
188
+ elsif @multiparts
189
+ # in the lingo of RFC 2046/5.1.1, we're sending an "epilog"
190
+ # consisting of a blank line. I really don't know how that is
191
+ # supposed to interact with the case where we leave the connection
192
+ # open after transmitting the multipart response.
193
+ send_data "\r\n--#{@multipart_boundary}--\r\n\r\n"
194
+ else
195
+ # no-op
196
+ end
197
+ end
198
+
199
+ def send_content
200
+ raise "sent content already" if @sent_content
201
+ @sent_content = true
202
+ send_data((@content || "").to_s)
203
+ end
204
+
205
+ # add a chunk to go to the output.
206
+ # Will cause the headers to pick up "content-transfer-encoding"
207
+ # Add the chunk to a list. Calling #send_chunks will send out the
208
+ # available chunks and clear the chunk list WITHOUT closing the connection,
209
+ # so it can be called any number of times.
210
+ # TODO!!! Per RFC2616, we may not send chunks to an HTTP/1.0 client.
211
+ # Raise an exception here if our user tries to do so.
212
+ # Chunked transfer coding is defined in RFC2616 pgh 3.6.1.
213
+ # The argument can be a string or a hash. The latter allows for
214
+ # sending chunks with extensions (someday).
215
+ #
216
+ def chunk text
217
+ @chunks ||= []
218
+ @chunks << text
219
+ end
220
+
221
+ # send the contents of the chunk list and clear it out.
222
+ # ASSUMES that headers have been sent.
223
+ # Does NOT close the connection.
224
+ # Can be called multiple times.
225
+ # According to RFC2616, phg 3.6.1, the last chunk will be zero length.
226
+ # But some caller could accidentally set a zero-length chunk in the middle
227
+ # of the stream. If that should happen, raise an exception.
228
+ # The reason for supporting chunks that are hashes instead of just strings
229
+ # is to enable someday supporting chunk-extension codes (cf the RFC).
230
+ # TODO!!! We're not supporting the final entity-header that may be
231
+ # transmitted after the last (zero-length) chunk.
232
+ #
233
+ def send_chunks
234
+ send_headers unless @sent_headers
235
+ while chunk = @chunks.shift
236
+ raise "last chunk already sent" if @last_chunk_sent
237
+ text = chunk.is_a?(Hash) ? chunk[:text] : chunk.to_s
238
+ send_data "#{format("%x", text.length).upcase}\r\n#{text}\r\n"
239
+ @last_chunk_sent = true if text.length == 0
240
+ end
241
+ end
242
+
243
+ # To add a multipart to the outgoing response, specify the headers and the
244
+ # body. If only a string is given, it's treated as the body (in this case,
245
+ # the header is assumed to be empty).
246
+ #
247
+ def multipart arg
248
+ vals = if arg.is_a?(String)
249
+ {:body => arg, :headers => {}}
250
+ else
251
+ arg
252
+ end
253
+
254
+ @multiparts ||= []
255
+ @multiparts << vals
256
+ end
257
+
258
+ # Multipart syntax is defined in RFC 2046, pgh 5.1.1 et seq.
259
+ # The CRLF which introduces the boundary line of each part (content entity)
260
+ # is defined as being part of the boundary, not of the preceding part.
261
+ # So we don't need to mess with interpreting the last bytes of a part
262
+ # to ensure they are CRLF-terminated.
263
+ #
264
+ def send_multiparts
265
+ send_headers unless @sent_headers
266
+ while part = @multiparts.shift
267
+ send_data "\r\n--#{@multipart_boundary}\r\n"
268
+ send_data( generate_header_lines( part[:headers] || {} ).join)
269
+ send_data "\r\n"
270
+ send_data part[:body].to_s
271
+ end
272
+ end
273
+
274
+ # TODO, this is going to be way too slow. Cache up the uuidgens.
275
+ #
276
+ def self.concoct_multipart_boundary
277
+ @multipart_index ||= 0
278
+ @multipart_index += 1
279
+ if @multipart_index >= 1000
280
+ @multipart_index = 0
281
+ @multipart_guid = nil
282
+ end
283
+ @multipart_guid ||= `uuidgen -r`.chomp.gsub(/\-/,"")
284
+ "#{@multipart_guid}#{@multipart_index}"
285
+ end
286
+
287
+ def send_redirect location
288
+ @status = 302 # TODO, make 301 available by parameter
289
+ @status_string = "Moved Temporarily"
290
+ @headers["Location"] = location
291
+ send_response
292
+ end
293
+ end
294
+ end
295
+
296
+ #----------------------------------------------------------------------------
297
+
298
+ require 'forwardable'
299
+
300
+ module EventMachine
301
+ class DelegatedHttpResponse < HttpResponse
302
+ extend Forwardable
303
+ def_delegators :@delegate,
304
+ :send_data,
305
+ :close_connection,
306
+ :close_connection_after_writing
307
+
308
+ def initialize dele
309
+ super()
310
+ @delegate = dele
311
+ end
312
+ end
313
+ end
314
+
@@ -0,0 +1,94 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+
4
+ module EventMachine
5
+ module Http
6
+ class Server < EM::P::HeaderAndContentProtocol
7
+
8
+ # everything starts from here.
9
+ # Protocol::HeaderAndContentProtocol does the dirty job for us
10
+ # it will pass headers and content, we just need to parse the headers
11
+ # the fill the right variables
12
+ def receive_request(headers, content)
13
+
14
+ # save the whole headers array, verbatim
15
+ @http_headers = headers
16
+
17
+ # parse the headers into an hash to be able to access them like:
18
+ # @http[:host]
19
+ # @http[:content_type]
20
+ @http = headers_2_hash headers
21
+
22
+ # parse the HTTP request
23
+ parse_first_header headers.first
24
+
25
+ # save the binary content
26
+ @http_content = content
27
+
28
+ # invoke the method in the user-provided instance
29
+ if respond_to?(:process_http_request)
30
+ process_http_request
31
+ end
32
+ end
33
+
34
+ # parse the first HTTP header line
35
+ # get the http METHOD, URI and PROTOCOL
36
+ def parse_first_header(line)
37
+
38
+ # split the line into: METHOD URI PROTOCOL
39
+ # eg: GET / HTTP1/1
40
+ parsed = line.split(' ')
41
+
42
+ # a correct request has three parts
43
+ send_error(400, "Bad request") unless parsed.size == 3
44
+
45
+ @http_request_method, uri, @http_protocol = parsed
46
+
47
+ # uri must begin with a /
48
+ send_error(400, "Bad request") unless uri.start_with?('/')
49
+
50
+ # optional query string
51
+ @http_request_uri, @http_query_string = uri.split('?')
52
+ end
53
+
54
+ # send back to the client an HTTP error
55
+ def send_error(code, desc)
56
+ string = "HTTP1/1 #{code} #{desc}\r\n"
57
+ string << "Connection: close\r\n"
58
+ string << "Content-type: text/plain\r\n"
59
+ string << "\r\n"
60
+ string << "Detected error: HTTP code #{code}"
61
+ send_data string
62
+ close_connection_after_writing
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ if __FILE__ == $0
71
+
72
+ class HTTPHandler < EM::Http::Server
73
+
74
+ def process_http_request
75
+ puts @http_request_method
76
+ puts @http_request_uri
77
+ puts @http_query_string
78
+ puts @http_protocol
79
+ puts @http[:cookie]
80
+ puts @http[:content_type]
81
+ puts @http_content
82
+ puts @http.inspect
83
+ end
84
+
85
+ end
86
+
87
+ port = 8080
88
+ # all the events are handled here
89
+ EM::run do
90
+ EM::start_server("0.0.0.0", port, HTTPHandler)
91
+ puts "Listening on port #{port}..."
92
+ end
93
+
94
+ end
data/test/test_app.rb ADDED
@@ -0,0 +1,270 @@
1
+ require 'test/unit'
2
+ require 'em-http-server'
3
+
4
+ begin
5
+ once = false
6
+ require 'eventmachine'
7
+ rescue LoadError => e
8
+ raise e if once
9
+ once = true
10
+ require 'rubygems'
11
+ retry
12
+ end
13
+
14
+
15
+
16
+ #--------------------------------------
17
+
18
+ module EventMachine
19
+ class HttpServer < EM::Http::Server
20
+ def process_http_request
21
+ send_data generate_response()
22
+ close_connection_after_writing
23
+ end
24
+ end
25
+ end
26
+
27
+ #--------------------------------------
28
+
29
+ require 'socket'
30
+
31
+ class TestApp < Test::Unit::TestCase
32
+
33
+ TestHost = "127.0.0.1"
34
+ TestPort = 8911
35
+
36
+ TestResponse_1 = <<EORESP
37
+ HTTP/1.0 200 ...
38
+ Content-length: 4
39
+ Content-type: text/plain
40
+ Connection: close
41
+
42
+ 1234
43
+ EORESP
44
+
45
+ Thread.abort_on_exception = true
46
+
47
+ def test_simple_get
48
+ received_response = nil
49
+
50
+ EventMachine::HttpServer.class_eval do
51
+ def generate_response
52
+ TestResponse_1
53
+ end
54
+ end
55
+
56
+
57
+ EventMachine.run do
58
+ EventMachine.start_server TestHost, TestPort, EventMachine::HttpServer
59
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
60
+
61
+ cb = proc do
62
+ tcp = TCPSocket.new TestHost, TestPort
63
+ tcp.write "GET / HTTP/1.0\r\n\r\n"
64
+ received_response = tcp.read
65
+ end
66
+ eb = proc { EventMachine.stop }
67
+ EventMachine.defer cb, eb
68
+ end
69
+
70
+ assert_equal( TestResponse_1, received_response )
71
+ end
72
+
73
+
74
+
75
+
76
+ # This frowsy-looking protocol handler allows the test harness to make some
77
+ # its local variables visible, so we can set them here and they can be asserted later.
78
+ class MyTestServer < EventMachine::HttpServer
79
+
80
+ def initialize *args
81
+ super
82
+ end
83
+ def generate_response
84
+ @assertions.call
85
+ TestResponse_1
86
+ end
87
+ end
88
+
89
+
90
+
91
+ def test_parameters
92
+ path_info = "/test.html"
93
+ query_string = "a=b&c=d"
94
+ cookie = "eat_me=I'm a cookie"
95
+ etag = "12345"
96
+
97
+ # collect all the stuff we want to assert outside the actual test,
98
+ # to ensure it gets asserted even if the test catches some exception.
99
+ received_response = nil
100
+ request_http = {}
101
+ request_uri = ""
102
+ request_query = ""
103
+ request_method = ""
104
+
105
+
106
+ EventMachine.run do
107
+ EventMachine.start_server(TestHost, TestPort, MyTestServer) do |conn|
108
+ # In each accepted connection, set up a procedure that will copy
109
+ # the request parameters into a local variable visible here, so
110
+ # we can assert the values later.
111
+ conn.instance_eval do
112
+ @assertions = proc {
113
+ request_method = @http_request_method
114
+ request_uri = @http_request_uri
115
+ request_query = @http_query_string
116
+ request_http = @http
117
+ }
118
+ end
119
+ end
120
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
121
+
122
+ cb = proc do
123
+ tcp = TCPSocket.new TestHost, TestPort
124
+ data = [
125
+ "GET #{path_info}?#{query_string} HTTP/1.1\r\n",
126
+ "Cookie: #{cookie}\r\n",
127
+ "If-none-match: #{etag}\r\n",
128
+ "\r\n"
129
+ ].join
130
+ tcp.write(data)
131
+ received_response = tcp.read
132
+ end
133
+ eb = proc { EventMachine.stop }
134
+ EventMachine.defer cb, eb
135
+ end
136
+
137
+ assert_equal( TestResponse_1, received_response )
138
+ assert_equal( path_info, request_uri )
139
+ assert_equal( query_string, request_query )
140
+ assert_equal( cookie, request_http[:cookie] )
141
+ assert_equal( etag, request_http[:if_none_match] )
142
+ assert_equal( nil, request_http[:content_type] )
143
+ assert_equal( "GET", request_method )
144
+ end
145
+
146
+
147
+ def test_headers
148
+ received_header_string = nil
149
+ received_header_ary = nil
150
+
151
+ EventMachine.run do
152
+ EventMachine.start_server(TestHost, TestPort, MyTestServer) do |conn|
153
+ # In each accepted connection, set up a procedure that will copy
154
+ # the request parameters into a local variable visible here, so
155
+ # we can assert the values later.
156
+ # The @http_headers is set automatically and can easily be parsed.
157
+ # It isn't automatically parsed into Ruby values because that is
158
+ # a costly operation, but we should provide an optional method that
159
+ # does the parsing so it doesn't need to be done by users.
160
+ conn.instance_eval do
161
+ @assertions = proc do
162
+ received_header_string = @http_headers
163
+ received_header_ary = @http.map {|line| line }
164
+ end
165
+ end
166
+ end
167
+
168
+ cb = proc do
169
+ tcp = TCPSocket.new TestHost, TestPort
170
+ data = [
171
+ "GET / HTTP/1.1\r\n",
172
+ "aaa: 111\r\n",
173
+ "bbb: 222\r\n",
174
+ "ccc: 333\r\n",
175
+ "ddd: 444\r\n",
176
+ "\r\n"
177
+ ].join
178
+ tcp.write data
179
+ received_response = tcp.read
180
+ end
181
+ eb = proc { EventMachine.stop }
182
+ EventMachine.defer cb, eb
183
+
184
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
185
+ end
186
+
187
+ assert_equal( ["GET / HTTP/1.1", "aaa: 111", "bbb: 222", "ccc: 333", "ddd: 444"], received_header_string )
188
+ assert_equal( [[:aaa,"111"], [:bbb,"222"], [:ccc,"333"], [:ddd,"444"]], received_header_ary )
189
+ end
190
+
191
+
192
+
193
+
194
+
195
+ def test_post
196
+ received_header_string = nil
197
+ post_content = "1234567890"
198
+ content_type = "text/plain"
199
+ received_post_content = ""
200
+ received_content_type = ""
201
+
202
+ EventMachine.run do
203
+ EventMachine.start_server(TestHost, TestPort, MyTestServer) do |conn|
204
+ # In each accepted connection, set up a procedure that will copy
205
+ # the request parameters into a local variable visible here, so
206
+ # we can assert the values later.
207
+ # The @http_post_content variable is set automatically.
208
+ conn.instance_eval do
209
+ @assertions = proc do
210
+ received_post_content = @http_content
211
+ received_content_type = @http[:content_type]
212
+ end
213
+ end
214
+ end
215
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
216
+
217
+ cb = proc do
218
+ tcp = TCPSocket.new TestHost, TestPort
219
+ data = [
220
+ "POST / HTTP/1.1\r\n",
221
+ "Content-type: #{content_type}\r\n",
222
+ "Content-length: #{post_content.length}\r\n",
223
+ "\r\n",
224
+ post_content
225
+ ].join
226
+ tcp.write(data)
227
+ received_response = tcp.read
228
+ end
229
+ eb = proc do
230
+ EventMachine.stop
231
+ end
232
+ EventMachine.defer cb, eb
233
+ end
234
+
235
+ assert_equal( post_content, received_post_content)
236
+ assert_equal( content_type, received_content_type)
237
+ end
238
+
239
+
240
+ def test_invalid
241
+ received_response = nil
242
+
243
+ EventMachine.run do
244
+ EventMachine.start_server(TestHost, TestPort, MyTestServer) do |conn|
245
+ conn.instance_eval do
246
+ @assertions = proc do
247
+ end
248
+ end
249
+ end
250
+
251
+ cb = proc do
252
+ tcp = TCPSocket.new TestHost, TestPort
253
+ data = [
254
+ "GET foo HTTP/1.1\r\n",
255
+ "\r\n"
256
+ ].join
257
+ tcp.write data
258
+ received_response = tcp.read
259
+ end
260
+ eb = proc { EventMachine.stop }
261
+ EventMachine.defer cb, eb
262
+
263
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
264
+ end
265
+
266
+ assert_equal( "HTTP1/1 400 Bad request\r\nConnection: close\r\nContent-type: text/plain\r\n\r\nDetected error: HTTP code 400", received_response )
267
+ end
268
+
269
+
270
+ end
@@ -0,0 +1,187 @@
1
+ require 'test/unit'
2
+ require 'em-http-server'
3
+
4
+ begin
5
+ once = false
6
+ require 'eventmachine'
7
+ rescue LoadError => e
8
+ raise e if once
9
+ once = true
10
+ require 'rubygems'
11
+ retry
12
+ end
13
+
14
+
15
+ #--------------------------------------
16
+
17
+
18
+ class TestDelegatedHttpResponse < Test::Unit::TestCase
19
+
20
+ # This is a delegate class that (trivially) implements the
21
+ # several classes needed to work with HttpResponse.
22
+ #
23
+ class D
24
+ attr_reader :output_data
25
+ attr_reader :closed_after_writing
26
+
27
+ def send_data data
28
+ @output_data ||= ""
29
+ @output_data << data
30
+ end
31
+ def close_connection_after_writing
32
+ @closed_after_writing = true
33
+ end
34
+ end
35
+
36
+ def setup
37
+ end
38
+
39
+
40
+ def teardown
41
+ end
42
+
43
+
44
+ def test_properties
45
+ a = EM::DelegatedHttpResponse.new( D.new )
46
+ a.status = 200
47
+ a.content = "Some content"
48
+ a.headers["Content-type"] = "text/xml"
49
+ end
50
+
51
+ def test_header_sugarings
52
+ a = EM::DelegatedHttpResponse.new( D.new )
53
+ a.content_type "text/xml"
54
+ a.set_cookie "a=b"
55
+ a.headers["X-bayshore"] = "aaaa"
56
+
57
+ assert_equal({
58
+ "Content-type" => "text/xml",
59
+ "Set-cookie" => ["a=b"],
60
+ "X-bayshore" => "aaaa"
61
+ }, a.headers)
62
+ end
63
+
64
+ def test_send_response
65
+ d = D.new
66
+ a = EM::DelegatedHttpResponse.new( d )
67
+ a.status = 200
68
+ a.send_response
69
+ assert_equal([
70
+ "HTTP/1.1 200 ...\r\n",
71
+ "Content-length: 0\r\n",
72
+ "\r\n"
73
+ ].join, d.output_data)
74
+ assert_equal( true, d.closed_after_writing )
75
+ end
76
+
77
+ def test_send_response_1
78
+ d = D.new
79
+ a = EM::DelegatedHttpResponse.new( d )
80
+ a.status = 200
81
+ a.content_type "text/plain"
82
+ a.content = "ABC"
83
+ a.send_response
84
+ assert_equal([
85
+ "HTTP/1.1 200 ...\r\n",
86
+ "Content-length: 3\r\n",
87
+ "Content-type: text/plain\r\n",
88
+ "\r\n",
89
+ "ABC"
90
+ ].join, d.output_data)
91
+ assert( d.closed_after_writing )
92
+ end
93
+
94
+ def test_send_response_no_close
95
+ d = D.new
96
+ a = EM::DelegatedHttpResponse.new( d )
97
+ a.status = 200
98
+ a.content_type "text/plain"
99
+ a.content = "ABC"
100
+ a.keep_connection_open
101
+ a.send_response
102
+ assert_equal([
103
+ "HTTP/1.1 200 ...\r\n",
104
+ "Content-length: 3\r\n",
105
+ "Content-type: text/plain\r\n",
106
+ "\r\n",
107
+ "ABC"
108
+ ].join, d.output_data)
109
+ assert( ! d.closed_after_writing )
110
+ end
111
+
112
+ def test_send_response_multiple_times
113
+ a = EM::DelegatedHttpResponse.new( D.new )
114
+ a.status = 200
115
+ a.send_response
116
+ assert_raise( RuntimeError ) {
117
+ a.send_response
118
+ }
119
+ end
120
+
121
+ def test_send_headers
122
+ d = D.new
123
+ a = EM::DelegatedHttpResponse.new( d )
124
+ a.status = 200
125
+ a.send_headers
126
+ assert_equal([
127
+ "HTTP/1.1 200 ...\r\n",
128
+ "Content-length: 0\r\n",
129
+ "\r\n"
130
+ ].join, d.output_data)
131
+ assert( ! d.closed_after_writing )
132
+ assert_raise( RuntimeError ) {
133
+ a.send_headers
134
+ }
135
+ end
136
+
137
+ def test_send_chunks
138
+ d = D.new
139
+ a = EM::DelegatedHttpResponse.new( d )
140
+ a.chunk "ABC"
141
+ a.chunk "DEF"
142
+ a.chunk "GHI"
143
+ a.keep_connection_open
144
+ a.send_response
145
+ assert_equal([
146
+ "HTTP/1.1 200 ...\r\n",
147
+ "Transfer-encoding: chunked\r\n",
148
+ "\r\n",
149
+ "3\r\n",
150
+ "ABC\r\n",
151
+ "3\r\n",
152
+ "DEF\r\n",
153
+ "3\r\n",
154
+ "GHI\r\n",
155
+ "0\r\n",
156
+ "\r\n"
157
+ ].join, d.output_data)
158
+ assert( !d.closed_after_writing )
159
+ end
160
+
161
+ def test_send_chunks_with_close
162
+ d = D.new
163
+ a = EM::DelegatedHttpResponse.new( d )
164
+ a.chunk "ABC"
165
+ a.chunk "DEF"
166
+ a.chunk "GHI"
167
+ a.send_response
168
+ assert_equal([
169
+ "HTTP/1.1 200 ...\r\n",
170
+ "Transfer-encoding: chunked\r\n",
171
+ "\r\n",
172
+ "3\r\n",
173
+ "ABC\r\n",
174
+ "3\r\n",
175
+ "DEF\r\n",
176
+ "3\r\n",
177
+ "GHI\r\n",
178
+ "0\r\n",
179
+ "\r\n"
180
+ ].join, d.output_data)
181
+ assert( d.closed_after_writing )
182
+ end
183
+
184
+ end
185
+
186
+
187
+
@@ -0,0 +1,242 @@
1
+ require 'test/unit'
2
+ require 'em-http-server'
3
+
4
+ begin
5
+ once = false
6
+ require 'eventmachine'
7
+ rescue LoadError => e
8
+ raise e if once
9
+ once = true
10
+ require 'rubygems'
11
+ retry
12
+ end
13
+
14
+
15
+ #--------------------------------------
16
+
17
+ module EventMachine
18
+
19
+ # This is a test harness wired into the HttpResponse class so we
20
+ # can test it without requiring any actual network communication.
21
+ #
22
+ class HttpResponse
23
+ attr_reader :output_data
24
+ attr_reader :closed_after_writing
25
+
26
+ def send_data data
27
+ @output_data ||= ""
28
+ @output_data << data
29
+ end
30
+ def close_connection_after_writing
31
+ @closed_after_writing = true
32
+ end
33
+ end
34
+ end
35
+
36
+ #--------------------------------------
37
+
38
+
39
+ class TestHttpResponse < Test::Unit::TestCase
40
+
41
+ def test_properties
42
+ a = EventMachine::HttpResponse.new
43
+ a.status = 200
44
+ a.content = "Some content"
45
+ a.headers["Content-type"] = "text/xml"
46
+ end
47
+
48
+ def test_header_sugarings
49
+ a = EventMachine::HttpResponse.new
50
+ a.content_type "text/xml"
51
+ a.set_cookie "a=b"
52
+ a.headers["X-bayshore"] = "aaaa"
53
+
54
+ assert_equal({
55
+ "Content-type" => "text/xml",
56
+ "Set-cookie" => ["a=b"],
57
+ "X-bayshore" => "aaaa"
58
+ }, a.headers)
59
+ end
60
+
61
+ def test_send_response
62
+ a = EventMachine::HttpResponse.new
63
+ a.status = 200
64
+ a.send_response
65
+ assert_equal([
66
+ "HTTP/1.1 200 ...\r\n",
67
+ "Content-length: 0\r\n",
68
+ "\r\n"
69
+ ].join, a.output_data)
70
+ assert_equal( true, a.closed_after_writing )
71
+ end
72
+
73
+ def test_send_response_with_status
74
+ a = EventMachine::HttpResponse.new
75
+ a.status = 200
76
+ a.status_string = "OK-TEST"
77
+ a.send_response
78
+ assert_equal([
79
+ "HTTP/1.1 200 OK-TEST\r\n",
80
+ "Content-length: 0\r\n",
81
+ "\r\n"
82
+ ].join, a.output_data)
83
+ assert_equal( true, a.closed_after_writing )
84
+ end
85
+
86
+ def test_send_response_1
87
+ a = EventMachine::HttpResponse.new
88
+ a.status = 200
89
+ a.content_type "text/plain"
90
+ a.content = "ABC"
91
+ a.send_response
92
+ assert_equal([
93
+ "HTTP/1.1 200 ...\r\n",
94
+ "Content-length: 3\r\n",
95
+ "Content-type: text/plain\r\n",
96
+ "\r\n",
97
+ "ABC"
98
+ ].join, a.output_data)
99
+ assert( a.closed_after_writing )
100
+ end
101
+
102
+ def test_send_response_no_close
103
+ a = EventMachine::HttpResponse.new
104
+ a.status = 200
105
+ a.content_type "text/plain"
106
+ a.content = "ABC"
107
+ a.keep_connection_open
108
+ a.send_response
109
+ assert_equal([
110
+ "HTTP/1.1 200 ...\r\n",
111
+ "Content-length: 3\r\n",
112
+ "Content-type: text/plain\r\n",
113
+ "\r\n",
114
+ "ABC"
115
+ ].join, a.output_data)
116
+ assert( ! a.closed_after_writing )
117
+ end
118
+
119
+ def test_send_response_no_close_with_a_404_response
120
+ a = EventMachine::HttpResponse.new
121
+ a.status = 404
122
+ a.content_type "text/plain"
123
+ a.content = "ABC"
124
+ a.keep_connection_open
125
+ a.send_response
126
+ assert_equal([
127
+ "HTTP/1.1 404 ...\r\n",
128
+ "Content-length: 3\r\n",
129
+ "Content-type: text/plain\r\n",
130
+ "\r\n",
131
+ "ABC"
132
+ ].join, a.output_data)
133
+ assert( ! a.closed_after_writing )
134
+ end
135
+
136
+ def test_send_response_no_close_with_a_201_response
137
+ a = EventMachine::HttpResponse.new
138
+ a.status = 201
139
+ a.content_type "text/plain"
140
+ a.content = "ABC"
141
+ a.keep_connection_open
142
+ a.send_response
143
+ assert_equal([
144
+ "HTTP/1.1 201 ...\r\n",
145
+ "Content-length: 3\r\n",
146
+ "Content-type: text/plain\r\n",
147
+ "\r\n",
148
+ "ABC"
149
+ ].join, a.output_data)
150
+ assert( ! a.closed_after_writing )
151
+ end
152
+
153
+ def test_send_response_no_close_with_a_500_response
154
+ a = EventMachine::HttpResponse.new
155
+ a.status = 500
156
+ a.content_type "text/plain"
157
+ a.content = "ABC"
158
+ a.keep_connection_open
159
+ a.send_response
160
+ assert_equal([
161
+ "HTTP/1.1 500 ...\r\n",
162
+ "Content-length: 3\r\n",
163
+ "Content-type: text/plain\r\n",
164
+ "\r\n",
165
+ "ABC"
166
+ ].join, a.output_data)
167
+ assert( a.closed_after_writing )
168
+ end
169
+
170
+ def test_send_response_multiple_times
171
+ a = EventMachine::HttpResponse.new
172
+ a.status = 200
173
+ a.send_response
174
+ assert_raise( RuntimeError ) {
175
+ a.send_response
176
+ }
177
+ end
178
+
179
+ def test_send_headers
180
+ a = EventMachine::HttpResponse.new
181
+ a.status = 200
182
+ a.send_headers
183
+ assert_equal([
184
+ "HTTP/1.1 200 ...\r\n",
185
+ "Content-length: 0\r\n",
186
+ "\r\n"
187
+ ].join, a.output_data)
188
+ assert( ! a.closed_after_writing )
189
+ assert_raise( RuntimeError ) {
190
+ a.send_headers
191
+ }
192
+ end
193
+
194
+ def test_send_chunks
195
+ a = EventMachine::HttpResponse.new
196
+ a.chunk "ABC"
197
+ a.chunk "DEF"
198
+ a.chunk "GHI"
199
+ a.keep_connection_open
200
+ a.send_response
201
+ assert_equal([
202
+ "HTTP/1.1 200 ...\r\n",
203
+ "Transfer-encoding: chunked\r\n",
204
+ "\r\n",
205
+ "3\r\n",
206
+ "ABC\r\n",
207
+ "3\r\n",
208
+ "DEF\r\n",
209
+ "3\r\n",
210
+ "GHI\r\n",
211
+ "0\r\n",
212
+ "\r\n"
213
+ ].join, a.output_data)
214
+ assert( !a.closed_after_writing )
215
+ end
216
+
217
+ def test_send_chunks_with_close
218
+ a = EventMachine::HttpResponse.new
219
+ a.chunk "ABC"
220
+ a.chunk "DEF"
221
+ a.chunk "GHI"
222
+ a.send_response
223
+ assert_equal([
224
+ "HTTP/1.1 200 ...\r\n",
225
+ "Transfer-encoding: chunked\r\n",
226
+ "\r\n",
227
+ "3\r\n",
228
+ "ABC\r\n",
229
+ "3\r\n",
230
+ "DEF\r\n",
231
+ "3\r\n",
232
+ "GHI\r\n",
233
+ "0\r\n",
234
+ "\r\n"
235
+ ].join, a.output_data)
236
+ assert( a.closed_after_writing )
237
+ end
238
+
239
+ end
240
+
241
+
242
+
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-http-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - alor
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: &70366152260060 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70366152260060
25
+ description: Simple http server for eventmachine
26
+ email:
27
+ - alberto.ornaghi@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - .gitignore
33
+ - Gemfile
34
+ - LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - em-http-server.gemspec
38
+ - lib/em-http-server.rb
39
+ - lib/em-http-server/response.rb
40
+ - lib/em-http-server/server.rb
41
+ - test/test_app.rb
42
+ - test/test_delegated.rb
43
+ - test/test_response.rb
44
+ homepage: ''
45
+ licenses: []
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubyforge_project:
64
+ rubygems_version: 1.8.15
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: Simple http server for eventmachine with the same interface as evma_httpserver
68
+ test_files:
69
+ - test/test_app.rb
70
+ - test/test_delegated.rb
71
+ - test/test_response.rb