em-http-server 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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