Class: Webtube::Frame

Inherits:
Struct
  • Object
show all
Defined in:
lib/webtube.rb,
lib/webtube.rb

Overview

Note that [[body]] holds the /raw/ data; that is, if

[masked?]

is true, it will need to be unmasked to get

the payload. Call [[payload]] in order to abstract this away.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#bodyObject

Returns the value of attribute body

Returns:

  • (Object)

    the current value of body



641
642
643
# File 'lib/webtube.rb', line 641

def body
  @body
end

#headerObject

Returns the value of attribute header

Returns:

  • (Object)

    the current value of header



641
642
643
# File 'lib/webtube.rb', line 641

def header
  @header
end

Class Method Details

.apply_mask(data, mask) ⇒ Object

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.



740
741
742
743
744
745
746
747
# 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) ⇒ Object

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.



838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
# 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) ⇒ Object

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.



800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
# 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) ⇒ Object

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.



759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
# 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

Instance Method Details

#control_frame?Boolean

Returns:

  • (Boolean)


681
682
683
# File 'lib/webtube.rb', line 681

def control_frame?
  return opcode >= 0x8
end

#extended_payload_length_field_sizeObject

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



691
692
693
694
695
696
697
# 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) ⇒ Object



647
648
649
650
651
# 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?Boolean

Returns:

  • (Boolean)


643
644
645
# File 'lib/webtube.rb', line 643

def fin?
  return (header.getbyte(0) & 0x80) != 0
end

#maskObject

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

[nil]

instead.



713
714
715
716
717
718
719
720
721
722
723
724
# 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?Boolean

Returns:

  • (Boolean)


685
686
687
# File 'lib/webtube.rb', line 685

def masked?
  return (header.getbyte(1) & 0x80) != 0
end

#opcodeObject



671
672
673
# File 'lib/webtube.rb', line 671

def opcode
  return header.getbyte(0) & 0x0F
end

#opcode=(new_opcode) ⇒ Object



675
676
677
678
679
# File 'lib/webtube.rb', line 675

def opcode= new_opcode
  header.setbyte 0, (header.getbyte(0) & ~0x0F) |
      (new_opcode & 0x0F)
  return new_opcode
end

#payloadObject

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.



729
730
731
# File 'lib/webtube.rb', line 729

def payload
  return Frame.apply_mask(body, mask)
end

#payload_lengthObject

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



702
703
704
705
706
707
708
# 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

#rsvObject

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



667
668
669
# File 'lib/webtube.rb', line 667

def rsv
  return (header.getbyte(0) & 0x70) >> 4
end

#rsv1Object



653
654
655
# File 'lib/webtube.rb', line 653

def rsv1
  return (header.getbyte(0) & 0x40) != 0
end

#rsv2Object



657
658
659
# File 'lib/webtube.rb', line 657

def rsv2
  return (header.getbyte(0) & 0x20) != 0
end

#rsv3Object



661
662
663
# File 'lib/webtube.rb', line 661

def rsv3
  return (header.getbyte(0) & 0x10) != 0
end