1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00
puma--puma/lib/puma/request.rb
Chien-Wei Huang (Michael) 31c72cf33e
Ignore illegal response header (#2439)
* Ignore illegal response header
2020-10-26 16:02:13 -06:00

451 lines
13 KiB
Ruby

# frozen_string_literal: true
module Puma
# The methods here are included in Server, but are separated into this file.
# All the methods here pertain to passing the request to the app, then
# writing the response back to the client.
#
# None of the methods here are called externally, with the exception of
# #handle_request, which is called in Server#process_client.
# @version 5.0.3
#
module Request
include Puma::Const
# Takes the request contained in +client+, invokes the Rack application to construct
# the response and writes it back to +client.io+.
#
# It'll return +false+ when the connection is closed, this doesn't mean
# that the response wasn't successful.
#
# It'll return +:async+ if the connection remains open but will be handled
# elsewhere, i.e. the connection has been hijacked by the Rack application.
#
# Finally, it'll return +true+ on keep-alive connections.
# @param client [Puma::Client]
# @param lines [Puma::IOBuffer]
# @return [Boolean,:async]
#
def handle_request(client, lines)
env = client.env
io = client.io
return false if closed_socket?(io)
normalize_env env, client
env[PUMA_SOCKET] = io
if env[HTTPS_KEY] && io.peercert
env[PUMA_PEERCERT] = io.peercert
end
env[HIJACK_P] = true
env[HIJACK] = client
body = client.body
head = env[REQUEST_METHOD] == HEAD
env[RACK_INPUT] = body
env[RACK_URL_SCHEME] = default_server_port(env) == PORT_443 ? HTTPS : HTTP
if @early_hints
env[EARLY_HINTS] = lambda { |headers|
begin
fast_write io, str_early_hints(headers)
rescue ConnectionError => e
@events.debug_error e
# noop, if we lost the socket we just won't send the early hints
end
}
end
req_env_post_parse env
# A rack extension. If the app writes #call'ables to this
# array, we will invoke them when the request is done.
#
after_reply = env[RACK_AFTER_REPLY] = []
begin
begin
status, headers, res_body = @thread_pool.with_force_shutdown do
@app.call(env)
end
return :async if client.hijacked
status = status.to_i
if status == -1
unless headers.empty? and res_body == []
raise "async response must have empty headers and body"
end
return :async
end
rescue ThreadPool::ForceShutdown => e
@events.unknown_error e, client, "Rack app"
@events.log "Detected force shutdown of a thread"
status, headers, res_body = lowlevel_error(e, env, 503)
rescue Exception => e
@events.unknown_error e, client, "Rack app"
status, headers, res_body = lowlevel_error(e, env, 500)
end
res_info = {}
res_info[:content_length] = nil
res_info[:no_body] = head
res_info[:content_length] = if res_body.kind_of? Array and res_body.size == 1
res_body[0].bytesize
else
nil
end
cork_socket io
str_headers(env, status, headers, res_info, lines)
line_ending = LINE_END
content_length = res_info[:content_length]
response_hijack = res_info[:response_hijack]
if res_info[:no_body]
if content_length and status != 204
lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
end
lines << LINE_END
fast_write io, lines.to_s
return res_info[:keep_alive]
end
if content_length
lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
chunked = false
elsif !response_hijack and res_info[:allow_chunked]
lines << TRANSFER_ENCODING_CHUNKED
chunked = true
end
lines << line_ending
fast_write io, lines.to_s
if response_hijack
response_hijack.call io
return :async
end
begin
res_body.each do |part|
next if part.bytesize.zero?
if chunked
str = part.bytesize.to_s(16) << line_ending << part << line_ending
fast_write io, str
else
fast_write io, part
end
io.flush
end
if chunked
fast_write io, CLOSE_CHUNKED
io.flush
end
rescue SystemCallError, IOError
raise ConnectionError, "Connection error detected during write"
end
ensure
uncork_socket io
body.close
client.tempfile.unlink if client.tempfile
res_body.close if res_body.respond_to? :close
after_reply.each { |o| o.call }
end
return res_info[:keep_alive]
end
# @param env [Hash] see Puma::Client#env, from request
# @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
#
def default_server_port(env)
if ['on', HTTPS].include?(env[HTTPS_KEY]) || env[HTTP_X_FORWARDED_PROTO].to_s[0...5] == HTTPS || env[HTTP_X_FORWARDED_SCHEME] == HTTPS || env[HTTP_X_FORWARDED_SSL] == "on"
PORT_443
else
PORT_80
end
end
# Writes to an io (normally Client#io) using #syswrite
# @param io [#syswrite] the io to write to
# @param str [String] the string written to the io
# @raise [ConnectionError]
#
def fast_write(io, str)
n = 0
while true
begin
n = io.syswrite str
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
if !IO.select(nil, [io], nil, WRITE_TIMEOUT)
raise ConnectionError, "Socket timeout writing data"
end
retry
rescue Errno::EPIPE, SystemCallError, IOError
raise ConnectionError, "Socket timeout writing data"
end
return if n == str.bytesize
str = str.byteslice(n..-1)
end
end
private :fast_write
# @param status [Integer] status from the app
# @return [String] the text description from Puma::HTTP_STATUS_CODES
#
def fetch_status_code(status)
HTTP_STATUS_CODES.fetch(status) { 'CUSTOM' }
end
private :fetch_status_code
# Given a Hash +env+ for the request read from +client+, add
# and fixup keys to comply with Rack's env guidelines.
# @param env [Hash] see Puma::Client#env, from request
# @param client [Puma::Client] only needed for Client#peerip
# @todo make private in 6.0.0
#
def normalize_env(env, client)
if host = env[HTTP_HOST]
if colon = host.index(":")
env[SERVER_NAME] = host[0, colon]
env[SERVER_PORT] = host[colon+1, host.bytesize]
else
env[SERVER_NAME] = host
env[SERVER_PORT] = default_server_port(env)
end
else
env[SERVER_NAME] = LOCALHOST
env[SERVER_PORT] = default_server_port(env)
end
unless env[REQUEST_PATH]
# it might be a dumbass full host request header
uri = URI.parse(env[REQUEST_URI])
env[REQUEST_PATH] = uri.path
raise "No REQUEST PATH" unless env[REQUEST_PATH]
# A nil env value will cause a LintError (and fatal errors elsewhere),
# so only set the env value if there actually is a value.
env[QUERY_STRING] = uri.query if uri.query
end
env[PATH_INFO] = env[REQUEST_PATH]
# From https://www.ietf.org/rfc/rfc3875 :
# "Script authors should be aware that the REMOTE_ADDR and
# REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
# may not identify the ultimate source of the request.
# They identify the client for the immediate request to the
# server; that client may be a proxy, gateway, or other
# intermediary acting on behalf of the actual source client."
#
unless env.key?(REMOTE_ADDR)
begin
addr = client.peerip
rescue Errno::ENOTCONN
# Client disconnects can result in an inability to get the
# peeraddr from the socket; default to localhost.
addr = LOCALHOST_IP
end
# Set unix socket addrs to localhost
addr = LOCALHOST_IP if addr.empty?
env[REMOTE_ADDR] = addr
end
end
# private :normalize_env
# @param header_key [#to_s]
# @return [Boolean]
#
def illegal_header_key?(header_key)
!!(ILLEGAL_HEADER_KEY_REGEX =~ header_key.to_s)
end
# @param header_value [#to_s]
# @return [Boolean]
#
def illegal_header_value?(header_value)
!!(ILLEGAL_HEADER_VALUE_REGEX =~ header_value.to_s)
end
private :illegal_header_key?, :illegal_header_value?
# Fixup any headers with `,` in the name to have `_` now. We emit
# headers with `,` in them during the parse phase to avoid ambiguity
# with the `-` to `_` conversion for critical headers. But here for
# compatibility, we'll convert them back. This code is written to
# avoid allocation in the common case (ie there are no headers
# with `,` in their names), that's why it has the extra conditionals.
# @param env [Hash] see Puma::Client#env, from request, modifies in place
# @version 5.0.3
#
def req_env_post_parse(env)
to_delete = nil
to_add = nil
env.each do |k,v|
if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING"
if to_delete
to_delete << k
else
to_delete = [k]
end
unless to_add
to_add = {}
end
to_add[k.tr(",", "_")] = v
end
end
if to_delete
to_delete.each { |k| env.delete(k) }
env.merge! to_add
end
end
private :req_env_post_parse
# Used in the lambda for env[ `Puma::Const::EARLY_HINTS` ]
# @param headers [Hash] the headers returned by the Rack application
# @return [String]
# @version 5.0.3
#
def str_early_hints(headers)
eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
headers.each_pair do |k, vs|
next if illegal_header_key?(k)
if vs.respond_to?(:to_s) && !vs.to_s.empty?
vs.to_s.split(NEWLINE).each do |v|
next if illegal_header_value?(v)
eh_str << "#{k}: #{v}\r\n"
end
else
eh_str << "#{k}: #{vs}\r\n"
end
end
"#{eh_str}\r\n".freeze
end
private :str_early_hints
# Processes and write headers to the IOBuffer.
# @param env [Hash] see Puma::Client#env, from request
# @param status [Integer] the status returned by the Rack application
# @param headers [Hash] the headers returned by the Rack application
# @param res_info [Hash] used to pass info between this method and #handle_request
# @param lines [Puma::IOBuffer] modified inn place
# @version 5.0.3
#
def str_headers(env, status, headers, res_info, lines)
line_ending = LINE_END
colon = COLON
http_11 = env[HTTP_VERSION] == HTTP_11
if http_11
res_info[:allow_chunked] = true
res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
# An optimization. The most common response is 200, so we can
# reply with the proper 200 status without having to compute
# the response header.
#
if status == 200
lines << HTTP_11_200
else
lines.append "HTTP/1.1 ", status.to_s, " ",
fetch_status_code(status), line_ending
res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
end
else
res_info[:allow_chunked] = false
res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
# Same optimization as above for HTTP/1.1
#
if status == 200
lines << HTTP_10_200
else
lines.append "HTTP/1.0 ", status.to_s, " ",
fetch_status_code(status), line_ending
res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
end
end
# regardless of what the client wants, we always close the connection
# if running without request queueing
res_info[:keep_alive] &&= @queue_requests
res_info[:response_hijack] = nil
headers.each do |k, vs|
next if illegal_header_key?(k)
case k.downcase
when CONTENT_LENGTH2
next if illegal_header_value?(vs)
res_info[:content_length] = vs
next
when TRANSFER_ENCODING
res_info[:allow_chunked] = false
res_info[:content_length] = nil
when HIJACK
res_info[:response_hijack] = vs
next
when BANNED_HEADER_KEY
next
end
if vs.respond_to?(:to_s) && !vs.to_s.empty?
vs.to_s.split(NEWLINE).each do |v|
next if illegal_header_value?(v)
lines.append k, colon, v, line_ending
end
else
lines.append k, colon, line_ending
end
end
# HTTP/1.1 & 1.0 assume different defaults:
# - HTTP 1.0 assumes the connection will be closed if not specified
# - HTTP 1.1 assumes the connection will be kept alive if not specified.
# Only set the header if we're doing something which is not the default
# for this protocol version
if http_11
lines << CONNECTION_CLOSE if !res_info[:keep_alive]
else
lines << CONNECTION_KEEP_ALIVE if res_info[:keep_alive]
end
end
private :str_headers
end
end