eventmachine_httpserver_update 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README +48 -0
- data/Rakefile +92 -0
- data/docs/COPYING +281 -0
- data/docs/README +8 -0
- data/docs/RELEASE_NOTES +3 -0
- data/eventmachine_httpserver.gemspec +31 -0
- data/eventmachine_httpserver.gemspec.tmpl +22 -0
- data/ext/extconf.rb +133 -0
- data/ext/http.cpp +585 -0
- data/ext/http.h +117 -0
- data/ext/rubyhttp.cpp +305 -0
- data/lib/evma_httpserver.rb +35 -0
- data/lib/evma_httpserver/response.rb +356 -0
- data/test/test_app.rb +239 -0
- data/test/test_delegated.rb +184 -0
- data/test/test_response.rb +175 -0
- metadata +67 -0
@@ -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
|
data/test/test_app.rb
ADDED
@@ -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
|