413 lines
12 KiB
Python
413 lines
12 KiB
Python
#!/usr/bin/env python
|
|
# 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.
|
|
|
|
import sys, os, threading, signal, traceback, socket, select, struct, logging, errno
|
|
import tempfile, json, time
|
|
if sys.version_info[0] >= 3 and sys.version_info[1] >= 5:
|
|
from importlib import util
|
|
else:
|
|
import imp
|
|
|
|
options = {}
|
|
|
|
def abort(message):
|
|
sys.stderr.write(message + "\n")
|
|
sys.exit(1)
|
|
|
|
def try_write_file(path, contents):
|
|
try:
|
|
with open(path, 'w') as f:
|
|
f.write(contents)
|
|
except IOError as e:
|
|
logging.warn('Warning: unable to write to ' + path + ': ' + e.strerror)
|
|
|
|
def initialize_logging():
|
|
logging.basicConfig(
|
|
level = logging.WARNING,
|
|
format = "[ pid=%(process)d, time=%(asctime)s ]: %(message)s")
|
|
if hasattr(logging, 'captureWarnings'):
|
|
logging.captureWarnings(True)
|
|
|
|
def read_startup_arguments():
|
|
global options
|
|
|
|
work_dir = os.getenv('PASSENGER_SPAWN_WORK_DIR')
|
|
assert work_dir is not None
|
|
path = work_dir + '/args.json'
|
|
with open(path, 'r') as f:
|
|
options = json.load(f)
|
|
|
|
def record_journey_step_begin(step, state):
|
|
work_dir = os.getenv('PASSENGER_SPAWN_WORK_DIR')
|
|
assert work_dir is not None
|
|
step_dir = work_dir + '/response/steps/' + step.lower()
|
|
try_write_file(step_dir + '/state', state)
|
|
try_write_file(step_dir + '/begin_time', str(time.time()))
|
|
|
|
def record_journey_step_end(step, state):
|
|
work_dir = os.getenv('PASSENGER_SPAWN_WORK_DIR')
|
|
assert work_dir is not None
|
|
step_dir = work_dir + '/response/steps/' + step.lower()
|
|
try_write_file(step_dir + '/state', state)
|
|
if not os.path.exists(step_dir + '/begin_time') and not os.path.exists(step_dir + '/begin_time_monotonic'):
|
|
try_write_file(step_dir + '/begin_time', str(time.time()))
|
|
try_write_file(step_dir + '/end_time', str(time.time()))
|
|
|
|
def load_app():
|
|
global options
|
|
|
|
sys.path.insert(0, os.getcwd())
|
|
startup_file = options.get('startup_file', 'passenger_wsgi.py')
|
|
if sys.version_info[0] >= 3 and sys.version_info[1] >= 5:
|
|
spec = util.spec_from_file_location("passenger_wsgi", startup_file)
|
|
assert spec is not None
|
|
app_module = util.module_from_spec(spec)
|
|
assert spec.loader is not None
|
|
spec.loader.exec_module(app_module)
|
|
return app_module
|
|
else:
|
|
return imp.load_source('passenger_wsgi', startup_file)
|
|
|
|
def create_server_socket():
|
|
global options
|
|
|
|
UNIX_PATH_MAX = int(options.get('UNIX_PATH_MAX', 100))
|
|
if 'socket_dir' in options:
|
|
socket_dir = options['socket_dir']
|
|
socket_prefix = 'wsgi'
|
|
else:
|
|
socket_dir = tempfile.gettempdir()
|
|
socket_prefix = 'PsgWsgiApp'
|
|
|
|
i = 0
|
|
while i < 128:
|
|
try:
|
|
return make_socket(socket_dir, socket_prefix, UNIX_PATH_MAX)
|
|
except socket.error as e:
|
|
if e.errno == errno.EADDRINUSE:
|
|
i += 1
|
|
if i == 128:
|
|
raise e
|
|
else:
|
|
raise e
|
|
|
|
def make_socket(socket_dir, socket_prefix, UNIX_PATH_MAX):
|
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
socket_suffix = format(struct.unpack('Q', os.urandom(8))[0], 'x')
|
|
filename = socket_dir + '/' + socket_prefix + '.' + socket_suffix
|
|
filename = filename[0:UNIX_PATH_MAX]
|
|
s.bind(filename)
|
|
s.listen(1000)
|
|
return (filename, s)
|
|
|
|
def install_signal_handlers():
|
|
def debug(sig, frame):
|
|
id2name = dict([(th.ident, th.name) for th in threading.enumerate()])
|
|
code = []
|
|
for thread_id, stack in sys._current_frames().items():
|
|
code.append("\n# Thread: %s(%d)" % (id2name.get(thread_id,""), thread_id))
|
|
for filename, lineno, name, line in traceback.extract_stack(stack):
|
|
code.append(' File: "%s", line %d, in %s' % (filename, lineno, name))
|
|
if line:
|
|
code.append(" %s" % (line.strip()))
|
|
print("\n".join(code))
|
|
|
|
def debug_and_exit(sig, frame):
|
|
debug(sig, frame)
|
|
sys.exit(1)
|
|
|
|
# Unfortunately, there's no way to install a signal handler that prints
|
|
# the backtrace without interrupting the current system call. os.siginterrupt()
|
|
# doesn't seem to work properly either. That is why we only have a SIGABRT
|
|
# handler and no SIGQUIT handler.
|
|
signal.signal(signal.SIGABRT, debug_and_exit)
|
|
|
|
def advertise_sockets(socket_filename):
|
|
work_dir = os.getenv('PASSENGER_SPAWN_WORK_DIR')
|
|
assert work_dir is not None
|
|
path = work_dir + '/response/properties.json'
|
|
doc = {
|
|
'sockets': [
|
|
{
|
|
'name': 'main',
|
|
'address': 'unix:' + socket_filename,
|
|
'protocol': 'session',
|
|
'concurrency': 1,
|
|
'accept_http_requests': True
|
|
}
|
|
]
|
|
}
|
|
with open(path, 'w') as f:
|
|
json.dump(doc, f)
|
|
|
|
def advertise_readiness():
|
|
work_dir = os.getenv('PASSENGER_SPAWN_WORK_DIR')
|
|
assert work_dir is not None
|
|
path = work_dir + '/response/finish'
|
|
with open(path, 'w') as f:
|
|
f.write('1')
|
|
|
|
if sys.version_info[0] >= 3:
|
|
def reraise_exception(exc_info):
|
|
raise exc_info[0].with_traceback(exc_info[1], exc_info[2])
|
|
|
|
def bytes_to_str(b):
|
|
return b.decode('latin-1')
|
|
|
|
def str_to_bytes(s):
|
|
if isinstance(s, bytes):
|
|
return s
|
|
else:
|
|
return s.encode('latin-1')
|
|
else:
|
|
def reraise_exception(exc_info):
|
|
exec("raise exc_info[0], exc_info[1], exc_info[2]")
|
|
|
|
def bytes_to_str(b):
|
|
return b
|
|
|
|
def str_to_bytes(s):
|
|
return s
|
|
|
|
class RequestHandler:
|
|
def __init__(self, server_socket, owner_pipe, app):
|
|
self.server = server_socket
|
|
self.owner_pipe = owner_pipe
|
|
self.app = app
|
|
|
|
def main_loop(self):
|
|
done = False
|
|
try:
|
|
while not done:
|
|
client, address = self.accept_connection()
|
|
if not client:
|
|
done = True
|
|
break
|
|
socket_hijacked = False
|
|
try:
|
|
try:
|
|
env, input_stream = self.parse_request(client)
|
|
if env:
|
|
if env['REQUEST_METHOD'] == 'ping':
|
|
self.process_ping(client)
|
|
else:
|
|
socket_hijacked = self.process_request(env, input_stream, client)
|
|
except KeyboardInterrupt:
|
|
done = True
|
|
except IOError as e:
|
|
if not getattr(e, 'passenger', False) or e.errno != errno.EPIPE:
|
|
logging.exception("WSGI application raised an I/O exception!")
|
|
except Exception:
|
|
logging.exception("WSGI application raised an exception!")
|
|
finally:
|
|
if not socket_hijacked:
|
|
try:
|
|
# Shutdown the socket like this just in case the app
|
|
# spawned a child process that keeps it open.
|
|
client.shutdown(socket.SHUT_WR)
|
|
except:
|
|
pass
|
|
try:
|
|
client.close()
|
|
except:
|
|
pass
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
def accept_connection(self):
|
|
result = select.select([self.owner_pipe, self.server.fileno()], [], [])[0]
|
|
if self.server.fileno() in result:
|
|
return self.server.accept()
|
|
else:
|
|
return (None, None)
|
|
|
|
def parse_request(self, client):
|
|
buf = b''
|
|
while len(buf) < 4:
|
|
tmp = client.recv(4 - len(buf))
|
|
if len(tmp) == 0:
|
|
return (None, None)
|
|
buf += tmp
|
|
header_size = struct.unpack('>I', buf)[0]
|
|
|
|
buf = b''
|
|
while len(buf) < header_size:
|
|
tmp = client.recv(header_size - len(buf))
|
|
if len(tmp) == 0:
|
|
return (None, None)
|
|
buf += tmp
|
|
|
|
headers = buf.split(b"\0")
|
|
headers.pop() # Remove trailing "\0"
|
|
env = {}
|
|
i = 0
|
|
while i < len(headers):
|
|
env[bytes_to_str(headers[i])] = bytes_to_str(headers[i + 1])
|
|
i += 2
|
|
|
|
return (env, client)
|
|
|
|
if hasattr(socket, '_fileobject'):
|
|
def wrap_input_socket(self, sock):
|
|
return socket._fileobject(sock, 'rb', 512)
|
|
else:
|
|
def wrap_input_socket(self, sock):
|
|
return socket.socket.makefile(sock, 'rb', 512)
|
|
|
|
def process_request(self, env, input_stream, output_stream):
|
|
# The WSGI specification says that the input parameter object passed needs to
|
|
# implement a few file-like methods. This is the reason why we "wrap" the socket._socket
|
|
# into the _fileobject to solve this.
|
|
#
|
|
# Otherwise, the POST data won't be correctly retrieved by Django.
|
|
#
|
|
# See: http://www.python.org/dev/peps/pep-0333/#input-and-error-streams
|
|
env['wsgi.input'] = self.wrap_input_socket(input_stream)
|
|
env['wsgi.errors'] = sys.stderr
|
|
env['wsgi.version'] = (1, 0)
|
|
env['wsgi.multithread'] = False
|
|
env['wsgi.multiprocess'] = True
|
|
env['wsgi.run_once'] = False
|
|
if env.get('HTTPS','off') in ('on', '1', 'true', 'yes'):
|
|
env['wsgi.url_scheme'] = 'https'
|
|
else:
|
|
env['wsgi.url_scheme'] = 'http'
|
|
|
|
headers_set = []
|
|
headers_sent = []
|
|
is_head = env['REQUEST_METHOD'] == 'HEAD'
|
|
|
|
def write(data):
|
|
try:
|
|
if not headers_set:
|
|
raise AssertionError("write() before start_response()")
|
|
elif not headers_sent:
|
|
# Before the first output, send the stored headers.
|
|
status, response_headers = headers_sent[:] = headers_set
|
|
output_stream.sendall(str_to_bytes(
|
|
'HTTP/1.1 %s\r\nStatus: %s\r\nConnection: close\r\n' %
|
|
(status, status)))
|
|
for header in response_headers:
|
|
output_stream.sendall(str_to_bytes('%s: %s\r\n' % header))
|
|
output_stream.sendall(b'\r\n')
|
|
if not is_head:
|
|
output_stream.sendall(str_to_bytes(data))
|
|
except IOError as e:
|
|
# Mark this exception as coming from the Phusion Passenger
|
|
# socket and not some other socket.
|
|
setattr(e, 'passenger', True)
|
|
raise e
|
|
|
|
def start_response(status, response_headers, exc_info = None):
|
|
if exc_info:
|
|
try:
|
|
if headers_sent:
|
|
# Re-raise original exception if headers sent.
|
|
reraise_exception(exc_info)
|
|
finally:
|
|
# Avoid dangling circular ref.
|
|
exc_info = None
|
|
elif headers_set:
|
|
raise AssertionError("Headers already set!")
|
|
|
|
headers_set[:] = [status, response_headers]
|
|
return write
|
|
|
|
# Django's django.template.base module goes through all WSGI
|
|
# environment values, and calls each value that is a callable.
|
|
# No idea why, but we work around that with the `do_it` parameter.
|
|
def hijack(do_it = False):
|
|
if do_it:
|
|
env['passenger.hijacked_socket'] = output_stream
|
|
return output_stream
|
|
|
|
env['passenger.hijack'] = hijack
|
|
|
|
result = self.app(env, start_response)
|
|
if 'passenger.hijacked_socket' in env:
|
|
# Socket connection hijacked. Don't do anything.
|
|
return True
|
|
try:
|
|
for data in result:
|
|
# Don't send headers until body appears.
|
|
if data:
|
|
write(data)
|
|
if not headers_sent:
|
|
# Send headers now if body was empty.
|
|
write(b'')
|
|
finally:
|
|
if hasattr(result, 'close'):
|
|
result.close()
|
|
return False
|
|
|
|
def process_ping(self, output_stream):
|
|
output_stream.sendall(b"pong")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
initialize_logging()
|
|
record_journey_step_end('SUBPROCESS_EXEC_WRAPPER', 'STEP_PERFORMED')
|
|
record_journey_step_begin('SUBPROCESS_WRAPPER_PREPARATION', 'STEP_IN_PROGRESS')
|
|
try:
|
|
read_startup_arguments()
|
|
except Exception:
|
|
record_journey_step_end('SUBPROCESS_WRAPPER_PREPARATION', 'STEP_ERRORED')
|
|
raise
|
|
else:
|
|
record_journey_step_end('SUBPROCESS_WRAPPER_PREPARATION', 'STEP_PERFORMED')
|
|
|
|
|
|
record_journey_step_begin('SUBPROCESS_APP_LOAD_OR_EXEC', 'STEP_IN_PROGRESS')
|
|
try:
|
|
app_module = load_app()
|
|
except Exception:
|
|
record_journey_step_end('SUBPROCESS_APP_LOAD_OR_EXEC', 'STEP_ERRORED')
|
|
raise
|
|
else:
|
|
record_journey_step_end('SUBPROCESS_APP_LOAD_OR_EXEC', 'STEP_PERFORMED')
|
|
|
|
|
|
record_journey_step_begin('SUBPROCESS_LISTEN', 'STEP_IN_PROGRESS')
|
|
try:
|
|
tuple = create_server_socket()
|
|
assert tuple is not None
|
|
socket_filename, server_socket = tuple
|
|
install_signal_handlers()
|
|
handler = RequestHandler(server_socket, sys.stdin, app_module.application)
|
|
advertise_sockets(socket_filename)
|
|
except Exception:
|
|
record_journey_step_end('SUBPROCESS_LISTEN', 'STEP_ERRORED')
|
|
raise
|
|
else:
|
|
record_journey_step_end('SUBPROCESS_LISTEN', 'STEP_PERFORMED')
|
|
|
|
|
|
advertise_readiness()
|
|
handler.main_loop()
|
|
try:
|
|
os.remove(socket_filename)
|
|
except OSError:
|
|
pass
|