You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bazarr/libs/waitress/server.py

437 lines
14 KiB

##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
import os
import os.path
import socket
import time
from waitress import trigger
from waitress.adjustments import Adjustments
from waitress.channel import HTTPChannel
from waitress.task import ThreadedTaskDispatcher
from waitress.utilities import cleanup_unix_socket
from waitress.compat import (
IPPROTO_IPV6,
IPV6_V6ONLY,
)
from . import wasyncore
from .proxy_headers import proxy_headers_middleware
def create_server(
application,
map=None,
_start=True, # test shim
_sock=None, # test shim
_dispatcher=None, # test shim
**kw # adjustments
):
"""
if __name__ == '__main__':
server = create_server(app)
server.run()
"""
if application is None:
raise ValueError(
'The "app" passed to ``create_server`` was ``None``. You forgot '
"to return a WSGI app within your application."
)
adj = Adjustments(**kw)
if map is None: # pragma: nocover
map = {}
dispatcher = _dispatcher
if dispatcher is None:
dispatcher = ThreadedTaskDispatcher()
dispatcher.set_thread_count(adj.threads)
if adj.unix_socket and hasattr(socket, "AF_UNIX"):
sockinfo = (socket.AF_UNIX, socket.SOCK_STREAM, None, None)
return UnixWSGIServer(
application,
map,
_start,
_sock,
dispatcher=dispatcher,
adj=adj,
sockinfo=sockinfo,
)
effective_listen = []
last_serv = None
if not adj.sockets:
for sockinfo in adj.listen:
# When TcpWSGIServer is called, it registers itself in the map. This
# side-effect is all we need it for, so we don't store a reference to
# or return it to the user.
last_serv = TcpWSGIServer(
application,
map,
_start,
_sock,
dispatcher=dispatcher,
adj=adj,
sockinfo=sockinfo,
)
effective_listen.append(
(last_serv.effective_host, last_serv.effective_port)
)
for sock in adj.sockets:
sockinfo = (sock.family, sock.type, sock.proto, sock.getsockname())
if sock.family == socket.AF_INET or sock.family == socket.AF_INET6:
last_serv = TcpWSGIServer(
application,
map,
_start,
sock,
dispatcher=dispatcher,
adj=adj,
bind_socket=False,
sockinfo=sockinfo,
)
effective_listen.append(
(last_serv.effective_host, last_serv.effective_port)
)
elif hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX:
last_serv = UnixWSGIServer(
application,
map,
_start,
sock,
dispatcher=dispatcher,
adj=adj,
bind_socket=False,
sockinfo=sockinfo,
)
effective_listen.append(
(last_serv.effective_host, last_serv.effective_port)
)
# We are running a single server, so we can just return the last server,
# saves us from having to create one more object
if len(effective_listen) == 1:
# In this case we have no need to use a MultiSocketServer
return last_serv
# Return a class that has a utility function to print out the sockets it's
# listening on, and has a .run() function. All of the TcpWSGIServers
# registered themselves in the map above.
return MultiSocketServer(map, adj, effective_listen, dispatcher)
# This class is only ever used if we have multiple listen sockets. It allows
# the serve() API to call .run() which starts the wasyncore loop, and catches
# SystemExit/KeyboardInterrupt so that it can atempt to cleanly shut down.
class MultiSocketServer(object):
asyncore = wasyncore # test shim
def __init__(
self, map=None, adj=None, effective_listen=None, dispatcher=None,
):
self.adj = adj
self.map = map
self.effective_listen = effective_listen
self.task_dispatcher = dispatcher
def print_listen(self, format_str): # pragma: nocover
for l in self.effective_listen:
l = list(l)
if ":" in l[0]:
l[0] = "[{}]".format(l[0])
print(format_str.format(*l))
def run(self):
try:
self.asyncore.loop(
timeout=self.adj.asyncore_loop_timeout,
map=self.map,
use_poll=self.adj.asyncore_use_poll,
)
except (SystemExit, KeyboardInterrupt):
self.close()
def close(self):
self.task_dispatcher.shutdown()
wasyncore.close_all(self.map)
class BaseWSGIServer(wasyncore.dispatcher, object):
channel_class = HTTPChannel
next_channel_cleanup = 0
socketmod = socket # test shim
asyncore = wasyncore # test shim
def __init__(
self,
application,
map=None,
_start=True, # test shim
_sock=None, # test shim
dispatcher=None, # dispatcher
adj=None, # adjustments
sockinfo=None, # opaque object
bind_socket=True,
**kw
):
if adj is None:
adj = Adjustments(**kw)
if adj.trusted_proxy or adj.clear_untrusted_proxy_headers:
# wrap the application to deal with proxy headers
# we wrap it here because webtest subclasses the TcpWSGIServer
# directly and thus doesn't run any code that's in create_server
application = proxy_headers_middleware(
application,
trusted_proxy=adj.trusted_proxy,
trusted_proxy_count=adj.trusted_proxy_count,
trusted_proxy_headers=adj.trusted_proxy_headers,
clear_untrusted=adj.clear_untrusted_proxy_headers,
log_untrusted=adj.log_untrusted_proxy_headers,
logger=self.logger,
)
if map is None:
# use a nonglobal socket map by default to hopefully prevent
# conflicts with apps and libs that use the wasyncore global socket
# map ala https://github.com/Pylons/waitress/issues/63
map = {}
if sockinfo is None:
sockinfo = adj.listen[0]
self.sockinfo = sockinfo
self.family = sockinfo[0]
self.socktype = sockinfo[1]
self.application = application
self.adj = adj
self.trigger = trigger.trigger(map)
if dispatcher is None:
dispatcher = ThreadedTaskDispatcher()
dispatcher.set_thread_count(self.adj.threads)
self.task_dispatcher = dispatcher
self.asyncore.dispatcher.__init__(self, _sock, map=map)
if _sock is None:
self.create_socket(self.family, self.socktype)
if self.family == socket.AF_INET6: # pragma: nocover
self.socket.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1)
self.set_reuse_addr()
if bind_socket:
self.bind_server_socket()
self.effective_host, self.effective_port = self.getsockname()
self.server_name = self.get_server_name(self.effective_host)
self.active_channels = {}
if _start:
self.accept_connections()
def bind_server_socket(self):
raise NotImplementedError # pragma: no cover
def get_server_name(self, ip):
"""Given an IP or hostname, try to determine the server name."""
if not ip:
raise ValueError("Requires an IP to get the server name")
server_name = str(ip)
# If we are bound to all IP's, just return the current hostname, only
# fall-back to "localhost" if we fail to get the hostname
if server_name == "0.0.0.0" or server_name == "::":
try:
return str(self.socketmod.gethostname())
except (socket.error, UnicodeDecodeError): # pragma: no cover
# We also deal with UnicodeDecodeError in case of Windows with
# non-ascii hostname
return "localhost"
# Now let's try and convert the IP address to a proper hostname
try:
server_name = self.socketmod.gethostbyaddr(server_name)[0]
except (socket.error, UnicodeDecodeError): # pragma: no cover
# We also deal with UnicodeDecodeError in case of Windows with
# non-ascii hostname
pass
# If it contains an IPv6 literal, make sure to surround it with
# brackets
if ":" in server_name and "[" not in server_name:
server_name = "[{}]".format(server_name)
return server_name
def getsockname(self):
raise NotImplementedError # pragma: no cover
def accept_connections(self):
self.accepting = True
self.socket.listen(self.adj.backlog) # Get around asyncore NT limit
def add_task(self, task):
self.task_dispatcher.add_task(task)
def readable(self):
now = time.time()
if now >= self.next_channel_cleanup:
self.next_channel_cleanup = now + self.adj.cleanup_interval
self.maintenance(now)
return self.accepting and len(self._map) < self.adj.connection_limit
def writable(self):
return False
def handle_read(self):
pass
def handle_connect(self):
pass
def handle_accept(self):
try:
v = self.accept()
if v is None:
return
conn, addr = v
except socket.error:
# Linux: On rare occasions we get a bogus socket back from
# accept. socketmodule.c:makesockaddr complains that the
# address family is unknown. We don't want the whole server
# to shut down because of this.
if self.adj.log_socket_errors:
self.logger.warning("server accept() threw an exception", exc_info=True)
return
self.set_socket_options(conn)
addr = self.fix_addr(addr)
self.channel_class(self, conn, addr, self.adj, map=self._map)
def run(self):
try:
self.asyncore.loop(
timeout=self.adj.asyncore_loop_timeout,
map=self._map,
use_poll=self.adj.asyncore_use_poll,
)
except (SystemExit, KeyboardInterrupt):
self.task_dispatcher.shutdown()
def pull_trigger(self):
self.trigger.pull_trigger()
def set_socket_options(self, conn):
pass
def fix_addr(self, addr):
return addr
def maintenance(self, now):
"""
Closes channels that have not had any activity in a while.
The timeout is configured through adj.channel_timeout (seconds).
"""
cutoff = now - self.adj.channel_timeout
for channel in self.active_channels.values():
if (not channel.requests) and channel.last_activity < cutoff:
channel.will_close = True
def print_listen(self, format_str): # pragma: nocover
print(format_str.format(self.effective_host, self.effective_port))
def close(self):
self.trigger.close()
return wasyncore.dispatcher.close(self)
class TcpWSGIServer(BaseWSGIServer):
def bind_server_socket(self):
(_, _, _, sockaddr) = self.sockinfo
self.bind(sockaddr)
def getsockname(self):
try:
return self.socketmod.getnameinfo(
self.socket.getsockname(), self.socketmod.NI_NUMERICSERV
)
except: # pragma: no cover
# This only happens on Linux because a DNS issue is considered a
# temporary failure that will raise (even when NI_NAMEREQD is not
# set). Instead we try again, but this time we just ask for the
# numerichost and the numericserv (port) and return those. It is
# better than nothing.
return self.socketmod.getnameinfo(
self.socket.getsockname(),
self.socketmod.NI_NUMERICHOST | self.socketmod.NI_NUMERICSERV,
)
def set_socket_options(self, conn):
for (level, optname, value) in self.adj.socket_options:
conn.setsockopt(level, optname, value)
if hasattr(socket, "AF_UNIX"):
class UnixWSGIServer(BaseWSGIServer):
def __init__(
self,
application,
map=None,
_start=True, # test shim
_sock=None, # test shim
dispatcher=None, # dispatcher
adj=None, # adjustments
sockinfo=None, # opaque object
**kw
):
if sockinfo is None:
sockinfo = (socket.AF_UNIX, socket.SOCK_STREAM, None, None)
super(UnixWSGIServer, self).__init__(
application,
map=map,
_start=_start,
_sock=_sock,
dispatcher=dispatcher,
adj=adj,
sockinfo=sockinfo,
**kw
)
def bind_server_socket(self):
cleanup_unix_socket(self.adj.unix_socket)
self.bind(self.adj.unix_socket)
if os.path.exists(self.adj.unix_socket):
os.chmod(self.adj.unix_socket, self.adj.unix_socket_perms)
def getsockname(self):
return ("unix", self.socket.getsockname())
def fix_addr(self, addr):
return ("localhost", None)
def get_server_name(self, ip):
return "localhost"
# Compatibility alias.
WSGIServer = TcpWSGIServer