adjustments.py 18 KB

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