Skip to content

Commit f9fb1e2

Browse files
Move all read logic to Framer.
1 parent 3cc82e1 commit f9fb1e2

4 files changed

Lines changed: 96 additions & 107 deletions

File tree

lib/protocol/websocket/frame.rb

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -168,67 +168,6 @@ def apply(connection)
168168
connection.receive_frame(self)
169169
end
170170

171-
# Read a full frame from the stream given pre-parsed header fields.
172-
# @parameter finished [Boolean] Whether the FIN bit was set.
173-
# @parameter flags [Integer] The RSV flag bits.
174-
# @parameter opcode [Integer] The frame opcode.
175-
# @parameter stream [IO] The stream to read from.
176-
# @parameter maximum_frame_size [Integer] The maximum allowed payload size in bytes.
177-
# @returns [Frame] The fully read and populated frame.
178-
# @raises [ProtocolError] If the frame violates protocol constraints.
179-
# @raises [EOFError] If the stream ends unexpectedly.
180-
def self.read(finished, flags, opcode, stream, maximum_frame_size, second_byte = nil)
181-
unless second_byte
182-
buffer = stream.read(1) or raise EOFError, "Could not read header!"
183-
second_byte = buffer.getbyte(0)
184-
end
185-
186-
mask = (second_byte & 0b1000_0000 != 0)
187-
length = second_byte & 0b0111_1111
188-
189-
if opcode & 0x8 != 0
190-
if length > 125
191-
raise ProtocolError, "Invalid control frame payload length: #{length} > 125!"
192-
elsif !finished
193-
raise ProtocolError, "Fragmented control frame!"
194-
end
195-
end
196-
197-
if length == 126
198-
if mask
199-
buffer = stream.read(6) or raise EOFError, "Could not read length and mask!"
200-
length = buffer.unpack1("n")
201-
mask = buffer.byteslice(2, 4)
202-
else
203-
buffer = stream.read(2) or raise EOFError, "Could not read length!"
204-
length = buffer.unpack1("n")
205-
end
206-
elsif length == 127
207-
if mask
208-
buffer = stream.read(12) or raise EOFError, "Could not read length and mask!"
209-
length = buffer.unpack1("Q>")
210-
mask = buffer.byteslice(8, 4)
211-
else
212-
buffer = stream.read(8) or raise EOFError, "Could not read length!"
213-
length = buffer.unpack1("Q>")
214-
end
215-
elsif mask
216-
mask = stream.read(4) or raise EOFError, "Could not read mask!"
217-
end
218-
219-
if length > maximum_frame_size
220-
raise ProtocolError, "Invalid payload length: #{length} > #{maximum_frame_size}!"
221-
end
222-
223-
payload = stream.read(length) or raise EOFError, "Could not read payload!"
224-
225-
if payload.bytesize != length
226-
raise EOFError, "Incorrect payload length: #{length} != #{payload.bytesize}!"
227-
end
228-
229-
return self.new(finished, payload, flags: flags, opcode: opcode, mask: mask)
230-
end
231-
232171
# Write this frame to the given stream.
233172
# @parameter stream [IO] The stream to write the serialized frame to.
234173
# @raises [ProtocolError] If the frame has invalid length or mask.

lib/protocol/websocket/framer.rb

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
5757
end
5858

5959
first_byte = buffer.getbyte(0)
60+
second_byte = buffer.getbyte(1)
6061

6162
finished = (first_byte & 0b1000_0000 != 0)
6263
flags = (first_byte & 0b0111_0000) >> 4
@@ -68,10 +69,51 @@ def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
6869
raise ProtocolError, "Control opcode = #{opcode} is reserved!"
6970
end
7071

71-
klass = @frames[opcode] || Frame
72-
frame = klass.read(finished, flags, opcode, @stream, maximum_frame_size, buffer.getbyte(1))
72+
mask = (second_byte & 0b1000_0000 != 0)
73+
length = second_byte & 0b0111_1111
74+
75+
if opcode & 0x8 != 0
76+
if length > 125
77+
raise ProtocolError, "Invalid control frame payload length: #{length} > 125!"
78+
elsif !finished
79+
raise ProtocolError, "Fragmented control frame!"
80+
end
81+
end
7382

74-
return frame
83+
if length == 126
84+
if mask
85+
buffer = @stream.read(6) or raise EOFError, "Could not read length and mask!"
86+
length = buffer.unpack1("n")
87+
mask = buffer.byteslice(2, 4)
88+
else
89+
buffer = @stream.read(2) or raise EOFError, "Could not read length!"
90+
length = buffer.unpack1("n")
91+
end
92+
elsif length == 127
93+
if mask
94+
buffer = @stream.read(12) or raise EOFError, "Could not read length and mask!"
95+
length = buffer.unpack1("Q>")
96+
mask = buffer.byteslice(8, 4)
97+
else
98+
buffer = @stream.read(8) or raise EOFError, "Could not read length!"
99+
length = buffer.unpack1("Q>")
100+
end
101+
elsif mask
102+
mask = @stream.read(4) or raise EOFError, "Could not read mask!"
103+
end
104+
105+
if length > maximum_frame_size
106+
raise ProtocolError, "Invalid payload length: #{length} > #{maximum_frame_size}!"
107+
end
108+
109+
payload = @stream.read(length) or raise EOFError, "Could not read payload!"
110+
111+
if payload.bytesize != length
112+
raise EOFError, "Incorrect payload length: #{length} != #{payload.bytesize}!"
113+
end
114+
115+
klass = @frames[opcode] || Frame
116+
return klass.new(finished, payload, flags: flags, opcode: opcode, mask: mask)
75117
end
76118

77119
# Write a frame to the underlying stream.

test/protocol/websocket/frame.rb

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -29,49 +29,6 @@
2929
end
3030
end
3131

32-
with ".read" do
33-
it "rejects invalid control frame payload length" do
34-
stream = StringIO.new("\xFF")
35-
36-
expect do
37-
subject.read(true, 0, 0x8, stream, 128)
38-
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Invalid control frame payload length/)
39-
end
40-
41-
it "rejects fragmented control frames" do
42-
stream = StringIO.new("\x0F")
43-
44-
expect do
45-
subject.read(false, 0, 0x8, stream, 128)
46-
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Fragmented control frame/)
47-
end
48-
49-
it "rejects frames bigger than the maximum frame size" do
50-
stream = StringIO.new("\x7D")
51-
52-
expect do
53-
subject.read(false, 0, 0, stream, 124)
54-
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Invalid payload length: \d+ > \d*!/)
55-
end
56-
57-
it "rejects frames with truncated payload" do
58-
stream = StringIO.new("\x051234")
59-
60-
expect do
61-
subject.read(false, 0, 0, stream, 128)
62-
end.to raise_exception(EOFError, message: be =~ /Incorrect payload length: \d+ != \d+!/)
63-
end
64-
65-
it "accepts a pre-read second byte" do
66-
stream = StringIO.new("Hello")
67-
second_byte = 0x05
68-
69-
frame = subject.read(true, 0, 0x1, stream, 128, second_byte)
70-
expect(frame.payload).to be == "Hello"
71-
expect(frame.mask).to be == false
72-
end
73-
end
74-
7532
with ".write" do
7633
let(:stream) {StringIO.new}
7734

test/protocol/websocket/framer.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,56 @@
3333
framer.read_frame
3434
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Control opcode.*reserved/)
3535
end
36+
37+
it "rejects invalid control frame payload length" do
38+
# FIN=1, opcode=0x8 (close), MASK=1, length=127 → violates max 125 for control frames
39+
stream.string = "\x88\xFF"
40+
stream.rewind
41+
42+
expect do
43+
framer.read_frame
44+
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Invalid control frame payload length/)
45+
end
46+
47+
it "rejects fragmented control frames" do
48+
# FIN=0, opcode=0x8 (close), MASK=0, length=15
49+
stream.string = "\x08\x0F"
50+
stream.rewind
51+
52+
expect do
53+
framer.read_frame
54+
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Fragmented control frame/)
55+
end
56+
57+
it "rejects frames bigger than the maximum frame size" do
58+
# FIN=1, opcode=0x2 (binary), MASK=0, length=125
59+
stream.string = "\x82\x7D"
60+
stream.rewind
61+
62+
expect do
63+
framer.read_frame(124)
64+
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Invalid payload length: \d+ > \d+!/)
65+
end
66+
67+
it "rejects frames with truncated payload" do
68+
# FIN=1, opcode=0x2 (binary), MASK=0, length=5, only 4 bytes of payload
69+
stream.string = "\x82\x051234"
70+
stream.rewind
71+
72+
expect do
73+
framer.read_frame
74+
end.to raise_exception(EOFError, message: be =~ /Incorrect payload length: \d+ != \d+!/)
75+
end
76+
77+
it "reads a text frame" do
78+
# FIN=1, opcode=0x1 (text), MASK=0, length=5, payload="Hello"
79+
stream.string = "\x81\x05Hello"
80+
stream.rewind
81+
82+
frame = framer.read_frame
83+
expect(frame).to be_a(Protocol::WebSocket::TextFrame)
84+
expect(frame.payload).to be == "Hello"
85+
expect(frame.mask).to be == false
86+
end
3687
end
3788
end

0 commit comments

Comments
 (0)