############################################################################## # # Copyright (c) 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. # ############################################################################## """Adjustments are tunable parameters. """ import getopt import socket import warnings from .compat import HAS_IPV6, WIN from .proxy_headers import PROXY_HEADERS truthy = frozenset(("t", "true", "y", "yes", "on", "1")) KNOWN_PROXY_HEADERS = frozenset( header.lower().replace("_", "-") for header in PROXY_HEADERS ) def asbool(s): """Return the boolean value ``True`` if the case-lowered value of string input ``s`` is any of ``t``, ``true``, ``y``, ``on``, or ``1``, otherwise return the boolean value ``False``. If ``s`` is the value ``None``, return ``False``. If ``s`` is already one of the boolean values ``True`` or ``False``, return it.""" if s is None: return False if isinstance(s, bool): return s s = str(s).strip() return s.lower() in truthy def asoctal(s): """Convert the given octal string to an actual number.""" return int(s, 8) def aslist_cronly(value): if isinstance(value, str): value = filter(None, [x.strip() for x in value.splitlines()]) return list(value) def aslist(value): """Return a list of strings, separating the input based on newlines and, if flatten=True (the default), also split on spaces within each line.""" values = aslist_cronly(value) result = [] for value in values: subvalues = value.split() result.extend(subvalues) return result def asset(value): return set(aslist(value)) def slash_fixed_str(s): s = s.strip() if s: # always have a leading slash, replace any number of leading slashes # with a single slash, and strip any trailing slashes s = "/" + s.lstrip("/").rstrip("/") return s def str_iftruthy(s): return str(s) if s else None def as_socket_list(sockets): """Checks if the elements in the list are of type socket and removes them if not.""" return [sock for sock in sockets if isinstance(sock, socket.socket)] class _str_marker(str): pass class _int_marker(int): pass class _bool_marker: pass class Adjustments: """This class contains tunable parameters.""" _params = ( ("host", str), ("port", int), ("ipv4", asbool), ("ipv6", asbool), ("listen", aslist), ("threads", int), ("trusted_proxy", str_iftruthy), ("trusted_proxy_count", int), ("trusted_proxy_headers", asset), ("log_untrusted_proxy_headers", asbool), ("clear_untrusted_proxy_headers", asbool), ("url_scheme", str), ("url_prefix", slash_fixed_str), ("backlog", int), ("recv_bytes", int), ("send_bytes", int), ("outbuf_overflow", int), ("outbuf_high_watermark", int), ("inbuf_overflow", int), ("connection_limit", int), ("cleanup_interval", int), ("channel_timeout", int), ("log_socket_errors", asbool), ("max_request_header_size", int), ("max_request_body_size", int), ("expose_tracebacks", asbool), ("ident", str_iftruthy), ("asyncore_loop_timeout", int), ("asyncore_use_poll", asbool), ("unix_socket", str), ("unix_socket_perms", asoctal), ("sockets", as_socket_list), ("channel_request_lookahead", int), ("server_name", str), ) _param_map = dict(_params) # hostname or IP address to listen on host = _str_marker("0.0.0.0") # TCP port to listen on port = _int_marker(8080) listen = [f"{host}:{port}"] # number of threads available for tasks threads = 4 # Host allowed to overrid ``wsgi.url_scheme`` via header trusted_proxy = None # How many proxies we trust when chained # # X-Forwarded-For: 192.0.2.1, "[2001:db8::1]" # # or # # Forwarded: for=192.0.2.1, For="[2001:db8::1]" # # means there were (potentially), two proxies involved. If we know there is # only 1 valid proxy, then that initial IP address "192.0.2.1" is not # trusted and we completely ignore it. If there are two trusted proxies in # the path, this value should be set to a higher number. trusted_proxy_count = None # Which of the proxy headers should we trust, this is a set where you # either specify forwarded or one or more of forwarded-host, forwarded-for, # forwarded-proto, forwarded-port. trusted_proxy_headers = set() # Would you like waitress to log warnings about untrusted proxy headers # that were encountered while processing the proxy headers? This only makes # sense to set when you have a trusted_proxy, and you expect the upstream # proxy server to filter invalid headers log_untrusted_proxy_headers = False # Should waitress clear any proxy headers that are not deemed trusted from # the environ? Change to True by default in 2.x clear_untrusted_proxy_headers = _bool_marker # default ``wsgi.url_scheme`` value url_scheme = "http" # default ``SCRIPT_NAME`` value, also helps reset ``PATH_INFO`` # when nonempty url_prefix = "" # server identity (sent in Server: header) ident = "waitress" # backlog is the value waitress passes to pass to socket.listen() This is # the maximum number of incoming TCP connections that will wait in an OS # queue for an available channel. From listen(1): "If a connection # request arrives when the queue is full, the client may receive an error # with an indication of ECONNREFUSED or, if the underlying protocol # supports retransmission, the request may be ignored so that a later # reattempt at connection succeeds." backlog = 1024 # recv_bytes is the argument to pass to socket.recv(). recv_bytes = 8192 # deprecated setting controls how many bytes will be buffered before # being flushed to the socket send_bytes = 1 # A tempfile should be created if the pending output is larger than # outbuf_overflow, which is measured in bytes. The default is 1MB. This # is conservative. outbuf_overflow = 1048576 # The app_iter will pause when pending output is larger than this value # in bytes. outbuf_high_watermark = 16777216 # A tempfile should be created if the pending input is larger than # inbuf_overflow, which is measured in bytes. The default is 512K. This # is conservative. inbuf_overflow = 524288 # Stop creating new channels if too many are already active (integer). # Each channel consumes at least one file descriptor, and, depending on # the input and output body sizes, potentially up to three. The default # is conservative, but you may need to increase the number of file # descriptors available to the Waitress process on most platforms in # order to safely change it (see ``ulimit -a`` "open files" setting). # Note that this doesn't control the maximum number of TCP connections # that can be waiting for processing; the ``backlog`` argument controls # that. connection_limit = 100 # Minimum seconds between cleaning up inactive channels. cleanup_interval = 30 # Maximum seconds to leave an inactive connection open. channel_timeout = 120 # Boolean: turn off to not log premature client disconnects. log_socket_errors = True # maximum number of bytes of all request headers combined (256K default) max_request_header_size = 262144 # maximum number of bytes in request body (1GB default) max_request_body_size = 1073741824 # expose tracebacks of uncaught exceptions expose_tracebacks = False # Path to a Unix domain socket to use. unix_socket = None # Path to a Unix domain socket to use. unix_socket_perms = 0o600 # The socket options to set on receiving a connection. It is a list of # (level, optname, value) tuples. TCP_NODELAY disables the Nagle # algorithm for writes (Waitress already buffers its writes). socket_options = [ (socket.SOL_TCP, socket.TCP_NODELAY, 1), ] # The asyncore.loop timeout value asyncore_loop_timeout = 1 # The asyncore.loop flag to use poll() instead of the default select(). asyncore_use_poll = False # Enable IPv4 by default ipv4 = True # Enable IPv6 by default ipv6 = True # A list of sockets that waitress will use to accept connections. They can # be used for e.g. socket activation sockets = [] # By setting this to a value larger than zero, each channel stays readable # and continues to read requests from the client even if a request is still # running, until the number of buffered requests exceeds this value. # This allows detecting if a client closed the connection while its request # is being processed. channel_request_lookahead = 0 # This setting controls the SERVER_NAME of the WSGI environment, this is # only ever used if the remote client sent a request without a Host header # (or when using the Proxy settings, without forwarding a Host header) server_name = "waitress.invalid" def __init__(self, **kw): if "listen" in kw and ("host" in kw or "port" in kw): raise ValueError("host or port may not be set if listen is set.") if "listen" in kw and "sockets" in kw: raise ValueError("socket may not be set if listen is set.") if "sockets" in kw and ("host" in kw or "port" in kw): raise ValueError("host or port may not be set if sockets is set.") if "sockets" in kw and "unix_socket" in kw: raise ValueError("unix_socket may not be set if sockets is set") if "unix_socket" in kw and ("host" in kw or "port" in kw): raise ValueError("unix_socket may not be set if host or port is set") if "unix_socket" in kw and "listen" in kw: raise ValueError("unix_socket may not be set if listen is set") if "send_bytes" in kw: warnings.warn( "send_bytes will be removed in a future release", DeprecationWarning ) for k, v in kw.items(): if k not in self._param_map: raise ValueError("Unknown adjustment %r" % k) setattr(self, k, self._param_map[k](v)) if not isinstance(self.host, _str_marker) or not isinstance( self.port, _int_marker ): self.listen = [f"{self.host}:{self.port}"] enabled_families = socket.AF_UNSPEC if not self.ipv4 and not HAS_IPV6: # pragma: no cover raise ValueError( "IPv4 is disabled but IPv6 is not available. Cowardly refusing to start." ) if self.ipv4 and not self.ipv6: enabled_families = socket.AF_INET if not self.ipv4 and self.ipv6 and HAS_IPV6: enabled_families = socket.AF_INET6 wanted_sockets = [] hp_pairs = [] for i in self.listen: if ":" in i: (host, port) = i.rsplit(":", 1) # IPv6 we need to make sure that we didn't split on the address if "]" in port: # pragma: nocover (host, port) = (i, str(self.port)) else: (host, port) = (i, str(self.port)) if WIN: # pragma: no cover try: # Try turning the port into an integer port = int(port) except Exception: raise ValueError( "Windows does not support service names instead of port numbers" ) try: if "[" in host and "]" in host: # pragma: nocover host = host.strip("[").rstrip("]") if host == "*": host = None for s in socket.getaddrinfo( host, port, enabled_families, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_PASSIVE, ): (family, socktype, proto, _, sockaddr) = s # It seems that getaddrinfo() may sometimes happily return # the same result multiple times, this of course makes # bind() very unhappy... # # Split on %, and drop the zone-index from the host in the # sockaddr. Works around a bug in OS X whereby # getaddrinfo() returns the same link-local interface with # two different zone-indices (which makes no sense what so # ever...) yet treats them equally when we attempt to bind(). if ( sockaddr[1] == 0 or (sockaddr[0].split("%", 1)[0], sockaddr[1]) not in hp_pairs ): wanted_sockets.append((family, socktype, proto, sockaddr)) hp_pairs.append((sockaddr[0].split("%", 1)[0], sockaddr[1])) except Exception: raise ValueError("Invalid host/port specified.") if self.trusted_proxy_count is not None and self.trusted_proxy is None: raise ValueError( "trusted_proxy_count has no meaning without setting " "trusted_proxy" ) elif self.trusted_proxy_count is None: self.trusted_proxy_count = 1 if self.trusted_proxy_headers and self.trusted_proxy is None: raise ValueError( "trusted_proxy_headers has no meaning without setting " "trusted_proxy" ) if self.trusted_proxy_headers: self.trusted_proxy_headers = { header.lower() for header in self.trusted_proxy_headers } unknown_values = self.trusted_proxy_headers - KNOWN_PROXY_HEADERS if unknown_values: raise ValueError( "Received unknown trusted_proxy_headers value (%s) expected one " "of %s" % (", ".join(unknown_values), ", ".join(KNOWN_PROXY_HEADERS)) ) if ( "forwarded" in self.trusted_proxy_headers and self.trusted_proxy_headers - {"forwarded"} ): raise ValueError( "The Forwarded proxy header and the " "X-Forwarded-{By,Host,Proto,Port,For} headers are mutually " "exclusive. Can't trust both!" ) elif self.trusted_proxy is not None: warnings.warn( "No proxy headers were marked as trusted, but trusted_proxy was set. " "Implicitly trusting X-Forwarded-Proto for backwards compatibility. " "This will be removed in future versions of waitress.", DeprecationWarning, ) self.trusted_proxy_headers = {"x-forwarded-proto"} if self.clear_untrusted_proxy_headers is _bool_marker: warnings.warn( "In future versions of Waitress clear_untrusted_proxy_headers will be " "set to True by default. You may opt-out by setting this value to " "False, or opt-in explicitly by setting this to True.", DeprecationWarning, ) self.clear_untrusted_proxy_headers = False self.listen = wanted_sockets self.check_sockets(self.sockets) @classmethod def parse_args(cls, argv): """Pre-parse command line arguments for input into __init__. Note that this does not cast values into adjustment types, it just creates a dictionary suitable for passing into __init__, where __init__ does the casting. """ long_opts = ["help", "call"] for opt, cast in cls._params: opt = opt.replace("_", "-") if cast is asbool: long_opts.append(opt) long_opts.append("no-" + opt) else: long_opts.append(opt + "=") kw = { "help": False, "call": False, } opts, args = getopt.getopt(argv, "", long_opts) for opt, value in opts: param = opt.lstrip("-").replace("-", "_") if param == "listen": kw["listen"] = "{} {}".format(kw.get("listen", ""), value) continue if param.startswith("no_"): param = param[3:] kw[param] = "false" elif param in ("help", "call"): kw[param] = True elif cls._param_map[param] is asbool: kw[param] = "true" else: kw[param] = value return kw, args @classmethod def check_sockets(cls, sockets): has_unix_socket = False has_inet_socket = False has_unsupported_socket = False for sock in sockets: if ( sock.family == socket.AF_INET or sock.family == socket.AF_INET6 ) and sock.type == socket.SOCK_STREAM: has_inet_socket = True elif ( hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX and sock.type == socket.SOCK_STREAM ): has_unix_socket = True else: has_unsupported_socket = True if has_unix_socket and has_inet_socket: raise ValueError("Internet and UNIX sockets may not be mixed.") if has_unsupported_socket: raise ValueError("Only Internet or UNIX stream sockets may be used.")