1e4b1c4d1a
* Added support for HTTP Forwarded header and combined parsing of other proxy headers. - Accessible via request.forwarded that tries parse_forwarded and then parse_xforwarded - parse_forwarded uses the Forwarded header, if config.FORWARDED_SECRET is provided and a matching header field is found - parse_xforwarded uses X-Real-IP and X-Forwarded-* much alike the existing implementation - This commit does not change existing request properties that still use the old code and won't make use of Forwarded headers. * Use req.forwarded in req properties server_name, server_port, scheme and remote_addr. X-Scheme handling moved to parse_xforwarded. * Cleanup and fix req.server_port; no longer reports socket port if any forwards headers are used. * Update docstrings to incidate that forwarded header is used first. * Remove testing function. * Fix tests and linting. - One test removed due to change of semantics - no socket port will be used if any forwarded headers are in effect. - Other tests augmented with X-Forwarded-For, to allow the header being tested take effect (shouldn't affect old implementation). * Try to workaround buggy tools complaining about incorrect ordering of imports. * Cleanup forwarded processing, add comments. secret is now also returned. * Added tests, fixed quoted string handling, cleanup. * Further tests for full coverage. * Try'n make linter happy. * Add support for multiple Forwarded headers. Unify parse_forwarded parameters with parse_xforwarded. * Implement multiple headers support for X-Forwarded-For. - Previously only the first header was used, so this BUGFIX may affect functionality. * Bugfix for request.server_name: strip port and other parts. - request.server_name docs claim that it returns the hostname only (no port). - config.SERVER_NAME may be full URL, so strip scheme, port and path - HTTP Host and consequently forwarded Host may include port number, so strip that also for forwarded hosts (previously only done for HTTP Host). - Possible performance benefit of limiting to one split. * Fallback to app.url_for and let it handle SERVER_NAME if defined (until a proper solution is implemented). * Revise previous commit. Only fallback for full URL SERVER_NAMEs; allows host to be defined and proxied information still being used. * Heil lintnazi. * Modify testcase not to use underscores in URLs. Use hyphens which the spec allows for. * Forwarded and Host header parsing improved. - request.forwarded lowercases hosts, separates host:port into their own fields and lowercases addresses - forwarded.parse_host helper function added and used for parsing all host-style headers (IPv6 cannot be simply split(":")). - more tests fixed not to use underscores in hosts as those are no longer accepted and lead to the field being rejected * Fixed typo in docstring. * Added IPv6 address tests for Host header. * Fix regex. * Further tests and stricter forwarded handling. * Fix merge commit * Linter * Linter * Linter * Add to avoid re-using the variable. Make a few raw strings non-raw. * Remove unnecessary or * Updated docs (work in progress). * Enable REAL_IP_HEADER parsing irregardless of PROXIES_COUNT setting. - Also cleanup and added comments * New defaults for PROXIES_COUNT and REAL_IP_HEADER, updated tests. * Remove support for PROXIES_COUNT=-1. * Linter errors. - This is getting ridiculous: cannot fit an URL on one line, linter requires splitting the string literal! * Add support for by=_proxySecret, updated docs, updated tests. * Forwarded headers' semantics tuning. - Forwarded host is now preserved in original format - request.host now returns a forwarded host if available, else the Host header - Forwarded options are preserved in original order, and later keys override earlier ones - Forwarded path is automatically URL-unquoted - Forwarded 'by' and 'for' are omitted if their value is unknown - Tests modified accordingly - Cleanup and improved documentation * Add ASGI test. * Linter * Linter #2
158 lines
5.0 KiB
Python
158 lines
5.0 KiB
Python
import os
|
|
import types
|
|
|
|
from sanic.exceptions import PyFileError
|
|
from sanic.helpers import import_string
|
|
|
|
|
|
SANIC_PREFIX = "SANIC_"
|
|
BASE_LOGO = """
|
|
|
|
Sanic
|
|
Build Fast. Run Fast.
|
|
|
|
"""
|
|
|
|
DEFAULT_CONFIG = {
|
|
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
|
"REQUEST_BUFFER_QUEUE_SIZE": 100,
|
|
"REQUEST_TIMEOUT": 60, # 60 seconds
|
|
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
|
"KEEP_ALIVE": True,
|
|
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
|
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabytes
|
|
"WEBSOCKET_MAX_QUEUE": 32,
|
|
"WEBSOCKET_READ_LIMIT": 2 ** 16,
|
|
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
|
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
|
"ACCESS_LOG": True,
|
|
"FORWARDED_SECRET": None,
|
|
"REAL_IP_HEADER": None,
|
|
"PROXIES_COUNT": None,
|
|
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
|
}
|
|
|
|
|
|
class Config(dict):
|
|
def __init__(self, defaults=None, load_env=True, keep_alive=None):
|
|
defaults = defaults or {}
|
|
super().__init__({**DEFAULT_CONFIG, **defaults})
|
|
|
|
self.LOGO = BASE_LOGO
|
|
|
|
if keep_alive is not None:
|
|
self.KEEP_ALIVE = keep_alive
|
|
|
|
if load_env:
|
|
prefix = SANIC_PREFIX if load_env is True else load_env
|
|
self.load_environment_vars(prefix=prefix)
|
|
|
|
def __getattr__(self, attr):
|
|
try:
|
|
return self[attr]
|
|
except KeyError as ke:
|
|
raise AttributeError("Config has no '{}'".format(ke.args[0]))
|
|
|
|
def __setattr__(self, attr, value):
|
|
self[attr] = value
|
|
|
|
def from_envvar(self, variable_name):
|
|
"""Load a configuration from an environment variable pointing to
|
|
a configuration file.
|
|
|
|
:param variable_name: name of the environment variable
|
|
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
|
"""
|
|
config_file = os.environ.get(variable_name)
|
|
if not config_file:
|
|
raise RuntimeError(
|
|
"The environment variable %r is not set and "
|
|
"thus configuration could not be loaded." % variable_name
|
|
)
|
|
return self.from_pyfile(config_file)
|
|
|
|
def from_pyfile(self, filename):
|
|
"""Update the values in the config from a Python file.
|
|
Only the uppercase variables in that module are stored in the config.
|
|
|
|
:param filename: an absolute path to the config file
|
|
"""
|
|
module = types.ModuleType("config")
|
|
module.__file__ = filename
|
|
try:
|
|
with open(filename) as config_file:
|
|
exec( # nosec
|
|
compile(config_file.read(), filename, "exec"),
|
|
module.__dict__,
|
|
)
|
|
except IOError as e:
|
|
e.strerror = "Unable to load configuration file (%s)" % e.strerror
|
|
raise
|
|
except Exception as e:
|
|
raise PyFileError(filename) from e
|
|
|
|
self.from_object(module)
|
|
return True
|
|
|
|
def from_object(self, obj):
|
|
"""Update the values from the given object.
|
|
Objects are usually either modules or classes.
|
|
|
|
Just the uppercase variables in that object are stored in the config.
|
|
Example usage::
|
|
|
|
from yourapplication import default_config
|
|
app.config.from_object(default_config)
|
|
|
|
or also:
|
|
app.config.from_object('myproject.config.MyConfigClass')
|
|
|
|
You should not use this function to load the actual configuration but
|
|
rather configuration defaults. The actual config should be loaded
|
|
with :meth:`from_pyfile` and ideally from a location not within the
|
|
package because the package might be installed system wide.
|
|
|
|
:param obj: an object holding the configuration
|
|
"""
|
|
if isinstance(obj, str):
|
|
obj = import_string(obj)
|
|
for key in dir(obj):
|
|
if key.isupper():
|
|
self[key] = getattr(obj, key)
|
|
|
|
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
|
"""
|
|
Looks for prefixed environment variables and applies
|
|
them to the configuration if present.
|
|
"""
|
|
for k, v in os.environ.items():
|
|
if k.startswith(prefix):
|
|
_, config_key = k.split(prefix, 1)
|
|
try:
|
|
self[config_key] = int(v)
|
|
except ValueError:
|
|
try:
|
|
self[config_key] = float(v)
|
|
except ValueError:
|
|
try:
|
|
self[config_key] = strtobool(v)
|
|
except ValueError:
|
|
self[config_key] = v
|
|
|
|
|
|
def strtobool(val):
|
|
"""
|
|
This function was borrowed from distutils.utils. While distutils
|
|
is part of stdlib, it feels odd to use distutils in main application code.
|
|
|
|
The function was modified to walk its talk and actually return bool
|
|
and not int.
|
|
"""
|
|
val = val.lower()
|
|
if val in ("y", "yes", "t", "true", "on", "1"):
|
|
return True
|
|
elif val in ("n", "no", "f", "false", "off", "0"):
|
|
return False
|
|
else:
|
|
raise ValueError("invalid truth value %r" % (val,))
|