Skip to content

Improve validation of Websocket requests #59

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 2 commits into from
Dec 9, 2023
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
2 changes: 1 addition & 1 deletion lib/async/websocket/connect_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

module Async
module WebSocket
# This is required for HTTP/1.x to upgrade the connection to the WebSocket protocol.
# This is required for HTTP/2 to establish a connection using the WebSocket protocol.
# See https://tools.ietf.org/html/rfc8441 for more details.
class ConnectRequest < ::Protocol::HTTP::Request
include ::Protocol::WebSocket::Headers
Expand Down
4 changes: 3 additions & 1 deletion lib/async/websocket/upgrade_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
module Async
module WebSocket
# This is required for HTTP/1.x to upgrade the connection to the WebSocket protocol.
# See https://tools.ietf.org/html/rfc6455
class UpgradeRequest < ::Protocol::HTTP::Request
include ::Protocol::WebSocket::Headers

Expand Down Expand Up @@ -75,8 +76,9 @@ def call(connection)
raise ProtocolError, "Invalid accept digest, expected #{expected_accept_digest.inspect}, got #{accept_digest.inspect}!"
end
end
verified = accept_digest && Array(response.protocol) == %w(websocket) && response.headers['connection']&.include?('upgrade')

return Wrapper.new(response, verified: !!accept_digest)
return Wrapper.new(response, verified: verified)
end
end
end
Expand Down
66 changes: 59 additions & 7 deletions test/async/websocket/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,53 @@
end
end

FailedToNegotiate = Sus::Shared("a failed websocket request") do
it 'raises an error' do
expect do
Async::WebSocket::Client.connect(client_endpoint) {}
end.to raise_exception(Async::WebSocket::ProtocolError, message: be =~ /Failed to negotiate connection/)
end
end

describe Async::WebSocket::Client do
include Sus::Fixtures::Async::HTTP::ServerContext

with 'http/1' do
let(:protocol) {Async::HTTP::Protocol::HTTP1}
it_behaves_like ClientExamples

def valid_headers(request)
{
'connection' => 'upgrade',
'upgrade' => 'websocket',
'sec-websocket-accept' => Protocol::WebSocket::Headers::Nounce.accept_digest(request.headers['sec-websocket-key'].first)
}
end

with 'invalid connection header' do
let(:app) do
Protocol::HTTP::Middleware.for do |request|
Protocol::HTTP::Response[101, valid_headers(request).except('connection'), []]
end
end

it_behaves_like FailedToNegotiate
end

with 'invalid upgrade header' do
let(:app) do
Protocol::HTTP::Middleware.for do |request|
Protocol::HTTP::Response[101, valid_headers(request).except('upgrade'), []]
end
end

it_behaves_like FailedToNegotiate
end

with 'invalid sec-websocket-accept header' do
let(:app) do
Protocol::HTTP::Middleware.for do |request|
Protocol::HTTP::Response[101, {'sec-websocket-accept'=>'wrong-digest'}, []]
Protocol::HTTP::Response[101, valid_headers(request).merge('sec-websocket-accept'=>'wrong-digest'), []]
end
end

Expand All @@ -125,24 +161,40 @@
end.to raise_exception(Async::WebSocket::ProtocolError, message: be =~ /Invalid accept digest/)
end
end

with 'missing sec-websocket-accept header' do
let(:app) do
Protocol::HTTP::Middleware.for do |request|
Protocol::HTTP::Response[101, {}, []]
Protocol::HTTP::Response[101, valid_headers(request).except('sec-websocket-accept'), []]
end
end

it 'raises an error' do
expect do
Async::WebSocket::Client.connect(client_endpoint) {}
end.to raise_exception(Async::WebSocket::ProtocolError, message: be =~ /Failed to negotiate connection/)
it_behaves_like FailedToNegotiate
end

with 'invalid status' do
let(:app) do
Protocol::HTTP::Middleware.for do |request|
Protocol::HTTP::Response[403, valid_headers(request), []]
end
end

it_behaves_like FailedToNegotiate
end
end

with 'http/2' do
let(:protocol) {Async::HTTP::Protocol::HTTP2}
it_behaves_like ClientExamples

with 'invalid status' do
let(:app) do
Protocol::HTTP::Middleware.for do |request|
Protocol::HTTP::Response[403, {}, []]
end
end

it_behaves_like FailedToNegotiate
end
end
end