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



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

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



82
83
84
# File 'lib/protocol/websocket/frame.rb', line 82

def finished
  @finished
end

#flagsObject

Returns the value of attribute flags.



83
84
85
# File 'lib/protocol/websocket/frame.rb', line 83

def flags
  @flags
end

#lengthObject

Returns the value of attribute length.



86
87
88
# File 'lib/protocol/websocket/frame.rb', line 86

def length
  @length
end

#maskObject

Returns the value of attribute mask.



85
86
87
# File 'lib/protocol/websocket/frame.rb', line 85

def mask
  @mask
end

#opcodeObject

Returns the value of attribute opcode.



84
85
86
# File 'lib/protocol/websocket/frame.rb', line 84

def opcode
  @opcode
end

#payloadObject

Returns the value of attribute payload.



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

def payload
  @payload
end

Class Method Details

.parse_header(buffer) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/protocol/websocket/frame.rb', line 130

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



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/protocol/websocket/frame.rb', line 146

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



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

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

#apply(connection) ⇒ Object



126
127
128
# File 'lib/protocol/websocket/frame.rb', line 126

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

#continued?Boolean



57
58
59
# File 'lib/protocol/websocket/frame.rb', line 57

def continued?
  @finished == false
end

#control?Boolean



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

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

#data?Boolean



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

def data?
  false
end

#finished?Boolean



53
54
55
# File 'lib/protocol/websocket/frame.rb', line 53

def finished?
  @finished == true
end

#pack(data = "") ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/protocol/websocket/frame.rb', line 89

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(encoding: Encoding::BINARY)
    
    for i in 0...data.bytesize do
      @payload << (data.getbyte(i) ^ mask.getbyte(i % 4))
    end
    
    @length = length
  else
    @payload = data
    @length = length
  end
  
  return self
end

#to_aryObject



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

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

#unpackObject



112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/protocol/websocket/frame.rb', line 112

def unpack
  if @mask and !@payload.empty?
    data = String.new(encoding: Encoding::BINARY)
    
    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) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/protocol/websocket/frame.rb', line 186

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