class Webtube::Frame

Public Class Methods

apply_mask(data, mask) click to toggle source

Apply the given [[mask]], specified as an integer, to the given [[data]]. Note that since the underlying operation is [[XOR]], the operation can be repeated to reverse itself.

[nil]

can be supplied instead of [[mask]] to indicate

that no processing is needed.

# File lib/webtube.rb, line 740
def self::apply_mask data, mask
  return data if mask.nil?
  return (data + "\0\0\0").        # pad to full tetras
      unpack('L>*').               # extract tetras
      map!{|i| i ^ mask}.          # XOR each with the mask
      pack('L>*').                 # pack back into a string
      byteslice(0, data.bytesize)  # remove padding
end
each_frame_for_message(message: '', opcode: OPCODE_TEXT, masked: false, max_frame_body_size: nil) { |prepare( opcode: opcode, payload: message, fin: fin, masked: masked)| ... } click to toggle source

Given a message and attributes, break it up into frames, and yields each such [[Frame]] separately for processing by the caller – usually, delivery to the other end via the socket. Takes care to not fragment control messages. If masking is required, uses [[SecureRandom]] to generate masks for each frame.

# File lib/webtube.rb, line 838
def self::each_frame_for_message message: '',
    opcode: OPCODE_TEXT,
    masked: false,
    max_frame_body_size: nil
  message = message.dup.force_encoding Encoding::ASCII_8BIT
  offset = 0
  fin = true
  begin
    frame_length = message.bytesize - offset
    fin = !(opcode <= 0x07 and
        max_frame_body_size and
        frame_length > max_frame_body_size)
    frame_length = max_frame_body_size unless fin
    yield Webtube::Frame.prepare(
        opcode: opcode,
        payload: message[offset, frame_length],
        fin: fin,
        masked: masked)
    offset += frame_length
    opcode = 0x00 # for continuation frames
  end until fin
  return
end
prepare( payload: '', opcode: OPCODE_TEXT, fin: true, masked: false) click to toggle source

Given a frame's payload, prepare the header and return a

[Frame]

instance representing such a frame. Optionally,

some header fields can also be set.

It's OK for the caller to modify some header fields, such as [[fin]] or [[opcode]], on the returned [[Frame]] by calling the appropriate methods. Its body should not be modified after construction, however, because its length and possibly its mask is already encoded in the header.

# File lib/webtube.rb, line 800
def self::prepare(
    payload: '',
    opcode: OPCODE_TEXT,
    fin: true,
    masked: false)
  header = [0].pack 'C' # we'll fill in the first byte later
  mask_flag = masked ? 0x80 : 0x00
  header << if payload.bytesize <= 125 then
    [mask_flag | payload.bytesize].pack 'C'
  elsif payload.bytesize <= 0xFFFF then
    [mask_flag | 126, payload.bytesize].pack 'C S>'
  elsif payload.bytesize <= 0x7FFF_FFFF_FFFF_FFFF then
    [mask_flag | 127, payload.bytesize].pack 'C Q>'
  else
    raise 'payload too big for a WebSocket frame'
  end
  frame = Frame.new(header)
  unless masked then
    frame.body = payload
  else
    mask = SecureRandom.random_bytes(4)
    frame.header << mask
    frame.body = apply_mask(payload, mask.unpack('L>')[0])
  end

  # now, it's time to fill out the first byte
  frame.fin = fin
  frame.opcode = opcode

  return frame
end
read_from_socket(socket) click to toggle source

Read all the bytes of one WebSocket frame from the given

[socket]

and return them in a [[Frame]] instance. In

case traffic ends before the frame is complete, raise [[BrokenFrame]].

Note that this will call [[socket.read]] twice or thrice, and assumes no other thread will consume bytes from the socket inbetween. In a multithreaded environment, it may be necessary to apply external locking.

# File lib/webtube.rb, line 759
def self::read_from_socket socket
  header = socket.read(2)
  unless header and header.bytesize == 2 then
    header ||= String.new encoding: Encoding::ASCII_8BIT
    raise BrokenFrame.new(header)
  end
  frame = Frame.new header

  header_tail_size =
      frame.extended_payload_length_field_size +
          (frame.masked? ? 4 : 0)
  unless header_tail_size.zero? then
    header_tail = socket.read(header_tail_size)
    frame.header << header_tail if header_tail
    unless header_tail and
        header_tail.bytesize == header_tail_size then
      raise BrokenFrame.new(frame.header)
    end
  end

  data_size = frame.payload_length
  frame.body = socket.read(data_size)
  unless frame.body and
      frame.body.bytesize == data_size then
    raise BrokenFrame.new(frame.body ?
        frame.header + frame.body :
        frame.header)
  end

  return frame
end

Public Instance Methods

control_frame?() click to toggle source
# File lib/webtube.rb, line 681
def control_frame?
  return opcode >= 0x8
end
extended_payload_length_field_size() click to toggle source

Determine the size of this frame's extended payload length field in bytes from the 7-bit short payload length field.

# File lib/webtube.rb, line 691
def extended_payload_length_field_size
  return case header.getbyte(1) & 0x7F
    when 126 then 2
    when 127 then 8
    else 0
  end
end
fin=(new_value) click to toggle source
# File lib/webtube.rb, line 647
def fin= new_value
  header.setbyte 0, header.getbyte(0) & 0x7F |
      (new_value ? 0x80 : 0x00)
  return new_value
end
fin?() click to toggle source
# File lib/webtube.rb, line 643
def fin?
  return (header.getbyte(0) & 0x80) != 0
end
mask() click to toggle source

Extracts the mask as a tetrabyte integer from this frame. If the frame has the [[masked?]] bit unset, returns

[nil]

instead.

# File lib/webtube.rb, line 713
def mask
  if masked? then
    mask_offset = 2 + case header.getbyte(1) & 0x7F
      when 126 then 2
      when 127 then 8
      else 0
    end
    return header.unpack('@%i L>' % mask_offset)[0]
  else
    return nil
  end
end
masked?() click to toggle source
# File lib/webtube.rb, line 685
def masked?
  return (header.getbyte(1) & 0x80) != 0
end
opcode() click to toggle source
# File lib/webtube.rb, line 671
def opcode
  return header.getbyte(0) & 0x0F
end
opcode=(new_opcode) click to toggle source
# File lib/webtube.rb, line 675
def opcode= new_opcode
  header.setbyte 0, (header.getbyte(0) & ~0x0F) |
      (new_opcode & 0x0F)
  return new_opcode
end
payload() click to toggle source

Extract the frame's payload and return it as a [[String]] instance of the [[ASCII-8BIT]] encoding. If the frame has the [[masked?]] bit set, this also involves demasking.

# File lib/webtube.rb, line 729
def payload
  return Frame.apply_mask(body, mask)
end
payload_length() click to toggle source

Extract the length of this frame's payload. Enough bytes of the header must already have been read; see [[extended_payload_lenth_field_size]].

# File lib/webtube.rb, line 702
def payload_length
  return case base = header.getbyte(1) & 0x7F
    when 126 then header.unpack('@2 S>')[0]
    when 127 then header.unpack('@2 Q>')[0]
    else base
  end
end
rsv() click to toggle source

The three reserved bits of the frame, shifted rightwards to meet the binary point

# File lib/webtube.rb, line 667
def rsv
  return (header.getbyte(0) & 0x70) >> 4
end
rsv1() click to toggle source
# File lib/webtube.rb, line 653
def rsv1
  return (header.getbyte(0) & 0x40) != 0
end
rsv2() click to toggle source
# File lib/webtube.rb, line 657
def rsv2
  return (header.getbyte(0) & 0x20) != 0
end
rsv3() click to toggle source
# File lib/webtube.rb, line 661
def rsv3
  return (header.getbyte(0) & 0x10) != 0
end