summary refs log tree commit
diff options
context:
space:
mode:
authorJames Tucker <jftucker@gmail.com>2014-11-27 12:35:14 -0800
committerJames Tucker <jftucker@gmail.com>2014-11-27 12:35:14 -0800
commit6839fc203339f021cb3267fb09cba89410f086e9 (patch)
tree7b5dd5b2e078cd592d884b4c153893b5971a9eec
parent8cb9b65b4e7c7157aba0f5421e59c70411c919cf (diff)
downloadrack-6839fc203339f021cb3267fb09cba89410f086e9.tar.gz
Rack::Lint and SPEC align with RFC7230 headers
-rw-r--r--SPEC15
-rw-r--r--lib/rack/lint.rb9
-rw-r--r--test/spec_lint.rb43
3 files changed, 37 insertions, 30 deletions
diff --git a/SPEC b/SPEC
index 9cf4b568..6871b815 100644
--- a/SPEC
+++ b/SPEC
@@ -176,20 +176,16 @@ 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
@@ -197,9 +193,7 @@ 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
@@ -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
@@ -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
@@ -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".
diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb
index f2dcd568..bb843ad6 100644
--- a/lib/rack/lint.rb
+++ b/lib/rack/lint.rb
@@ -635,12 +635,11 @@ module Rack
         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 " +
diff --git a/test/spec_lint.rb b/test/spec_lint.rb
index ea7836ee..64278c71 100644
--- a/test/spec_lint.rb
+++ b/test/spec_lint.rb
@@ -200,19 +200,36 @@ describe Rack::Lint do
     }.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|