Skip to content

Commit

Permalink
Introduce rack.protocol header for handling version-agnostic protoc…
Browse files Browse the repository at this point in the history
…ol upgrades.
  • Loading branch information
ioquatix committed Aug 28, 2022
1 parent 856c4f9 commit cd89c6b
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file. For info on
- `rack.hijack?` (partial hijack) and `rack.hijack` (full hijack) are now independently optional.
- `rack.hijack_io` has been removed completely.
- `rack.response_finished` is an optional environment key which contains an array of callable objects that must accept `#call(env, status, headers, error)` and are invoked after the response is finished (either successfully or unsucessfully).
- `rack.protocol` is an optional environment key and response header for handling connection upgrades.

### Removed

Expand Down
18 changes: 16 additions & 2 deletions SPEC.rdoc
Expand Up @@ -82,6 +82,10 @@ Rack-specific variables:
<tt>rack.hijack</tt>:: See below, if present, an object responding
to +call+ that is used to perform a full
hijack.
<tt>rack.protocol</tt>:: If the request is an HTTP/1 upgrade or
HTTP/2 CONNECT with +:protocol+ pseudo
header, this is set to the value of that
header.
Additional environment specifications have approved to
standardized middleware APIs. None of these are required to
be implemented by the server.
Expand Down Expand Up @@ -248,16 +252,26 @@ Header values must be either a String instance,
or an Array of String instances,
such that each String instance must not contain characters below 037.

=== The content-type
==== The +content-type+ Header

There must not be a <tt>content-type</tt> header key when the +Status+ is 1xx,
204, or 304.

=== The content-length
==== The +content-length+ Header

There must not be a <tt>content-length</tt> header key when the
+Status+ is 1xx, 204, or 304.

=== The +rack.protocol+ Header

If the +rack.protocol+ header is present, it must be a String, and
must be one of the values from the +rack.protocols+ array from the
environment.

Setting this value informs the server that it should perform a
connection upgrade. In HTTP/1, this is done using the +upgrade+
header. In HTTP/2, this is done by accepting the request.

=== The Body

The Body is typically an +Array+ of +String+ instances, an enumerable
Expand Down
40 changes: 34 additions & 6 deletions lib/rack/lint.rb
Expand Up @@ -78,8 +78,9 @@ def response
end

## and the *body*.
check_content_type(@status, @headers)
check_content_length(@status, @headers)
check_content_type_header(@status, @headers)
check_content_length_header(@status, @headers)
check_rack_protocol_header(@status, @headers)
@head_request = @env[REQUEST_METHOD] == HEAD

@lint = (@env['rack.lint'] ||= []) << self
Expand Down Expand Up @@ -179,6 +180,11 @@ def check_environment(env)
## to +call+ that is used to perform a full
## hijack.

## <tt>rack.protocol</tt>:: If the request is an HTTP/1 upgrade or
## HTTP/2 CONNECT with +:protocol+ pseudo
## header, this is set to the value of that
## header.

## Additional environment specifications have approved to
## standardized middleware APIs. None of these are required to
## be implemented by the server.
Expand Down Expand Up @@ -671,9 +677,9 @@ def check_header_value(key, value)
end

##
## === The content-type
## ==== The +content-type+ Header
##
def check_content_type(status, headers)
def check_content_type_header(status, headers)
headers.each { |key, value|
## There must not be a <tt>content-type</tt> header key when the +Status+ is 1xx,
## 204, or 304.
Expand All @@ -687,9 +693,9 @@ def check_content_type(status, headers)
end

##
## === The content-length
## ==== The +content-length+ Header
##
def check_content_length(status, headers)
def check_content_length_header(status, headers)
headers.each { |key, value|
if key == 'content-length'
## There must not be a <tt>content-length</tt> header key when the
Expand All @@ -714,6 +720,28 @@ def verify_content_length(size)
end
end

##
## === The +rack.protocol+ Header
##
def check_rack_protocol_header(status, headers)
## If the +rack.protocol+ header is present, it must be a String, and
## must be one of the values from the +rack.protocols+ array from the
## environment.
protocol = headers['rack.protocol']
if protocol
request_protocols = Array(@env['rack.protocol'])

if request_protocols.empty?
raise LintError, "rack.protocol header is #{protocol.inspect}, but rack.protocol was not set in request!"
elsif !request_protocols.include?(protocol)
raise LintError, "rack.protocol header is #{protocol.inspect}, but should be one of #{request_protocols.inspect} from the request!"
end
end
end
##
## Setting this value informs the server that it should perform a
## connection upgrade. In HTTP/1, this is done using the +upgrade+
## header. In HTTP/2, this is done by accepting the request.
##
## === The Body
##
Expand Down
34 changes: 34 additions & 0 deletions test/spec_lint.rb
Expand Up @@ -832,4 +832,38 @@ def call(env, status, headers, error)
[200, {}, ["foo"]]
}).call(env({ "rack.response_finished" => [-> (env) {}, lambda { |env| }, callable_object], "content-length" => "3" })).first.must_equal 200
end

it "notices when the respones protocol is specified in the response but not in the request" do
app = Rack::Lint.new(lambda{|env|
[101, {'rack.protocol' => 'websocket'}, ["foo"]]
})

lambda do
app.call(env())
end
.must_raise(Rack::Lint::LintError)
.message.must_match(/rack.protocol header is "websocket", but rack.protocol was not set in request/)
end

it "notices when the respones protocol is specified in the response but not in the request" do
app = Rack::Lint.new(lambda{|env|
[101, {'rack.protocol' => 'websocket'}, ["foo"]]
})

lambda do
app.call(env('rack.protocol' => ['smtp']))
end
.must_raise(Rack::Lint::LintError)
.message.must_match(/rack.protocol header is "websocket", but should be one of \["smtp"\] from the request!/)
end

it "pass valid rack.protocol" do
app = Rack::Lint.new(lambda{|env|
[101, {'rack.protocol' => 'websocket'}, ["foo"]]
})

response = app.call(env({'rack.protocol' => 'websocket'}))

response.first.must_equal 101
end
end

0 comments on commit cd89c6b

Please sign in to comment.