mongrel2 0.24.0 → 0.25.0.pre.285
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.
- 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
|