Skip to content

Commit

Permalink
Rack::Lint and SPEC align with RFC7230 headers
Browse files Browse the repository at this point in the history
  • Loading branch information
raggi committed Nov 27, 2014
1 parent 8cb9b65 commit 6839fc2
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 30 deletions.
15 changes: 3 additions & 12 deletions SPEC
Original file line number Diff line number Diff line change
Expand Up @@ -176,30 +176,24 @@ The error stream must respond to +puts+, +write+ and +flush+.
If rack.hijack? is true then rack.hijack must respond to #call.
rack.hijack must return the io that will also be assigned (or is
already present, in rack.hijack_io.

rack.hijack_io must respond to:
<tt>read, write, read_nonblock, write_nonblock, flush, close,
close_read, close_write, closed?</tt>

The semantics of these IO methods must be a best effort match to
those of a normal ruby IO or Socket object, using standard
arguments and raising standard exceptions. Servers are encouraged
to simply pass on real IO objects, although it is recognized that
this approach is not directly compatible with SPDY and HTTP 2.0.

IO provided in rack.hijack_io should preference the
IO::WaitReadable and IO::WaitWritable APIs wherever supported.

There is a deliberate lack of full specification around
rack.hijack_io, as semantics will change from server to server.
Users are encouraged to utilize this API with a knowledge of their
server choice, and servers may extend the functionality of
hijack_io to provide additional features to users. The purpose of
rack.hijack is for Rack to "get out of the way", as such, Rack only
provides the minimum of specification and support.

If rack.hijack? is false, then rack.hijack should not be set.

If rack.hijack? is false, then rack.hijack_io should not be set.
==== Response (after headers)
It is also possible to hijack a response after the status and headers
Expand All @@ -208,7 +202,6 @@ In order to do this, an application may set the special header
<tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
accepting an argument that conforms to the <tt>rack.hijack_io</tt>
protocol.

After the headers have been sent, and this hijack callback has been
called, the application is now responsible for the remaining lifecycle
of the IO. The application is also responsible for maintaining HTTP
Expand All @@ -217,10 +210,8 @@ applications will have wanted to specify the header Connection:close in
HTTP/1.1, and not Connection:keep-alive, as there is no protocol for
returning hijacked sockets to the web server. For that purpose, use the
body streaming API instead (progressively yielding strings via each).

Servers must ignore the <tt>body</tt> part of the response tuple when
the <tt>rack.hijack</tt> response API is in use.

The special response header <tt>rack.hijack</tt> must only be set
if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
==== Conventions
Expand All @@ -238,9 +229,9 @@ The header must respond to +each+, and yield values of key and value.
Special headers starting "rack." are for communicating with the
server, and must not be sent back to the client.
The header keys must be Strings.
The header must not contain a +Status+ key,
contain keys with <tt>:</tt> or newlines in their name,
but only contain keys that match the token rule according to RFC 2616.
The header must not contain a +Status+ key.
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".
Expand Down
9 changes: 4 additions & 5 deletions lib/rack/lint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -635,12 +635,11 @@ def check_headers(header)
assert("header key must be a string, was #{key.class}") {
key.kind_of? String
}
## The header must not contain a +Status+ key,
## The header must not contain a +Status+ key.
assert("header must not contain Status") { key.downcase != "status" }
## contain keys with <tt>:</tt> or newlines in their name,
assert("header names must not contain : or \\n") { key !~ /[:\n]/ }
## The header must match the token rule according to RFC 2616
assert("invalid header name: #{key}") { key =~ /\A[\!#\$%&'\*\+-.0-9A-Z\^_`a-z\|~]+\z/ }
## 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 " +
Expand Down
43 changes: 30 additions & 13 deletions test/spec_lint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -200,19 +200,36 @@ def result.name
}.should.raise(Rack::Lint::LintError).
message.should.match(/must not contain Status/)

lambda {
Rack::Lint.new(lambda { |env|
[200, {"Content-Type:" => "text/plain"}, []]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
message.should.match(/must not contain :/)

lambda {
Rack::Lint.new(lambda { |env|
[200, {"([{<quark>}])?" => "text/plain"}, []]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
message.should.equal("invalid header name: ([{<quark>}])?")
# From RFC 7230:<F24><F25>
# Most HTTP header field values are defined using common syntax
# components (token, quoted-string, and comment) separated by
# whitespace or specific delimiting characters. Delimiters are chosen
# from the set of US-ASCII visual characters not allowed in a token
# (DQUOTE and "(),/:;<=>?@[\]{}").
#
# token = 1*tchar
#
# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
# / DIGIT / ALPHA
# ; any VCHAR, except delimiters
invalid_headers = 0.upto(31).map(&:chr) + %W<( ) , / : ; < = > ? @ [ \\ ] { } \x7F>
invalid_headers.each do |invalid_header|
lambda {
Rack::Lint.new(lambda { |env|
[200, {invalid_header => "text/plain"}, []]
}).call(env({}))
}.should.raise(Rack::Lint::LintError, "on invalid header: #{invalid_header}").
message.should.equal("invalid header name: #{invalid_header}")
end
valid_headers = 0.upto(127).map(&:chr) - invalid_headers
valid_headers.each do |valid_header|
lambda {
Rack::Lint.new(lambda { |env|
[200, {valid_header => "text/plain"}, []]
}).call(env({}))
}.should.not.raise(Rack::Lint::LintError, "on valid header: #{valid_header}")
end

lambda {
Rack::Lint.new(lambda { |env|
Expand Down

0 comments on commit 6839fc2

Please sign in to comment.