mongrel2 0.24.0 → 0.25.0.pre.285
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/ChangeLog +53 -1
- data/History.rdoc +14 -0
- data/examples/Procfile +1 -1
- data/examples/async-upload.rb +4 -1
- data/examples/config.rb +4 -3
- data/examples/request-dumper.rb +1 -0
- data/examples/request-dumper.tmpl +6 -5
- data/examples/ws-echo.rb +3 -3
- data/lib/mongrel2.rb +2 -2
- data/lib/mongrel2/connection.rb +4 -2
- data/lib/mongrel2/constants.rb +7 -0
- data/lib/mongrel2/httprequest.rb +0 -42
- data/lib/mongrel2/httpresponse.rb +20 -22
- data/lib/mongrel2/request.rb +97 -3
- data/lib/mongrel2/response.rb +34 -6
- data/lib/mongrel2/testing.rb +2 -4
- data/lib/mongrel2/websocket.rb +104 -45
- data/spec/mongrel2/httprequest_spec.rb +0 -40
- data/spec/mongrel2/httpresponse_spec.rb +6 -12
- data/spec/mongrel2/request_spec.rb +112 -3
- data/spec/mongrel2/response_spec.rb +22 -7
- data/spec/mongrel2/websocket_spec.rb +32 -13
- metadata +2 -2
- metadata.gz.sig +0 -0
data/lib/mongrel2/response.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
2
|
|
3
|
+
require 'stringio'
|
3
4
|
require 'tnetstring'
|
4
5
|
require 'yajl'
|
5
6
|
require 'loggability'
|
@@ -31,10 +32,13 @@ class Mongrel2::Response
|
|
31
32
|
|
32
33
|
### Create a new Response object for the specified +sender_id+, +conn_id+, and +body+.
|
33
34
|
def initialize( sender_id, conn_id, body='' )
|
35
|
+
body = StringIO.new( body, 'a+' ) unless body.respond_to?( :read )
|
36
|
+
|
34
37
|
@sender_id = sender_id
|
35
38
|
@conn_id = conn_id
|
36
39
|
@body = body
|
37
40
|
@request = nil
|
41
|
+
@chunksize = DEFAULT_CHUNKSIZE
|
38
42
|
end
|
39
43
|
|
40
44
|
|
@@ -50,12 +54,23 @@ class Mongrel2::Response
|
|
50
54
|
# the response will be routed to by the mongrel2 server
|
51
55
|
attr_accessor :conn_id
|
52
56
|
|
53
|
-
# The body of the response
|
54
|
-
|
57
|
+
# The body of the response as an IO (or IOish) object
|
58
|
+
attr_reader :body
|
55
59
|
|
56
60
|
# The request that this response is for, if there is one
|
57
61
|
attr_accessor :request
|
58
62
|
|
63
|
+
# The number of bytes to write to Mongrel in a single "chunk"
|
64
|
+
attr_accessor :chunksize
|
65
|
+
|
66
|
+
|
67
|
+
### Set the response body to +newbody+. If +newbody+ is not a IO-like object (i.e., it
|
68
|
+
### doesn't respond to #eof?, it will be wrapped in a StringIO in 'a+' mode).
|
69
|
+
def body=( newbody )
|
70
|
+
newbody = StringIO.new( newbody, 'a+' ) unless newbody.respond_to?( :eof? )
|
71
|
+
@body = newbody
|
72
|
+
end
|
73
|
+
|
59
74
|
|
60
75
|
### Append the given +object+ to the response body. Returns the response for
|
61
76
|
### chaining.
|
@@ -67,15 +82,28 @@ class Mongrel2::Response
|
|
67
82
|
|
68
83
|
### Write the given +objects+ to the response body, calling #to_s on each one.
|
69
84
|
def puts( *objects )
|
70
|
-
|
71
|
-
self << obj.to_s.chomp << $/
|
72
|
-
end
|
85
|
+
self.body.puts( *objects )
|
73
86
|
end
|
74
87
|
|
75
88
|
|
76
89
|
### Stringify the response, which just returns its body.
|
77
90
|
def to_s
|
78
|
-
|
91
|
+
pos = self.body.pos
|
92
|
+
self.body.pos = 0
|
93
|
+
return self.body.read
|
94
|
+
ensure
|
95
|
+
self.body.pos = pos
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
### Yield chunks of the response to the caller's block. By default, just yields
|
100
|
+
### the result of calling #to_s on the response.
|
101
|
+
def each_chunk
|
102
|
+
if block_given?
|
103
|
+
yield( self.to_s )
|
104
|
+
else
|
105
|
+
return [ self.to_s ].to_enum
|
106
|
+
end
|
79
107
|
end
|
80
108
|
|
81
109
|
|
data/lib/mongrel2/testing.rb
CHANGED
@@ -159,8 +159,7 @@ module Mongrel2
|
|
159
159
|
headers = self.make_merged_headers( :POST, uri, headers )
|
160
160
|
rclass = Mongrel2::Request.subclass_for_method( :POST )
|
161
161
|
|
162
|
-
req = rclass.new( self.sender_id, self.conn_id.to_s, uri.to_s, headers )
|
163
|
-
req.body = body
|
162
|
+
req = rclass.new( self.sender_id, self.conn_id.to_s, uri.to_s, headers, body )
|
164
163
|
|
165
164
|
return req
|
166
165
|
end
|
@@ -175,8 +174,7 @@ module Mongrel2
|
|
175
174
|
headers = self.make_merged_headers( :PUT, uri, headers )
|
176
175
|
rclass = Mongrel2::Request.subclass_for_method( :PUT )
|
177
176
|
|
178
|
-
req = rclass.new( self.sender_id, self.conn_id.to_s, uri.to_s, headers )
|
179
|
-
req.body = body
|
177
|
+
req = rclass.new( self.sender_id, self.conn_id.to_s, uri.to_s, headers, body )
|
180
178
|
|
181
179
|
return req
|
182
180
|
end
|
data/lib/mongrel2/websocket.rb
CHANGED
@@ -26,6 +26,11 @@ module Mongrel2::WebSocket
|
|
26
26
|
|
27
27
|
# WebSocket-related header and status constants
|
28
28
|
module Constants
|
29
|
+
|
30
|
+
# The default number of bytes to write out to Mongrel for a single "chunk"
|
31
|
+
DEFAULT_CHUNKSIZE = 512 * 1024 # 512 kilobytes
|
32
|
+
|
33
|
+
|
29
34
|
# WebSocket frame header
|
30
35
|
# 0 1 2 3
|
31
36
|
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
@@ -245,9 +250,10 @@ module Mongrel2::WebSocket
|
|
245
250
|
|
246
251
|
super
|
247
252
|
|
248
|
-
@flags
|
253
|
+
@flags = Integer( self.headers.flags || DEFAULT_FLAGS )
|
249
254
|
@request_frame = nil
|
250
|
-
@errors
|
255
|
+
@errors = []
|
256
|
+
@chunksize = DEFAULT_CHUNKSIZE
|
251
257
|
end
|
252
258
|
|
253
259
|
|
@@ -270,6 +276,8 @@ module Mongrel2::WebSocket
|
|
270
276
|
# The Array of validation errors
|
271
277
|
attr_reader :errors
|
272
278
|
|
279
|
+
# The number of bytes to write to Mongrel in a single "chunk"
|
280
|
+
attr_accessor :chunksize
|
273
281
|
|
274
282
|
### Returns +true+ if the request's FIN flag is set. This flag indicates that
|
275
283
|
### this is the final fragment in a message. The first fragment MAY also be
|
@@ -332,9 +340,7 @@ module Mongrel2::WebSocket
|
|
332
340
|
|
333
341
|
### Write the given +objects+ to the payload, calling #to_s on each one.
|
334
342
|
def puts( *objects )
|
335
|
-
|
336
|
-
self << obj.to_s.chomp << $/
|
337
|
-
end
|
343
|
+
self.payload.puts( *objects )
|
338
344
|
end
|
339
345
|
|
340
346
|
|
@@ -344,61 +350,101 @@ module Mongrel2::WebSocket
|
|
344
350
|
self.log.warn "Unknown status code %d" unless CLOSING_STATUS_DESC.key?( statuscode )
|
345
351
|
status_msg = "%d %s" % [ statuscode, CLOSING_STATUS_DESC[statuscode] ]
|
346
352
|
|
347
|
-
self.payload.
|
353
|
+
self.payload.truncate( 0 )
|
354
|
+
self.payload.puts( status_msg )
|
348
355
|
end
|
349
356
|
|
350
357
|
|
351
|
-
###
|
352
|
-
###
|
358
|
+
### Validate the frame, raising a Mongrel2::WebSocket::FrameError if there
|
359
|
+
### are validation problems.
|
353
360
|
def validate
|
361
|
+
unless self.valid?
|
362
|
+
self.log.error "Validation failed."
|
363
|
+
raise Mongrel2::WebSocket::FrameError, "invalid frame: %s" % [ self.errors.join(', ') ]
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
|
368
|
+
### Sanity-checks the frame and returns +false+ if any problems are found.
|
369
|
+
### Error messages will be in #errors.
|
370
|
+
def valid?
|
354
371
|
self.errors.clear
|
355
372
|
|
356
373
|
self.validate_payload_encoding
|
357
374
|
self.validate_control_frame
|
358
375
|
self.validate_opcode
|
359
376
|
self.validate_reserved_flags
|
377
|
+
|
378
|
+
return self.errors.empty?
|
360
379
|
end
|
361
380
|
|
362
381
|
|
363
|
-
###
|
364
|
-
###
|
365
|
-
def
|
382
|
+
### Mongrel2::Connection API -- Yield the response in chunks if called with a block, else
|
383
|
+
### return an Enumerator that will do the same.
|
384
|
+
def each_chunk
|
366
385
|
self.validate
|
367
|
-
|
386
|
+
|
387
|
+
iter = Enumerator.new do |yielder|
|
388
|
+
self.bytes.each_slice( self.chunksize ) do |bytes|
|
389
|
+
yielder.yield( bytes.pack('C*') )
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
if block_given?
|
394
|
+
block = Proc.new
|
395
|
+
iter.each( &block )
|
396
|
+
else
|
397
|
+
return iter
|
398
|
+
end
|
368
399
|
end
|
369
400
|
|
370
401
|
|
371
402
|
### Stringify into a response suitable for sending to the client.
|
372
403
|
def to_s
|
373
|
-
|
404
|
+
self.remember_payload_settings do
|
405
|
+
self.payload.rewind
|
406
|
+
self.payload.set_encoding( 'binary' )
|
374
407
|
|
375
|
-
|
376
|
-
|
377
|
-
if self.opcode != :binary && data.encoding != Encoding::UTF_8
|
378
|
-
self.log.debug "Transcoding %s payload data to UTF-8" % [ data.encoding.name ]
|
379
|
-
data.encode!( Encoding::UTF_8 )
|
380
|
-
end
|
408
|
+
header = self.make_header
|
409
|
+
data = self.payload.read
|
381
410
|
|
382
|
-
|
383
|
-
unless self.valid?
|
384
|
-
self.log.error "Validation failed."
|
385
|
-
raise Mongrel2::WebSocket::FrameError, "invalid frame: %s" %
|
386
|
-
[ self.errors.join( ', ' ) ]
|
411
|
+
return header + data
|
387
412
|
end
|
388
|
-
|
389
|
-
# Now force everything into binary so it can be catenated
|
390
|
-
data.force_encoding( Encoding::ASCII_8BIT )
|
391
|
-
return [
|
392
|
-
self.make_header( data ),
|
393
|
-
data
|
394
|
-
].join
|
395
413
|
end
|
396
414
|
|
397
415
|
|
398
416
|
### Return an Enumerator for the bytes of the raw frame as it appears
|
399
417
|
### on the wire.
|
400
418
|
def bytes
|
401
|
-
|
419
|
+
self.remember_payload_settings do
|
420
|
+
self.payload.rewind
|
421
|
+
self.log.debug "Making a bytes iterator for a %s payload" %
|
422
|
+
[ self.payload.external_encoding.name ]
|
423
|
+
|
424
|
+
return Enumerator.new do |yielder|
|
425
|
+
self.payload.set_encoding( 'binary' )
|
426
|
+
self.payload.rewind
|
427
|
+
|
428
|
+
header_i = self.make_header.bytes
|
429
|
+
body_i = self.payload.bytes
|
430
|
+
|
431
|
+
header_i.each_with_index {|byte, i| yielder.yield(byte) }
|
432
|
+
body_i.each_with_index {|byte, i| yielder.yield(byte) }
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
|
438
|
+
### Remember the payload IO's external encoding, position, etc. and restore them
|
439
|
+
### when the block returns.
|
440
|
+
def remember_payload_settings
|
441
|
+
original_enc = self.payload.external_encoding
|
442
|
+
original_pos = self.payload.pos
|
443
|
+
|
444
|
+
yield
|
445
|
+
ensure
|
446
|
+
self.payload.set_encoding( original_enc ) if original_enc
|
447
|
+
self.payload.pos = original_pos if original_pos
|
402
448
|
end
|
403
449
|
|
404
450
|
|
@@ -414,7 +460,7 @@ module Mongrel2::WebSocket
|
|
414
460
|
self.log.debug "Setting up response %p with symmetrical flags" % [ @response ]
|
415
461
|
if self.opcode == :ping
|
416
462
|
@response.opcode = :pong
|
417
|
-
@response.payload
|
463
|
+
IO.copy_stream( self.payload, @response.payload, 4096 )
|
418
464
|
else
|
419
465
|
@response.opcode = self.opcode
|
420
466
|
end
|
@@ -472,15 +518,16 @@ module Mongrel2::WebSocket
|
|
472
518
|
self.rsv3? ? 1 : 0,
|
473
519
|
self.opcode,
|
474
520
|
self.numeric_opcode,
|
475
|
-
(self.payload.
|
521
|
+
(self.payload.size / 1024.0),
|
476
522
|
]
|
477
523
|
end
|
478
524
|
|
479
525
|
|
480
|
-
### Make a WebSocket header for the
|
481
|
-
def make_header
|
526
|
+
### Make a WebSocket header for the frame and return it.
|
527
|
+
def make_header
|
482
528
|
header = ''.force_encoding( Encoding::ASCII_8BIT )
|
483
|
-
length =
|
529
|
+
length = self.payload.size
|
530
|
+
|
484
531
|
self.log.debug "Making wire protocol header for payload of %d bytes" % [ length ]
|
485
532
|
|
486
533
|
# Pack the frame according to its size
|
@@ -505,12 +552,14 @@ module Mongrel2::WebSocket
|
|
505
552
|
### #errors.
|
506
553
|
def validate_payload_encoding
|
507
554
|
if self.opcode == :binary
|
508
|
-
self.log.debug "Binary payload:
|
509
|
-
self.payload.
|
555
|
+
self.log.debug "Binary payload: setting external encoding to ASCII-8BIT"
|
556
|
+
self.payload.set_encoding( Encoding::ASCII_8BIT )
|
510
557
|
else
|
511
|
-
self.log.debug "Non-binary payload:
|
512
|
-
self.payload.
|
513
|
-
|
558
|
+
self.log.debug "Non-binary payload: setting external encoding to UTF-8"
|
559
|
+
self.payload.set_encoding( Encoding::UTF_8 )
|
560
|
+
# :TODO: Is there a way to check that the data in a File or Socket will
|
561
|
+
# transcode successfully? Probably not.
|
562
|
+
# self.errors << "Invalid UTF8 in payload" unless self.payload.valid_encoding?
|
514
563
|
end
|
515
564
|
end
|
516
565
|
|
@@ -520,8 +569,8 @@ module Mongrel2::WebSocket
|
|
520
569
|
def validate_control_frame
|
521
570
|
return unless self.control?
|
522
571
|
|
523
|
-
if self.payload.
|
524
|
-
self.log.error "Payload of control frame exceeds 125 bytes (%d)" % [ self.payload.
|
572
|
+
if self.payload.size > 125
|
573
|
+
self.log.error "Payload of control frame exceeds 125 bytes (%d)" % [ self.payload.size ]
|
525
574
|
self.errors << "payload of control frame cannot exceed 125 bytes"
|
526
575
|
end
|
527
576
|
|
@@ -536,7 +585,7 @@ module Mongrel2::WebSocket
|
|
536
585
|
### opcodes, you'll want to override this.
|
537
586
|
def validate_opcode
|
538
587
|
if self.opcode == :reserved
|
539
|
-
self.log.error "Frame uses reserved opcode 0x%x" % [ self.numeric_opcode ]
|
588
|
+
self.log.error "Frame uses reserved opcode 0x%x" % [ self.numeric_opcode ]
|
540
589
|
self.errors << "Frame uses reserved opcode"
|
541
590
|
end
|
542
591
|
end
|
@@ -552,6 +601,16 @@ module Mongrel2::WebSocket
|
|
552
601
|
end
|
553
602
|
|
554
603
|
|
604
|
+
#######
|
605
|
+
private
|
606
|
+
#######
|
607
|
+
|
608
|
+
### Return a simple hexdump of the specified +data+.
|
609
|
+
def hexdump( data )
|
610
|
+
data.bytes.to_a.map {|byte| sprintf('%#02x',byte) }.join( ' ' )
|
611
|
+
end
|
612
|
+
|
613
|
+
|
555
614
|
end # class Frame
|
556
615
|
|
557
616
|
end # module Mongrel2::WebSocket
|
@@ -143,46 +143,6 @@ describe Mongrel2::HTTPRequest do
|
|
143
143
|
@req.remote_ip.to_s.should == '127.0.0.1'
|
144
144
|
end
|
145
145
|
|
146
|
-
it "knows if it's an 'async upload started' notification" do
|
147
|
-
@req.headers.x_mongrel2_upload_start = '/tmp/mongrel2.upload.20120503-54578-rs3l2g'
|
148
|
-
@req.should be_upload_started()
|
149
|
-
@req.should_not be_upload_done()
|
150
|
-
end
|
151
|
-
|
152
|
-
it "knows if it's an 'async upload done' notification" do
|
153
|
-
@req.headers.x_mongrel2_upload_start = '/tmp/mongrel2.upload.20120503-54578-rs3l2g'
|
154
|
-
@req.headers.x_mongrel2_upload_done = '/tmp/mongrel2.upload.20120503-54578-rs3l2g'
|
155
|
-
@req.should_not be_upload_started()
|
156
|
-
@req.should be_upload_done()
|
157
|
-
@req.should be_valid_upload()
|
158
|
-
end
|
159
|
-
|
160
|
-
it "knows if it's not a valid 'async upload done' notification" do
|
161
|
-
@req.headers.x_mongrel2_upload_start = '/tmp/mongrel2.upload.20120503-54578-rs3l2g'
|
162
|
-
@req.headers.x_mongrel2_upload_done = '/etc/passwd'
|
163
|
-
@req.should_not be_upload_started()
|
164
|
-
@req.should be_upload_done()
|
165
|
-
@req.should_not be_valid_upload()
|
166
|
-
end
|
167
|
-
|
168
|
-
it "raises an exception if the uploaded file fetched with mismatched headers" do
|
169
|
-
@req.headers.x_mongrel2_upload_start = '/tmp/mongrel2.upload.20120503-54578-rs3l2g'
|
170
|
-
@req.headers.x_mongrel2_upload_done = '/etc/passwd'
|
171
|
-
|
172
|
-
expect {
|
173
|
-
@req.uploaded_file
|
174
|
-
}.to raise_error( Mongrel2::UploadError, /upload headers/i )
|
175
|
-
end
|
176
|
-
|
177
|
-
it "can return a Pathname object for the uploaded file if it's valid" do
|
178
|
-
@req.headers.x_mongrel2_upload_start = '/tmp/mongrel2.upload.20120503-54578-rs3l2g'
|
179
|
-
@req.headers.x_mongrel2_upload_done = '/tmp/mongrel2.upload.20120503-54578-rs3l2g'
|
180
|
-
|
181
|
-
@req.should be_valid_upload()
|
182
|
-
@req.uploaded_file.should be_a( Pathname )
|
183
|
-
@req.uploaded_file.to_s.should == '/tmp/mongrel2.upload.20120503-54578-rs3l2g'
|
184
|
-
end
|
185
|
-
|
186
146
|
end
|
187
147
|
|
188
148
|
end
|
@@ -75,7 +75,7 @@ describe Mongrel2::HTTPResponse do
|
|
75
75
|
end
|
76
76
|
|
77
77
|
it "doesn't have a body" do
|
78
|
-
@response.body.should
|
78
|
+
@response.body.size.should == 0
|
79
79
|
end
|
80
80
|
|
81
81
|
it "stringifies to a valid RFC2616 response string" do
|
@@ -94,7 +94,8 @@ describe Mongrel2::HTTPResponse do
|
|
94
94
|
@response.reset
|
95
95
|
|
96
96
|
@response.should_not be_handled()
|
97
|
-
@response.body.should
|
97
|
+
@response.body.should be_a( StringIO )
|
98
|
+
@response.body.size.should == 0
|
98
99
|
@response.headers.should have(1).keys
|
99
100
|
end
|
100
101
|
|
@@ -116,7 +117,7 @@ describe Mongrel2::HTTPResponse do
|
|
116
117
|
|
117
118
|
it "can find the length of its body if it's a String with multi-byte characters in it" do
|
118
119
|
test_body = 'Хорошая собака, Стрелке! Очень хорошо.'
|
119
|
-
@response
|
120
|
+
@response << test_body
|
120
121
|
|
121
122
|
@response.get_content_length.should == test_body.bytesize
|
122
123
|
end
|
@@ -238,14 +239,6 @@ describe Mongrel2::HTTPResponse do
|
|
238
239
|
@response.get_content_length.should == 0
|
239
240
|
end
|
240
241
|
|
241
|
-
it "raises a descriptive error message if it can't get the body's length" do
|
242
|
-
@response.body = Object.new
|
243
|
-
|
244
|
-
lambda {
|
245
|
-
@response.get_content_length
|
246
|
-
}.should raise_error( Mongrel2::ResponseError, /content length/i )
|
247
|
-
end
|
248
|
-
|
249
242
|
|
250
243
|
it "can build a valid HTTP status line for its status" do
|
251
244
|
@response.status = HTTP::SEE_OTHER
|
@@ -271,7 +264,8 @@ describe Mongrel2::HTTPResponse do
|
|
271
264
|
|
272
265
|
it "has a puts method for appending objects to the body" do
|
273
266
|
@response.puts( :something_to_sable )
|
274
|
-
@response.body.
|
267
|
+
@response.body.rewind
|
268
|
+
@response.body.read.should == "something_to_sable\n"
|
275
269
|
end
|
276
270
|
|
277
271
|
end
|