eventmachine_httpserver 0.0.1

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.
@@ -0,0 +1,37 @@
1
+ # $Id: evma_httpserver.rb 3897 2007-03-06 20:21:08Z francis $
2
+ #
3
+ # EventMachine HTTP Server
4
+ #
5
+ # Author:: blackhedd (gmail address: garbagecat10).
6
+ #
7
+ # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
8
+ #
9
+ # This program is made available under the terms of the GPL version 2.
10
+ #
11
+ #----------------------------------------------------------------------------
12
+ #
13
+ # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
14
+ #
15
+ # Gmail: garbagecat10
16
+ #
17
+ # This program is free software; you can redistribute it and/or modify
18
+ # it under the terms of the GNU General Public License as published by
19
+ # the Free Software Foundation; either version 2 of the License, or
20
+ # (at your option) any later version.
21
+ #
22
+ # This program is distributed in the hope that it will be useful,
23
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
24
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
+ # GNU General Public License for more details.
26
+ #
27
+ # You should have received a copy of the GNU General Public License
28
+ # along with this program; if not, write to the Free Software
29
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
30
+ #
31
+ #---------------------------------------------------------------------------
32
+ #
33
+
34
+
35
+ require 'eventmachine_httpserver'
36
+ require 'evma_httpserver/response'
37
+
@@ -0,0 +1,280 @@
1
+ # $Id: response.rb 3897 2007-03-06 20:21:08Z francis $
2
+ #
3
+ # EventMachine HTTP Server
4
+ # HTTP Response-support class
5
+ #
6
+ # Author:: blackhedd (gmail address: garbagecat10).
7
+ #
8
+ # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
9
+ #
10
+ # This program is made available under the terms of the GPL version 2.
11
+ #
12
+ #----------------------------------------------------------------------------
13
+ #
14
+ # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
15
+ #
16
+ # Gmail: garbagecat10
17
+ #
18
+ # This program is free software; you can redistribute it and/or modify
19
+ # it under the terms of the GNU General Public License as published by
20
+ # the Free Software Foundation; either version 2 of the License, or
21
+ # (at your option) any later version.
22
+ #
23
+ # This program is distributed in the hope that it will be useful,
24
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
+ # GNU General Public License for more details.
27
+ #
28
+ # You should have received a copy of the GNU General Public License
29
+ # along with this program; if not, write to the Free Software
30
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
31
+ #
32
+ #---------------------------------------------------------------------------
33
+ #
34
+
35
+ module EventMachine
36
+
37
+ class HttpResponse
38
+ attr_accessor :status, :content, :headers, :chunks, :multiparts
39
+
40
+ def initialize
41
+ @headers = {}
42
+ end
43
+
44
+ def keep_connection_open arg=true
45
+ @keep_connection_open = arg
46
+ end
47
+
48
+ # sugarings for headers
49
+ def content_type *mime
50
+ if mime.length > 0
51
+ @headers ["Content-type"] = mime.first.to_s
52
+ else
53
+ @headers ["Content-type"]
54
+ end
55
+ end
56
+
57
+ # Sugaring for Set-cookie headers. These are a pain because there can easily and
58
+ # legitimately be more than one. So we use an ugly verb to signify that.
59
+ # #add_set_cookies does NOT disturb the set-cookie headers which may have been
60
+ # added on a prior call. #set_cookie clears them out first.
61
+ def add_set_cookie *ck
62
+ if ck.length > 0
63
+ h = (@headers ["Set-cookie"] ||= [])
64
+ ck.each {|c| h << c}
65
+ end
66
+ end
67
+ def set_cookie *ck
68
+ h = (@headers ["Set-cookie"] ||= [])
69
+ if ck.length > 0
70
+ h.clear
71
+ add_set_cookie *ck
72
+ else
73
+ h
74
+ end
75
+ end
76
+
77
+
78
+ # This is intended to send a complete HTTP response, including closing the connection
79
+ # if appropriate at the end of the transmission. Don't use this method to send partial
80
+ # or iterated responses. This method will send chunks and multiparts provided they
81
+ # are all available when we get here.
82
+ # Note that the default @status is 200 if the value doesn't exist.
83
+ def send_response
84
+ send_headers
85
+ send_body
86
+ send_trailer
87
+ close_connection_after_writing unless (@keep_connection_open and (@status || 200) == 200)
88
+ end
89
+
90
+ # Send the headers out in alpha-sorted order. This will degrade performance to some
91
+ # degree, and is intended only to simplify the construction of unit tests.
92
+ #
93
+ def send_headers
94
+ raise "sent headers already" if @sent_headers
95
+ @sent_headers = true
96
+
97
+ fixup_headers
98
+
99
+ ary = []
100
+ ary << "HTTP/1.1 #{@status || 200} ...\r\n"
101
+ ary += generate_header_lines(@headers)
102
+ ary << "\r\n"
103
+
104
+ send_data ary.join
105
+ end
106
+
107
+
108
+ def generate_header_lines in_hash
109
+ out_ary = []
110
+ in_hash.keys.sort.each {|k|
111
+ v = @headers[k]
112
+ if v.is_a?(Array)
113
+ v.each {|v1| out_ary << "#{k}: #{v1}\r\n" }
114
+ else
115
+ out_ary << "#{k}: #{v}\r\n"
116
+ end
117
+ }
118
+ out_ary
119
+ end
120
+ private :generate_header_lines
121
+
122
+
123
+ # Examine the content type and data and other things, and perform a final
124
+ # fixup of the header array. We expect this to be called just before sending
125
+ # headers to the remote peer.
126
+ # In the case of multiparts, we ASSUME we will get called before any content
127
+ # gets sent out, because the multipart boundary is created here.
128
+ #
129
+ def fixup_headers
130
+ if @content
131
+ @headers ["Content-length"] = @content.to_s.length
132
+ elsif @chunks
133
+ @headers ["Transfer-encoding"] = "chunked"
134
+ # Might be nice to ENSURE there is no content-length header,
135
+ # but how to detect all the possible permutations of upper/lower case?
136
+ elsif @multiparts
137
+ @multipart_boundary = self.class.concoct_multipart_boundary
138
+ @headers ["Content-type"] = "multipart/x-mixed-replace; boundary=\"#{@multipart_boundary}\""
139
+ else
140
+ @headers ["Content-length"] = 0
141
+ end
142
+ end
143
+
144
+ # we send either content, chunks, or multiparts. Content can only be sent once.
145
+ # Chunks and multiparts can be sent any number of times.
146
+ # DO NOT close the connection or send any goodbye kisses. This method can
147
+ # be called multiple times to send out chunks or multiparts.
148
+ def send_body
149
+ if @content
150
+ send_content
151
+ elsif @chunks
152
+ send_chunks
153
+ elsif @multiparts
154
+ send_multiparts
155
+ else
156
+ @content = ""
157
+ send_content
158
+ end
159
+ end
160
+
161
+ # send a trailer which depends on the type of body we're dealing with.
162
+ # The assumption is that we're about to end the transmission of this particular
163
+ # HTTP response. (A connection-close may or may not follow.)
164
+ #
165
+ def send_trailer
166
+ send_headers unless @sent_headers
167
+ if @content
168
+ # no-op
169
+ elsif @chunks
170
+ unless @last_chunk_sent
171
+ chunk ""
172
+ send_chunks
173
+ end
174
+ elsif @multiparts
175
+ # in the lingo of RFC 2046/5.1.1, we're sending an "epilog"
176
+ # consisting of a blank line. I really don't know how that is
177
+ # supposed to interact with the case where we leave the connection
178
+ # open after transmitting the multipart response.
179
+ send_data "\r\n--#{@multipart_boundary}--\r\n\r\n"
180
+ else
181
+ # no-op
182
+ end
183
+ end
184
+
185
+ def send_content
186
+ raise "sent content already" if @sent_content
187
+ @sent_content = true
188
+ send_data((@content || "").to_s)
189
+ end
190
+
191
+ # add a chunk to go to the output.
192
+ # Will cause the headers to pick up "content-transfer-encoding"
193
+ # Add the chunk to a list. Calling #send_chunks will send out the
194
+ # available chunks and clear the chunk list WITHOUT closing the connection,
195
+ # so it can be called any number of times.
196
+ # TODO!!! Per RFC2616, we may not send chunks to an HTTP/1.0 client.
197
+ # Raise an exception here if our user tries to do so.
198
+ # Chunked transfer coding is defined in RFC2616 pgh 3.6.1.
199
+ # The argument can be a string or a hash. The latter allows for
200
+ # sending chunks with extensions (someday).
201
+ #
202
+ def chunk text
203
+ @chunks ||= []
204
+ @chunks << text
205
+ end
206
+
207
+ # send the contents of the chunk list and clear it out.
208
+ # ASSUMES that headers have been sent.
209
+ # Does NOT close the connection.
210
+ # Can be called multiple times.
211
+ # According to RFC2616, phg 3.6.1, the last chunk will be zero length.
212
+ # But some caller could accidentally set a zero-length chunk in the middle
213
+ # of the stream. If that should happen, raise an exception.
214
+ # The reason for supporting chunks that are hashes instead of just strings
215
+ # is to enable someday supporting chunk-extension codes (cf the RFC).
216
+ # TODO!!! We're not supporting the final entity-header that may be
217
+ # transmitted after the last (zero-length) chunk.
218
+ #
219
+ def send_chunks
220
+ send_headers unless @sent_headers
221
+ while chunk = @chunks.shift
222
+ raise "last chunk already sent" if @last_chunk_sent
223
+ text = chunk.is_a?(Hash) ? chunk[:text] : chunk.to_s
224
+ send_data "#{format("%x", text.length).upcase}\r\n#{text}\r\n"
225
+ @last_chunk_sent = true if text.length == 0
226
+ end
227
+ end
228
+
229
+ # To add a multipart to the outgoing response, specify the headers and the
230
+ # body. If only a string is given, it's treated as the body (in this case,
231
+ # the header is assumed to be empty).
232
+ #
233
+ def multipart arg
234
+ vals = if arg.is_a?(String)
235
+ {:body => arg, :headers => {}}
236
+ else
237
+ arg
238
+ end
239
+
240
+ @multiparts ||= []
241
+ @multiparts << vals
242
+ end
243
+
244
+ # Multipart syntax is defined in RFC 2046, pgh 5.1.1 et seq.
245
+ # The CRLF which introduces the boundary line of each part (content entity)
246
+ # is defined as being part of the boundary, not of the preceding part.
247
+ # So we don't need to mess with interpreting the last bytes of a part
248
+ # to ensure they are CRLF-terminated.
249
+ #
250
+ def send_multiparts
251
+ send_headers unless @sent_headers
252
+ while part = @multiparts.shift
253
+ send_data "\r\n--#{@multipart_boundary}\r\n"
254
+ send_data( generate_header_lines( part[:headers] || {} ).join)
255
+ send_data "\r\n"
256
+ send_data part[:body].to_s
257
+ end
258
+ end
259
+
260
+ #
261
+ #
262
+ def self.concoct_multipart_boundary
263
+ @multipart_index ||= 0
264
+ @multipart_index += 1
265
+ if @multipart_index >= 1000
266
+ @multipart_index = 0
267
+ @multipart_guid = nil
268
+ end
269
+ @multipart_guid ||= `uuidgen -r`.chomp.gsub(/\-/,"")
270
+ "#{@multipart_guid}#{@multipart_index}"
271
+ end
272
+
273
+ def send_redirect location
274
+ @status = 302 # TODO, make 301 available by parameter
275
+ @headers["Location"] = location
276
+ send_response
277
+ end
278
+ end
279
+ end
280
+
@@ -0,0 +1,235 @@
1
+ # $Id: app.rb 3893 2007-03-06 20:12:09Z francis $
2
+ #
3
+ #
4
+
5
+
6
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
7
+
8
+ require 'eventmachine'
9
+ require 'evma_httpserver'
10
+
11
+
12
+ #--------------------------------------
13
+
14
+ module EventMachine
15
+ module HttpServer
16
+ def process_http_request
17
+ send_data generate_response()
18
+ close_connection_after_writing
19
+ end
20
+ end
21
+ end
22
+
23
+ #--------------------------------------
24
+
25
+
26
+ class TestApp < Test::Unit::TestCase
27
+
28
+ TestHost = "127.0.0.1"
29
+ TestPort = 8911
30
+
31
+ TestResponse_1 = %Q(HTTP/1.0 200 ...
32
+ Content-length: 4
33
+ Content-type: text/plain
34
+ Connection: close
35
+
36
+ 1234)
37
+
38
+ def setup
39
+ Thread.abort_on_exception = true
40
+ end
41
+
42
+ def teardown
43
+ end
44
+
45
+
46
+
47
+ def test_simple_get
48
+ received_response = nil
49
+
50
+ EventMachine::HttpServer.module_eval {
51
+ def generate_response
52
+ TestResponse_1
53
+ end
54
+ }
55
+
56
+
57
+ EventMachine.run {
58
+ EventMachine.start_server TestHost, TestPort, EventMachine::HttpServer
59
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
60
+
61
+ EventMachine.defer proc {
62
+ tcp = TCPSocket.new TestHost, TestPort
63
+ tcp.write "GET / HTTP/1.0\r\n\r\n"
64
+ received_response = tcp.read
65
+ }, proc {
66
+ EventMachine.stop
67
+ }
68
+ }
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 {
104
+ EventMachine.start_server(TestHost, TestPort, MyTestServer) {|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 {@assertions = proc {
109
+ %w( PATH_INFO
110
+ QUERY_STRING
111
+ HTTP_COOKIE
112
+ IF_NONE_MATCH
113
+ CONTENT_TYPE
114
+ REQUEST_METHOD
115
+ REQUEST_URI
116
+ ).each {|parm|
117
+ # request_parms is bound to a local variable visible in this context.
118
+ request_parms[parm] = ENV[parm]
119
+ }
120
+ }}
121
+ }
122
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
123
+
124
+ EventMachine.defer proc {
125
+ tcp = TCPSocket.new TestHost, TestPort
126
+ tcp.write [
127
+ "GET #{path_info}?#{query_string} HTTP/1.1\r\n",
128
+ "Cookie: #{cookie}\r\n",
129
+ "If-none-match: #{etag}\r\n",
130
+ "\r\n"
131
+ ].join
132
+ received_response = tcp.read
133
+ }, proc {
134
+ EventMachine.stop
135
+ }
136
+ }
137
+
138
+ assert_equal( TestResponse_1, received_response )
139
+ assert_equal( path_info, request_parms["PATH_INFO"] )
140
+ assert_equal( query_string, request_parms["QUERY_STRING"] )
141
+ assert_equal( cookie, request_parms["HTTP_COOKIE"] )
142
+ assert_equal( etag, request_parms["IF_NONE_MATCH"] )
143
+ assert_equal( nil, request_parms["CONTENT_TYPE"] )
144
+ assert_equal( "GET", request_parms["REQUEST_METHOD"] )
145
+ assert_equal( path_info, request_parms["REQUEST_URI"] )
146
+ end
147
+
148
+
149
+ def test_headers
150
+ received_header_string = nil
151
+ received_header_ary = nil
152
+
153
+ EventMachine.run {
154
+ EventMachine.start_server(TestHost, TestPort, MyTestServer) {|conn|
155
+ # In each accepted connection, set up a procedure that will copy
156
+ # the request parameters into a local variable visible here, so
157
+ # we can assert the values later.
158
+ # The @http_headers is set automatically and can easily be parsed.
159
+ # It isn't automatically parsed into Ruby values because that is
160
+ # a costly operation, but we should provide an optional method that
161
+ # does the parsing so it doesn't need to be done by users.
162
+ conn.instance_eval {@assertions = proc {
163
+ received_header_string = @http_headers
164
+ received_header_ary = @http_headers.split(/\0/).map {|line| line.split(/:\s*/, 2) }
165
+ }}
166
+ }
167
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
168
+
169
+ EventMachine.defer proc {
170
+ tcp = TCPSocket.new TestHost, TestPort
171
+ tcp.write [
172
+ "GET / HTTP/1.1\r\n",
173
+ "aaa: 111\r\n",
174
+ "bbb: 222\r\n",
175
+ "ccc: 333\r\n",
176
+ "ddd: 444\r\n",
177
+ "\r\n"
178
+ ].join
179
+ received_response = tcp.read
180
+ }, proc {
181
+ EventMachine.stop
182
+ }
183
+ }
184
+
185
+ assert_equal( "aaa: 111\0bbb: 222\0ccc: 333\0ddd: 444\0\0", received_header_string )
186
+ assert_equal( [["aaa","111"], ["bbb","222"], ["ccc","333"], ["ddd","444"]], received_header_ary )
187
+ end
188
+
189
+
190
+
191
+
192
+
193
+ def test_post
194
+ received_header_string = nil
195
+ post_content = "1234567890"
196
+ content_type = "text/plain"
197
+ received_post_content = ""
198
+ received_content_type = ""
199
+
200
+ EventMachine.run {
201
+ EventMachine.start_server(TestHost, TestPort, MyTestServer) {|conn|
202
+ # In each accepted connection, set up a procedure that will copy
203
+ # the request parameters into a local variable visible here, so
204
+ # we can assert the values later.
205
+ # The @http_post_content variable is set automatically.
206
+ conn.instance_eval {@assertions = proc {
207
+ received_post_content = @http_post_content
208
+ received_content_type = ENV["CONTENT_TYPE"]
209
+ }}
210
+ }
211
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
212
+
213
+ EventMachine.defer proc {
214
+ tcp = TCPSocket.new TestHost, TestPort
215
+ tcp.write [
216
+ "POST / HTTP/1.1\r\n",
217
+ "Content-type: #{content_type}\r\n",
218
+ "Content-length: #{post_content.length}\r\n",
219
+ "\r\n",
220
+ post_content
221
+ ].join
222
+ received_response = tcp.read
223
+ }, proc {
224
+ EventMachine.stop
225
+ }
226
+ }
227
+
228
+ assert_equal( received_post_content, post_content )
229
+ assert_equal( received_content_type, content_type )
230
+ end
231
+
232
+ end
233
+
234
+
235
+