class Protocol::WebSocket::Frame

Constants

OPCODE

Attributes

finished[RW]

The generic frame header uses the following binary representation:

0                   1                   2                   3
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

--+--——--————-------------------------------- |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | --+--——--————-+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - ------------------------------- | |Masking-key, if MASK set to 1 | -------------------------------——————————-+ | Masking-key (continued) | Payload Data | +——————————– - - - - - - - - - - - - - - - + : Payload Data continued … : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued … | ---------------------------------------------------------------

length[RW]
mask[RW]
opcode[RW]
payload[RW]

Public Class Methods

new(finished = true, payload = nil, opcode: self.class::OPCODE, mask: false) click to toggle source

@parameter length [Integer] The length of the payload, or nil if the header has not been read yet. @parameter mask [Boolean | String] An optional 4-byte string which is used to mask the payload.

# File lib/protocol/websocket/frame.rb, line 32
def initialize(finished = true, payload = nil, opcode: self.class::OPCODE, mask: false)
        if mask == true
                mask = SecureRandom.bytes(4)
        end
        
        @finished = finished
        @opcode = opcode
        @mask = mask
        @length = payload&.bytesize
        @payload = payload
end
parse_header(buffer) click to toggle source
# File lib/protocol/websocket/frame.rb, line 134
def self.parse_header(buffer)
        byte = buffer.unpack("C").first
        
        finished = (byte & 0b1000_0000 != 0)
        # rsv = byte & 0b0111_0000
        opcode = byte & 0b0000_1111
        
        return finished, opcode
end
read(finished, opcode, stream, maximum_frame_size) click to toggle source
# File lib/protocol/websocket/frame.rb, line 144
def self.read(finished, opcode, stream, maximum_frame_size)
        buffer = stream.read(1) or raise EOFError, "Could not read header!"
        byte = buffer.unpack("C").first
        
        mask = (byte & 0b1000_0000 != 0)
        length = byte & 0b0111_1111
        
        if length == 126
                buffer = stream.read(2) or raise EOFError, "Could not read length!"
                length = buffer.unpack('n').first
        elsif length == 127
                buffer = stream.read(8) or raise EOFError, "Could not read length!"
                length = buffer.unpack('Q>').first
        end
        
        if length > maximum_frame_size
                raise ProtocolError, "Invalid payload length: #{@length} > #{maximum_frame_size}!"
        end
        
        if mask
                mask = stream.read(4) or raise EOFError, "Could not read mask!"
        end
        
        payload = stream.read(length) or raise EOFError, "Could not read payload!"
        
        if payload.bytesize != length
                raise EOFError, "Incorrect payload length: #{@length} != #{@payload.bytesize}!"
        end
        
        return self.new(finished, payload, opcode: opcode, mask: mask)
end

Public Instance Methods

<=>(other) click to toggle source
# File lib/protocol/websocket/frame.rb, line 44
def <=> other
        to_ary <=> other.to_ary
end
apply(connection) click to toggle source
# File lib/protocol/websocket/frame.rb, line 130
def apply(connection)
        connection.receive_frame(self)
end
continued?() click to toggle source
# File lib/protocol/websocket/frame.rb, line 64
def continued?
        @finished == false
end
control?() click to toggle source
# File lib/protocol/websocket/frame.rb, line 52
def control?
        @opcode & 0x8
end
data?() click to toggle source
# File lib/protocol/websocket/frame.rb, line 56
def data?
        false
end
finished?() click to toggle source
# File lib/protocol/websocket/frame.rb, line 60
def finished?
        @finished == true
end
pack(data) click to toggle source
# File lib/protocol/websocket/frame.rb, line 95
def pack(data)
        length = data.bytesize
        
        if length.bit_length > 63
                raise ProtocolError, "Frame length #{@length} bigger than allowed maximum!"
        end

        if @mask
                @payload = String.new.b
                
                for i in 0...data.bytesize do
                        @payload << (data.getbyte(i) ^ mask.getbyte(i % 4))
                end
                
                @length = length
        else
                @payload = data
                @length = length
        end
end
to_ary() click to toggle source
# File lib/protocol/websocket/frame.rb, line 48
def to_ary
        [@finished, @opcode, @mask, @length, @payload]
end
unpack() click to toggle source
# File lib/protocol/websocket/frame.rb, line 116
def unpack
        if @mask and !@payload.empty?
                data = String.new.b
                
                for i in 0...@payload.bytesize do
                        data << (@payload.getbyte(i) ^ @mask.getbyte(i % 4))
                end
                
                return data
        else
                return @payload
        end
end
write(stream) click to toggle source
# File lib/protocol/websocket/frame.rb, line 176
def write(stream)
        buffer = String.new.b
        
        if @payload&.bytesize != @length
                raise ProtocolError, "Invalid payload length: #{@length} != #{@payload.bytesize} for #{self}!"
        end
        
        if @mask and @mask.bytesize != 4
                raise ProtocolError, "Invalid mask length!"
        end
        
        if length <= 125
                short_length = length
        elsif length.bit_length <= 16
                short_length = 126
        else
                short_length = 127
        end
        
        buffer << [
                (@finished ? 0b1000_0000 : 0) | @opcode,
                (@mask ? 0b1000_0000 : 0) | short_length,
        ].pack('CC')
        
        if short_length == 126
                buffer << [@length].pack('n')
        elsif short_length == 127
                buffer << [@length].pack('Q>')
        end
        
        buffer << @mask if @mask
        
        stream.write(buffer)
        stream.write(@payload)
end