diff options
author | Michael Merickel <michael@merickel.org> | 2019-06-29 15:53:50 -0500 |
---|---|---|
committer | Michael Merickel <michael@merickel.org> | 2019-06-29 15:58:45 -0500 |
commit | 3e68af8684000ad3b01b51ad12e68e9f48a2d7ee (patch) | |
tree | 7bc41c44148202b6d76dff7b4b7129a3849de71e | |
parent | e2210c9258702b7a46fa23f3f5e8389d56999748 (diff) | |
download | waitress-3e68af8684000ad3b01b51ad12e68e9f48a2d7ee.tar.gz |
move proxy headers to a middleware and return 400 for malformed values
-rw-r--r-- | waitress/proxy_headers.py | 309 | ||||
-rw-r--r-- | waitress/server.py | 20 | ||||
-rw-r--r-- | waitress/task.py | 302 | ||||
-rw-r--r-- | waitress/utilities.py | 115 |
4 files changed, 392 insertions, 354 deletions
diff --git a/waitress/proxy_headers.py b/waitress/proxy_headers.py new file mode 100644 index 0000000..713e13e --- /dev/null +++ b/waitress/proxy_headers.py @@ -0,0 +1,309 @@ +from collections import namedtuple + +from .utilities import logger, undquote, BadRequest + + +PROXY_HEADERS = frozenset({ + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED_HOST', + 'HTTP_X_FORWARDED_PROTO', + 'HTTP_X_FORWARDED_PORT', + 'HTTP_X_FORWARDED_BY', + 'HTTP_FORWARDED', +}) + +Forwarded = namedtuple('Forwarded', ['by', 'for_', 'host', 'proto']) + + +def proxy_headers_middleware( + app, + trusted_proxy, + trusted_proxy_count, + trusted_proxy_headers, + clear_untrusted=True, + log_untrusted=False, + logger=logger, +): + def translate_proxy_headers(environ, start_response): + try: + untrusted_headers = PROXY_HEADERS + remote_peer = environ['REMOTE_ADDR'] + if trusted_proxy == '*' or remote_peer == trusted_proxy: + untrusted_headers = parse_proxy_headers( + environ, + trusted_proxy_count=trusted_proxy_count, + trusted_proxy_headers=trusted_proxy_headers, + logger=logger, + ) + + # Clear out the untrusted proxy headers + if clear_untrusted: + clear_untrusted_headers( + environ, + untrusted_headers, + log_warning=log_untrusted, + logger=logger, + ) + except ValueError as ex: + error = BadRequest(str(ex)) + return error.wsgi_response(environ, start_response) + + return app(environ, start_response) + return translate_proxy_headers + + +def parse_proxy_headers( + environ, + trusted_proxy_count=1, + trusted_proxy_headers=None, + logger=logger, +): + if trusted_proxy_headers is None: + trusted_proxy_headers = set() + + forwarded_for = [] + forwarded_host = forwarded_proto = forwarded_port = forwarded = "" + client_addr = None + untrusted_headers = set(PROXY_HEADERS) + + def warn_unspecified_behavior(header): + logger.warning( + "Found multiple values in %s, this has unspecified behaviour. " + "Ignoring header value.", + header, + ) + + if ( + "x-forwarded-for" in trusted_proxy_headers + and "HTTP_X_FORWARDED_FOR" in environ + ): + forwarded_for = [] + + for forward_hop in environ["HTTP_X_FORWARDED_FOR"].split(","): + forward_hop = forward_hop.strip() + forward_hop = undquote(forward_hop) + + # Make sure that all IPv6 addresses are surrounded by brackets, + # this is assuming that the IPv6 representation here does not + # include a port number. + + if "." not in forward_hop and ( + ":" in forward_hop and forward_hop[-1] != "]" + ): + forwarded_for.append("[{}]".format(forward_hop)) + else: + forwarded_for.append(forward_hop) + + forwarded_for = forwarded_for[-trusted_proxy_count:] + client_addr = forwarded_for[0] + + untrusted_headers.remove("HTTP_X_FORWARDED_FOR") + + if ( + "x-forwarded-host" in trusted_proxy_headers + and "HTTP_X_FORWARDED_HOST" in environ + ): + forwarded_host_multiple = [] + + for forward_host in environ["HTTP_X_FORWARDED_HOST"].split(","): + forward_host = forward_host.strip() + forward_host = undquote(forward_host) + forwarded_host_multiple.append(forward_host) + + forwarded_host_multiple = forwarded_host_multiple[-trusted_proxy_count:] + forwarded_host = forwarded_host_multiple[0] + + untrusted_headers.remove("HTTP_X_FORWARDED_HOST") + + if "x-forwarded-proto" in trusted_proxy_headers: + forwarded_proto = undquote(environ.get("HTTP_X_FORWARDED_PROTO", "")) + untrusted_headers.remove("HTTP_X_FORWARDED_PROTO") + + if "," in forwarded_proto: + forwarded_proto = "" + warn_unspecified_behavior("X-Forwarded-Proto") + + if "x-forwarded-port" in trusted_proxy_headers: + forwarded_port = undquote(environ.get("HTTP_X_FORWARDED_PORT", "")) + untrusted_headers.remove("HTTP_X_FORWARDED_PORT") + + if "," in forwarded_port: + forwarded_port = "" + warn_unspecified_behavior("X-Forwarded-Port") + + if "x-forwarded-by" in trusted_proxy_headers: + # Waitress itself does not use X-Forwarded-By, but we can not + # remove it so it can get set in the environ + untrusted_headers.remove("HTTP_X_FORWARDED_BY") + + if "forwarded" in trusted_proxy_headers: + forwarded = environ.get("HTTP_FORWARDED", None) + untrusted_headers = PROXY_HEADERS - {"HTTP_FORWARDED"} + + # If the Forwarded header exists, it gets priority + if forwarded: + proxies = [] + + for forwarded_element in forwarded.split(","): + # Remove whitespace that may have been introduced when + # appending a new entry + forwarded_element = forwarded_element.strip() + + forwarded_for = forwarded_host = forwarded_proto = "" + forwarded_port = forwarded_by = "" + + for pair in forwarded_element.split(";"): + pair = pair.lower() + + if not pair: + continue + + token, equals, value = pair.partition("=") + + if equals != "=": + raise ValueError("Invalid forwarded-pair in Forwarded element") + + if token.strip() != token: + raise ValueError("token may not be surrounded by whitespace") + + if value.strip() != value: + raise ValueError("value may not be surrounded by whitespace") + + if token == "by": + forwarded_by = undquote(value) + + elif token == "for": + forwarded_for = undquote(value) + + elif token == "host": + forwarded_host = undquote(value) + + elif token == "proto": + forwarded_proto = undquote(value) + + else: + logger.warning("Unknown Forwarded token: %s" % token) + + proxies.append( + Forwarded( + forwarded_by, forwarded_for, forwarded_host, forwarded_proto + ) + ) + + proxies = proxies[-trusted_proxy_count:] + + # Iterate backwards and fill in some values, the oldest entry that + # contains the information we expect is the one we use. We expect + # that intermediate proxies may re-write the host header or proto, + # but the oldest entry is the one that contains the information the + # client expects when generating URL's + # + # Forwarded: for="[2001:db8::1]";host="example.com:8443";proto="https" + # Forwarded: for=192.0.2.1;host="example.internal:8080" + # + # (After HTTPS header folding) should mean that we use as values: + # + # Host: example.com + # Protocol: https + # Port: 8443 + + for proxy in proxies[::-1]: + client_addr = proxy.for_ or client_addr + forwarded_host = proxy.host or forwarded_host + forwarded_proto = proxy.proto or forwarded_proto + + if forwarded_proto: + forwarded_proto = forwarded_proto.lower() + + if forwarded_proto not in {"http", "https"}: + raise ValueError( + 'Invalid "Forwarded Proto=" or "X-Forwarded-Proto" value.' + ) + + # Set the URL scheme to the proxy provided proto + environ["wsgi.url_scheme"] = forwarded_proto + + if not forwarded_port: + if forwarded_proto == "http": + forwarded_port = "80" + + if forwarded_proto == "https": + forwarded_port = "443" + + if forwarded_host: + if ":" in forwarded_host and forwarded_host[-1] != "]": + host, port = forwarded_host.rsplit(":", 1) + host, port = host.strip(), str(port) + + # We trust the port in the Forwarded Host/X-Forwarded-Host over + # X-Forwarded-Port, or whatever we got from Forwarded + # Proto/X-Forwarded-Proto. + + if forwarded_port != port: + forwarded_port = port + + # We trust the proxy server's forwarded Host + environ["SERVER_NAME"] = host + environ["HTTP_HOST"] = forwarded_host + else: + # We trust the proxy server's forwarded Host + environ["SERVER_NAME"] = forwarded_host + environ["HTTP_HOST"] = forwarded_host + + if forwarded_port: + if forwarded_port not in {"443", "80"}: + environ["HTTP_HOST"] = "{}:{}".format( + forwarded_host, forwarded_port + ) + elif ( + forwarded_port == "80" and environ["wsgi.url_scheme"] != "http" + ): + environ["HTTP_HOST"] = "{}:{}".format( + forwarded_host, forwarded_port + ) + elif ( + forwarded_port == "443" + and environ["wsgi.url_scheme"] != "https" + ): + environ["HTTP_HOST"] = "{}:{}".format( + forwarded_host, forwarded_port + ) + + if forwarded_port: + environ["SERVER_PORT"] = str(forwarded_port) + + if client_addr: + def strip_brackets(addr): + if addr[0] == "[" and addr[-1] == "]": + return addr[1:-1] + return addr + + if ":" in client_addr and client_addr[-1] != "]": + addr, port = client_addr.rsplit(":", 1) + environ["REMOTE_ADDR"] = strip_brackets(addr.strip()) + environ["REMOTE_PORT"] = port.strip() + else: + environ["REMOTE_ADDR"] = strip_brackets(client_addr.strip()) + + return untrusted_headers + + +def clear_untrusted_headers( + environ, untrusted_headers, log_warning=False, logger=logger +): + untrusted_headers_removed = [ + header + for header in untrusted_headers + if environ.pop(header, False) is not False + ] + + if log_warning and untrusted_headers_removed: + untrusted_headers_removed = [ + "-".join(x.lstrip('HTTP_').capitalize() for x in header.split("_")) + for header in untrusted_headers_removed + ] + logger.warning( + "Removed untrusted headers (%s). Waitress recommends these be " + "removed upstream.", + ", ".join(untrusted_headers_removed), + ) diff --git a/waitress/server.py b/waitress/server.py index 7ef930e..bc68c77 100644 --- a/waitress/server.py +++ b/waitress/server.py @@ -28,6 +28,7 @@ from waitress.compat import ( IPV6_V6ONLY, ) from . import wasyncore +from .proxy_headers import proxy_headers_middleware def create_server(application, map=None, @@ -178,10 +179,29 @@ class BaseWSGIServer(wasyncore.dispatcher, object): adj=None, # adjustments sockinfo=None, # opaque object bind_socket=True, + logger=None, # test shim **kw ): if adj is None: adj = Adjustments(**kw) + + if logger is not None: + self.logger = logger + + if adj.trusted_proxy or adj.clear_untrusted_proxy_headers: + # wrap the application to deal with proxy headers + # we wrap it here because webtest subclasses the TcpWSGIServer + # directly and thus doesn't run any code that's in create_server + application = proxy_headers_middleware( + application, + trusted_proxy=adj.trusted_proxy, + trusted_proxy_count=adj.trusted_proxy_count, + trusted_proxy_headers=adj.trusted_proxy_headers, + clear_untrusted=adj.clear_untrusted_proxy_headers, + log_untrusted=adj.log_untrusted_proxy_headers, + logger=self.logger, + ) + if map is None: # use a nonglobal socket map by default to hopefully prevent # conflicts with apps and libs that use the wasyncore global socket diff --git a/waitress/task.py b/waitress/task.py index 81e512f..96451a7 100644 --- a/waitress/task.py +++ b/waitress/task.py @@ -21,13 +21,9 @@ import time from .buffers import ReadOnlyFileBasedBuffer from .compat import reraise, tobytes from .utilities import ( - Forwarded, - PROXY_HEADERS, build_http_date, - clear_untrusted_headers, logger, queue_logger, - undquote, ) rename_headers = { # or keep them without the HTTP_ prefix added @@ -357,23 +353,10 @@ class ErrorTask(Task): def execute(self): e = self.request.error - body = '%s\r\n\r\n%s' % (e.reason, e.body) - tag = '\r\n\r\n(generated by waitress)' - body = body + tag - self.status = '%s %s' % (e.code, e.reason) - cl = len(body) - self.content_length = cl - self.response_headers.append(('Content-Length', str(cl))) - self.response_headers.append(('Content-Type', 'text/plain')) - if self.version == '1.1': - connection = self.request.headers.get('CONNECTION', '').lower() - if connection == 'close': - self.response_headers.append(('Connection', 'close')) - # under HTTP 1.1 keep-alive is default, no need to set the header - else: - # HTTP 1.0 - self.response_headers.append(('Connection', 'close')) - self.close_on_finish = True + status, headers, body = e.to_response() + self.status = status + self.response_headers.extend(headers) + self.content_length = len(body) self.write(tobytes(body)) class WSGITask(Task): @@ -382,7 +365,7 @@ class WSGITask(Task): environ = None def execute(self): - env = self.get_environment() + environ = self.get_environment() def start_response(status, headers, exc_info=None): if self.complete and not exc_info: @@ -444,7 +427,7 @@ class WSGITask(Task): return self.write # Call the application to handle the request and write a response - app_iter = self.channel.server.application(env, start_response) + app_iter = self.channel.server.application(environ, start_response) can_close_app_iter = True try: @@ -501,236 +484,6 @@ class WSGITask(Task): if can_close_app_iter and hasattr(app_iter, 'close'): app_iter.close() - def parse_proxy_headers( - self, - environ, - headers, - trusted_proxy_count=1, - trusted_proxy_headers=None, - ): - if trusted_proxy_headers is None: - trusted_proxy_headers = set() - - forwarded_for = [] - forwarded_host = forwarded_proto = forwarded_port = forwarded = "" - client_addr = None - untrusted_headers = set(PROXY_HEADERS) - - def warn_unspecified_behavior(header): - self.logger.warning( - "Found multiple values in %s, this has unspecified behaviour. " - "Ignoring header value.", - header, - ) - - if "x-forwarded-for" in trusted_proxy_headers and "X_FORWARDED_FOR" in headers: - forwarded_for = [] - - for forward_hop in headers["X_FORWARDED_FOR"].split(","): - forward_hop = forward_hop.strip() - forward_hop = undquote(forward_hop) - - # Make sure that all IPv6 addresses are surrounded by brackets, - # this is assuming that the IPv6 representation here does not - # include a port number. - - if "." not in forward_hop and ( - ":" in forward_hop and forward_hop[-1] != "]" - ): - forwarded_for.append("[{}]".format(forward_hop)) - else: - forwarded_for.append(forward_hop) - - forwarded_for = forwarded_for[-trusted_proxy_count:] - client_addr = forwarded_for[0] - - untrusted_headers.remove("X_FORWARDED_FOR") - - if "x-forwarded-host" in trusted_proxy_headers and "X_FORWARDED_HOST" in headers: - forwarded_host_multiple = [] - - for forward_host in headers["X_FORWARDED_HOST"].split(","): - forward_host = forward_host.strip() - forward_host = undquote(forward_host) - forwarded_host_multiple.append(forward_host) - - forwarded_host_multiple = forwarded_host_multiple[-trusted_proxy_count:] - forwarded_host = forwarded_host_multiple[0] - - untrusted_headers.remove("X_FORWARDED_HOST") - - if "x-forwarded-proto" in trusted_proxy_headers: - forwarded_proto = undquote(headers.get("X_FORWARDED_PROTO", "")) - untrusted_headers.remove("X_FORWARDED_PROTO") - - if "," in forwarded_proto: - forwarded_proto = "" - warn_unspecified_behavior("X-Forwarded-Proto") - - if "x-forwarded-port" in trusted_proxy_headers: - forwarded_port = undquote(headers.get("X_FORWARDED_PORT", "")) - untrusted_headers.remove("X_FORWARDED_PORT") - - if "," in forwarded_port: - forwarded_port = "" - warn_unspecified_behavior("X-Forwarded-Port") - - if "x-forwarded-by" in trusted_proxy_headers: - # Waitress itself does not use X-Forwarded-By, but we can not - # remove it so it can get set in the environ - untrusted_headers.remove("X_FORWARDED_BY") - - if "forwarded" in trusted_proxy_headers: - forwarded = headers.get("FORWARDED", None) - untrusted_headers = PROXY_HEADERS - {"FORWARDED"} - - # If the Forwarded header exists, it gets priority - if forwarded: - proxies = [] - - for forwarded_element in forwarded.split(","): - # Remove whitespace that may have been introduced when - # appending a new entry - forwarded_element = forwarded_element.strip() - - forwarded_for = forwarded_host = forwarded_proto = "" - forwarded_port = forwarded_by = "" - - for pair in forwarded_element.split(";"): - pair = pair.lower() - - if not pair: - continue - - token, equals, value = pair.partition("=") - - if equals != "=": - raise ValueError("Invalid forwarded-pair in Forwarded element") - - if token.strip() != token: - raise ValueError("token may not be surrounded by whitespace") - - if value.strip() != value: - raise ValueError("value may not be surrounded by whitespace") - - if token == "by": - forwarded_by = undquote(value) - - elif token == "for": - forwarded_for = undquote(value) - - elif token == "host": - forwarded_host = undquote(value) - - elif token == "proto": - forwarded_proto = undquote(value) - - else: - self.logger.warning("Unknown Forwarded token: %s" % token) - - proxies.append( - Forwarded( - forwarded_by, forwarded_for, forwarded_host, forwarded_proto - ) - ) - - proxies = proxies[-trusted_proxy_count:] - - # Iterate backwards and fill in some values, the oldest entry that - # contains the information we expect is the one we use. We expect - # that intermediate proxies may re-write the host header or proto, - # but the oldest entry is the one that contains the information the - # client expects when generating URL's - # - # Forwarded: for="[2001:db8::1]";host="example.com:8443";proto="https" - # Forwarded: for=192.0.2.1;host="example.internal:8080" - # - # (After HTTPS header folding) should mean that we use as values: - # - # Host: example.com - # Protocol: https - # Port: 8443 - - for proxy in proxies[::-1]: - client_addr = proxy.for_ or client_addr - forwarded_host = proxy.host or forwarded_host - forwarded_proto = proxy.proto or forwarded_proto - - if forwarded_proto: - forwarded_proto = forwarded_proto.lower() - - if forwarded_proto not in {"http", "https"}: - raise ValueError( - 'Invalid "Forwarded Proto=" or "X-Forwarded-Proto" value.' - ) - - # Set the URL scheme to the proxy provided proto - environ["wsgi.url_scheme"] = forwarded_proto - - if not forwarded_port: - if forwarded_proto == "http": - forwarded_port = "80" - - if forwarded_proto == "https": - forwarded_port = "443" - - if forwarded_host: - if ":" in forwarded_host and forwarded_host[-1] != "]": - host, port = forwarded_host.rsplit(":", 1) - host, port = host.strip(), str(port) - - # We trust the port in the Forwarded Host/X-Forwarded-Host over - # X-Forwarded-Port, or whatever we got from Forwarded - # Proto/X-Forwarded-Proto. - - if forwarded_port != port: - forwarded_port = port - - # We trust the proxy server's forwarded Host - environ["SERVER_NAME"] = host - environ["HTTP_HOST"] = forwarded_host - else: - # We trust the proxy server's forwarded Host - environ["SERVER_NAME"] = forwarded_host - environ["HTTP_HOST"] = forwarded_host - - if forwarded_port: - if forwarded_port not in {"443", "80"}: - environ["HTTP_HOST"] = "{}:{}".format( - forwarded_host, forwarded_port - ) - elif ( - forwarded_port == "80" and environ["wsgi.url_scheme"] != "http" - ): - environ["HTTP_HOST"] = "{}:{}".format( - forwarded_host, forwarded_port - ) - elif ( - forwarded_port == "443" - and environ["wsgi.url_scheme"] != "https" - ): - environ["HTTP_HOST"] = "{}:{}".format( - forwarded_host, forwarded_port - ) - - if forwarded_port: - environ["SERVER_PORT"] = str(forwarded_port) - - if client_addr: - def strip_brackets(addr): - if addr[0] == "[" and addr[-1] == "]": - return addr[1:-1] - return addr - - if ":" in client_addr and client_addr[-1] != "]": - addr, port = client_addr.rsplit(":", 1) - environ["REMOTE_ADDR"] = strip_brackets(addr.strip()) - environ["REMOTE_PORT"] = port.strip() - else: - environ["REMOTE_ADDR"] = strip_brackets(client_addr.strip()) - - return untrusted_headers - def get_environment(self): """Returns a WSGI environment.""" environ = self.environ @@ -766,6 +519,14 @@ class WSGITask(Task): path = path[len(url_prefix):] environ = { + 'REMOTE_ADDR': channel.addr[0], + # Nah, we aren't actually going to look up the reverse DNS for + # REMOTE_ADDR, but we will happily set this environment variable + # for the WSGI application. Spec says we can just set this to + # REMOTE_ADDR, so we do. + 'REMOTE_HOST': channel.addr[0], + # try and set the REMOTE_PORT to something useful, but maybe None + 'REMOTE_PORT': str(channel.addr[1]), 'REQUEST_METHOD': request.command.upper(), 'SERVER_PORT': str(server.effective_port), 'SERVER_NAME': server.server_name, @@ -788,43 +549,12 @@ class WSGITask(Task): 'wsgi.file_wrapper': ReadOnlyFileBasedBuffer, 'wsgi.input_terminated': True, # wsgi.input is EOF terminated } - remote_peer = environ['REMOTE_ADDR'] = channel.addr[0] - - headers = dict(request.headers) - - untrusted_headers = PROXY_HEADERS - if server.adj.trusted_proxy == '*' or remote_peer == server.adj.trusted_proxy: - untrusted_headers = self.parse_proxy_headers( - environ, - headers, - trusted_proxy_count=server.adj.trusted_proxy_count, - trusted_proxy_headers=server.adj.trusted_proxy_headers, - ) - else: - # If we are not relying on a proxy, we still want to try and set - # the REMOTE_PORT to something useful, maybe None though. - environ["REMOTE_PORT"] = str(channel.addr[1]) - - # Nah, we aren't actually going to look up the reverse DNS for - # REMOTE_ADDR, but we will happily set this environment variable for - # the WSGI application. Spec says we can just set this to REMOTE_ADDR, - # so we do. - environ["REMOTE_HOST"] = environ["REMOTE_ADDR"] - - # Clear out the untrusted proxy headers - if server.adj.clear_untrusted_proxy_headers: - clear_untrusted_headers( - headers, - untrusted_headers, - log_warning=server.adj.log_untrusted_proxy_headers, - logger=self.logger, - ) - for key, value in headers.items(): + for key, value in dict(request.headers).items(): value = value.strip() mykey = rename_headers.get(key, None) if mykey is None: - mykey = 'HTTP_%s' % key + mykey = 'HTTP_' + key if mykey not in environ: environ[mykey] = value diff --git a/waitress/utilities.py b/waitress/utilities.py index a21ca4e..d32affc 100644 --- a/waitress/utilities.py +++ b/waitress/utilities.py @@ -21,20 +21,10 @@ import os import re import stat import time -from collections import namedtuple logger = logging.getLogger('waitress') queue_logger = logging.getLogger('waitress.queue') -PROXY_HEADERS = frozenset({ - 'X_FORWARDED_FOR', - 'X_FORWARDED_HOST', - 'X_FORWARDED_PROTO', - 'X_FORWARDED_PORT', - 'X_FORWARDED_BY', - 'FORWARDED', -}) - def find_double_newline(s): """Returns the position just after a double newline in the given string.""" pos1 = s.find(b'\n\r\n') # One kind of double newline @@ -180,42 +170,6 @@ def parse_http_date(d): return retval -def cleanup_unix_socket(path): - try: - st = os.stat(path) - except OSError as exc: - if exc.errno != errno.ENOENT: - raise # pragma: no cover - else: - if stat.S_ISSOCK(st.st_mode): - try: - os.remove(path) - except OSError: # pragma: no cover - # avoid race condition error during tests - pass - -class Error(object): - - def __init__(self, body): - self.body = body - -class BadRequest(Error): - code = 400 - reason = 'Bad Request' - -class RequestHeaderFieldsTooLarge(BadRequest): - code = 431 - reason = 'Request Header Fields Too Large' - -class RequestEntityTooLarge(BadRequest): - code = 413 - reason = 'Request Entity Too Large' - -class InternalServerError(Error): - code = 500 - reason = 'Internal Server Error' - - # RFC 5234 Appendix B.1 "Core Rules": # VCHAR = %x21-7E # ; visible (printing) characters @@ -259,25 +213,50 @@ def undquote(value): raise ValueError('Invalid quoting in value') -Forwarded = namedtuple('Forwarded', ['by', 'for_', 'host', 'proto']) - - -def clear_untrusted_headers( - headers, untrusted_headers, log_warning=False, logger=logger -): - untrusted_headers_removed = [ - header - for header in untrusted_headers - if headers.pop(header, False) is not False - ] - - if log_warning and untrusted_headers_removed: - untrusted_headers_removed = [ - "-".join([x.capitalize() for x in header.split("_")]) - for header in untrusted_headers_removed - ] - logger.warning( - "Removed untrusted headers (%s). Waitress recommends these be " - "removed upstream.", - ", ".join(untrusted_headers_removed), - ) +def cleanup_unix_socket(path): + try: + st = os.stat(path) + except OSError as exc: + if exc.errno != errno.ENOENT: + raise # pragma: no cover + else: + if stat.S_ISSOCK(st.st_mode): + try: + os.remove(path) + except OSError: # pragma: no cover + # avoid race condition error during tests + pass + +class Error(object): + + def __init__(self, body): + self.body = body + + def to_response(self): + status = '%s %s' % (self.code, self.reason) + body = '%s\r\n\r\n%s' % (self.reason, self.body) + tag = '\r\n\r\n(generated by waitress)' + body = body + tag + headers = [('Content-Type', 'text/plain')] + return status, headers, body + + def wsgi_response(self, environ, start_response): + status, headers, body = self.to_response() + start_response(status, headers) + yield body + +class BadRequest(Error): + code = 400 + reason = 'Bad Request' + +class RequestHeaderFieldsTooLarge(BadRequest): + code = 431 + reason = 'Request Header Fields Too Large' + +class RequestEntityTooLarge(BadRequest): + code = 413 + reason = 'Request Entity Too Large' + +class InternalServerError(Error): + code = 500 + reason = 'Internal Server Error' |