Skip to content

Commit

Permalink
Allow headers to be an Array of String instances.
Browse files Browse the repository at this point in the history
  • Loading branch information
ioquatix committed Feb 23, 2022
1 parent 7a60367 commit 3e8e2ad
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 200 deletions.
7 changes: 3 additions & 4 deletions SPEC.rdoc
Expand Up @@ -258,10 +258,9 @@ The header must not contain a +Status+ key.
Header keys must conform to RFC7230 token specification, i.e. cannot
contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
Header keys must not contain uppercase ASCII characters (A-Z).
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.
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

Expand Down
12 changes: 6 additions & 6 deletions lib/rack/handler/webrick.rb
Expand Up @@ -95,15 +95,15 @@ def service(req, res)
begin
res.status = status.to_i
io_lambda = nil
headers.each { |k, vs|
if k == RACK_HIJACK
io_lambda = vs
elsif k == "set-cookie"
res.cookies.concat vs.split("\n")
headers.each { |key, value|
if key == RACK_HIJACK
io_lambda = value
elsif key == "set-cookie"
res.cookies.concat(Array(value))
else
# Since WEBrick won't accept repeated headers,
# merge the values per RFC 1945 section 4.2.
res[k] = vs.split("\n").join(", ")
res[key] = Array(value).join(", ")
end
}

Expand Down
34 changes: 19 additions & 15 deletions lib/rack/lint.rb
Expand Up @@ -655,15 +655,15 @@ def check_headers(headers)
raise LintError, "headers object should not be frozen, but is"
end

headers.each { |key, value|
headers.each do |key, value|
## The header keys must be Strings.
unless key.kind_of? String
raise LintError, "header key must be a string, was #{key.class}"
end

## Special headers starting "rack." are for communicating with the
## server, and must not be sent back to the client.
next if key =~ /^rack\..+$/
next if key.start_with?("rack.")

## The header must not contain a +Status+ key.
raise LintError, "header must not contain status" if key == "status"
Expand All @@ -673,19 +673,23 @@ def check_headers(headers)
## Header keys must not contain uppercase ASCII characters (A-Z).
raise LintError, "uppercase character in header name: #{key}" if key =~ /[A-Z]/

## The values of the header must be Strings,
unless value.kind_of? String
raise LintError, "a header value must be a String, but the value of '#{key}' is a #{value.class}"
end
## 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.
if item =~ /[\000-\037]/
raise LintError, "invalid header value #{key}: #{item.inspect}"
end
}
}
## Header values must be either a String instance,
if value.kind_of?(String)
check_header_value(key, value)
elsif value.kind_of?(Array)
## or an Array of String instances,
value.each{|value| check_header_value(key, value)}
else
raise LintError, "a header value must be a String or Array of Strings, but the value of '#{key}' is a #{value.class}"
end
end
end

def check_header_value(key, value)
## such that each String instance must not contain characters below 037.
if value =~ /[\000-\037]/
raise LintError, "invalid header value #{key}: #{value.inspect}"
end
end

##
Expand Down
72 changes: 47 additions & 25 deletions lib/rack/response.rb
Expand Up @@ -143,19 +143,19 @@ def empty?

def has_header?(key)
raise ArgumentError unless key.is_a?(String)
headers.key? key
@headers.key?(key)
end
def get_header(key)
raise ArgumentError unless key.is_a?(String)
headers[key]
@headers[key]
end
def set_header(key, v)
def set_header(key, value)
raise ArgumentError unless key.is_a?(String)
headers[key] = v
@headers[key] = value
end
def delete_header(key)
raise ArgumentError unless key.is_a?(String)
headers.delete key
@headers.delete key
end

alias :[] :get_header
Expand Down Expand Up @@ -186,7 +186,7 @@ def unprocessable?; status == 422; end
def redirect?; [301, 302, 303, 307, 308].include? status; end

def include?(header)
has_header? header
has_header?(header)
end

# Add a header that may have multiple values.
Expand All @@ -198,15 +198,23 @@ def include?(header)
# assert_equal 'Accept-Encoding,Cookie', response.get_header('Vary')
#
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
def add_header(key, v)
def add_header(key, value)
raise ArgumentError unless key.is_a?(String)

if v.nil?
get_header key
elsif has_header? key
set_header key, "#{get_header key},#{v}"
if value.nil?
return get_header(key)
end

value = value.to_s

if header = get_header(key)
if header.is_a?(Array)
header << value
else
set_header(key, [header, value])
end
else
set_header key, v
set_header(key, value)
end
end

Expand Down Expand Up @@ -242,28 +250,31 @@ def location=(location)
end

def set_cookie(key, value)
cookie_header = get_header SET_COOKIE
set_header SET_COOKIE, ::Rack::Utils.add_cookie_to_header(cookie_header, key, value)
add_header SET_COOKIE, Utils.set_cookie_header(key, value)
end

def delete_cookie(key, value = {})
set_header SET_COOKIE, ::Rack::Utils.add_remove_cookie_to_header(get_header(SET_COOKIE), key, value)
set_header(SET_COOKIE,
Utils.delete_set_cookie_header!(
get_header(SET_COOKIE), key, value
)
)
end

def set_cookie_header
get_header SET_COOKIE
end

def set_cookie_header=(v)
set_header SET_COOKIE, v
def set_cookie_header=(value)
set_header SET_COOKIE, value
end

def cache_control
get_header CACHE_CONTROL
end

def cache_control=(v)
set_header CACHE_CONTROL, v
def cache_control=(value)
set_header CACHE_CONTROL, value
end

# Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
Expand All @@ -286,8 +297,8 @@ def etag
get_header ETAG
end

def etag=(v)
set_header ETAG, v
def etag=(value)
set_header ETAG, value
end

protected
Expand Down Expand Up @@ -345,10 +356,21 @@ def initialize(status, headers)
@headers = headers
end

def has_header?(key); headers.key? key; end
def get_header(key); headers[key]; end
def set_header(key, v); headers[key] = v; end
def delete_header(key); headers.delete key; end
def has_header?(key)
headers.key?(key)
end

def get_header(key)
headers[key]
end

def set_header(key, value)
headers[key] = value
end

def delete_header(key)
headers.delete(key)
end
end
end
end
98 changes: 43 additions & 55 deletions lib/rack/utils.rb
Expand Up @@ -222,7 +222,7 @@ def parse_cookies_header(header)
end
end

def add_cookie_to_header(header, key, value)
def set_cookie_header(key, value)
case value
when Hash
domain = "; domain=#{value[:domain]}" if value[:domain]
Expand All @@ -248,71 +248,59 @@ def add_cookie_to_header(header, key, value)
end
value = [value] unless Array === value

cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
return "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
end

case header
when nil, ''
cookie
when String
[header, cookie].join("\n")
when Array
(header + [cookie]).join("\n")
def set_cookie_header!(headers, key, value)
if header = headers[SET_COOKIE]
if header.is_a?(Array)
header << set_cookie_header(key, value)
else
headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
end
else
raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
headers[SET_COOKIE] = set_cookie_header(key, value)
end
end

def set_cookie_header!(header, key, value)
header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
nil
end

def make_delete_cookie_header(header, key, value)
case header
when nil, ''
cookies = []
when String
cookies = header.split("\n")
when Array
cookies = header
end

key = escape(key)
domain = value[:domain]
path = value[:path]
regexp = if domain
if path
/\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
# Adds a cookie that will *remove* a cookie from the client. Hence the
# strange method name.
def delete_set_cookie_header(key, value = {})
set_cookie_header(key, {
value: '', path: nil, domain: nil,
max_age: '0',
expires: Time.at(0)
}.merge(value))
end

def delete_set_cookie_header!(header, key, value = {})
if header
header = Array(header)

key = escape(key)
domain = value[:domain]
path = value[:path]
regexp = if domain
if path
/\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
else
/\A#{key}=.*domain=#{domain}(?:;|$)/
end
elsif path
/\A#{key}=.*path=#{path}(?:;|$)/
else
/\A#{key}=.*domain=#{domain}(?:;|$)/
/\A#{key}=/
end
elsif path
/\A#{key}=.*path=#{path}(?:;|$)/
else
/\A#{key}=/
end

cookies.reject! { |cookie| regexp.match? cookie }

cookies.join("\n")
end

def delete_cookie_header!(header, key, value = {})
header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
nil
end

# Adds a cookie that will *remove* a cookie from the client. Hence the
# strange method name.
def add_remove_cookie_to_header(header, key, value = {})
new_header = make_delete_cookie_header(header, key, value)
header.reject! { |cookie| regexp.match? cookie }

add_cookie_to_header(new_header, key,
{ value: '', path: nil, domain: nil,
max_age: '0',
expires: Time.at(0) }.merge(value))
header << delete_set_cookie_header(key, value)
else
header = delete_set_cookie_header(key, value)
end

return header
end

def rfc2822(time)
Expand Down
15 changes: 1 addition & 14 deletions test/spec_lint.rb
Expand Up @@ -365,15 +365,7 @@ def obj.each; end
[200, { "foo" => Object.new }, []]
}).call(env({}))
}.must_raise(Rack::Lint::LintError).
message.must_equal "a header value must be a String, but the value of 'foo' is a Object"

lambda {
Rack::Lint.new(lambda { |env|
[200, { "foo" => [1, 2, 3] }, []]
}).call(env({}))
}.must_raise(Rack::Lint::LintError).
message.must_equal "a header value must be a String, but the value of 'foo' is a Array"

message.must_equal "a header value must be a String or Array of Strings, but the value of 'foo' is a Object"

lambda {
Rack::Lint.new(lambda { |env|
Expand All @@ -382,11 +374,6 @@ def obj.each; end
}.must_raise(Rack::Lint::LintError).
message.must_match(/invalid header/)

# line ends (010).must_be :allowed in header values.?
Rack::Lint.new(lambda { |env|
[200, { "foo-bar" => "one\ntwo\nthree", "content-length" => "0", "content-type" => "text/plain" }, []]
}).call(env({})).first.must_equal 200

lambda {
Rack::Lint.new(lambda { |env|
[200, [%w(content-type text/plain), %w(content-length 0)], []]
Expand Down

0 comments on commit 3e8e2ad

Please sign in to comment.