411 lines
16 KiB
Ruby
411 lines
16 KiB
Ruby
# encoding: binary
|
|
# Phusion Passenger - https://www.phusionpassenger.com/
|
|
# Copyright (c) 2010-2025 Asynchronous B.V.
|
|
#
|
|
# "Passenger", "Phusion Passenger" and "Union Station" are registered
|
|
# trademarks of Asynchronous B.V.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
|
|
PhusionPassenger.require_passenger_lib 'utils/tee_input'
|
|
|
|
if defined?(::Rack::BodyProxy) && !::Rack::BodyProxy.new("").respond_to?(:each)
|
|
module ::Rack
|
|
class BodyProxy
|
|
def each
|
|
@body.each { |body| yield body }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
module PhusionPassenger
|
|
module Rack
|
|
|
|
module ThreadHandlerExtension
|
|
# Constants which exist to relieve Ruby's garbage collector.
|
|
RACK_VERSION = "rack.version" # :nodoc:
|
|
RACK_VERSION_VALUE = [1, 3] # :nodoc:
|
|
RACK_INPUT = "rack.input" # :nodoc:
|
|
RACK_ERRORS = "rack.errors" # :nodoc:
|
|
RACK_MULTITHREAD = "rack.multithread" # :nodoc:
|
|
RACK_MULTIPROCESS = "rack.multiprocess" # :nodoc:
|
|
RACK_RUN_ONCE = "rack.run_once" # :nodoc:
|
|
RACK_URL_SCHEME = "rack.url_scheme" # :nodoc:
|
|
RACK_HIJACK_P = "rack.hijack?" # :nodoc:
|
|
RACK_HIJACK = "rack.hijack" # :nodoc:
|
|
HTTP_VERSION = "HTTP_VERSION" # :nodoc:
|
|
HTTP_1_1 = "HTTP/1.1" # :nodoc:
|
|
SCRIPT_NAME = "SCRIPT_NAME" # :nodoc:
|
|
REQUEST_METHOD = "REQUEST_METHOD" # :nodoc:
|
|
TRANSFER_ENCODING_HEADER = "Transfer-Encoding" # :nodoc:
|
|
TRANSFER_ENCODING_HEADERS = ["Transfer-Encoding", "Transfer-encoding", "transfer-encoding"] # :nodoc:
|
|
CONTENT_LENGTH_HEADER = "Content-Length" # :nodoc:
|
|
CONTENT_LENGTH_HEADERS = ["Content-Length", "Content-length", "content-length"] # :nodoc:
|
|
X_SENDFILE_HEADER = "X-Sendfile" # :nodoc:
|
|
X_ACCEL_REDIRECT_HEADER = "X-Accel-Redirect" # :nodoc:
|
|
CONTENT_LENGTH_HEADER_AND_SEPARATOR = "Content-Length: " # :nodoc
|
|
TRANSFER_ENCODING_HEADER_AND_VALUE_CRLF = "Transfer-Encoding: chunked\r\n" # :nodoc:
|
|
CONNECTION_CLOSE_CRLF2 = "Connection: close\r\n\r\n" # :nodoc:
|
|
HEAD = "HEAD" # :nodoc:
|
|
HTTPS = "HTTPS" # :nodoc:
|
|
HTTPS_DOWNCASE = "https" # :nodoc:
|
|
HTTP = "http" # :nodoc:
|
|
YES = "yes" # :nodoc:
|
|
ON = "on" # :nodoc:
|
|
ONE = "1" # :nodoc:
|
|
CRLF = "\r\n" # :nodoc:
|
|
NEWLINE = "\n" # :nodoc:
|
|
STATUS = "Status: " # :nodoc:
|
|
NAME_VALUE_SEPARATOR = ": " # :nodoc:
|
|
TERMINATION_CHUNK = "0\r\n\r\n" # :nodoc:
|
|
|
|
def process_request(env, connection, socket_wrapper, full_http_response)
|
|
rewindable_input = PhusionPassenger::Utils::TeeInput.new(connection, env)
|
|
begin
|
|
env[RACK_VERSION] = RACK_VERSION_VALUE
|
|
env[RACK_INPUT] = rewindable_input
|
|
env[RACK_ERRORS] = STDERR
|
|
env[RACK_MULTITHREAD] = @request_handler.concurrency > 1
|
|
env[RACK_MULTIPROCESS] = true
|
|
env[RACK_RUN_ONCE] = false
|
|
if env[HTTPS] == YES || env[HTTPS] == ON || env[HTTPS] == ONE
|
|
env[RACK_URL_SCHEME] = HTTPS_DOWNCASE
|
|
else
|
|
env[RACK_URL_SCHEME] = HTTP
|
|
end
|
|
env[RACK_HIJACK_P] = true
|
|
env[RACK_HIJACK] = lambda do
|
|
env[RACK_HIJACK_IO] ||= begin
|
|
connection.stop_simulating_eof!
|
|
connection
|
|
end
|
|
end
|
|
env[HTTP_VERSION] = HTTP_1_1
|
|
|
|
# Rails somehow modifies env['REQUEST_METHOD'], so we perform the comparison
|
|
# before the Rack application object is called.
|
|
is_head_request = env[REQUEST_METHOD] == HEAD
|
|
|
|
begin
|
|
status, headers, body = @app.call(env)
|
|
rescue => e
|
|
if e.is_a?(Errno::ENOBUFS)
|
|
raise e
|
|
end
|
|
if !should_swallow_app_error?(e, socket_wrapper)
|
|
# It's a good idea to catch application exceptions here because
|
|
# otherwise maliciously crafted responses can crash the app,
|
|
# forcing it to be respawned, and thereby effectively DoSing it.
|
|
print_exception("Rack application object", e)
|
|
end
|
|
return false
|
|
end
|
|
|
|
# Application requested a full socket hijack.
|
|
if env[RACK_HIJACK_IO]
|
|
# Since the app hijacked the socket, we don't know what state we're
|
|
# in and we don't know whether we can recover from it, so we don't
|
|
# catch any exception here.
|
|
#
|
|
# The Rack specification doesn't specify whether the body should
|
|
# be closed when the socket is hijacked. As of February 2 2015,
|
|
# Puma and Thin close the body, while Unicorn does not.
|
|
# However, Rack::Lock and possibly many other middlewares count
|
|
# on the body being closed, as described here:
|
|
# https://github.com/ngauthier/tubesock/issues/10#issuecomment-72539461
|
|
# So we have chosen to close the body.
|
|
body.close if body && body.respond_to?(:close)
|
|
return true
|
|
end
|
|
|
|
# Application requested a partial socket hijack.
|
|
if hijack_callback = headers[RACK_HIJACK]
|
|
# We don't catch exceptions here. EPIPE is already handled
|
|
# by ThreadHandler's #accept_and_process_next_request.
|
|
# On any other exception, we don't know what state we're
|
|
# in and we don't know whether we can recover from it.
|
|
begin
|
|
headers_output = generate_headers_array(status, headers)
|
|
headers_output << CONNECTION_CLOSE_CRLF2
|
|
connection.writev(headers_output)
|
|
connection.flush
|
|
hijacked_socket = env[RACK_HIJACK].call
|
|
hijack_callback.call(hijacked_socket)
|
|
return true
|
|
ensure
|
|
body.close if body && body.respond_to?(:close)
|
|
end
|
|
end
|
|
|
|
begin
|
|
process_body(env, connection, socket_wrapper, status.to_i, is_head_request,
|
|
headers, body)
|
|
rescue => e
|
|
if e.is_a?(Errno::ENOBUFS)
|
|
raise e
|
|
end
|
|
if !should_swallow_app_error?(e, socket_wrapper)
|
|
print_exception("Rack response body object", e)
|
|
end
|
|
ensure
|
|
close_body(body, env, socket_wrapper)
|
|
end
|
|
false
|
|
ensure
|
|
rewindable_input.close
|
|
end
|
|
end
|
|
|
|
private
|
|
def process_body(env, connection, socket_wrapper, status, is_head_request, headers, body)
|
|
if @ush_reporter
|
|
ush_log_id = @ush_reporter.log_writing_rack_body_begin
|
|
end
|
|
|
|
# Fix up incompliant body objects. Ensure that the body object
|
|
# can respond to #each.
|
|
output_body = should_output_body?(status, is_head_request)
|
|
if body.is_a?(String)
|
|
body = [body]
|
|
elsif body.nil?
|
|
body = []
|
|
elsif output_body && body.is_a?(Array)
|
|
# The body may be an ActionController::StringCoercion::UglyBody
|
|
# object instead of a real Array, even when #is_a? claims so.
|
|
# Call #to_a just to be sure that connection.writev() can
|
|
# accept the body object.
|
|
body = body.to_a
|
|
end
|
|
|
|
# Generate preliminary headers and determine whether we need to output a body.
|
|
headers_output = generate_headers_array(status, headers)
|
|
|
|
|
|
# Determine how big the body is, determine whether we should try to keep-alive
|
|
# the connection, and fix up the headers according to the situation.
|
|
#
|
|
# It may not be possible to determine the body's size (e.g. because it's streamed
|
|
# through #each). In that case we'll want to output the body with a chunked transfer
|
|
# encoding. But it matters whether the app has already chunked the body or not.
|
|
#
|
|
# Note that if the Rack response header contains "Transfer-Encoding: chunked",
|
|
# then we assume that the Rack body is already in chunked form. This is the way
|
|
# Rails streaming and Rack::Chunked::Body behave.
|
|
# The only gem that doesn't behave this way is JRuby-Rack (see
|
|
# https://blog.engineyard.com/2011/taking-stock-jruby-web-servers), but I'm not
|
|
# aware of anybody using JRuby-Rack these days.
|
|
#
|
|
# We only try to keep-alive the connection if we are able to determine ahead of
|
|
# time that the body we write out is guaranteed to match what the headers say.
|
|
# Otherwise we disable keep-alive to prevent the app from being able to mess
|
|
# up the keep-alive connection.
|
|
if header = lookup_header(headers, CONTENT_LENGTH_HEADERS)
|
|
# Easiest case: app has a Content-Length header. The headers
|
|
# need no fixing.
|
|
message_length_type = :content_length
|
|
content_length = header.to_i
|
|
if lookup_header(headers, TRANSFER_ENCODING_HEADERS)
|
|
# Disallowed by the HTTP spec
|
|
raise "Response object may not contain both Content-Length and Transfer-Encoding"
|
|
end
|
|
if output_body
|
|
if !body.is_a?(Array) || headers.has_key?(X_SENDFILE_HEADER) ||
|
|
headers.has_key?(X_ACCEL_REDIRECT_HEADER)
|
|
# If X-Sendfile or X-Accel-Redirect is set, don't check the
|
|
# body size. Passenger's Core Controller will ignore the
|
|
# body anyway. See
|
|
# ServerKit::HttpHeaderParser::processParseResult(const HttpParseResponse &)
|
|
@can_keepalive = false
|
|
else
|
|
body_size = 0
|
|
body.each { |part| body_size += bytesize(part) }
|
|
if body_size != content_length
|
|
raise "Response body size doesn't match Content-Length header: #{body_size} vs #{content_length}"
|
|
end
|
|
end
|
|
end
|
|
elsif lookup_header(headers, TRANSFER_ENCODING_HEADERS)
|
|
# App has a Transfer-Encoding header. We assume that the app
|
|
# has already chunked the body. The headers need no fixing.
|
|
message_length_type = :chunked_by_app
|
|
if output_body
|
|
# We have no way to determine whether the body was correct (save for
|
|
# parsing the chunking headers), so we don't keep-alive the connection
|
|
# just to be safe.
|
|
@can_keepalive = false
|
|
end
|
|
if lookup_header(headers, CONTENT_LENGTH_HEADERS)
|
|
# Disallowed by the HTTP spec
|
|
raise "Response object may not contain both Content-Length and Transfer-Encoding"
|
|
end
|
|
elsif status_code_allows_body?(status)
|
|
# This is a response for which a body is allowed, although the request
|
|
# may be one which does not expect a body (HEAD requests).
|
|
#
|
|
# The app has set neither the Content-Length nor the Transfer-Encoding
|
|
# header. This means we'll have to add one of those headers. We know exactly how
|
|
# big our body will be, so we can keep-alive the connection.
|
|
if body.is_a?(Array)
|
|
message_length_type = :content_length
|
|
content_length = 0
|
|
body.each { |part| content_length += bytesize(part.to_s) }
|
|
|
|
headers_output << CONTENT_LENGTH_HEADER_AND_SEPARATOR
|
|
headers_output << content_length.to_s
|
|
headers_output << CRLF
|
|
else
|
|
message_length_type = :needs_chunking
|
|
headers_output << TRANSFER_ENCODING_HEADER_AND_VALUE_CRLF
|
|
end
|
|
end
|
|
|
|
# Finalize headers data.
|
|
if @can_keepalive
|
|
headers_output << CRLF
|
|
else
|
|
headers_output << CONNECTION_CLOSE_CRLF2
|
|
end
|
|
|
|
|
|
# If this is a request without body, write out headers without body.
|
|
if !output_body
|
|
connection.writev(headers_output)
|
|
else
|
|
# Otherwise, write out headers and body.
|
|
case message_length_type
|
|
when :content_length
|
|
if body.is_a?(Array)
|
|
connection.writev2(headers_output, body)
|
|
else
|
|
connection.writev(headers_output)
|
|
body.each do |part|
|
|
connection.write(part.to_s)
|
|
end
|
|
end
|
|
when :chunked_by_app
|
|
connection.writev(headers_output)
|
|
body.each do |part|
|
|
connection.write(part.to_s)
|
|
end
|
|
when :needs_chunking
|
|
connection.writev(headers_output)
|
|
body.each do |part|
|
|
size = bytesize(part.to_s)
|
|
if size != 0
|
|
connection.writev(chunk_data(part.to_s, size))
|
|
end
|
|
end
|
|
connection.write(TERMINATION_CHUNK)
|
|
end
|
|
end
|
|
|
|
signal_keep_alive_allowed!
|
|
ensure
|
|
if @ush_reporter && ush_log_id
|
|
@ush_reporter.log_writing_rack_body_end(ush_log_id)
|
|
end
|
|
end
|
|
|
|
def close_body(body, env, socket_wrapper)
|
|
if @ush_reporter
|
|
ush_log_id = @ush_reporter.log_closing_rack_body_begin
|
|
end
|
|
begin
|
|
body.close if body && body.respond_to?(:close)
|
|
rescue => e
|
|
if e.is_a?(Errno::ENOBUFS)
|
|
raise e
|
|
end
|
|
if !should_swallow_app_error?(e, socket_wrapper)
|
|
print_exception("Rack response body object's #close method", e)
|
|
end
|
|
ensure
|
|
if @ush_reporter && ush_log_id
|
|
@ush_reporter.log_closing_rack_body_end(ush_log_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
def generate_headers_array(status, headers)
|
|
status_str = status.to_s
|
|
result = ["HTTP/1.1 #{status_str} Whatever\r\n"]
|
|
headers.each do |key, values|
|
|
if values.is_a?(String)
|
|
values = values.split(NEWLINE)
|
|
elsif values.is_a?(Array)
|
|
# values already array
|
|
elsif key == RACK_HIJACK
|
|
# We do not check for this key name in every loop
|
|
# iteration as an optimization.
|
|
next
|
|
else
|
|
values = values.to_s.split(NEWLINE)
|
|
end
|
|
values.each do |value|
|
|
result << key
|
|
result << NAME_VALUE_SEPARATOR
|
|
result << value
|
|
result << CRLF
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
def lookup_header(haystack, needles)
|
|
needles.each do |needle|
|
|
if result = haystack[needle]
|
|
return result
|
|
end
|
|
end
|
|
nil
|
|
end
|
|
|
|
def status_code_allows_body?(status)
|
|
status < 100 || (status >= 200 && status != 204 && status != 304)
|
|
end
|
|
|
|
def should_output_body?(status, is_head_request)
|
|
status_code_allows_body?(status) && !is_head_request
|
|
end
|
|
|
|
def chunk_data(data, size)
|
|
[size.to_s(16), CRLF, data, CRLF]
|
|
end
|
|
|
|
# Called when body is written out successfully. Indicates that we should
|
|
# keep-alive the connection if we can.
|
|
def signal_keep_alive_allowed!
|
|
@keepalive_performed = @can_keepalive
|
|
end
|
|
|
|
if "".respond_to?(:bytesize)
|
|
def bytesize(str)
|
|
str.bytesize
|
|
end
|
|
else
|
|
def bytesize(str)
|
|
str.size
|
|
end
|
|
end
|
|
end
|
|
|
|
end # module Rack
|
|
end # module PhusionPassenger
|