Skip to content

Commit

Permalink
Rack 3 requires all response headers to be lowercase
Browse files Browse the repository at this point in the history
  • Loading branch information
dentarg committed Jan 2, 2024
1 parent 8f4764a commit dc702de
Show file tree
Hide file tree
Showing 21 changed files with 51 additions and 51 deletions.
34 changes: 17 additions & 17 deletions lib/sinatra/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ def finish
result = body

if drop_content_info?
headers.delete 'Content-Length'
headers.delete 'Content-Type'
headers.delete 'content-length'
headers.delete 'content-type'
end

if drop_body?
Expand All @@ -186,9 +186,9 @@ def finish
end

if calculate_content_length?
# if some other code has already set Content-Length, don't muck with it
# if some other code has already set content-length, don't muck with it
# currently, this would be the static file-handler
headers['Content-Length'] = body.map(&:bytesize).reduce(0, :+).to_s
headers['content-length'] = body.map(&:bytesize).reduce(0, :+).to_s
end

[status, headers, result]
Expand All @@ -197,7 +197,7 @@ def finish
private

def calculate_content_length?
headers['Content-Type'] && !headers['Content-Length'] && (Array === body)
headers['content-type'] && !headers['content-length'] && (Array === body)
end

def drop_content_info?
Expand Down Expand Up @@ -291,8 +291,8 @@ def block.each; yield(call) end
elsif value
# Rack 2.0 returns a Rack::File::Iterator here instead of
# Rack::File as it was in the previous API.
unless request.head? || value.is_a?(Rack::Files::Iterator) || value.is_a?(Stream)
headers.delete 'Content-Length'
unless request.head? || value.is_a?(Rack::File::Iterator) || value.is_a?(Stream)
headers.delete 'content-length'
end
response.body = value
else
Expand Down Expand Up @@ -372,10 +372,10 @@ def mime_type(type)
Base.mime_type(type)
end

# Set the Content-Type of the response body given a media type or file
# Set the content-type of the response body given a media type or file
# extension.
def content_type(type = nil, params = {})
return response['Content-Type'] unless type
return response['content-type'] unless type

default = params.delete :default
mime_type = mime_type(type) || default
Expand All @@ -393,7 +393,7 @@ def content_type(type = nil, params = {})
"#{key}=#{val}"
end.join(', ')
end
response['Content-Type'] = mime_type
response['content-type'] = mime_type
end

# https://html.spec.whatwg.org/#multipart-form-data
Expand All @@ -412,12 +412,12 @@ def attachment(filename = nil, disposition = :attachment)
params = format('; filename="%s"', File.basename(filename).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE))
response['Content-Disposition'] << params
ext = File.extname(filename)
content_type(ext) unless response['Content-Type'] || ext.empty?
content_type(ext) unless response['content-type'] || ext.empty?
end

# Use the contents of the file at +path+ as the response body.
def send_file(path, opts = {})
if opts[:type] || !response['Content-Type']
if opts[:type] || !response['content-type']
content_type opts[:type] || File.extname(path), default: 'application/octet-stream'
end

Expand All @@ -433,7 +433,7 @@ def send_file(path, opts = {})
result = file.serving(request, path)

result[1].each { |k, v| headers[k] ||= v }
headers['Content-Length'] = result[1]['Content-Length']
headers['content-length'] = result[1]['content-length']
opts[:status] &&= Integer(opts[:status])
halt (opts[:status] || result[0]), result[2]
rescue Errno::ENOENT
Expand Down Expand Up @@ -995,7 +995,7 @@ def call!(env) # :nodoc:
invoke { dispatch! }
invoke { error_block!(response.status) } unless @env['sinatra.error']

unless @response['Content-Type']
unless @response['content-type']
if Array === body && body[0].respond_to?(:content_type)
content_type body[0].content_type
elsif (default = settings.default_content_type)
Expand Down Expand Up @@ -1058,7 +1058,7 @@ def route!(base = settings, pass_block = nil)
routes = base.routes[@request.request_method]

routes&.each do |pattern, conditions, block|
response.delete_header('Content-Type') unless @pinned_response
response.delete_header('content-type') unless @pinned_response

returned_pass_block = process_route(pattern, conditions) do |*args|
env['sinatra.route'] = "#{@request.request_method} #{pattern}"
Expand Down Expand Up @@ -1179,7 +1179,7 @@ def dispatch!
invoke do
static! if settings.static? && (request.get? || request.head?)
filter! :before do
@pinned_response = !response['Content-Type'].nil?
@pinned_response = !response['content-type'].nil?
end
route!
end
Expand Down Expand Up @@ -1724,7 +1724,7 @@ def provides(*types)
types.map! { |t| mime_types(t) }
types.flatten!
condition do
response_content_type = response['Content-Type']
response_content_type = response['content-type']
preferred_type = request.preferred_type(types)

if response_content_type
Expand Down
4 changes: 2 additions & 2 deletions lib/sinatra/show_exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ def call(env)
[
500,
{
'Content-Type' => content_type,
'Content-Length' => body.bytesize.to_s
'content-type' => content_type,
'content-length' => body.bytesize.to_s
},
[body]
]
Expand Down
2 changes: 1 addition & 1 deletion rack-protection/lib/rack/protection/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def instrument(env)

def deny(env)
warn env, "attack prevented by #{self.class}"
[options[:status], { 'Content-Type' => 'text/plain' }, [options[:message]]]
[options[:status], { 'content-type' => 'text/plain' }, [options[:message]]]
end

def report(env)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module Protection
# https://scotthelme.co.uk/csp-cheat-sheet/
# http://www.html5rocks.com/en/tutorials/security/content-security-policy/
#
# Sets the 'Content-Security-Policy[-Report-Only]' header.
# Sets the 'content-security-policy[-report-only]' header.
#
# Options: ContentSecurityPolicy configuration is a complex topic with
# several levels of support that has evolved over time.
Expand Down Expand Up @@ -71,7 +71,7 @@ def csp_policy

def call(env)
status, headers, body = @app.call(env)
header = options[:report_only] ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
header = options[:report_only] ? 'content-security-policy-report-only' : 'content-security-policy'
headers[header] ||= csp_policy if html? headers
[status, headers, body]
end
Expand Down
2 changes: 1 addition & 1 deletion rack-protection/lib/rack/protection/cookie_tossing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def remove_bad_cookies(request, response)
def redirect(env)
request = Request.new(env)
warn env, "attack prevented by #{self.class}"
[302, { 'Content-Type' => 'text/html', 'Location' => request.path }, []]
[302, { 'content-type' => 'text/html', 'location' => request.path }, []]
end

def bad_cookies
Expand Down
2 changes: 1 addition & 1 deletion rack-protection/lib/rack/protection/frame_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def frame_options

def call(env)
status, headers, body = @app.call(env)
headers['X-Frame-Options'] ||= frame_options if html? headers
headers['x-frame-options'] ||= frame_options if html? headers
[status, headers, body]
end
end
Expand Down
2 changes: 1 addition & 1 deletion rack-protection/lib/rack/protection/json_csrf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def call(env)
def has_vector?(request, headers)
return false if request.xhr?
return false if options[:allow_if]&.call(request.env)
return false unless headers['Content-Type'].to_s.split(';', 2).first =~ %r{^\s*application/json\s*$}
return false unless headers['content-type'].to_s.split(';', 2).first =~ %r{^\s*application/json\s*$}

origin(request.env).nil? and referrer(request.env) != request.host
end
Expand Down
2 changes: 1 addition & 1 deletion rack-protection/lib/rack/protection/referrer_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ReferrerPolicy < Base

def call(env)
status, headers, body = @app.call(env)
headers['Referrer-Policy'] ||= options[:referrer_policy]
headers['referrer-policy'] ||= options[:referrer_policy]
[status, headers, body]
end
end
Expand Down
2 changes: 1 addition & 1 deletion rack-protection/lib/rack/protection/strict_transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def strict_transport

def call(env)
status, headers, body = @app.call(env)
headers['Strict-Transport-Security'] ||= strict_transport
headers['strict-transport-security'] ||= strict_transport
[status, headers, body]
end
end
Expand Down
4 changes: 2 additions & 2 deletions rack-protection/lib/rack/protection/xss_header.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class XSSHeader < Base

def call(env)
status, headers, body = @app.call(env)
headers['X-XSS-Protection'] ||= "1; mode=#{options[:xss_mode]}" if html? headers
headers['X-Content-Type-Options'] ||= 'nosniff' if options[:nosniff]
headers['x-xss-protection'] ||= "1; mode=#{options[:xss_mode]}" if html? headers
headers['x-content-type-options'] ||= 'nosniff' if options[:nosniff]
[status, headers, body]
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
it 'allows for a custom authenticity token param' do
mock_app do
use Rack::Protection::AuthenticityToken, authenticity_param: 'csrf_param'
run proc { |_e| [200, { 'Content-Type' => 'text/plain' }, ['hi']] }
run proc { |_e| [200, { 'content-type' => 'text/plain' }, ['hi']] }
end

post('/', { 'csrf_param' => token }, 'rack.session' => { csrf: token })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
end

it 'should not override the header if already set' do
mock_app with_headers('Content-Security-Policy' => 'default-src: none')
mock_app with_headers('content-security-policy' => 'default-src: none')
expect(get('/', {}, 'wants' => 'text/html').headers['Content-Security-Policy']).to eq('default-src: none')
end
end
10 changes: 5 additions & 5 deletions rack-protection/spec/lib/rack/protection/escaped_params_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
it 'escapes html entities' do
mock_app do |env|
request = Rack::Request.new(env)
[200, { 'Content-Type' => 'text/plain' }, [request.params['foo']]]
[200, { 'content-type' => 'text/plain' }, [request.params['foo']]]
end
get '/', foo: '<bar>'
expect(body).to eq('&lt;bar&gt;')
Expand All @@ -16,7 +16,7 @@
it 'leaves normal params untouched' do
mock_app do |env|
request = Rack::Request.new(env)
[200, { 'Content-Type' => 'text/plain' }, [request.params['foo']]]
[200, { 'content-type' => 'text/plain' }, [request.params['foo']]]
end
get '/', foo: 'bar'
expect(body).to eq('bar')
Expand All @@ -25,15 +25,15 @@
it 'copes with nested arrays' do
mock_app do |env|
request = Rack::Request.new(env)
[200, { 'Content-Type' => 'text/plain' }, [request.params['foo']['bar']]]
[200, { 'content-type' => 'text/plain' }, [request.params['foo']['bar']]]
end
get '/', foo: { bar: '<bar>' }
expect(body).to eq('&lt;bar&gt;')
end

it 'leaves cache-breaker params untouched' do
mock_app do |_env|
[200, { 'Content-Type' => 'text/plain' }, ['hi']]
[200, { 'content-type' => 'text/plain' }, ['hi']]
end

get '/?95df8d9bf5237ad08df3115ee74dcb10'
Expand All @@ -43,7 +43,7 @@
it 'leaves TempFiles untouched' do
mock_app do |env|
request = Rack::Request.new(env)
[200, { 'Content-Type' => 'text/plain' }, ["#{request.params['file'][:filename]}\n#{request.params['file'][:tempfile].read}\n#{request.params['other']}"]]
[200, { 'content-type' => 'text/plain' }, ["#{request.params['file'][:filename]}\n#{request.params['file'][:tempfile].read}\n#{request.params['other']}"]]
end

temp_file = File.open('_escaped_params_tmp_file', 'w')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
end

it 'should not override the header if already set' do
mock_app with_headers('X-Frame-Options' => 'allow')
mock_app with_headers('x-frame-options' => 'allow')
expect(get('/', {}, 'wants' => 'text/html').headers['X-Frame-Options']).to eq('allow')
end
end
10 changes: 5 additions & 5 deletions rack-protection/spec/lib/rack/protection/json_csrf_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ def self.body

def self.call(env)
Thread.current[:last_env] = env
[200, { 'Content-Type' => 'application/json' }, body]
[200, { 'content-type' => 'application/json' }, body]
end
end

describe 'json response' do
before do
mock_app { |_e| [200, { 'Content-Type' => 'application/json' }, []] }
mock_app { |_e| [200, { 'content-type' => 'application/json' }, []] }
end

it 'denies get requests with json responses with a remote referrer' do
Expand All @@ -39,7 +39,7 @@ def self.call(env)

it 'closes the body returned by the app if it denies the get request' do
mock_app DummyAppWithBody do |_e|
[200, { 'Content-Type' => 'application/json' }, []]
[200, { 'content-type' => 'application/json' }, []]
end

get('/', {}, 'HTTP_REFERER' => 'http://evil.com')
Expand All @@ -50,7 +50,7 @@ def self.call(env)
it 'accepts requests with json responses with a remote referrer when allow_if is true' do
mock_app do
use Rack::Protection::JsonCsrf, allow_if: ->(env) { env['HTTP_REFERER'] == 'http://good.com' }
run proc { |_e| [200, { 'Content-Type' => 'application/json' }, []] }
run proc { |_e| [200, { 'content-type' => 'application/json' }, []] }
end

expect(get('/', {}, 'HTTP_REFERER' => 'http://good.com')).to be_ok
Expand Down Expand Up @@ -88,7 +88,7 @@ def self.call(env)
it 'still denies' do
mock_app do
use Rack::Protection, reaction: :drop_session
run proc { |_e| [200, { 'Content-Type' => 'application/json' }, []] }
run proc { |_e| [200, { 'content-type' => 'application/json' }, []] }
end

session = { foo: :bar }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

context 'escaping' do
before do
mock_app { |e| [200, { 'Content-Type' => 'text/plain' }, [e['PATH_INFO']]] }
mock_app { |e| [200, { 'content-type' => 'text/plain' }, [e['PATH_INFO']]] }
end

%w[/foo/bar /foo/bar/ / /.f /a.x].each do |path|
Expand All @@ -28,7 +28,7 @@

context "PATH_INFO's encoding" do
before do
@app = Rack::Protection::PathTraversal.new(proc { |e| [200, { 'Content-Type' => 'text/plain' }, [e['PATH_INFO'].encoding.to_s]] })
@app = Rack::Protection::PathTraversal.new(proc { |e| [200, { 'content-type' => 'text/plain' }, [e['PATH_INFO'].encoding.to_s]] })
end

it 'should remain unchanged as ASCII-8BIT' do
Expand Down
4 changes: 2 additions & 2 deletions rack-protection/spec/lib/rack/protection/protection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
it 'passes on options' do
mock_app do
use Rack::Protection, track: ['HTTP_FOO']
run proc { |_e| [200, { 'Content-Type' => 'text/plain' }, ['hi']] }
run proc { |_e| [200, { 'content-type' => 'text/plain' }, ['hi']] }
end

session = { foo: :bar }
Expand All @@ -21,7 +21,7 @@
it 'passes errors through if :reaction => :report is used' do
mock_app do
use Rack::Protection, reaction: :report
run proc { |e| [200, { 'Content-Type' => 'text/plain' }, [e['protection.failed'].to_s]] }
run proc { |e| [200, { 'content-type' => 'text/plain' }, [e['protection.failed'].to_s]] }
end

session = { foo: :bar }
Expand Down
4 changes: 2 additions & 2 deletions rack-protection/spec/lib/rack/protection/xss_header_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
end

it 'should not override the header if already set' do
mock_app with_headers('X-XSS-Protection' => '0')
mock_app with_headers('x-xss-protection' => '0')
expect(get('/', {}, 'wants' => 'text/html').headers['X-XSS-Protection']).to eq('0')
end

Expand All @@ -48,7 +48,7 @@
end

it 'should not override the header if already set X-Content-Type-Options' do
mock_app with_headers('X-Content-Type-Options' => 'sniff')
mock_app with_headers('x-content-type-options' => 'sniff')
expect(get('/', {}, 'wants' => 'text/html').headers['X-Content-Type-Options']).to eq('sniff')
end
end
2 changes: 1 addition & 1 deletion rack-protection/spec/support/dummy_app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ module DummyApp
def self.call(env)
Thread.current[:last_env] = env
body = (env['REQUEST_METHOD'] == 'HEAD' ? '' : 'ok')
[200, { 'Content-Type' => env['wants'] || 'text/plain' }, [body]]
[200, { 'content-type' => env['wants'] || 'text/plain' }, [body]]
end
end

0 comments on commit dc702de

Please sign in to comment.