123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- ##############################################################################
- #
- # 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.")
|