class Mongrel2::WebSocket::Frame
WebSocket
frame class; this is used for both requests and responses in WebSocket
services.
Constants
- DEFAULT_FLAGS
The default frame header flags: FIN + CLOSE
- DEFAULT_FRAGMENT_SIZE
The default size of the payload of fragment frames
Attributes
The payload data
The payload data
The number of bytes to write to Mongrel in a single “chunk”
The Array of validation errors
The frame's header flags as an Integer
The payload data
Public Class Methods
Define accessors for the flag of the specified name
and bit
.
# File lib/mongrel2/websocket.rb, line 475 def self::attr_flag( name, bitmask ) define_method( "#{name}?" ) do (self.flags & bitmask).nonzero? end define_method( "#{name}=" ) do |newvalue| if newvalue self.flags |= bitmask else self.flags ^= ( self.flags & bitmask ) end end end
Create one or more fragmented frames for the data read from io
and yield each to the specified block. If no block is given, return a iterator that will yield the frames instead. The io
can be any object that responds to readpartial, and the blocking semantics follow those of that method when iterating.
# File lib/mongrel2/websocket.rb, line 438 def self::each_fragment( io, opcode, size: DEFAULT_FRAGMENT_SIZE, &block ) raise ArgumentError, "Invalid opcode %p" % [opcode] unless OPCODE.key?( opcode ) iter = Enumerator.new do |yielder| count = 0 until io.eof? self.log.debug "Reading frame %d" % [ count ] data = io.readpartial( size ) frame = if count.zero? new( data, opcode ) else new( data, :continuation ) end frame.fin = io.eof? yielder.yield( frame ) count += 1 end end return iter.each( &block ) if block return iter end
Create a new websocket frame that will be the body of a request or response.
# File lib/mongrel2/websocket.rb, line 494 def initialize( payload='', *flags ) @payload = StringIO.new( payload.dup ) @flags = DEFAULT_FLAGS @errors = [] @chunksize = DEFAULT_CHUNKSIZE self.set_flags( *flags ) unless flags.empty? end
Public Instance Methods
Append the given object
to the payload. Returns the Frame
for chaining.
# File lib/mongrel2/websocket.rb, line 614 def <<( object ) self.payload << object return self end
Returns true
if the request is a WebSocket
control frame.
# File lib/mongrel2/websocket.rb, line 607 def control? return ( self.flags & OPCODE_CONTROL_MASK ).nonzero? end
Return an Enumerator for the bytes of the raw frame as it appears on the wire.
# File lib/mongrel2/websocket.rb, line 693 def each_byte( &block ) self.log.debug "Making a bytes iterator for a %s payload" % [ self.payload.external_encoding.name ] payload_copy = self.payload.clone payload_copy.set_encoding( 'binary' ) payload_copy.rewind iter = self.make_header.each_byte + payload_copy.each_byte return iter unless block return iter.each( &block ) end
Mongrel2::Connection
API – Yield the response in chunks if called with a block, else return an Enumerator that will do the same.
# File lib/mongrel2/websocket.rb, line 671 def each_chunk( &block ) self.validate iter = Enumerator.new do |yielder| self.bytes.each_slice( self.chunksize ) do |bytes| yielder.yield( bytes.pack('C*') ) end end return iter unless block return iter.each( &block ) end
Returns true if one or more of the RSV1-3 bits is set.
# File lib/mongrel2/websocket.rb, line 571 def has_rsv_flags? return ( self.flags & RSV_FLAG_MASK ).nonzero? end
Return the frame as a human-readable string suitable for debugging.
# File lib/mongrel2/websocket.rb, line 710 def inspect return "#<%p:%#0x %s>" % [ self.class, self.object_id * 2, self.inspect_details, ] end
Set the :close opcode on this frame and set its status to statuscode
.
# File lib/mongrel2/websocket.rb, line 627 def make_close_frame( statuscode=Mongrel2::WebSocket::CLOSE_NORMAL ) self.opcode = :close self.set_status( statuscode ) end
Return the numeric opcode of the frame.
# File lib/mongrel2/websocket.rb, line 584 def numeric_opcode return self.flags & OPCODE_BITMASK end
Returns the name of the frame's opcode as a Symbol. The numeric_opcode
method returns the numeric one.
# File lib/mongrel2/websocket.rb, line 578 def opcode return OPCODE_NAME[ self.numeric_opcode ] end
Set the frame's opcode to code
, which should be either a numeric opcode or its equivalent name (i.e., :continuation, :text, :binary, :close, :ping, :pong)
# File lib/mongrel2/websocket.rb, line 591 def opcode=( code ) opcode = nil if code.is_a?( Numeric ) opcode = Integer( code ) else opcode = OPCODE[ code.to_sym ] or raise ArgumentError, "unknown opcode %p" % [ code ] end self.flags ^= ( self.flags & OPCODE_BITMASK ) self.flags |= opcode end
Write the given objects
to the payload, calling to_s
on each one.
# File lib/mongrel2/websocket.rb, line 621 def puts( *objects ) self.payload.puts( *objects ) end
Apply flag bits and opcodes: (:fin, :rsv1, :rsv2, :rsv3, :continuation, :text, :binary, :close, :ping, :pong) to the frame.
# Transform the frame into a CLOSE frame and set its FIN flag frame.set_flags( :fin, :close )
# File lib/mongrel2/websocket.rb, line 544 def set_flags( *flag_symbols ) flag_symbols.flatten! flag_symbols.compact! self.log.debug "Setting flags for symbols: %p" % [ flag_symbols ] flag_symbols.each do |flag| case flag when :fin, :rsv1, :rsv2, :rsv3 self.__send__( "#{flag}=", true ) when :continuation, :text, :binary, :close, :ping, :pong self.opcode = flag when Integer self.log.debug " setting Integer flags directly: %#08b" % [ flag ] self.flags |= flag when /\A0x\h{2}\z/ val = Integer( flag ) self.log.debug " setting (stringified) Integer flags directly: %#08b" % [ val ] self.flags = val else raise ArgumentError, "Don't know what the %p flag is." % [ flag ] end end end
Overwrite the frame's payload with a status message based on statuscode
.
# File lib/mongrel2/websocket.rb, line 635 def set_status( statuscode ) self.log.warn "Unknown status code %d" unless CLOSING_STATUS_DESC.key?( statuscode ) status_msg = "%d %s" % [ statuscode, CLOSING_STATUS_DESC[statuscode] ] self.payload.truncate( 0 ) self.payload.puts( status_msg ) end
Stringify into a response suitable for sending to the client.
# File lib/mongrel2/websocket.rb, line 686 def to_s return self.each_byte.to_a.pack( 'C*' ) end
Sanity-checks the frame and returns false
if any problems are found. Error
messages will be in errors
.
# File lib/mongrel2/websocket.rb, line 657 def valid? self.errors.clear self.validate_payload_encoding self.validate_control_frame self.validate_opcode self.validate_reserved_flags return self.errors.empty? end
Validate the frame, raising a Mongrel2::WebSocket::FrameError
if there are validation problems.
# File lib/mongrel2/websocket.rb, line 646 def validate unless self.valid? self.log.error "Validation failed." raise Mongrel2::WebSocket::FrameError, "invalid frame: %s" % [ self.errors.join(', ') ] end end
Protected Instance Methods
Return the details to include in the contents of the inspected object.
# File lib/mongrel2/websocket.rb, line 724 def inspect_details return %Q{FIN:%d RSV1:%d RSV2:%d RSV3:%d OPCODE:%s (0x%x) -- %0.2fK body} % [ self.fin? ? 1 : 0, self.rsv1? ? 1 : 0, self.rsv2? ? 1 : 0, self.rsv3? ? 1 : 0, self.opcode, self.numeric_opcode, (self.payload.size / 1024.0), ] end
Make a WebSocket
header for the frame and return it.
# File lib/mongrel2/websocket.rb, line 738 def make_header header = nil length = self.payload.size self.log.debug "Making wire protocol header for payload of %d bytes" % [ length ] # Pack the frame according to its size if length >= 2**16 self.log.debug " giant size, using 8-byte (64-bit int) length field" header = [ self.flags, 127, length ].pack( 'c2q>' ) elsif length > 125 self.log.debug " big size, using 2-byte (16-bit int) length field" header = [ self.flags, 126, length ].pack( 'c2n' ) else self.log.debug " small size, using payload length field" header = [ self.flags, length ].pack( 'c2' ) end self.log.debug " header is: 0: %02x %02x" % header.unpack('C*') return header end
Sanity-check control frame data
, adding an error message to errors
if there's a problem.
# File lib/mongrel2/websocket.rb, line 780 def validate_control_frame return unless self.control? if self.payload.size > 125 self.log.error "Payload of control frame exceeds 125 bytes (%d)" % [ self.payload.size ] self.errors << "payload of control frame cannot exceed 125 bytes" end unless self.fin? self.log.error "Control frame fragmented (FIN is unset)" self.errors << "control frame is fragmented (no FIN flag set)" end end
Ensure that the frame has a valid opcode in its header. If you're using reserved opcodes, you'll want to override this.
# File lib/mongrel2/websocket.rb, line 797 def validate_opcode if self.opcode == :reserved self.log.error "Frame uses reserved opcode 0x%x" % [ self.numeric_opcode ] self.errors << "Frame uses reserved opcode" end end
Validate that the payload encoding is correct for its opcode, attempting to transcode it if it's not. If the transcoding fails, adds an error to errors
.
# File lib/mongrel2/websocket.rb, line 764 def validate_payload_encoding if self.opcode == :binary self.log.debug "Binary payload: setting external encoding to ASCII-8BIT" self.payload.set_encoding( Encoding::ASCII_8BIT ) else self.log.debug "Non-binary payload: setting external encoding to UTF-8" self.payload.set_encoding( Encoding::UTF_8 ) # :TODO: Is there a way to check that the data in a File or Socket will # transcode successfully? Probably not. # self.errors << "Invalid UTF8 in payload" unless self.payload.valid_encoding? end end
Ensure that the frame doesn't have any of the reserved flags set (RSV1-3). If your subprotocol uses one or more of these, you'll want to override this method.
# File lib/mongrel2/websocket.rb, line 807 def validate_reserved_flags if self.has_rsv_flags? self.log.error "Frame has one or more reserved flags set." self.errors << "Frame has one or more reserved flags set." end end
Private Instance Methods
Return a simple hexdump of the specified data
.
# File lib/mongrel2/websocket.rb, line 820 def hexdump( data ) data.bytes.to_a.map {|byte| sprintf('%#02x',byte) }.join( ' ' ) end