Skip to content

Implement enough to make this pass autobahn-testsuite #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,22 @@ jobs:
ruby: head
experimental: true

env:
BUNDLE_WITH: autobahn_tests

steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{matrix.ruby}}
bundler-cache: true
- uses: actions/setup-python@v2
with:
python-version: pypy2

- name: Run tests
timeout-minutes: 5
run: ${{matrix.env}} bundle exec rspec
- name: Run Autobahn server tests
timeout-minutes: 5
run: ${{matrix.env}} ruby autobahn-tests/autobahn-server-tests.rb
24 changes: 24 additions & 0 deletions autobahn-tests/autobahn-echo-server.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env -S falcon serve --bind http://127.0.0.1:9001 --count 1 -c

require "async/websocket/adapters/rack"

# TODO: This should probably be part of the library long-term
class RawConnection < Async::WebSocket::Connection
def parse(buffer)
buffer
end

def dump(buffer)
buffer
end
end

app = lambda do |env|
Async::WebSocket::Adapters::Rack.open(env, handler: RawConnection) do |c|
while message = c.read
c.write(message)
end
end
end

run app
25 changes: 25 additions & 0 deletions autobahn-tests/autobahn-server-tests.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env ruby

require "fileutils"
require "json"

Kernel.system("pip install autobahntestsuite", exception: true)

falcon = Process.spawn("bundle exec #{__dir__}/autobahn-echo-server.ru", pgroup: true)
falcon_pg = Process.getpgid(falcon)

Kernel.system("wstest -m fuzzingclient", chdir: __dir__, exception: true)

Process.kill("KILL", -falcon_pg)

result = JSON.parse(File.read("/tmp/autobahn-server/index.json"))["protocol-websocket"]

FileUtils.rm_r("/tmp/autobahn-server/")

failed = result.select { |_, e| e["behavior"] != "OK" || e["behaviorClose"] != "OK" }

puts "#{result.count - failed.count} / #{result.count} tests OK"

failed.each { |k, _| puts "#{k} failed" }

exit(1) if failed.count > 0
14 changes: 14 additions & 0 deletions autobahn-tests/fuzzingclient.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"outdir": "/tmp/autobahn-server",
"servers": [
{
"agent": "protocol-websocket",
"url": "ws://127.0.0.1:9001"
}
],
"cases": ["*"],
"exclude-cases": ["6.4.*", "7.1.6", "7.13.*", "12.*", "13.*"],
"exclude-agent-cases": {
"protocol-websocket": ["3.3", "7.7.*"]
}
}
5 changes: 5 additions & 0 deletions gems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@
gem "bake-bundler"
gem "bake-modernize"
end

group :autobahn_tests, optional: true do
gem "async-websocket", github: "socketry/async-websocket"
gem "falcon"
end
4 changes: 4 additions & 0 deletions lib/protocol/websocket/binary_frame.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def data?
true
end

def decode_message(buffer)
buffer
end

def apply(connection)
connection.receive_binary(self)
end
Expand Down
34 changes: 32 additions & 2 deletions lib/protocol/websocket/close_frame.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,41 @@ class CloseFrame < Frame
def unpack
data = super

return data.unpack(FORMAT)
case data.length
when 0
[nil, ""]
when 1
raise ProtocolError, "invalid close frame length!"
else
code, reason = *data.unpack(FORMAT)

case code
when 0 .. 999, 1005 .. 1006, 1015, 5000 .. 0xFFFF
raise ProtocolError, "invalid close code!"
when 1004, 1016 .. 2999
raise ProtocolError, "reserved close code!"
end

reason.force_encoding(Encoding::UTF_8)

unless reason.valid_encoding?
raise ProtocolError, "invalid UTF-8 in close reason!"
end

[code, reason]
end
end

def pack(code, reason)
super [code, reason].pack(FORMAT)
if code
unless reason.encoding == Encoding::UTF_8
reason = reason.encode(Encoding::UTF_8)
end

super [code, reason].pack(FORMAT)
else
super String.new(encoding: Encoding::BINARY)
end
end

def apply(connection)
Expand Down
18 changes: 11 additions & 7 deletions lib/protocol/websocket/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ def closed?
@state == :closed
end

def close
send_close unless closed?
def close(code = Error::NO_ERROR, message = "")
send_close(code, message) unless closed?

@framer.close
end
Expand Down Expand Up @@ -120,9 +120,9 @@ def send_binary(buffer)
write_frame(frame)
end

def send_close(code = Error::NO_ERROR, message = nil)
def send_close(code = Error::NO_ERROR, reason = "")
frame = CloseFrame.new(mask: @mask)
frame.pack(code, message)
frame.pack(code, reason)

self.write_frame(frame)
self.flush
Expand All @@ -133,7 +133,9 @@ def send_close(code = Error::NO_ERROR, message = nil)
def receive_close(frame)
@state = :closed

code, message = frame.unpack
code, reason = frame.unpack

send_close(code, reason)

if code and code != Error::NO_ERROR
raise ClosedError.new message, code
Expand Down Expand Up @@ -191,10 +193,12 @@ def read

while read_frame
if @frames.last&.finished?
buffer = @frames.map(&:unpack).join
buffer = @frames.map(&:unpack).join("")
message = @frames.first.decode_message(buffer)

@frames = []

return buffer
return message
end
end
end
Expand Down
28 changes: 23 additions & 5 deletions lib/protocol/websocket/frame.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def to_ary
end

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

def data?
Expand Down Expand Up @@ -100,7 +100,7 @@ def pack(data)
end

if @mask
@payload = String.new.b
@payload = String.new(encoding: Encoding::BINARY)

for i in 0...data.bytesize do
@payload << (data.getbyte(i) ^ mask.getbyte(i % 4))
Expand All @@ -115,7 +115,7 @@ def pack(data)

def unpack
if @mask and !@payload.empty?
data = String.new.b
data = String.new(encoding: Encoding::BINARY)

for i in 0...@payload.bytesize do
data << (@payload.getbyte(i) ^ @mask.getbyte(i % 4))
Expand All @@ -135,8 +135,18 @@ def self.parse_header(buffer)
byte = buffer.unpack("C").first

finished = (byte & 0b1000_0000 != 0)
# rsv = byte & 0b0111_0000
rsv = byte & 0b0111_0000
opcode = byte & 0b0000_1111

unless rsv == 0
raise ProtocolError, "RSV = #{rsv >> 4}, expected 0!"
end

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, opcode
end
Expand All @@ -148,6 +158,14 @@ def self.read(finished, opcode, stream, maximum_frame_size)
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
Expand All @@ -174,7 +192,7 @@ def self.read(finished, opcode, stream, maximum_frame_size)
end

def write(stream)
buffer = String.new.b
buffer = String.new(encoding: Encoding::BINARY)

if @payload&.bytesize != @length
raise ProtocolError, "Invalid payload length: #{@length} != #{@payload.bytesize} for #{self}!"
Expand Down
27 changes: 23 additions & 4 deletions lib/protocol/websocket/text_frame.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,31 @@ def data?
true
end

def unpack
super.force_encoding(Encoding::UTF_8)
end
# NOTE: This is here as a reminder that there is no guarantee that a
# frame is valid UTF-8, only the full message.
#
# FIXME: In theory we should fail fast on invalid codepoints here.
#
# def unpack
# super
# end

def pack(data)
super(data.b)
if data.encoding == Encoding::UTF_8
super(data)
else
super(data.encode(Encoding::UTF_8))
end
end

def decode_message(buffer)
buffer.force_encoding(Encoding::UTF_8)

unless buffer.valid_encoding?
raise ProtocolError, "invalid UTF-8 in text frame!"
end

buffer
end

def apply(connection)
Expand Down