Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
37 changes: 37 additions & 0 deletions static.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand Down Expand Up @@ -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'
Expand Down