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.
@@ -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
- attr_accessor :body
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
- objects.each do |obj|
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
- return self.body
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
 
@@ -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
@@ -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 = Integer( self.headers.flags || DEFAULT_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
- objects.each do |obj|
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.replace( status_msg )
353
+ self.payload.truncate( 0 )
354
+ self.payload.puts( status_msg )
348
355
  end
349
356
 
350
357
 
351
- ### Check the frame for problems, appending descriptions of any issues to
352
- ### the #errors array.
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
- ### Sanity-checks the frame and returns +false+ if any problems are found.
364
- ### Error messages will be in #errors.
365
- def valid?
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
- return self.errors.empty?
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
- data = self.payload.to_s
404
+ self.remember_payload_settings do
405
+ self.payload.rewind
406
+ self.payload.set_encoding( 'binary' )
374
407
 
375
- # Make sure the outgoing payload is UTF-8, except in the case of a
376
- # binary frame.
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
- # Make sure everything's in order
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
- return self.to_s.bytes
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 = self.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.bytesize / 1024.0),
521
+ (self.payload.size / 1024.0),
476
522
  ]
477
523
  end
478
524
 
479
525
 
480
- ### Make a WebSocket header for the receiving frame and return it.
481
- def make_header( data )
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 = data.bytesize
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: forcing to ASCII-8BIT"
509
- self.payload.force_encoding( Encoding::ASCII_8BIT )
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: forcing to UTF-8"
512
- self.payload.force_encoding( Encoding::UTF_8 )
513
- self.errors << "Invalid UTF8 in payload" unless self.payload.valid_encoding?
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.bytesize > 125
524
- self.log.error "Payload of control frame exceeds 125 bytes (%d)" % [ self.payload.bytesize ]
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 be_empty()
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.body = test_body
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.should == "something_to_sable\n"
267
+ @response.body.rewind
268
+ @response.body.read.should == "something_to_sable\n"
275
269
  end
276
270
 
277
271
  end