| ############################################################################## |
| # |
| # 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 .utilities import PROXY_HEADERS |
| from .compat import ( |
| PY2, |
| WIN, |
| string_types, |
| HAS_IPV6, |
| ) |
| |
| truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1')) |
| |
| KNOWN_PROXY_HEADERS = {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, string_types): |
| 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(object): |
| pass |
| |
| class Adjustments(object): |
| """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), |
| ('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), |
| ) |
| |
| _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 = ['{}:{}'.format(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 = 1 |
| |
| # 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 |
| |
| # send_bytes is the number of bytes to send to socket.send(). Multiples |
| # of 9000 should avoid partly-filled packets, but don't set this larger |
| # than the TCP write buffer size. In Linux, /proc/sys/net/ipv4/tcp_wmem |
| # controls the minimum, default, and maximum sizes of TCP write buffers. |
| send_bytes = 18000 |
| |
| # 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 |
| |
| # 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 = [] |
| |
| 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') |
| |
| 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 = ['{}:{}'.format(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 and PY2: # pragma: no cover |
| try: |
| # Try turning the port into an integer |
| port = int(port) |
| except: |
| 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: |
| raise ValueError('Invalid host/port specified.') |
| |
| if ( |
| self.trusted_proxy is None and |
| ( |
| self.trusted_proxy_headers or |
| (self.clear_untrusted_proxy_headers is not _bool_marker) |
| ) |
| ): |
| raise ValueError( |
| "The values trusted_proxy_headers and clear_untrusted_proxy_headers " |
| "have no meaning without setting trusted_proxy. Cowardly refusing to " |
| "continue." |
| ) |
| |
| 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.trusted_proxy and 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.') |