[Spambayes-checkins] spambayes/spambayes Dibbler.py, 1.5, 1.6 Options.py, 1.68, 1.69 ProxyUI.py, 1.19, 1.20 UserInterface.py, 1.20, 1.21

Richie Hindle richiehindle at users.sourceforge.net
Mon Sep 1 00:07:29 EDT 2003


Update of /cvsroot/spambayes/spambayes/spambayes
In directory sc8-pr-cvs1:/tmp/cvs-serv10484/spambayes

Modified Files:
	Dibbler.py Options.py ProxyUI.py UserInterface.py 
Log Message:
HTTP-Auth support for the web interface (many thanks to Romain Guy).


Index: Dibbler.py
===================================================================
RCS file: /cvsroot/spambayes/spambayes/spambayes/Dibbler.py,v
retrieving revision 1.5
retrieving revision 1.6
diff -C2 -d -r1.5 -r1.6
*** Dibbler.py	31 Aug 2003 02:09:40 -0000	1.5
--- Dibbler.py	1 Sep 2003 06:07:27 -0000	1.6
***************
*** 170,174 ****
      import StringIO
  
! import os, sys, re, time, traceback
  import socket, asyncore, asynchat, cgi, urlparse, webbrowser
  
--- 170,174 ----
      import StringIO
  
! import os, sys, re, time, traceback, md5, base64
  import socket, asyncore, asynchat, cgi, urlparse, webbrowser
  
***************
*** 291,294 ****
--- 291,298 ----
      """
  
+     NO_AUTHENTICATION     = "None"
+     BASIC_AUTHENTICATION  = "Basic"
+     DIGEST_AUTHENTICATION = "Digest"
+ 
      def __init__(self, port=('', 80), context=_defaultContext):
          """Create an `HTTPServer` for the given port."""
***************
*** 307,310 ****
--- 311,338 ----
              self._plugins.append(plugin)
  
+     def requestAuthenticationMode(self):
+         """Override: HTTP Authentication. It should return a value among
+         NO_AUTHENTICATION, BASIC_AUTHENTICATION and DIGEST_AUTHENTICATION.
+         The two last values will force HTTP authentication respectively
+         through Base64 and MD5 encodings."""
+         return self.NO_AUTHENTICATION
+ 
+     def isValidUser(self, name, password):
+         """Override: Return True for authorized logins."""
+         return True
+ 
+     def getPasswordForUser(self, name):
+         """Override: Return the password associated to the specified user
+         name."""
+         return ''
+ 
+     def getRealm(self):
+         """Override: Specify the HTTP authentication realm."""
+         return "Dibbler application server"
+ 
+     def getCancelMessage(self):
+         """Override: Specify the cancel message for an HTTP Authentication."""
+         return "You must log in."""
+ 
  
  class _HTTPHandler(BrighterAsyncChat):
***************
*** 385,388 ****
--- 413,442 ----
              params[name] = value[0]
  
+         # Parse the headers.
+         headersRegex = re.compile('([^:]*):\s*(.*)')
+         headersDict = dict([headersRegex.match(line).groups(2)
+                            for line in headers.split('\r\n')
+                            if headersRegex.match(line)])
+         
+         # HTTP Basic/Digest Authentication support.
+         serverAuthMode = self._server.requestAuthenticationMode()
+         if serverAuthMode != HTTPServer.NO_AUTHENTICATION:
+             # The server wants us to authenticate the user.
+             authResult = False
+             authHeader = headersDict.get('Authorization')
+             if authHeader:
+                 authMatch = re.search('(\w+)\s+(.*)', authHeader)
+                 authenticationMode, login = authMatch.groups()
+ 
+                 if authenticationMode == HTTPServer.BASIC_AUTHENTICATION:
+                     authResult = self._basicAuthentication(login)
+                 elif authenticationMode == HTTPServer.DIGEST_AUTHENTICATION:
+                     authResult = self._digestAuthentication(login, method)
+                 else:
+                     print >>sys.stdout, "Unknown mode: %s" % authenticationMode
+ 
+             if not authResult:
+                 self.writeUnauthorizedAccess(serverAuthMode)
+ 
          # Find and call the methlet.  '/eggs.gif' becomes 'onEggsGif'.
          if path == '/':
***************
*** 492,495 ****
--- 546,637 ----
              content = ''
          self.push('\r\n'.join(headers) + str(content))
+ 
+     def writeUnauthorizedAccess(self, authenticationMode):
+         """Access is protected by HTTP authentication."""
+         if authenticationMode == HTTPServer.BASIC_AUTHENTICATION:
+             authString = self._getBasicAuthString()
+         elif authenticationMode == HTTPServer.DIGEST_AUTHENTICATION:
+             authString = self._getDigestAuthString()
+         else:
+             self.writeError(500, "Inconsistent authentication mode.")
+             return
+         
+         headers = []
+         headers.append('HTTP/1.0 401 Unauthorized')
+         headers.append('WWW-Authenticate: ' + authString)
+         headers.append('Connection: close')
+         headers.append('Content-Type: text/html')
+         headers.append('')
+         headers.append('')
+         self.write('\r\n'.join(headers) + self._server.getCancelMessage())
+         self.close_when_done()
+ 
+     def _getDigestAuthString(self):
+         """Builds the WWW-Authenticate header for Digest authentication."""
+         authString  = 'Digest realm="' + self._server.getRealm() + '"'
+         authString += ', nonce="' + self._getCurrentNonce() + '"'
+         authString += ', opaque="0000000000000000"'
+         authString += ', stale="false"'
+         authString += ', algorithm="MD5"'
+         authString += ', qop="auth"'
+         return authString
+ 
+     def _getBasicAuthString(self):
+         """Builds the WWW-Authenticate header for Basic authentication."""
+         return 'Basic realm="' + self._server.getRealm() + '"'
+ 
+     def _getCurrentNonce(self):
+         """Returns the current nonce value. This value is a Base64 encoding 
+         of current time plus one minute. This means the nonce will expire a
+         minute from now."""
+         timeString = time.asctime(time.localtime(time.time() + 60))
+         return base64.encodestring(timeString).rstrip('\n=')
+ 
+     def _isValidNonce(self, nonce):
+         """Check if the specified nonce is still valid. A nonce is invalid
+         when its time converted value is lower than current time."""
+         padAmount = len(nonce) % 4
+         if padAmount > 0: padAmount = 4 - padAmount
+         nonce += '=' * (len(nonce) + padAmount)
+ 
+         decoded = base64.decodestring(nonce)
+         return time.time() < time.mktime(time.strptime(decoded))
+ 
+     def _basicAuthentication(self, login):
+         """Performs a Basic HTTP authentication. Returns True when the user
+         has logged in successfully, False otherwise."""
+         userName, password = base64.decodestring(login).split(':')
+         return self._server.isValidUser(userName, password)
+ 
+     def _digestAuthentication(self, login, method):
+         """Performs a Digest HTTP authentication. Returns True when the user
+         has logged in successfully, False otherwise."""
+         def stripQuotes(s):
+             return (s[0] == '"' and s[-1] == '"') and s[1:-1] or s
+         
+         options  = dict([s.split('=') for s in login.split(", ")])
+         userName = stripQuotes(options["username"])
+         password = self._server.getPasswordForUser(userName)
+         nonce    = stripQuotes(options["nonce"])
+ 
+         # The following computations are based upon RFC 2617.
+         A1  = "%s:%s:%s" % (userName, self._server.getRealm(), password)
+         HA1 = md5.new(A1).hexdigest()
+         A2  = "%s:%s" % (method, stripQuotes(options["uri"]))
+         HA2 = md5.new(A2).hexdigest()
+ 
+         unhashedDigest = ""
+         if options.has_key("qop"):
+             unhashedDigest = "%s:%s:%s:%s:%s:%s" % \
+                             (HA1, nonce,
+                              stripQuotes(options["nc"]),
+                              stripQuotes(options["cnonce"]),
+                              stripQuotes(options["qop"]), HA2)
+         else:
+             unhashedDigest = "%s:%s:%s" % (HA1, nonce, HA2)
+         hashedDigest = md5.new(unhashedDigest).hexdigest()
+ 
+         return (stripQuotes(options["response"]) == hashedDigest and
+                 self._isValidNonce(nonce))
  
  

Index: Options.py
===================================================================
RCS file: /cvsroot/spambayes/spambayes/spambayes/Options.py,v
retrieving revision 1.68
retrieving revision 1.69
diff -C2 -d -r1.68 -r1.69
*** Options.py	28 Aug 2003 21:15:06 -0000	1.68
--- Options.py	1 Sep 2003 06:07:27 -0000	1.69
***************
*** 832,835 ****
--- 832,853 ----
       want to quickly identify mail received via a mailing list.""",
       BOOLEAN, RESTORE),
+ 
+     ("http_authentication", "HTTP Authentication", "None",
+      """This option lets you choose the security level of the web interface.
+      When selecting Basic or Digest, the user will be prompted a login and a
+      password to access the web interface. The Basic option is faster, but
+      transmits the password in clear on the network. The Digest option
+      encrypts the password before transmission.""",
+      ("None", "Basic", "Digest"), RESTORE),
+ 
+     ("http_user_name", "User name", "admin",
+      """If you activated the HTTP authentication option, you can modify the
+      authorized user name here.""",
+      r"[\w]+", RESTORE),
+ 
+     ("http_password", "Password", "admin",
+      """If you activated the HTTP authentication option, you can modify the
+      authorized user password here.""",
+      r"[\w]+", RESTORE),
    ),
  

Index: ProxyUI.py
===================================================================
RCS file: /cvsroot/spambayes/spambayes/spambayes/ProxyUI.py,v
retrieving revision 1.19
retrieving revision 1.20
diff -C2 -d -r1.19 -r1.20
*** ProxyUI.py	26 Aug 2003 04:30:41 -0000	1.19
--- ProxyUI.py	1 Sep 2003 06:07:27 -0000	1.20
***************
*** 85,88 ****
--- 85,91 ----
      ('html_ui',             'display_to'),
      ('html_ui',             'allow_remote_connections'),
+     ('html_ui',             'http_authentication'),
+     ('html_ui',             'http_user_name'),
+     ('html_ui',             'http_password'),
      ('Header Options',      None),
      ('pop3proxy',           'notate_to'),

Index: UserInterface.py
===================================================================
RCS file: /cvsroot/spambayes/spambayes/spambayes/UserInterface.py,v
retrieving revision 1.20
retrieving revision 1.21
diff -C2 -d -r1.20 -r1.21
*** UserInterface.py	30 Aug 2003 21:35:16 -0000	1.20
--- UserInterface.py	1 Sep 2003 06:07:27 -0000	1.21
***************
*** 95,98 ****
--- 95,115 ----
          print 'User interface url is http://localhost:%d/' % (uiPort)
  
+     def requestAuthenticationMode(self):
+         return options["html_ui", "http_authentication"]
+ 
+     def getRealm(self):
+         return "SpamBayes Web Interface"
+ 
+     def isValidUser(self, name, password):
+         return (name == options["html_ui", "http_user_name"] and
+                 password == options["html_ui", "http_password"])
+ 
+     def getPasswordForUser(self, name):
+         # There is only one login available in the web interface.
+         return options["html_ui", "http_password"]
+ 
+     def getCancelMessage(self):
+         return "You must login to use SpamBayes."""
+ 
  
  class BaseUserInterface(Dibbler.HTTPPlugin):





More information about the Spambayes-checkins mailing list