fotonauts-eventmachine_httpserver 0.0.1

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