Mongrel2::WebSocket::

Frame

class
Superclass
Mongrel2::Request

WebSocket frame class; this is used for both requests and responses in WebSocket services.

Constants

DEFAULT_FLAGS

The default frame header flags: FIN + CLOSE

Attributes

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

payload=[RW]

The payload data

request_frame[RW]

The frame that this one is a response to

Public Class Methods

anchor
attr_flag( name, bitmask )

Define accessors for the flag of the specified name and bit.

# File lib/mongrel2/websocket.rb, line 339
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
anchor
from_request( frame )

Create a response frame from the given request frame.

# File lib/mongrel2/websocket.rb, line 329
def self::from_request( frame )
        self.log.debug "Creating a %p response to request %p" % [ self, frame ]
        response = new( frame.sender_id, frame.conn_id, frame.path )
        response.request_frame = frame

        return response
end
anchor
new( sender_id, conn_id, path, headers={}, payload='', raw=nil )

Override the constructor to add Integer flags extracted from the FLAGS header.

# File lib/mongrel2/websocket.rb, line 359
def initialize( sender_id, conn_id, path, headers={}, payload='', raw=nil )
        payload.force_encoding( Encoding::UTF_8 ) if
                payload.encoding == Encoding::ASCII_8BIT

        super

        @flags         = Integer( self.headers.flags || DEFAULT_FLAGS )
        @request_frame = nil
        @errors        = []
        @chunksize     = DEFAULT_CHUNKSIZE
end
anchor
response_class()

Override the type of response returned by this request type. Since WebSocket connections are symmetrical, responses are just new Mongrel2::WebSocket::Frames with the same Mongrel2 sender and connection IDs.

# File lib/mongrel2/websocket.rb, line 323
def self::response_class
        return self
end

Public Instance Methods

anchor
<<( object )

Append the given object to the payload. Returns the Frame for chaining.

# File lib/mongrel2/websocket.rb, line 453
def <<( object )
        self.payload << object
        return self
end
anchor
bytes()

Return an Enumerator for the bytes of the raw frame as it appears on the wire.

# File lib/mongrel2/websocket.rb, line 543
def bytes
        self.remember_payload_settings do
                self.payload.rewind
                self.log.debug "Making a bytes iterator for a %s payload" %
                        [ self.payload.external_encoding.name ]

                return Enumerator.new do |yielder|
                        self.payload.set_encoding( 'binary' )
                        self.payload.rewind

                        header_i = self.make_header.each_byte
                        body_i   = self.payload.each_byte

                        header_i.each_with_index {|byte, i| yielder.yield(byte) }
                        body_i.each_with_index   {|byte, i| yielder.yield(byte) }
                end
        end
end
anchor
control?()

Returns true if the request is a WebSocket control frame.

# File lib/mongrel2/websocket.rb, line 446
def control?
        return ( self.flags & OPCODE_CONTROL_MASK ).nonzero?
end
anchor
each_chunk()

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 509
def each_chunk
        self.validate

        iter = Enumerator.new do |yielder|
                self.bytes.each_slice( self.chunksize ) do |bytes|
                        yielder.yield( bytes.pack('C*') )
                end
        end

        if block_given?
                block = Proc.new
                iter.each( &block )
        else
                return iter
        end
end
anchor
has_rsv_flags?()

Returns true if one or more of the RSV1-3 bits is set.

# File lib/mongrel2/websocket.rb, line 410
def has_rsv_flags?
        return ( self.flags & RSV_FLAG_MASK ).nonzero?
end
anchor
make_close_frame( statuscode=Mongrel2::WebSocket::CLOSE_NORMAL )

Set the :close opcode on this frame and set its status to statuscode.

# File lib/mongrel2/websocket.rb, line 466
def make_close_frame( statuscode=Mongrel2::WebSocket::CLOSE_NORMAL )
        self.opcode = :close
        self.set_status( statuscode )
end
anchor
numeric_opcode()

Return the numeric opcode of the frame.

# File lib/mongrel2/websocket.rb, line 423
def numeric_opcode
        return self.flags & OPCODE_BITMASK
end
anchor
opcode()

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 417
def opcode
        return OPCODE_NAME[ self.numeric_opcode ]
end
anchor
opcode=( code )

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 430
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
anchor
puts( *objects )

Write the given objects to the payload, calling to_s on each one.

# File lib/mongrel2/websocket.rb, line 460
def puts( *objects )
        self.payload.puts( *objects )
end
anchor
remember_payload_settings() { || ... }

Remember the payload IO's external encoding, position, etc. and restore them when the block returns.

# File lib/mongrel2/websocket.rb, line 565
def remember_payload_settings
        original_enc = self.payload.external_encoding
        original_pos = self.payload.pos

        yield
ensure
        self.payload.set_encoding( original_enc ) if original_enc
        self.payload.pos = original_pos if original_pos
end
anchor
response( *flags )

Create a frame in response to the receiving Frame (i.e., with the same Mongrel2 connection ID and sender).

# File lib/mongrel2/websocket.rb, line 578
def response( *flags )
        unless @response
                @response = super()

                # Set the opcode
                self.log.debug "Setting up response %p with symmetrical flags" % [ @response ]
                if self.opcode == :ping
                        @response.opcode = :pong
                        IO.copy_stream( self.payload, @response.payload, 4096 )
                else
                        @response.opcode = self.numeric_opcode
                end

                # Set flags in the response
                unless flags.empty?
                        self.log.debug "  applying custom flags: %p" % [ flags ]
                        @response.set_flags( *flags )
                end

        end

        return @response
end
anchor
set_flags( *flag_symbols )

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 609
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: 0b%08b" % [ flag ]
                        self.flags |= flag
                else
                        raise ArgumentError, "Don't know what the %p flag is." % [ flag ]
                end
        end
end
anchor
set_status( statuscode )

Overwrite the frame's payload with a status message based on statuscode.

# File lib/mongrel2/websocket.rb, line 474
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
anchor
to_s()

Stringify into a response suitable for sending to the client.

# File lib/mongrel2/websocket.rb, line 528
def to_s
        self.remember_payload_settings do
                self.payload.rewind
                self.payload.set_encoding( 'binary' )

                header = self.make_header
                data = self.payload.read

                return header + data
        end
end
anchor
valid?()

Sanity-checks the frame and returns false if any problems are found. Error messages will be in errors.

# File lib/mongrel2/websocket.rb, line 495
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
anchor
validate()

Validate the frame, raising a Mongrel2::WebSocket::FrameError if there are validation problems.

# File lib/mongrel2/websocket.rb, line 485
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

anchor
inspect_details()

Return the details to include in the contents of the inspected object.

# File lib/mongrel2/websocket.rb, line 642
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
anchor
make_header()

Make a WebSocket header for the frame and return it.

# File lib/mongrel2/websocket.rb, line 656
def make_header
        header = ''.force_encoding( Encoding::ASCII_8BIT )
        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
anchor
validate_control_frame()

Sanity-check control frame data, adding an error message to errors if there's a problem.

# File lib/mongrel2/websocket.rb, line 698
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
anchor
validate_opcode()

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 715
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
anchor
validate_payload_encoding()

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 682
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
anchor
validate_reserved_flags()

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 725
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