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

body[RW]

The payload data

body=[RW]

The payload data

chunksize[RW]

The number of bytes to write to Mongrel in a single “chunk”

errors[R]

The Array of validation errors

flags[RW]

The frame's header flags as an Integer

payload[RW]

The payload data

Public Class Methods

attr_flag( name, bitmask ) click to toggle source

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
each_fragment( io, opcode, size: DEFAULT_FRAGMENT_SIZE, &block ) click to toggle source

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
new( payload='', *flags ) click to toggle source

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

<<( object ) click to toggle source

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
bytes( &block )
Alias for: each_byte
control?() click to toggle source

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
each_byte( &block ) click to toggle source

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
Also aliased as: bytes
each_chunk( &block ) click to toggle source

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
has_rsv_flags?() click to toggle source

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
inspect() click to toggle source

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
make_close_frame( statuscode=Mongrel2::WebSocket::CLOSE_NORMAL ) click to toggle source

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
numeric_opcode() click to toggle source

Return the numeric opcode of the frame.

# File lib/mongrel2/websocket.rb, line 584
def numeric_opcode
        return self.flags & OPCODE_BITMASK
end
opcode() click to toggle source

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
opcode=( code ) click to toggle source

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
puts( *objects ) click to toggle source

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
set_flags( *flag_symbols ) click to toggle source

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
set_status( statuscode ) click to toggle source

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
to_s() click to toggle source

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
valid?() click to toggle source

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() click to toggle source

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

inspect_details() click to toggle source

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_header() click to toggle source

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
validate_control_frame() click to toggle source

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
validate_opcode() click to toggle source

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_payload_encoding() click to toggle source

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
validate_reserved_flags() click to toggle source

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

hexdump( data ) click to toggle source

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