class Framer
Wraps an underlying Async::IO::Stream for reading and writing binary data into structured frames.
Definitions
def initialize(stream, frames = FRAMES)
Initialize a new framer wrapping the given stream.
Signature
-
parameter
streamIO The underlying stream to read from and write to.
-
parameter
framesHash A mapping of opcodes to frame classes.
Implementation
def initialize(stream, frames = FRAMES)
@stream = stream
@frames = frames
end
def close
Close the underlying stream.
Implementation
def close
@stream.close
end
def flush
Flush the underlying stream.
Implementation
def flush
@stream.flush
end
def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
Read a frame from the underlying stream.
Signature
-
returns
Frame the frame read from the stream.
Implementation
def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
buffer = @stream.read(2)
unless buffer and buffer.bytesize == 2
raise EOFError, "Could not read frame header!"
end
first_byte = buffer.getbyte(0)
second_byte = buffer.getbyte(1)
finished = (first_byte & 0b1000_0000 != 0)
flags = (first_byte & 0b0111_0000) >> 4
opcode = first_byte & 0b0000_1111
if opcode >= 0x3 && opcode <= 0x7
raise ProtocolError, "Non-control opcode = #{opcode} is reserved!"
elsif opcode >= 0xB
raise ProtocolError, "Control opcode = #{opcode} is reserved!"
end
mask = (second_byte & 0b1000_0000 != 0)
length = second_byte & 0b0111_1111
if opcode & 0x8 != 0
if length > 125
raise ProtocolError, "Invalid control frame payload length: #{length} > 125!"
elsif !finished
raise ProtocolError, "Fragmented control frame!"
end
end
if length == 126
if mask
buffer = @stream.read(6) or raise EOFError, "Could not read length and mask!"
length = buffer.unpack1("n")
mask = buffer.byteslice(2, 4)
else
buffer = @stream.read(2) or raise EOFError, "Could not read length!"
length = buffer.unpack1("n")
end
elsif length == 127
if mask
buffer = @stream.read(12) or raise EOFError, "Could not read length and mask!"
length = buffer.unpack1("Q>")
mask = buffer.byteslice(8, 4)
else
buffer = @stream.read(8) or raise EOFError, "Could not read length!"
length = buffer.unpack1("Q>")
end
elsif mask
mask = @stream.read(4) or raise EOFError, "Could not read mask!"
end
if length > maximum_frame_size
raise ProtocolError, "Invalid payload length: #{length} > #{maximum_frame_size}!"
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
klass = @frames[opcode] || Frame
return klass.new(finished, payload, flags: flags, opcode: opcode, mask: mask)
end
def write_frame(frame)
Write a frame to the underlying stream.
Signature
-
parameter
frameFrame The frame to serialize and write.
-
raises
ProtocolError If the frame has an invalid mask.
Implementation
def write_frame(frame)
if frame.mask and frame.mask.bytesize != 4
raise ProtocolError, "Invalid mask length!"
end
length = frame.length
if length <= 125
short_length = length
elsif length.bit_length <= 16
short_length = 126
else
short_length = 127
end
buffer = [
(frame.finished ? 0b1000_0000 : 0) | (frame.flags << 4) | frame.opcode,
(frame.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 << frame.mask if frame.mask
@stream.write(buffer)
@stream.write(frame.payload)
end