Skip to content

Commit

Permalink
Improve validation of Websocket requests. (#59)
Browse files Browse the repository at this point in the history
* Verify `connection` and `upgrade` headers for HTTP/1 requests.

* Add test for status for HTTP/2.
  • Loading branch information
zarqman authored Dec 9, 2023
1 parent 5694b4e commit 6146dd1
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 9 deletions.
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

0 comments on commit 6146dd1

Please sign in to comment.