seafile_ynh/sources/seafile-server-3.1.1/seahub/thirdpart/wsgidav/http_authenticator.py
Elie 0f063400be Add sources
Former-commit-id: 2e47976ec5
2014-08-06 22:42:37 -04:00

383 lines
16 KiB
Python

# (c) 2009-2011 Martin Wendt and contributors; see WsgiDAV http://wsgidav.googlecode.com/
# Original PyFileServer (c) 2005 Ho Chun Wei.
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
"""
WSGI middleware for HTTP basic and digest authentication.
Usage::
from http_authenticator import HTTPAuthenticator
WSGIApp = HTTPAuthenticator(ProtectedWSGIApp, domain_controller, acceptbasic,
acceptdigest, defaultdigest)
where:
ProtectedWSGIApp is the application requiring authenticated access
domain_controller is a domain controller object meeting specific
requirements (below)
acceptbasic is a boolean indicating whether to accept requests using
the basic authentication scheme (default = True)
acceptdigest is a boolean indicating whether to accept requests using
the digest authentication scheme (default = True)
defaultdigest is a boolean. if True, an unauthenticated request will
be sent a digest authentication required response, else the unauthenticated
request will be sent a basic authentication required response
(default = True)
The HTTPAuthenticator will put the following authenticated information in the
environ dictionary::
environ["http_authenticator.realm"] = realm name
environ["http_authenticator.username"] = username
**Domain Controllers**
The HTTP basic and digest authentication schemes are based on the following
concept:
Each requested relative URI can be resolved to a realm for authentication,
for example:
/fac_eng/courses/ee5903/timetable.pdf -> might resolve to realm 'Engineering General'
/fac_eng/examsolns/ee5903/thisyearssolns.pdf -> might resolve to realm 'Engineering Lecturers'
/med_sci/courses/m500/surgery.htm -> might resolve to realm 'Medical Sciences General'
and each realm would have a set of username and password pairs that would
allow access to the resource.
A domain controller provides this information to the HTTPAuthenticator.
This allows developers to write their own domain controllers, that might,
for example, interface with their own user database.
for simple applications, a SimpleDomainController is provided that will take
in a single realm name (for display) and a single dictionary of username (key)
and password (value) string pairs
Usage::
from http_authenticator import SimpleDomainController
users = dict(({'John Smith': 'YouNeverGuessMe', 'Dan Brown': 'DontGuessMeEither'})
realm = 'Sample Realm'
domain_controller = SimpleDomainController(users, realm)
Domain Controllers must provide the methods as described in
``wsgidav.interfaces.domaincontrollerinterface`` (interface_)
.. _interface : interfaces/domaincontrollerinterface.py
The environ variable here is the WSGI 'environ' dictionary. It is passed to
all methods of the domain controller as a means for developers to pass information
from previous middleware or server config (if required).
See `Developers info`_ for more information about the WsgiDAV architecture.
.. _`Developers info`: http://docs.wsgidav.googlecode.com/hg/html/develop.html
"""
__docformat__ = "reStructuredText"
import random
import base64
try:
from hashlib import md5
except ImportError:
from md5 import md5
import time
import re
import util
_logger = util.getModuleLogger(__name__, True)
# HOTFIX for Windows XP (Microsoft-WebDAV-MiniRedir/5.1.2600):
# When accessing a share '/dav/', XP sometimes sends digests for '/'.
# With this fix turned on, we allow '/' digests, when a matching '/dav' account
# is present.
HOTFIX_WINXP_AcceptRootShareLogin = True
class SimpleDomainController(object):
"""SimpleDomainController : Simple domain controller for HTTPAuthenticator."""
def __init__(self, dictusers = None, realmname = "SimpleDomain"):
if dictusers is None:
self._users = dict({"John Smith": "YouNeverGuessMe"})
else:
self._users = dictusers
self._realmname = realmname
def getDomainRealm(self, inputRelativeURL, environ):
return self._realmname
def requireAuthentication(self, realmname, environ):
return True
def isRealmUser(self, realmname, username, environ):
return username in self._users
def getRealmUserPassword(self, realmname, username, environ):
if username in self._users:
return self._users[username]
return None
def authDomainUser(self, realmname, username, password, environ):
if username in self._users:
return self._users[username] == password
return False
#===============================================================================
# HTTPAuthenticator
#===============================================================================
class HTTPAuthenticator(object):
"""WSGI Middleware for basic and digest authenticator."""
def __init__(self, application, domaincontroller, acceptbasic=True, acceptdigest=True, defaultdigest=True):
self._domaincontroller = domaincontroller
self._application = application
self._noncedict = dict([])
self._headerparser = re.compile(r"([\w]+)=([^,]*),")
self._headermethod = re.compile(r"^([\w]+)")
self._acceptbasic = acceptbasic
self._acceptdigest = acceptdigest
self._defaultdigest = defaultdigest
def __call__(self, environ, start_response):
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"], environ)
if not self._domaincontroller.requireAuthentication(realmname, environ):
# no authentication needed
_logger.debug("No authorization required for realm '%s'" % realmname)
environ["http_authenticator.realm"] = realmname
environ["http_authenticator.username"] = ""
return self._application(environ, start_response)
if "HTTP_AUTHORIZATION" in environ:
authheader = environ["HTTP_AUTHORIZATION"]
authmatch = self._headermethod.search(authheader)
authmethod = "None"
if authmatch:
authmethod = authmatch.group(1).lower()
if authmethod == "digest" and self._acceptdigest:
return self.authDigestAuthRequest(environ, start_response)
elif authmethod == "digest" and self._acceptbasic:
return self.sendBasicAuthResponse(environ, start_response)
elif authmethod == "basic" and self._acceptbasic:
return self.authBasicAuthRequest(environ, start_response)
util.log("HTTPAuthenticator: respond with 400 Bad request; Auth-Method: %s" % authmethod)
start_response("400 Bad Request", [("Content-Length", "0"),
("Date", util.getRfc1123Time()),
])
return [""]
if self._defaultdigest:
return self.sendDigestAuthResponse(environ, start_response)
return self.sendBasicAuthResponse(environ, start_response)
def sendBasicAuthResponse(self, environ, start_response):
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
_logger.debug("401 Not Authorized for realm '%s' (basic)" % realmname)
wwwauthheaders = "Basic realm=\"" + realmname + "\""
body = self.getErrorMessage()
start_response("401 Not Authorized", [("WWW-Authenticate", wwwauthheaders),
("Content-Type", "text/html"),
("Content-Length", str(len(body))),
("Date", util.getRfc1123Time()),
])
return [ body ]
def authBasicAuthRequest(self, environ, start_response):
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
authheader = environ["HTTP_AUTHORIZATION"]
authvalue = ""
try:
authvalue = authheader[len("Basic "):]
except:
authvalue = ""
authvalue = authvalue.strip().decode("base64")
username, password = authvalue.split(":",1)
if self._domaincontroller.authDomainUser(realmname, username, password, environ):
environ["http_authenticator.realm"] = realmname
environ["http_authenticator.username"] = username
return self._application(environ, start_response)
return self.sendBasicAuthResponse(environ, start_response)
def sendDigestAuthResponse(self, environ, start_response):
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
random.seed()
serverkey = hex(random.getrandbits(32))[2:]
etagkey = md5(environ["PATH_INFO"]).hexdigest()
timekey = str(time.time())
nonce = base64.b64encode(timekey + md5(timekey + ":" + etagkey + ":" + serverkey).hexdigest())
wwwauthheaders = "Digest realm=\"" + realmname + "\", nonce=\"" + nonce + \
"\", algorithm=\"MD5\", qop=\"auth\""
_logger.debug("401 Not Authorized for realm '%s' (digest): %s" % (realmname, wwwauthheaders))
body = self.getErrorMessage()
# start_response("403 Forbidden", [("WWW-Authenticate", wwwauthheaders),
start_response("401 Not Authorized", [("WWW-Authenticate", wwwauthheaders),
("Content-Type", "text/html"),
("Content-Length", str(len(body))),
("Date", util.getRfc1123Time()),
])
return [ body ]
def authDigestAuthRequest(self, environ, start_response):
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
isinvalidreq = False
authheaderdict = dict([])
authheaders = environ["HTTP_AUTHORIZATION"] + ","
if not authheaders.lower().strip().startswith("digest"):
isinvalidreq = True
authheaderlist = self._headerparser.findall(authheaders)
for authheader in authheaderlist:
authheaderkey = authheader[0]
authheadervalue = authheader[1].strip().strip("\"")
authheaderdict[authheaderkey] = authheadervalue
_logger.debug("authDigestAuthRequest: %s" % environ["HTTP_AUTHORIZATION"])
_logger.debug(" -> %s" % authheaderdict)
if "username" in authheaderdict:
req_username = authheaderdict["username"]
req_username_org = req_username
# Hotfix for Windows XP:
# net use W: http://127.0.0.1/dav /USER:DOMAIN\tester tester
# will send the name with double backslashes ('DOMAIN\\tester')
# but send the digest for the simple name ('DOMAIN\tester').
if r"\\" in req_username:
req_username = req_username.replace("\\\\", "\\")
_logger.info("Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username))
if not self._domaincontroller.isRealmUser(realmname, req_username, environ):
isinvalidreq = True
else:
isinvalidreq = True
# TODO: Chun added this comments, but code was commented out
# Do not do realm checking - a hotfix for WinXP using some other realm's
# auth details for this realm - if user/password match
# print authheaderdict.get("realm"), realmname
if 'realm' in authheaderdict:
if authheaderdict["realm"].upper() != realmname.upper():
if HOTFIX_WINXP_AcceptRootShareLogin:
# Hotfix: also accept '/'
if authheaderdict["realm"].upper() != "/":
isinvalidreq = True
else:
isinvalidreq = True
if "algorithm" in authheaderdict:
if authheaderdict["algorithm"].upper() != "MD5":
isinvalidreq = True # only MD5 supported
if "uri" in authheaderdict:
req_uri = authheaderdict["uri"]
if "nonce" in authheaderdict:
req_nonce = authheaderdict["nonce"]
else:
isinvalidreq = True
req_hasqop = False
if "qop" in authheaderdict:
req_hasqop = True
req_qop = authheaderdict["qop"]
if req_qop.lower() != "auth":
isinvalidreq = True # only auth supported, auth-int not supported
else:
req_qop = None
if "cnonce" in authheaderdict:
req_cnonce = authheaderdict["cnonce"]
else:
req_cnonce = None
if req_hasqop:
isinvalidreq = True
if "nc" in authheaderdict: # is read but nonce-count checking not implemented
req_nc = authheaderdict["nc"]
else:
req_nc = None
if req_hasqop:
isinvalidreq = True
if "response" in authheaderdict:
req_response = authheaderdict["response"]
else:
isinvalidreq = True
if not isinvalidreq:
req_password = self._domaincontroller.getRealmUserPassword(realmname, req_username, environ)
req_method = environ["REQUEST_METHOD"]
required_digest = self.computeDigestResponse(req_username, realmname, req_password, req_method, req_uri, req_nonce, req_cnonce, req_qop, req_nc)
if required_digest != req_response:
_logger.warning("computeDigestResponse('%s', '%s', ...): %s != %s" % (realmname, req_username, required_digest, req_response))
if HOTFIX_WINXP_AcceptRootShareLogin:
# Hotfix: also accept '/' digest
root_digest = self.computeDigestResponse(req_username, "/", req_password, req_method, req_uri, req_nonce, req_cnonce, req_qop, req_nc)
if root_digest == req_response:
_logger.warning("authDigestAuthRequest: HOTFIX: accepting '/' login for '%s'." % realmname)
else:
isinvalidreq = True
else:
isinvalidreq = True
else:
# _logger.debug("digest succeeded for realm '%s', user '%s'" % (realmname, req_username))
pass
if isinvalidreq:
_logger.warning("Authentication failed for user '%s', realm '%s'" % (req_username, realmname))
return self.sendDigestAuthResponse(environ, start_response)
environ["http_authenticator.realm"] = realmname
environ["http_authenticator.username"] = req_username
return self._application(environ, start_response)
def computeDigestResponse(self, username, realm, password, method, uri, nonce, cnonce, qop, nc):
A1 = username + ":" + realm + ":" + password
A2 = method + ":" + uri
if qop:
digestresp = self.md5kd( self.md5h(A1), nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + self.md5h(A2))
else:
digestresp = self.md5kd( self.md5h(A1), nonce + ":" + self.md5h(A2))
return digestresp
def md5h(self, data):
return md5(data).hexdigest()
def md5kd(self, secret, data):
return self.md5h(secret + ":" + data)
def getErrorMessage(self):
message = """\
<html><head><title>401 Access not authorized</title></head>
<body>
<h1>401 Access not authorized</h1>
</body>
</html>
"""
return message