From b3bf967ded597bf48f65ad03d828d5959428c8fe Mon Sep 17 00:00:00 2001 From: David Downes Date: Wed, 16 Nov 2016 08:41:09 +0000 Subject: [PATCH] Added ability to restrict requesting IPs to a whitelist Use environment variables to white list allowed IP addresses and/or network ranges. Updated the README with instructions for use, python 3.3+ only --- README.rst | 20 ++++++++++++++++++++ static.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/README.rst b/README.rst index 903590a..edeb1bb 100644 --- a/README.rst +++ b/README.rst @@ -138,3 +138,23 @@ If you have templates encoded in different formats, an instance of .. _static: https://pypi.python.org/pypi/static .. _kid: https://pypi.python.org/pypi/kid .. _Genshi: https://pypi.python.org/pypi/Genshi + +IP Restiction and Whitelisting (Python 3.3+) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The server can be configured (using environment variables) to restrict access +to a white list of IP addresses or IP network ranges. Python 3.3 or newer is +needed for this feature, as it makes use of the ipaddress library. To turn on IP +restriction set a truthy environment variable called RESTICT_IPS, and a comma +separated list of IP addresses and/or network ranges to allow access to the +server, as a variable called ALLOWED_IPS. + +For example: + + export RESTRICT_IPS=true + export ALLOWED_IPS='12.34.56.78,123.456.789.0/24' + +Would allow the IP 12.34.56.78, and also any IP in the range 123.456.789.0/24 +to access the server. You can specify as many, or as few, addresses/ranges as +you like. Note: setting RESTRICT_IPS to a truthy value, and not specifying any +ALLOWED_IPS, will block all requests. diff --git a/static.py b/static.py index a053382..bb8ff48 100755 --- a/static.py +++ b/static.py @@ -41,6 +41,11 @@ from wsgiref.simple_server import make_server from optparse import OptionParser +try: + import ipaddress +except ImportError: + pass + try: from pkg_resources import resource_filename, Requirement except: @@ -132,6 +137,7 @@ class Cling(object): not_found = StatusApp('404 Not Found') not_modified = StatusApp('304 Not Modified', "") moved_permanently = StatusApp('301 Moved Permanently') + forbidden = StatusApp('403 Forbidden') method_not_allowed = StatusApp('405 Method Not Allowed') def __init__(self, root, **kw): @@ -144,6 +150,8 @@ def __init__(self, root, **kw): def __call__(self, environ, start_response): """Respond to a request when called in the usual WSGI way.""" + if not self._is_allowed_ip(environ): + return self.forbidden(environ, start_response) if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'): headers = [('Allow', 'GET, HEAD')] return self.method_not_allowed(environ, start_response, headers) @@ -205,6 +213,35 @@ def _is_under_root(self, full_path): else: return False + def _is_allowed_ip(self, environ): + try: + ipaddress + except NameError: + return True + restrict = environ.get('RESTRICT_IPS', '') + if restrict.lower() in ['true', 'yes', '1']: + try: + request_ip_str = environ['HTTP_X_FORWARDED_FOR'].split(',')[-1].strip() + except KeyError: + request_ip_str = environ['REMOTE_ADDR'] + allowed_ips = environ.get('ALLOWED_IPS', '').split(',') + request_ip = ipaddress.ip_address(request_ip_str) + for allowed_item in allowed_ips: + try: + allowed_ip = ipaddress.ip_address(allowed_item.strip()) + if request_ip == allowed_ip: + break + except ValueError: + try: + allowed_range = ipaddress.ip_network(allowed_item.strip()) + if request_ip in ipaddress.ip_network(allowed_range): + break + except ValueError: + pass + else: + return False + return True + def _guess_type(self, full_path): """Guess the mime type using the mimetypes module.""" return mimetypes.guess_type(full_path)[0] or 'text/plain'