eventmachine_httpserver_update 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,356 @@
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
+ STATUS_CODES = {
53
+ 100 => "100 Continue",
54
+ 101 => "101 Switching Protocols",
55
+ 200 => "200 OK",
56
+ 201 => "201 Created",
57
+ 202 => "202 Accepted",
58
+ 203 => "203 Non-Authoritative Information",
59
+ 204 => "204 No Content",
60
+ 205 => "205 Reset Content",
61
+ 206 => "206 Partial Content",
62
+ 300 => "300 Multiple Choices",
63
+ 301 => "301 Moved Permanently",
64
+ 302 => "302 Found",
65
+ 303 => "303 See Other",
66
+ 304 => "304 Not Modified",
67
+ 305 => "305 Use Proxy",
68
+ 307 => "307 Temporary Redirect",
69
+ 400 => "400 Bad Request",
70
+ 401 => "401 Unauthorized",
71
+ 402 => "402 Payment Required",
72
+ 403 => "403 Forbidden",
73
+ 404 => "404 Not Found",
74
+ 405 => "405 Method Not Allowed",
75
+ 406 => "406 Not Acceptable",
76
+ 407 => "407 Proxy Authentication Required",
77
+ 408 => "408 Request Timeout",
78
+ 409 => "409 Conflict",
79
+ 410 => "410 Gone",
80
+ 411 => "411 Length Required",
81
+ 412 => "412 Precondition Failed",
82
+ 413 => "413 Request Entity Too Large",
83
+ 414 => "414 Request-URI Too Long",
84
+ 415 => "415 Unsupported Media Type",
85
+ 416 => "416 Requested Range Not Satisfiable",
86
+ 417 => "417 Expectation Failed",
87
+ 500 => "500 Internal Server Error",
88
+ 501 => "501 Not Implemented",
89
+ 502 => "502 Bad Gateway",
90
+ 503 => "503 Service Unavailable",
91
+ 504 => "504 Gateway Timeout",
92
+ 505 => "505 HTTP Version Not Supported"
93
+ }
94
+
95
+ attr_accessor :status, :headers, :chunks, :multiparts
96
+
97
+ def initialize
98
+ @headers = {}
99
+ end
100
+
101
+ def content=(value) @content = value.to_s end
102
+ def content() @content || '' end
103
+ def content?() !!@content end
104
+
105
+ def keep_connection_open arg=true
106
+ @keep_connection_open = arg
107
+ end
108
+
109
+ def status=(status)
110
+ @status = STATUS_CODES[status.to_i] || status
111
+ end
112
+
113
+ # sugarings for headers
114
+ def content_type *mime
115
+ if mime.length > 0
116
+ @headers["Content-Type"] = mime.first.to_s
117
+ else
118
+ @headers["Content-Type"]
119
+ end
120
+ end
121
+
122
+ # Sugaring for Set-Cookie headers. These are a pain because there can easily and
123
+ # legitimately be more than one. So we use an ugly verb to signify that.
124
+ # #add_set_cookies does NOT disturb the Set-Cookie headers which may have been
125
+ # added on a prior call. #set_cookie clears them out first.
126
+ def add_set_cookie *ck
127
+ if ck.length > 0
128
+ h = (@headers["Set-Cookie"] ||= [])
129
+ ck.each {|c| h << c}
130
+ end
131
+ end
132
+ def set_cookie *ck
133
+ h = (@headers["Set-Cookie"] ||= [])
134
+ if ck.length > 0
135
+ h.clear
136
+ add_set_cookie *ck
137
+ else
138
+ h
139
+ end
140
+ end
141
+
142
+
143
+ # This is intended to send a complete HTTP response, including closing the connection
144
+ # if appropriate at the end of the transmission. Don't use this method to send partial
145
+ # or iterated responses. This method will send chunks and multiparts provided they
146
+ # are all available when we get here.
147
+ # Note that the default @status is 200 if the value doesn't exist.
148
+ def send_response
149
+ send_headers
150
+ send_body
151
+ send_trailer
152
+ close_connection_after_writing unless (@keep_connection_open and (@status || "200 OK") == "200 OK")
153
+ end
154
+
155
+ # Send the headers out in alpha-sorted order. This will degrade performance to some
156
+ # degree, and is intended only to simplify the construction of unit tests.
157
+ #
158
+ def send_headers
159
+ raise "sent headers already" if @sent_headers
160
+ @sent_headers = true
161
+
162
+ fixup_headers
163
+
164
+ ary = []
165
+ ary << "HTTP/1.1 #{@status || '200 OK'}\r\n"
166
+ ary += generate_header_lines(@headers)
167
+ ary << "\r\n"
168
+
169
+ send_data ary.join
170
+ end
171
+
172
+
173
+ def generate_header_lines in_hash
174
+ out_ary = []
175
+ in_hash.keys.sort.each {|k|
176
+ v = in_hash[k]
177
+ if v.is_a?(Array)
178
+ v.each {|v1| out_ary << "#{k}: #{v1}\r\n" }
179
+ else
180
+ out_ary << "#{k}: #{v}\r\n"
181
+ end
182
+ }
183
+ out_ary
184
+ end
185
+ private :generate_header_lines
186
+
187
+
188
+ # Examine the content type and data and other things, and perform a final
189
+ # fixup of the header array. We expect this to be called just before sending
190
+ # headers to the remote peer.
191
+ # In the case of multiparts, we ASSUME we will get called before any content
192
+ # gets sent out, because the multipart boundary is created here.
193
+ #
194
+ def fixup_headers
195
+ if @content
196
+ @headers["Content-Length"] = @content.bytesize
197
+ elsif @chunks
198
+ @headers["Transfer-Encoding"] = "chunked"
199
+ # Might be nice to ENSURE there is no content-length header,
200
+ # but how to detect all the possible permutations of upper/lower case?
201
+ elsif @multiparts
202
+ @multipart_boundary = self.class.concoct_multipart_boundary
203
+ @headers["Content-Type"] = "multipart/x-mixed-replace; boundary=\"#{@multipart_boundary}\""
204
+ else
205
+ @headers["Content-Length"] = 0
206
+ end
207
+ end
208
+
209
+ # we send either content, chunks, or multiparts. Content can only be sent once.
210
+ # Chunks and multiparts can be sent any number of times.
211
+ # DO NOT close the connection or send any goodbye kisses. This method can
212
+ # be called multiple times to send out chunks or multiparts.
213
+ def send_body
214
+ if @chunks
215
+ send_chunks
216
+ elsif @multiparts
217
+ send_multiparts
218
+ else
219
+ send_content
220
+ end
221
+ end
222
+
223
+ # send a trailer which depends on the type of body we're dealing with.
224
+ # The assumption is that we're about to end the transmission of this particular
225
+ # HTTP response. (A connection-close may or may not follow.)
226
+ #
227
+ def send_trailer
228
+ send_headers unless @sent_headers
229
+ if @chunks
230
+ unless @last_chunk_sent
231
+ chunk ""
232
+ send_chunks
233
+ end
234
+ elsif @multiparts
235
+ # in the lingo of RFC 2046/5.1.1, we're sending an "epilog"
236
+ # consisting of a blank line. I really don't know how that is
237
+ # supposed to interact with the case where we leave the connection
238
+ # open after transmitting the multipart response.
239
+ send_data "\r\n--#{@multipart_boundary}--\r\n\r\n"
240
+ end
241
+ end
242
+
243
+ def send_content
244
+ raise "sent content already" if @sent_content
245
+ @sent_content = true
246
+ send_data(content)
247
+ end
248
+
249
+ # add a chunk to go to the output.
250
+ # Will cause the headers to pick up "content-transfer-encoding"
251
+ # Add the chunk to a list. Calling #send_chunks will send out the
252
+ # available chunks and clear the chunk list WITHOUT closing the connection,
253
+ # so it can be called any number of times.
254
+ # TODO!!! Per RFC2616, we may not send chunks to an HTTP/1.0 client.
255
+ # Raise an exception here if our user tries to do so.
256
+ # Chunked transfer coding is defined in RFC2616 pgh 3.6.1.
257
+ # The argument can be a string or a hash. The latter allows for
258
+ # sending chunks with extensions (someday).
259
+ #
260
+ def chunk text
261
+ @chunks ||= []
262
+ @chunks << text
263
+ end
264
+
265
+ # send the contents of the chunk list and clear it out.
266
+ # ASSUMES that headers have been sent.
267
+ # Does NOT close the connection.
268
+ # Can be called multiple times.
269
+ # According to RFC2616, phg 3.6.1, the last chunk will be zero length.
270
+ # But some caller could accidentally set a zero-length chunk in the middle
271
+ # of the stream. If that should happen, raise an exception.
272
+ # The reason for supporting chunks that are hashes instead of just strings
273
+ # is to enable someday supporting chunk-extension codes (cf the RFC).
274
+ # TODO!!! We're not supporting the final entity-header that may be
275
+ # transmitted after the last (zero-length) chunk.
276
+ #
277
+ def send_chunks
278
+ send_headers unless @sent_headers
279
+ while chunk = @chunks.shift
280
+ raise "last chunk already sent" if @last_chunk_sent
281
+ text = chunk.is_a?(Hash) ? chunk[:text] : chunk.to_s
282
+ send_data "#{format("%x", text.length).upcase}\r\n#{text}\r\n"
283
+ @last_chunk_sent = true if text.length == 0
284
+ end
285
+ end
286
+
287
+ # To add a multipart to the outgoing response, specify the headers and the
288
+ # body. If only a string is given, it's treated as the body (in this case,
289
+ # the header is assumed to be empty).
290
+ #
291
+ def multipart arg
292
+ vals = if arg.is_a?(String)
293
+ {:body => arg, :headers => {}}
294
+ else
295
+ arg
296
+ end
297
+
298
+ @multiparts ||= []
299
+ @multiparts << vals
300
+ end
301
+
302
+ # Multipart syntax is defined in RFC 2046, pgh 5.1.1 et seq.
303
+ # The CRLF which introduces the boundary line of each part (content entity)
304
+ # is defined as being part of the boundary, not of the preceding part.
305
+ # So we don't need to mess with interpreting the last bytes of a part
306
+ # to ensure they are CRLF-terminated.
307
+ #
308
+ def send_multiparts
309
+ send_headers unless @sent_headers
310
+ while part = @multiparts.shift
311
+ send_data "\r\n--#{@multipart_boundary}\r\n"
312
+ send_data( generate_header_lines( part[:headers] || {} ).join)
313
+ send_data "\r\n"
314
+ send_data part[:body].to_s
315
+ end
316
+ end
317
+
318
+ # TODO, this is going to be way too slow. Cache up the uuidgens.
319
+ #
320
+ def self.concoct_multipart_boundary
321
+ @multipart_index ||= 0
322
+ @multipart_index += 1
323
+ if @multipart_index >= 1000
324
+ @multipart_index = 0
325
+ @multipart_guid = nil
326
+ end
327
+ @multipart_guid ||= `uuidgen -r`.chomp.gsub(/\-/,"")
328
+ "#{@multipart_guid}#{@multipart_index}"
329
+ end
330
+
331
+ def send_redirect location
332
+ @status = 302 # TODO, make 301 available by parameter
333
+ @headers["Location"] = location
334
+ send_response
335
+ end
336
+ end
337
+ end
338
+
339
+ #----------------------------------------------------------------------------
340
+
341
+ require 'forwardable'
342
+
343
+ module EventMachine
344
+ class DelegatedHttpResponse < HttpResponse
345
+ extend Forwardable
346
+ def_delegators :@delegate,
347
+ :send_data,
348
+ :close_connection,
349
+ :close_connection_after_writing
350
+
351
+ def initialize dele
352
+ super()
353
+ @delegate = dele
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,239 @@
1
+ require 'test/unit'
2
+ require 'evma_httpserver'
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
+ module HttpServer
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 OK
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.module_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::Connection
79
+ include EventMachine::HttpServer
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_parms = {}
101
+
102
+
103
+ EventMachine.run do
104
+ EventMachine.start_server(TestHost, TestPort, MyTestServer) do |conn|
105
+ # In each accepted connection, set up a procedure that will copy
106
+ # the request parameters into a local variable visible here, so
107
+ # we can assert the values later.
108
+ conn.instance_eval do
109
+ @assertions = proc {
110
+ parms = %w( PATH_INFO QUERY_STRING HTTP_COOKIE IF_NONE_MATCH
111
+ CONTENT_TYPE REQUEST_METHOD REQUEST_URI )
112
+ parms.each {|parm|
113
+ # request_parms is bound to a local variable visible in this context.
114
+ request_parms[parm] = ENV[parm]
115
+ }
116
+ }
117
+ end
118
+ end
119
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
120
+
121
+ cb = proc do
122
+ tcp = TCPSocket.new TestHost, TestPort
123
+ data = [
124
+ "GET #{path_info}?#{query_string} HTTP/1.1\r\n",
125
+ "Cookie: #{cookie}\r\n",
126
+ "If-none-match: #{etag}\r\n",
127
+ "\r\n"
128
+ ].join
129
+ tcp.write(data)
130
+ received_response = tcp.read
131
+ end
132
+ eb = proc { EventMachine.stop }
133
+ EventMachine.defer cb, eb
134
+ end
135
+
136
+ assert_equal( TestResponse_1, received_response )
137
+ assert_equal( path_info, request_parms["PATH_INFO"] )
138
+ assert_equal( query_string, request_parms["QUERY_STRING"] )
139
+ assert_equal( cookie, request_parms["HTTP_COOKIE"] )
140
+ assert_equal( etag, request_parms["IF_NONE_MATCH"] )
141
+ assert_equal( nil, request_parms["CONTENT_TYPE"] )
142
+ assert_equal( "GET", request_parms["REQUEST_METHOD"] )
143
+ assert_equal( path_info, request_parms["REQUEST_URI"] )
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_headers.split(/\0/).map {|line| line.split(/:\s*/, 2) }
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( "aaa: 111\0bbb: 222\0ccc: 333\0ddd: 444\0\0", 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_post_content
211
+ received_content_type = ENV["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( received_post_content, post_content )
236
+ assert_equal( received_content_type, content_type )
237
+ end
238
+
239
+ end