[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