diff options
Diffstat (limited to 'waitress/adjustments.py')
-rw-r--r-- | waitress/adjustments.py | 134 |
1 files changed, 128 insertions, 6 deletions
diff --git a/waitress/adjustments.py b/waitress/adjustments.py index d5b237b..1a56621 100644 --- a/waitress/adjustments.py +++ b/waitress/adjustments.py @@ -15,7 +15,13 @@ """ import getopt import socket -import sys + +from waitress.compat import ( + PY2, + WIN, + string_types, + HAS_IPV6, + ) truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1')) @@ -36,6 +42,22 @@ 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 slash_fixed_str(s): s = s.strip() if s: @@ -44,6 +66,12 @@ def slash_fixed_str(s): s = '/' + s.lstrip('/').rstrip('/') return s +class _str_marker(str): + pass + +class _int_marker(int): + pass + class Adjustments(object): """This class contains tunable parameters. """ @@ -51,6 +79,9 @@ class Adjustments(object): _params = ( ('host', str), ('port', int), + ('ipv4', asbool), + ('ipv6', asbool), + ('listen', aslist), ('threads', int), ('trusted_proxy', str), ('url_scheme', str), @@ -77,10 +108,12 @@ class Adjustments(object): _param_map = dict(_params) # hostname or IP address to listen on - host = '0.0.0.0' + host = _str_marker('0.0.0.0') # TCP port to listen on - port = 8080 + port = _int_marker(8080) + + listen = ['{}:{}'.format(host, port)] # mumber of threads available for tasks threads = 4 @@ -174,14 +207,96 @@ class Adjustments(object): # 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 + def __init__(self, **kw): + + if 'listen' in kw and ('host' in kw or 'port' in kw): + raise ValueError('host and or port 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 (sys.platform[:3] == "win" and - self.host == 'localhost'): # pragma: no cover - self.host = '' + + 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.') + + self.listen = wanted_sockets @classmethod def parse_args(cls, argv): @@ -203,9 +318,15 @@ class Adjustments(object): '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' @@ -215,4 +336,5 @@ class Adjustments(object): kw[param] = 'true' else: kw[param] = value + return kw, args |