Skip to content

Commit

Permalink
Add a linter for rack.early_hints
Browse files Browse the repository at this point in the history
  • Loading branch information
byroot committed Mar 24, 2022
1 parent 9a018c0 commit 66ac5ea
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 0 deletions.
16 changes: 16 additions & 0 deletions SPEC.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ There are the following restrictions:
* There must be a valid input stream in <tt>rack.input</tt>.
* There must be a valid error stream in <tt>rack.errors</tt>.
* There may be a valid hijack stream in <tt>rack.hijack_io</tt>
* There may be a valid early hints callback in <tt>rack.early_hints</tt>
* The <tt>REQUEST_METHOD</tt> must be a valid token.
* The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
* The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
Expand Down Expand Up @@ -241,6 +242,21 @@ if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
* Middleware should not wrap the IO object for the request pattern. The
request pattern is intended to provide the hijacker with "raw tcp".

=== Early Hints

The application or any middleware may call the <tt>rack.early_hints</tt>
with pairs of headers that will be sent as a 103 Early Hints provisional
response.

If rack.early_hints is present it must respond to #call.
The header must respond to +each+, and yield values of key and value.
The header keys must be Strings.
The header must conform to RFC7230 token specification, i.e. cannot
contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
The values of the header must be Strings,
consisting of lines (for multiple header values, e.g. multiple
<tt>Set-Cookie</tt> values) separated by "\\n".
The lines must not contain characters below 037.
== The Response

=== The Status
Expand Down
1 change: 1 addition & 0 deletions lib/rack/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ module Rack
RACK_HIJACK = 'rack.hijack'
RACK_IS_HIJACK = 'rack.hijack?'
RACK_HIJACK_IO = 'rack.hijack_io'
RACK_EARLY_HINTS = 'rack.early_hints'
RACK_RECURSIVE_INCLUDE = 'rack.recursive.include'
RACK_MULTIPART_BUFFER_SIZE = 'rack.multipart.buffer_size'
RACK_MULTIPART_TEMPFILE_FACTORY = 'rack.multipart.tempfile_factory'
Expand Down
46 changes: 46 additions & 0 deletions lib/rack/lint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ def check_environment(env)
check_error env[RACK_ERRORS]
## * There may be a valid hijack stream in <tt>rack.hijack_io</tt>
check_hijack env
## * There may be a valid early hints callback in <tt>rack.early_hints</tt>
check_early_hints env

## * The <tt>REQUEST_METHOD</tt> must be a valid token.
unless env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
Expand Down Expand Up @@ -630,6 +632,50 @@ def check_hijack_response(headers, env)
## request pattern is intended to provide the hijacker with "raw tcp".
##

## === Early Hints
##
## The application or any middleware may call the <tt>rack.early_hints</tt>
## with pairs of headers that will be sent as a 103 Early Hints provisional
## response.
def check_early_hints(env)
if env[RACK_EARLY_HINTS]
##
## If rack.early_hints is present it must respond to #call.
assert("rack.early_hints must respond to call") { env[RACK_EARLY_HINTS].respond_to?(:call) }
original_callback = env[RACK_EARLY_HINTS]
env[RACK_EARLY_HINTS] = lambda do |headers|
## The header must respond to +each+, and yield values of key and value.
assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
header.respond_to? :each
}

header.each { |key, value|
## The header keys must be Strings.
assert("header key must be a string, was #{key.class}") {
key.kind_of? String
}

## The header must conform to RFC7230 token specification, i.e. cannot
## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
assert("invalid header name: #{key}") { key !~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/ }

## The values of the header must be Strings,
assert("a header value must be a String, but the value of " +
"'#{key}' is a #{value.class}") { value.kind_of? String }
## consisting of lines (for multiple header values, e.g. multiple
## <tt>Set-Cookie</tt> values) separated by "\\n".
value.split("\n").each { |item|
## The lines must not contain characters below 037.
assert("invalid header value #{key}: #{item.inspect}") {
item !~ /[\000-\037]/
}
}
}
original_callback.call(headers)
end
end
end

## == The Response
##
## === The Status
Expand Down

0 comments on commit 66ac5ea

Please sign in to comment.