Class: Protocol::WebSocket::Frame

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/protocol/websocket/frame.rb

Constant Summary collapse

RSV1 =
0b0100
RSV2 =
0b0010
RSV3 =
0b0001
RESERVED =
RSV1 | RSV2 | RSV3
OPCODE =
0

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(finished = true, payload = nil, flags: 0, opcode: self.class::OPCODE, mask: false) ⇒ Frame

Returns a new instance of Frame.



25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/protocol/websocket/frame.rb', line 25

def initialize(finished = true, payload = nil, flags: 0, opcode: self.class::OPCODE, mask: false)
  if mask == true
    mask = SecureRandom.bytes(4)
  end
  
  @finished = finished
  @flags = flags
  @opcode = opcode
  @mask = mask
  @length = payload&.bytesize
  @payload = payload
end

Instance Attribute Details

#finishedObject

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 … | ---------------------------------------------------------------



88
89
90
# File 'lib/protocol/websocket/frame.rb', line 88

def finished
  @finished
end

#flagsObject

Returns the value of attribute flags.



89
90
91
# File 'lib/protocol/websocket/frame.rb', line 89

def flags
  @flags
end

#lengthObject

Returns the value of attribute length.



92
93
94
# File 'lib/protocol/websocket/frame.rb', line 92

def length
  @length
end

#maskObject

Returns the value of attribute mask.



91
92
93
# File 'lib/protocol/websocket/frame.rb', line 91

def mask
  @mask
end

#opcodeObject

Returns the value of attribute opcode.



90
91
92
# File 'lib/protocol/websocket/frame.rb', line 90

def opcode
  @opcode
end

#payloadObject

Returns the value of attribute payload.



93
94
95
# File 'lib/protocol/websocket/frame.rb', line 93

def payload
  @payload
end

Class Method Details

.parse_header(buffer) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/protocol/websocket/frame.rb', line 149

def self.parse_header(buffer)
  byte = buffer.unpack("C").first
  
  finished = (byte & 0b1000_0000 != 0)
  flags = (byte & 0b0111_0000) >> 4
  opcode = byte & 0b0000_1111
  
  if (0x3 .. 0x7).include?(opcode)
    raise ProtocolError, "Non-control opcode = #{opcode} is reserved!"
  elsif (0xB .. 0xF).include?(opcode)
    raise ProtocolError, "Control opcode = #{opcode} is reserved!"
  end
  
  return finished, flags, opcode
end

.read(finished, flags, opcode, stream, maximum_frame_size) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/protocol/websocket/frame.rb', line 165

def self.read(finished, flags, 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 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
    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, flags: flags, opcode: opcode, mask: mask)
end

Instance Method Details

#<=>(other) ⇒ Object



42
43
44
# File 'lib/protocol/websocket/frame.rb', line 42

def <=> other
  to_ary <=> other.to_ary
end

#apply(connection) ⇒ Object



145
146
147
# File 'lib/protocol/websocket/frame.rb', line 145

def apply(connection)
  connection.receive_frame(self)
end

#continued?Boolean

Returns:

  • (Boolean)


63
64
65
# File 'lib/protocol/websocket/frame.rb', line 63

def continued?
  @finished == false
end

#control?Boolean

Returns:

  • (Boolean)


50
51
52
# File 'lib/protocol/websocket/frame.rb', line 50

def control?
  @opcode & 0x8 != 0
end

#data?Boolean

Returns:

  • (Boolean)


55
56
57
# File 'lib/protocol/websocket/frame.rb', line 55

def data?
  false
end

#finished?Boolean

Returns:

  • (Boolean)


59
60
61
# File 'lib/protocol/websocket/frame.rb', line 59

def finished?
  @finished == true
end

#flag?(value) ⇒ Boolean

Returns:

  • (Boolean)


38
39
40
# File 'lib/protocol/websocket/frame.rb', line 38

def flag?(value)
  @flags & value != 0
end

#pack(data = "") ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/protocol/websocket/frame.rb', line 119

def pack(data = "")
  length = data.bytesize
  
  if length.bit_length > 63
    raise ProtocolError, "Frame length #{@length} bigger than allowed maximum!"
  end
  
  if @mask
    @payload = mask_xor(data, mask)
    @length = length
  else
    @payload = data
    @length = length
  end
  
  return self
end

#to_aryObject



46
47
48
# File 'lib/protocol/websocket/frame.rb', line 46

def to_ary
  [@finished, @flags, @opcode, @mask, @length, @payload]
end

#unpackObject



137
138
139
140
141
142
143
# File 'lib/protocol/websocket/frame.rb', line 137

def unpack
  if @mask and !@payload.empty?
    return mask_xor(@payload, @mask)
  else
    return @payload
  end
end

#write(stream) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/protocol/websocket/frame.rb', line 205

def write(stream)
  buffer = String.new(encoding: Encoding::BINARY)
  
  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) | (@flags << 4) | @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