/usr/lib/python3/dist-packages/klaus/httpauth.py is in python3-klaus 1.2.1-3.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | """
Copyright (c) 2012 Jonas Haag <jonas@lophus.org>. License: ISC
This implements Digest Auth as specified in RFC 2069, i.e. without the
`qop` quality-of-protection, `cnonce` nonce count, ... options.
References to the algorithm (HA1, HA2, nonce, ...) are taken from Wikipedia:
http://en.wikipedia.org/wiki/Digest_access_authentication
"""
import os
import re
import time
import hashlib
try: # Python 3
from urllib.request import parse_http_list, parse_keqv_list
except ImportError: # Python 2
from urllib2 import parse_http_list, parse_keqv_list
def md5(x):
return hashlib.md5(x).hexdigest()
def md5_str(x):
return md5(x.encode('utf8'))
def sha256(x):
return hashlib.sha256(x).hexdigest()
def reconstruct_uri(environ):
"""
Reconstruct the relative part of the request URI. I.e. if the requested URL
is https://foo.bar/spam?eggs, ``reconstruct_uri`` returns ``'/spam?eggs'``.
"""
uri = environ.get('SCRIPT_NAME', '') + environ['PATH_INFO']
if environ.get('QUERY_STRING'):
uri += '?' + environ['QUERY_STRING']
return uri
def make_www_authenticate_header(realm=None):
return 'Digest realm="%s", nonce="%s"' % (realm, generate_nonce())
def generate_nonce():
return sha256(os.urandom(1000) + str(time.time()).encode())
def make_auth_response(nonce, HA1, HA2):
""" response := md5(HA1 : nonce : HA2) """
return md5_str(HA1 + ':' + nonce + ':' + HA2)
def make_HA2(http_method, uri):
""" HA2 := http_method : uri (as reconstructed by ``reconstruct_uri``) """
return md5_str(http_method + ':' + uri)
def parse_dict_header(value):
"""
Parses a HTTP dict header value -- i.e. ``"foo=bar, spam=eggs"`` is parsed
into ``{'foo': 'bar', 'spam': 'eggs'}``.
"""
return parse_keqv_list(parse_http_list(value))
class BaseHttpAuthMiddleware(object):
"""
Abstract HTTP Digest Auth middleware. Contains all the functionality
except for credential validation -- this happens using the ``make_HA1``
method which needs to be overriden by subclasses.
`wsgi_app`
The WSGI app to be secured.
`realm`
The HTTP Auth realm to be displayed in the browser.
`routes`
(optional) A list of regular expressions that specify which URLs should
be secured. If not given, all routes are secured by default.
"""
def __init__(self, wsgi_app, realm=None, routes=()):
self.wsgi_app = wsgi_app
self.realm = realm or ''
self.routes = self.compile_routes(routes)
def __call__(self, environ, start_response):
environ['httpauth.uri'] = reconstruct_uri(environ)
if (self.should_require_authentication(environ['httpauth.uri']) and
not self.authenticate(environ)):
# URL is secured and user hasn't sent authentication/wrong credentials.
return self.challenge(environ, start_response)
else:
# Wave-through to real WSGI app.
return self.wsgi_app(environ, start_response)
def compile_routes(self, routes):
return [re.compile(route) for route in routes]
def should_require_authentication(self, url):
""" Returns True if we should require authentication for the URL given """
return (not self.routes # require auth for all URLs
or any(route.match(url) for route in self.routes))
def authenticate(self, environ):
"""
Returns True if the credentials passed in the Authorization header are
valid, False otherwise.
"""
try:
hd = parse_dict_header(environ['HTTP_AUTHORIZATION'])
except (KeyError, ValueError):
return False
return self.credentials_valid(
hd['response'],
environ['REQUEST_METHOD'],
environ['httpauth.uri'],
hd['nonce'],
hd['Digest username'],
)
def credentials_valid(self, response, http_method, uri, nonce, user):
try:
HA1 = self.make_HA1(user)
except KeyError:
# Invalid user
return False
return response == make_auth_response(nonce, HA1, make_HA2(http_method, uri))
def challenge(self, environ, start_response):
start_response(
'401 Authentication Required',
[('WWW-Authenticate', make_www_authenticate_header(self.realm))],
)
return ['<h1>401 - Authentication Required</h1>']
class DigestFileHttpAuthMiddleware(BaseHttpAuthMiddleware):
"""
Reads credentials from an Apache-style .htdigest file.
`filelike`
Any file-like object that has a ``.read()`` method.
Note: Don't pass filenames, only open files/file-likes.
"""
def __init__(self, filelike, **kwargs):
realm, self.user_HA1_map = self.parse_htdigest_file(filelike)
BaseHttpAuthMiddleware.__init__(self, realm=realm, **kwargs)
def make_HA1(self, username):
return self.user_HA1_map[username]
def parse_htdigest_file(self, filelike):
"""
.htdigest files consist of lines in the following format::
username:realm:passwordhash
where both `username` and `realm` are plain-text without any colons
and `passwordhash` is the result of ``md5(username : realm : password)``
and thus `passwordhash` == HA1.
"""
realm = None
user_HA1_map = {}
for lineno, line in enumerate(filter(None, filelike.read().splitlines()), 1):
try:
username, realm2, password_hash = line.split(':')
except ValueError:
raise ValueError("Line %d invalid: %r (username/password may not contain ':')" % (lineno, line))
if realm is not None and realm != realm2:
raise ValueError("Line %d: realm may not vary (got %r and %r)" % (lineno, realm, realm2))
else:
realm = realm2
user_HA1_map[username] = password_hash
return realm, user_HA1_map
class DictHttpAuthMiddleware(BaseHttpAuthMiddleware):
"""
Reads credentials from ``user_password_map`` which is a
`username -> plaintext password` map.
"""
def __init__(self, user_password_map, **kwargs):
self.user_password_map = user_password_map
BaseHttpAuthMiddleware.__init__(self, **kwargs)
def make_HA1(self, username):
password = self.user_password_map[username]
return md5_str(username + ':' + self.realm + ':' + password)
class AlwaysFailingAuthMiddleware(BaseHttpAuthMiddleware):
""" This thing just keeps asking for credentials all the time """
def authenticate(self, environ):
return False
|