351 lines
13 KiB
Ruby
351 lines
13 KiB
Ruby
# 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 'constants'
|
|
PhusionPassenger.require_passenger_lib 'debug_logging'
|
|
PhusionPassenger.require_passenger_lib 'message_channel'
|
|
PhusionPassenger.require_passenger_lib 'utils'
|
|
PhusionPassenger.require_passenger_lib 'utils/native_support_utils'
|
|
PhusionPassenger.require_passenger_lib 'utils/unseekable_socket'
|
|
|
|
module PhusionPassenger
|
|
class RequestHandler
|
|
|
|
# This class encapsulates the logic of a single RequestHandler thread.
|
|
class ThreadHandler
|
|
include DebugLogging
|
|
include Utils
|
|
|
|
class Interrupted < StandardError
|
|
end
|
|
|
|
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
|
GET = 'GET'.freeze
|
|
PING = 'PING'.freeze
|
|
OOBW = 'OOBW'.freeze
|
|
PASSENGER_CONNECT_PASSWORD = 'PASSENGER_CONNECT_PASSWORD'.freeze
|
|
CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze
|
|
HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'.freeze
|
|
|
|
MAX_HEADER_SIZE = 128 * 1024
|
|
|
|
OBJECT_SPACE_SUPPORTS_LIVE_OBJECTS = ObjectSpace.respond_to?(:live_objects)
|
|
OBJECT_SPACE_SUPPORTS_ALLOCATED_OBJECTS = ObjectSpace.respond_to?(:allocated_objects)
|
|
OBJECT_SPACE_SUPPORTS_COUNT_OBJECTS = ObjectSpace.respond_to?(:count_objects)
|
|
GC_SUPPORTS_TIME = GC.respond_to?(:time)
|
|
GC_SUPPORTS_CLEAR_STATS = GC.respond_to?(:clear_stats)
|
|
|
|
attr_reader :thread
|
|
attr_reader :stats_mutex
|
|
attr_reader :interruptable
|
|
attr_reader :iteration
|
|
|
|
def initialize(request_handler, options = {})
|
|
@request_handler = request_handler
|
|
@server_socket = Utils.require_option(options, :server_socket)
|
|
@socket_name = Utils.require_option(options, :socket_name)
|
|
@protocol = Utils.require_option(options, :protocol)
|
|
@app_group_name = Utils.require_option(options, :app_group_name)
|
|
Utils.install_options_as_ivars(self, options,
|
|
:app,
|
|
:connect_password,
|
|
:keepalive_enabled
|
|
)
|
|
|
|
@stats_mutex = Mutex.new
|
|
@interruptable = false
|
|
@iteration = 0
|
|
|
|
if @protocol == :session
|
|
metaclass = class << self; self; end
|
|
metaclass.class_eval do
|
|
alias parse_request parse_session_request
|
|
end
|
|
elsif @protocol == :http
|
|
metaclass = class << self; self; end
|
|
metaclass.class_eval do
|
|
alias parse_request parse_http_request
|
|
end
|
|
else
|
|
raise ArgumentError, "Unknown protocol specified"
|
|
end
|
|
end
|
|
|
|
def install
|
|
@thread = Thread.current
|
|
Thread.current[:passenger_thread_handler] = self
|
|
PhusionPassenger.call_event(:starting_request_handler_thread)
|
|
end
|
|
|
|
def main_loop(finish_callback)
|
|
socket_wrapper = Utils::UnseekableSocket.new
|
|
channel = MessageChannel.new
|
|
buffer = ''
|
|
buffer.force_encoding('binary') if buffer.respond_to?(:force_encoding)
|
|
|
|
begin
|
|
finish_callback.call
|
|
while true
|
|
hijacked = accept_and_process_next_request(socket_wrapper, channel, buffer)
|
|
socket_wrapper = Utils::UnseekableSocket.new if hijacked
|
|
end
|
|
rescue Interrupted
|
|
# Do nothing.
|
|
end
|
|
debug("Thread handler main loop exited normally")
|
|
ensure
|
|
@stats_mutex.synchronize { @interruptable = true }
|
|
end
|
|
|
|
private
|
|
# Returns true if the socket has been hijacked, false otherwise.
|
|
def accept_and_process_next_request(socket_wrapper, channel, buffer)
|
|
@stats_mutex.synchronize do
|
|
@interruptable = true
|
|
end
|
|
if @last_connection
|
|
connection = @last_connection
|
|
channel.io = connection
|
|
@last_connection = nil
|
|
headers = parse_request(connection, channel, buffer)
|
|
else
|
|
connection = socket_wrapper.wrap(@server_socket.accept)
|
|
end
|
|
@stats_mutex.synchronize do
|
|
@interruptable = false
|
|
@iteration += 1
|
|
end
|
|
trace(3, "Accepted new request on socket #{@socket_name}")
|
|
if !headers
|
|
# New socket accepted, instead of keeping-alive an old one
|
|
channel.io = connection
|
|
headers = parse_request(connection, channel, buffer)
|
|
end
|
|
if headers
|
|
prepare_request(connection, headers)
|
|
begin
|
|
if headers[REQUEST_METHOD] == GET
|
|
process_request(headers, connection, socket_wrapper, @protocol == :http)
|
|
elsif headers[REQUEST_METHOD] == PING
|
|
process_ping(headers, connection)
|
|
false
|
|
elsif headers[REQUEST_METHOD] == OOBW
|
|
process_oobw(headers, connection)
|
|
false
|
|
else
|
|
process_request(headers, connection, socket_wrapper, @protocol == :http)
|
|
end
|
|
rescue Exception
|
|
has_error = true
|
|
raise
|
|
ensure
|
|
if headers[RACK_HIJACK_IO]
|
|
socket_wrapper = nil
|
|
connection = nil
|
|
channel = nil
|
|
end
|
|
finalize_request(connection, headers, has_error)
|
|
trace(3, "Request done.")
|
|
end
|
|
else
|
|
trace(2, "No headers parsed; disconnecting client.")
|
|
false
|
|
end
|
|
rescue Interrupted
|
|
raise
|
|
rescue => e
|
|
if socket_wrapper && socket_wrapper.source_of_exception?(e)
|
|
# EPIPE and ECONNRESET are harmless, it just means that the client closed the connection.
|
|
if !should_swallow_app_error?(e, socket_wrapper)
|
|
print_exception("Passenger RequestHandler's client socket", e)
|
|
end
|
|
else
|
|
# should_reraise_error? returns true except in unit tests,
|
|
# so we normally stop the request handler upon encountering
|
|
# non-EPIPE errors. We do this because process_request is already
|
|
# supposed to catch application-level exceptions. If an
|
|
# exception happened outside of process_request, then it's
|
|
# probably serious enough to warrant stopping the request handler.
|
|
raise e if should_reraise_error?(e)
|
|
end
|
|
# Here we do not know whether the connection was hijacked, but
|
|
# to be on the safe side (and have a new socket_wrapper created)
|
|
# let's say that it is.
|
|
true
|
|
ensure
|
|
# Close connection if keep-alive not possible
|
|
if connection && !connection.closed? && !@last_connection
|
|
# The 'close_write' here prevents forked child
|
|
# processes from unintentionally keeping the
|
|
# connection open.
|
|
begin
|
|
connection.close_write
|
|
rescue SystemCallError, IOError
|
|
end
|
|
begin
|
|
connection.close
|
|
rescue SystemCallError
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_session_request(connection, channel, buffer)
|
|
headers_data = channel.read_scalar(buffer, MAX_HEADER_SIZE)
|
|
if headers_data.nil?
|
|
return
|
|
end
|
|
headers = Utils::NativeSupportUtils.split_by_null_into_hash(headers_data)
|
|
if @connect_password && headers[PASSENGER_CONNECT_PASSWORD] != @connect_password
|
|
warn "*** Passenger RequestHandler warning: " <<
|
|
"someone tried to connect with an invalid connect password."
|
|
return
|
|
else
|
|
return headers
|
|
end
|
|
rescue SecurityError => e
|
|
warn("*** Passenger RequestHandler warning: " <<
|
|
"HTTP header size exceeded maximum.")
|
|
return
|
|
end
|
|
|
|
# Like parse_session_request, but parses an HTTP request. This is a very minimalistic
|
|
# HTTP parser and is not intended to be complete, fast or secure, since the HTTP server
|
|
# socket is intended to be used for debugging purposes only.
|
|
def parse_http_request(connection, channel, buffer)
|
|
headers = {}
|
|
|
|
data = ""
|
|
while data !~ /\r\n\r\n/ && data.size < MAX_HEADER_SIZE
|
|
data << connection.readpartial(16 * 1024)
|
|
end
|
|
if data.size >= MAX_HEADER_SIZE
|
|
warn("*** Passenger RequestHandler warning: " <<
|
|
"HTTP header size exceeded maximum.")
|
|
return
|
|
end
|
|
|
|
data.gsub!(/\r\n\r\n.*/, '')
|
|
data.split("\r\n").each_with_index do |line, i|
|
|
if i == 0
|
|
# GET / HTTP/1.1
|
|
line =~ /^([A-Za-z]+) (.+?) (HTTP\/\d\.\d)$/
|
|
request_method = $1
|
|
request_uri = $2
|
|
protocol = $3
|
|
if request_method.nil?
|
|
warn("*** Passenger RequestHandler warning: " <<
|
|
"Invalid HTTP request.")
|
|
return
|
|
end
|
|
path_info, query_string = request_uri.split("?", 2)
|
|
headers[REQUEST_METHOD] = request_method
|
|
headers["REQUEST_URI"] = request_uri
|
|
headers["QUERY_STRING"] = query_string || ""
|
|
headers["SCRIPT_NAME"] = ""
|
|
headers["PATH_INFO"] = path_info
|
|
headers["SERVER_NAME"] = "127.0.0.1"
|
|
headers["SERVER_PORT"] = connection.addr[1].to_s
|
|
headers["SERVER_PROTOCOL"] = protocol
|
|
headers["REMOTE_PORT"],
|
|
headers["REMOTE_ADDR"] = connection.to_io.is_a?(TCPSocket) ?
|
|
connection.to_io.peeraddr(:numeric)[1..2] :
|
|
[nil, '127.0.0.1']
|
|
|
|
else
|
|
header, value = line.split(/\s*:\s*/, 2)
|
|
header.upcase! # "Foo-Bar" => "FOO-BAR"
|
|
header.gsub!("-", "_") # => "FOO_BAR"
|
|
if header == CONTENT_LENGTH || header == "CONTENT_TYPE"
|
|
headers[header] = value
|
|
else
|
|
headers["HTTP_#{header}"] = value
|
|
end
|
|
end
|
|
end
|
|
|
|
if @connect_password && headers["HTTP_X_PASSENGER_CONNECT_PASSWORD"] != @connect_password
|
|
warn "*** Passenger RequestHandler warning: " <<
|
|
"someone tried to connect with an invalid connect password."
|
|
return
|
|
else
|
|
return headers
|
|
end
|
|
rescue EOFError
|
|
return
|
|
end
|
|
|
|
def process_ping(env, connection)
|
|
connection.write("pong")
|
|
end
|
|
|
|
def process_oobw(env, connection)
|
|
PhusionPassenger.call_event(:oob_work)
|
|
connection.write("oobw done")
|
|
end
|
|
|
|
# def process_request(env, connection, socket_wrapper, full_http_response)
|
|
# raise NotImplementedError, "Override with your own implementation!"
|
|
# end
|
|
|
|
def prepare_request(connection, headers)
|
|
transfer_encoding = headers[HTTP_TRANSFER_ENCODING]
|
|
content_length = headers[CONTENT_LENGTH]
|
|
@can_keepalive = @keepalive_enabled &&
|
|
!transfer_encoding &&
|
|
!content_length
|
|
@keepalive_performed = false
|
|
|
|
if !transfer_encoding && !content_length
|
|
connection.simulate_eof!
|
|
end
|
|
|
|
#################
|
|
end
|
|
|
|
def finalize_request(connection, headers, has_error)
|
|
if connection
|
|
connection.stop_simulating_eof!
|
|
end
|
|
|
|
if !has_error && @keepalive_performed && connection
|
|
trace(3, "Keep-aliving connection.")
|
|
@last_connection = connection
|
|
end
|
|
|
|
#################
|
|
end
|
|
|
|
def should_reraise_error?(e)
|
|
# Stubable by unit tests.
|
|
return true
|
|
end
|
|
|
|
def should_swallow_app_error?(e, socket_wrapper)
|
|
return socket_wrapper && socket_wrapper.source_of_exception?(e) && [Errno::EPIPE, Errno::ECONNRESET].any?{|er| e.is_a?(er)}
|
|
end
|
|
end
|
|
|
|
end # class RequestHandler
|
|
end # module PhusionPassenger
|