You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
341 lines
11 KiB
341 lines
11 KiB
6 years ago
|
##############################################################################
|
||
|
#
|
||
|
# 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
|
||
|
|
||
|
from waitress.compat import (
|
||
|
PY2,
|
||
|
WIN,
|
||
|
string_types,
|
||
|
HAS_IPV6,
|
||
|
)
|
||
|
|
||
|
truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1'))
|
||
|
|
||
|
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 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
|
||
|
|
||
|
class _str_marker(str):
|
||
|
pass
|
||
|
|
||
|
class _int_marker(int):
|
||
|
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),
|
||
|
('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),
|
||
|
('asyncore_loop_timeout', int),
|
||
|
('asyncore_use_poll', asbool),
|
||
|
('unix_socket', str),
|
||
|
('unix_socket_perms', asoctal),
|
||
|
)
|
||
|
|
||
|
_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)]
|
||
|
|
||
|
# mumber of threads available for tasks
|
||
|
threads = 4
|
||
|
|
||
|
# Host allowed to overrid ``wsgi.url_scheme`` via header
|
||
|
trusted_proxy = None
|
||
|
|
||
|
# 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
|
||
|
|
||
|
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 (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):
|
||
|
"""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
|