"""
import logging
-import time
import re
import base64
import binascii
-from ganeti import constants
from ganeti import utils
from ganeti import http
from cStringIO import StringIO
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import new as md5
+
# Digest types from RFC2617
HTTP_BASIC_AUTH = "Basic"
HTTP_DIGEST_AUTH = "Digest"
# Not exactly as described in RFC2616, section 2.2, but good enough
-_NOQUOTE = re.compile(r"^[-_a-z0-9]$", re.I)
+_NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I)
def _FormatAuthHeader(scheme, params):
class HttpServerRequestAuthentication(object):
# Default authentication realm
- AUTH_REALM = None
+ AUTH_REALM = "Unspecified"
+
+ # Schemes for passwords
+ _CLEARTEXT_SCHEME = "{CLEARTEXT}"
+ _HA1_SCHEME = "{HA1}"
def GetAuthRealm(self, req):
"""Returns the authentication realm for a request.
- MAY be overriden by a subclass, which then can return different realms for
- different paths. Returning "None" means no authentication is needed for a
- request.
+ May be overridden by a subclass, which then can return different realms for
+ different paths.
@type req: L{http.server._HttpServerRequest}
@param req: HTTP request context
- @rtype: str or None
+ @rtype: string
@return: Authentication realm
"""
+ # today we don't have per-request filtering, but we might want to
+ # add it in the future
+ # pylint: disable-msg=W0613
return self.AUTH_REALM
+ def AuthenticationRequired(self, req):
+ """Determines whether authentication is required for a request.
+
+ To enable authentication, override this function in a subclass and return
+ C{True}. L{AUTH_REALM} must be set.
+
+ @type req: L{http.server._HttpServerRequest}
+ @param req: HTTP request context
+
+ """
+ # Unused argument, method could be a function
+ # pylint: disable-msg=W0613,R0201
+ return False
+
def PreHandleRequest(self, req):
"""Called before a request is handled.
@param req: HTTP request context
"""
+ # Authentication not required, and no credentials given?
+ if not (self.AuthenticationRequired(req) or
+ (req.request_headers and
+ http.HTTP_AUTHORIZATION in req.request_headers)):
+ return
+
realm = self.GetAuthRealm(req)
- # Authentication required?
- if realm is None:
- return
+ if not realm:
+ raise AssertionError("No authentication realm")
# Check "Authorization" header
if self._CheckAuthorization(req):
def Authenticate(self, req, user, password):
"""Checks the password for a user.
- This function MUST be overriden by a subclass.
+ This function MUST be overridden by a subclass.
"""
raise NotImplementedError()
+ def VerifyBasicAuthPassword(self, req, username, password, expected):
+ """Checks the password for basic authentication.
+
+ As long as they don't start with an opening brace ("E{lb}"), old passwords
+ are supported. A new scheme uses H(A1) from RFC2617, where H is MD5 and A1
+ consists of the username, the authentication realm and the actual password.
+
+ @type req: L{http.server._HttpServerRequest}
+ @param req: HTTP request context
+ @type username: string
+ @param username: Username from HTTP headers
+ @type password: string
+ @param password: Password from HTTP headers
+ @type expected: string
+ @param expected: Expected password with optional scheme prefix (e.g. from
+ users file)
+
+ """
+ # Backwards compatibility for old-style passwords without a scheme
+ if not expected.startswith("{"):
+ expected = self._CLEARTEXT_SCHEME + expected
+
+ # Check again, just to be sure
+ if not expected.startswith("{"):
+ raise AssertionError("Invalid scheme")
+
+ scheme_end_idx = expected.find("}", 1)
+
+ # Ensure scheme has a length of at least one character
+ if scheme_end_idx <= 1:
+ logging.warning("Invalid scheme in password for user '%s'", username)
+ return False
+
+ scheme = expected[:scheme_end_idx + 1].upper()
+ expected_password = expected[scheme_end_idx + 1:]
+
+ # Good old plain text password
+ if scheme == self._CLEARTEXT_SCHEME:
+ return password == expected_password
+
+ # H(A1) as described in RFC2617
+ if scheme == self._HA1_SCHEME:
+ realm = self.GetAuthRealm(req)
+ if not realm:
+ # There can not be a valid password for this case
+ raise AssertionError("No authentication realm")
+
+ expha1 = md5()
+ expha1.update("%s:%s:%s" % (username, realm, password))
+
+ return (expected_password.lower() == expha1.hexdigest().lower())
+
+ logging.warning("Unknown scheme '%s' in password for user '%s'",
+ scheme, username)
+
+ return False
+
class PasswordFileUser(object):
"""Data structure for users from password file.