1############################################################################## 2# 3# Copyright (c) 2002 Zope Foundation and Contributors. 4# All Rights Reserved. 5# 6# This software is subject to the provisions of the Zope Public License, 7# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11# FOR A PARTICULAR PURPOSE. 12# 13############################################################################## 14"""Adjustments are tunable parameters. 15""" 16import getopt 17import socket 18import warnings 19 20from .compat import HAS_IPV6, WIN 21from .proxy_headers import PROXY_HEADERS 22 23truthy = frozenset(("t", "true", "y", "yes", "on", "1")) 24 25KNOWN_PROXY_HEADERS = frozenset( 26 header.lower().replace("_", "-") for header in PROXY_HEADERS 27) 28 29 30def asbool(s): 31 """Return the boolean value ``True`` if the case-lowered value of string 32 input ``s`` is any of ``t``, ``true``, ``y``, ``on``, or ``1``, otherwise 33 return the boolean value ``False``. If ``s`` is the value ``None``, 34 return ``False``. If ``s`` is already one of the boolean values ``True`` 35 or ``False``, return it.""" 36 if s is None: 37 return False 38 if isinstance(s, bool): 39 return s 40 s = str(s).strip() 41 return s.lower() in truthy 42 43 44def asoctal(s): 45 """Convert the given octal string to an actual number.""" 46 return int(s, 8) 47 48 49def aslist_cronly(value): 50 if isinstance(value, str): 51 value = filter(None, [x.strip() for x in value.splitlines()]) 52 return list(value) 53 54 55def aslist(value): 56 """Return a list of strings, separating the input based on newlines 57 and, if flatten=True (the default), also split on spaces within 58 each line.""" 59 values = aslist_cronly(value) 60 result = [] 61 for value in values: 62 subvalues = value.split() 63 result.extend(subvalues) 64 return result 65 66 67def asset(value): 68 return set(aslist(value)) 69 70 71def slash_fixed_str(s): 72 s = s.strip() 73 if s: 74 # always have a leading slash, replace any number of leading slashes 75 # with a single slash, and strip any trailing slashes 76 s = "/" + s.lstrip("/").rstrip("/") 77 return s 78 79 80def str_iftruthy(s): 81 return str(s) if s else None 82 83 84def as_socket_list(sockets): 85 """Checks if the elements in the list are of type socket and 86 removes them if not.""" 87 return [sock for sock in sockets if isinstance(sock, socket.socket)] 88 89 90class _str_marker(str): 91 pass 92 93 94class _int_marker(int): 95 pass 96 97 98class _bool_marker: 99 pass 100 101 102class Adjustments: 103 """This class contains tunable parameters.""" 104 105 _params = ( 106 ("host", str), 107 ("port", int), 108 ("ipv4", asbool), 109 ("ipv6", asbool), 110 ("listen", aslist), 111 ("threads", int), 112 ("trusted_proxy", str_iftruthy), 113 ("trusted_proxy_count", int), 114 ("trusted_proxy_headers", asset), 115 ("log_untrusted_proxy_headers", asbool), 116 ("clear_untrusted_proxy_headers", asbool), 117 ("url_scheme", str), 118 ("url_prefix", slash_fixed_str), 119 ("backlog", int), 120 ("recv_bytes", int), 121 ("send_bytes", int), 122 ("outbuf_overflow", int), 123 ("outbuf_high_watermark", int), 124 ("inbuf_overflow", int), 125 ("connection_limit", int), 126 ("cleanup_interval", int), 127 ("channel_timeout", int), 128 ("log_socket_errors", asbool), 129 ("max_request_header_size", int), 130 ("max_request_body_size", int), 131 ("expose_tracebacks", asbool), 132 ("ident", str_iftruthy), 133 ("asyncore_loop_timeout", int), 134 ("asyncore_use_poll", asbool), 135 ("unix_socket", str), 136 ("unix_socket_perms", asoctal), 137 ("sockets", as_socket_list), 138 ("channel_request_lookahead", int), 139 ("server_name", str), 140 ) 141 142 _param_map = dict(_params) 143 144 # hostname or IP address to listen on 145 host = _str_marker("0.0.0.0") 146 147 # TCP port to listen on 148 port = _int_marker(8080) 149 150 listen = ["{}:{}".format(host, port)] 151 152 # number of threads available for tasks 153 threads = 4 154 155 # Host allowed to overrid ``wsgi.url_scheme`` via header 156 trusted_proxy = None 157 158 # How many proxies we trust when chained 159 # 160 # X-Forwarded-For: 192.0.2.1, "[2001:db8::1]" 161 # 162 # or 163 # 164 # Forwarded: for=192.0.2.1, For="[2001:db8::1]" 165 # 166 # means there were (potentially), two proxies involved. If we know there is 167 # only 1 valid proxy, then that initial IP address "192.0.2.1" is not 168 # trusted and we completely ignore it. If there are two trusted proxies in 169 # the path, this value should be set to a higher number. 170 trusted_proxy_count = None 171 172 # Which of the proxy headers should we trust, this is a set where you 173 # either specify forwarded or one or more of forwarded-host, forwarded-for, 174 # forwarded-proto, forwarded-port. 175 trusted_proxy_headers = set() 176 177 # Would you like waitress to log warnings about untrusted proxy headers 178 # that were encountered while processing the proxy headers? This only makes 179 # sense to set when you have a trusted_proxy, and you expect the upstream 180 # proxy server to filter invalid headers 181 log_untrusted_proxy_headers = False 182 183 # Should waitress clear any proxy headers that are not deemed trusted from 184 # the environ? Change to True by default in 2.x 185 clear_untrusted_proxy_headers = _bool_marker 186 187 # default ``wsgi.url_scheme`` value 188 url_scheme = "http" 189 190 # default ``SCRIPT_NAME`` value, also helps reset ``PATH_INFO`` 191 # when nonempty 192 url_prefix = "" 193 194 # server identity (sent in Server: header) 195 ident = "waitress" 196 197 # backlog is the value waitress passes to pass to socket.listen() This is 198 # the maximum number of incoming TCP connections that will wait in an OS 199 # queue for an available channel. From listen(1): "If a connection 200 # request arrives when the queue is full, the client may receive an error 201 # with an indication of ECONNREFUSED or, if the underlying protocol 202 # supports retransmission, the request may be ignored so that a later 203 # reattempt at connection succeeds." 204 backlog = 1024 205 206 # recv_bytes is the argument to pass to socket.recv(). 207 recv_bytes = 8192 208 209 # deprecated setting controls how many bytes will be buffered before 210 # being flushed to the socket 211 send_bytes = 1 212 213 # A tempfile should be created if the pending output is larger than 214 # outbuf_overflow, which is measured in bytes. The default is 1MB. This 215 # is conservative. 216 outbuf_overflow = 1048576 217 218 # The app_iter will pause when pending output is larger than this value 219 # in bytes. 220 outbuf_high_watermark = 16777216 221 222 # A tempfile should be created if the pending input is larger than 223 # inbuf_overflow, which is measured in bytes. The default is 512K. This 224 # is conservative. 225 inbuf_overflow = 524288 226 227 # Stop creating new channels if too many are already active (integer). 228 # Each channel consumes at least one file descriptor, and, depending on 229 # the input and output body sizes, potentially up to three. The default 230 # is conservative, but you may need to increase the number of file 231 # descriptors available to the Waitress process on most platforms in 232 # order to safely change it (see ``ulimit -a`` "open files" setting). 233 # Note that this doesn't control the maximum number of TCP connections 234 # that can be waiting for processing; the ``backlog`` argument controls 235 # that. 236 connection_limit = 100 237 238 # Minimum seconds between cleaning up inactive channels. 239 cleanup_interval = 30 240 241 # Maximum seconds to leave an inactive connection open. 242 channel_timeout = 120 243 244 # Boolean: turn off to not log premature client disconnects. 245 log_socket_errors = True 246 247 # maximum number of bytes of all request headers combined (256K default) 248 max_request_header_size = 262144 249 250 # maximum number of bytes in request body (1GB default) 251 max_request_body_size = 1073741824 252 253 # expose tracebacks of uncaught exceptions 254 expose_tracebacks = False 255 256 # Path to a Unix domain socket to use. 257 unix_socket = None 258 259 # Path to a Unix domain socket to use. 260 unix_socket_perms = 0o600 261 262 # The socket options to set on receiving a connection. It is a list of 263 # (level, optname, value) tuples. TCP_NODELAY disables the Nagle 264 # algorithm for writes (Waitress already buffers its writes). 265 socket_options = [ 266 (socket.SOL_TCP, socket.TCP_NODELAY, 1), 267 ] 268 269 # The asyncore.loop timeout value 270 asyncore_loop_timeout = 1 271 272 # The asyncore.loop flag to use poll() instead of the default select(). 273 asyncore_use_poll = False 274 275 # Enable IPv4 by default 276 ipv4 = True 277 278 # Enable IPv6 by default 279 ipv6 = True 280 281 # A list of sockets that waitress will use to accept connections. They can 282 # be used for e.g. socket activation 283 sockets = [] 284 285 # By setting this to a value larger than zero, each channel stays readable 286 # and continues to read requests from the client even if a request is still 287 # running, until the number of buffered requests exceeds this value. 288 # This allows detecting if a client closed the connection while its request 289 # is being processed. 290 channel_request_lookahead = 0 291 292 # This setting controls the SERVER_NAME of the WSGI environment, this is 293 # only ever used if the remote client sent a request without a Host header 294 # (or when using the Proxy settings, without forwarding a Host header) 295 server_name = "waitress.invalid" 296 297 def __init__(self, **kw): 298 299 if "listen" in kw and ("host" in kw or "port" in kw): 300 raise ValueError("host or port may not be set if listen is set.") 301 302 if "listen" in kw and "sockets" in kw: 303 raise ValueError("socket may not be set if listen is set.") 304 305 if "sockets" in kw and ("host" in kw or "port" in kw): 306 raise ValueError("host or port may not be set if sockets is set.") 307 308 if "sockets" in kw and "unix_socket" in kw: 309 raise ValueError("unix_socket may not be set if sockets is set") 310 311 if "unix_socket" in kw and ("host" in kw or "port" in kw): 312 raise ValueError("unix_socket may not be set if host or port is set") 313 314 if "unix_socket" in kw and "listen" in kw: 315 raise ValueError("unix_socket may not be set if listen is set") 316 317 if "send_bytes" in kw: 318 warnings.warn( 319 "send_bytes will be removed in a future release", DeprecationWarning 320 ) 321 322 for k, v in kw.items(): 323 if k not in self._param_map: 324 raise ValueError("Unknown adjustment %r" % k) 325 setattr(self, k, self._param_map[k](v)) 326 327 if not isinstance(self.host, _str_marker) or not isinstance( 328 self.port, _int_marker 329 ): 330 self.listen = ["{}:{}".format(self.host, self.port)] 331 332 enabled_families = socket.AF_UNSPEC 333 334 if not self.ipv4 and not HAS_IPV6: # pragma: no cover 335 raise ValueError( 336 "IPv4 is disabled but IPv6 is not available. Cowardly refusing to start." 337 ) 338 339 if self.ipv4 and not self.ipv6: 340 enabled_families = socket.AF_INET 341 342 if not self.ipv4 and self.ipv6 and HAS_IPV6: 343 enabled_families = socket.AF_INET6 344 345 wanted_sockets = [] 346 hp_pairs = [] 347 for i in self.listen: 348 if ":" in i: 349 (host, port) = i.rsplit(":", 1) 350 351 # IPv6 we need to make sure that we didn't split on the address 352 if "]" in port: # pragma: nocover 353 (host, port) = (i, str(self.port)) 354 else: 355 (host, port) = (i, str(self.port)) 356 357 if WIN: # pragma: no cover 358 try: 359 # Try turning the port into an integer 360 port = int(port) 361 362 except Exception: 363 raise ValueError( 364 "Windows does not support service names instead of port numbers" 365 ) 366 367 try: 368 if "[" in host and "]" in host: # pragma: nocover 369 host = host.strip("[").rstrip("]") 370 371 if host == "*": 372 host = None 373 374 for s in socket.getaddrinfo( 375 host, 376 port, 377 enabled_families, 378 socket.SOCK_STREAM, 379 socket.IPPROTO_TCP, 380 socket.AI_PASSIVE, 381 ): 382 (family, socktype, proto, _, sockaddr) = s 383 384 # It seems that getaddrinfo() may sometimes happily return 385 # the same result multiple times, this of course makes 386 # bind() very unhappy... 387 # 388 # Split on %, and drop the zone-index from the host in the 389 # sockaddr. Works around a bug in OS X whereby 390 # getaddrinfo() returns the same link-local interface with 391 # two different zone-indices (which makes no sense what so 392 # ever...) yet treats them equally when we attempt to bind(). 393 if ( 394 sockaddr[1] == 0 395 or (sockaddr[0].split("%", 1)[0], sockaddr[1]) not in hp_pairs 396 ): 397 wanted_sockets.append((family, socktype, proto, sockaddr)) 398 hp_pairs.append((sockaddr[0].split("%", 1)[0], sockaddr[1])) 399 400 except Exception: 401 raise ValueError("Invalid host/port specified.") 402 403 if self.trusted_proxy_count is not None and self.trusted_proxy is None: 404 raise ValueError( 405 "trusted_proxy_count has no meaning without setting " "trusted_proxy" 406 ) 407 408 elif self.trusted_proxy_count is None: 409 self.trusted_proxy_count = 1 410 411 if self.trusted_proxy_headers and self.trusted_proxy is None: 412 raise ValueError( 413 "trusted_proxy_headers has no meaning without setting " "trusted_proxy" 414 ) 415 416 if self.trusted_proxy_headers: 417 self.trusted_proxy_headers = { 418 header.lower() for header in self.trusted_proxy_headers 419 } 420 421 unknown_values = self.trusted_proxy_headers - KNOWN_PROXY_HEADERS 422 if unknown_values: 423 raise ValueError( 424 "Received unknown trusted_proxy_headers value (%s) expected one " 425 "of %s" 426 % (", ".join(unknown_values), ", ".join(KNOWN_PROXY_HEADERS)) 427 ) 428 429 if ( 430 "forwarded" in self.trusted_proxy_headers 431 and self.trusted_proxy_headers - {"forwarded"} 432 ): 433 raise ValueError( 434 "The Forwarded proxy header and the " 435 "X-Forwarded-{By,Host,Proto,Port,For} headers are mutually " 436 "exclusive. Can't trust both!" 437 ) 438 439 elif self.trusted_proxy is not None: 440 warnings.warn( 441 "No proxy headers were marked as trusted, but trusted_proxy was set. " 442 "Implicitly trusting X-Forwarded-Proto for backwards compatibility. " 443 "This will be removed in future versions of waitress.", 444 DeprecationWarning, 445 ) 446 self.trusted_proxy_headers = {"x-forwarded-proto"} 447 448 if self.clear_untrusted_proxy_headers is _bool_marker: 449 warnings.warn( 450 "In future versions of Waitress clear_untrusted_proxy_headers will be " 451 "set to True by default. You may opt-out by setting this value to " 452 "False, or opt-in explicitly by setting this to True.", 453 DeprecationWarning, 454 ) 455 self.clear_untrusted_proxy_headers = False 456 457 self.listen = wanted_sockets 458 459 self.check_sockets(self.sockets) 460 461 @classmethod 462 def parse_args(cls, argv): 463 """Pre-parse command line arguments for input into __init__. Note that 464 this does not cast values into adjustment types, it just creates a 465 dictionary suitable for passing into __init__, where __init__ does the 466 casting. 467 """ 468 long_opts = ["help", "call"] 469 for opt, cast in cls._params: 470 opt = opt.replace("_", "-") 471 if cast is asbool: 472 long_opts.append(opt) 473 long_opts.append("no-" + opt) 474 else: 475 long_opts.append(opt + "=") 476 477 kw = { 478 "help": False, 479 "call": False, 480 } 481 482 opts, args = getopt.getopt(argv, "", long_opts) 483 for opt, value in opts: 484 param = opt.lstrip("-").replace("-", "_") 485 486 if param == "listen": 487 kw["listen"] = "{} {}".format(kw.get("listen", ""), value) 488 continue 489 490 if param.startswith("no_"): 491 param = param[3:] 492 kw[param] = "false" 493 elif param in ("help", "call"): 494 kw[param] = True 495 elif cls._param_map[param] is asbool: 496 kw[param] = "true" 497 else: 498 kw[param] = value 499 500 return kw, args 501 502 @classmethod 503 def check_sockets(cls, sockets): 504 has_unix_socket = False 505 has_inet_socket = False 506 has_unsupported_socket = False 507 for sock in sockets: 508 if ( 509 sock.family == socket.AF_INET or sock.family == socket.AF_INET6 510 ) and sock.type == socket.SOCK_STREAM: 511 has_inet_socket = True 512 elif ( 513 hasattr(socket, "AF_UNIX") 514 and sock.family == socket.AF_UNIX 515 and sock.type == socket.SOCK_STREAM 516 ): 517 has_unix_socket = True 518 else: 519 has_unsupported_socket = True 520 if has_unix_socket and has_inet_socket: 521 raise ValueError("Internet and UNIX sockets may not be mixed.") 522 if has_unsupported_socket: 523 raise ValueError("Only Internet or UNIX stream sockets may be used.") 524