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.
@@ -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