WebSocket
frame class; this is used for both requests and responses in WebSocket
services.
The default frame header flags: FIN + CLOSE
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
The payload data
The frame that this one is a response to
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
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
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
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
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
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
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
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
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
Return the numeric opcode of the frame.
# File lib/mongrel2/websocket.rb, line 423
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 417
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 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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