"A CGI Framework in Python" by A. M. Kuchling Web Techniques, February 1998 Web Techniques grants permission to use these listings and code for private or commercial use provided that credit to Web Techniques and the author is maintained within the comments of the source. For questions, contact editors@web-techniques.com. This file consists of five listings that accompany the article " A CGI Framework in Python ", published in the February 1998 issue of Web Techniques: LISTING ONE #!/usr/bin/python # showpage.cgi : The top-level driver for our development framework import os, sys # Ensure that a path has been provided if not os.environ.has_key('PATH_INFO'): print "Content-type: text/html\n" print "No path information provided for showpage.cgi." sys.exit(0) # Determine the root of the server's document tree try: doc_root = os.environ['DOCUMENT_ROOT'] except KeyError: doc_root = '/www/' # os.path.join combines a path and a filename and returns # the resulting filename. filename = os.environ['PATH_INFO'] filename = os.path.join(doc_root, filename[1:] ) # Set up the import cgi headers = {'Content-type':'text/html'} webvars = cgi.FieldStorage() namespace = { 'headers': headers, 'webvars': webvars, 'environ': os.environ } import StringIO real_stdout = sys.stdout sys.stdout = StringIO.StringIO() try: execfile(filename, namespace) except: # The page's code raised some sort of exception. # Write information about the crash to a file, and output # an error page real_stdout.write("""Content-type: text/html
The site is having technical difficulties with this page. An error has been logged, and the problem will be fixed as soon as possible. Sorry!""") import traceback, time sys.stderr.write('Error while executing script %s\n' % (filename) ) traceback.print_exc(file=sys.stderr) sys.exit(0) else: for header, value in headers.items(): real_stdout.write("%s: %s\n" % (header, value) ) real_stdout.write('\n') real_stdout.write( sys.stdout.getvalue() ) LISTING TWO # User registration module Error = "User.Error exception" # The following function will open a GDBM database and return the # database object; a module-level private variable is used so that # the file will only be opened once. _gdbmDB = None def _openDB(): global _gdbmDB if _gdbmDB == None: import gdbm _gdbmDB = gdbm.open('userDatabase', 'c', 0600) return _gdbmDB class User: "The fundamental class that represents a user" # The constructor for a User object; if more fields were # added, they should be initialized here. def __init__(self, userID="", password=""): self.userID, self.password = userID, password def getCookie(self): "Produce the string required to set a cookie containing a login token" token = makeToken(self.userID) return 'userID=%s; path=/' % (token,) def save(self): "Save the object's current state into the database" import pickle db = _openDB() db[ self.userID ] = pickle.dumps(self) # This is some super-secret information, hashed together # with the userID to generate a (hopefully) unforgeable # token. It's some random bytes from /dev/urandom on my Linux box. _secretInfo = "\016\327\132\154\215\256\373\023\362\132\047\023" def makeToken(userID): "Given a user ID, compute the hash and return the resulting token" import md5, base64, string digest = md5.new(_secretInfo + userID + _secretInfo).digest() # The token will contain the user ID and the digest, separated by / token = userID + '/' + digest # To avoid illegal characters in the digest, the token is base-64 encoded. token = base64.encodestring(token) # base64.encodestring adds a newline to its result, so we have to # strip it off return string.strip(token) def checkToken(token): """Given a user ID and a token, verify that the digest portion matches the user ID. Returns false if it fails, and the user ID if it succeeds.""" import md5, base64, string # Undo the base-64 encoding token = base64.decodestring(token+'\n') slash = string.find(token, '/') if slash == -1: return "" # Malformed authentication token # Check that the digest matches. userID = token[:slash] digest = md5.new(_secretInfo + userID + _secretInfo).digest() if digest!=token[slash+1:]: return "" # Digest portion of token doesn't match # The token checks out. return userID def createUser(userID, password, headers): """Create a new user with the given ID and password. Raises User.Error if the name's already taken, or if the password fails some simple checks. A cookie is handed out containing the authentication token.""" import regex db = _openDB() if db.has_key(userID): raise Error, "There's already a user by that name." elif password == "": raise Error, "You can't leave the password empty." elif password == userID: raise Error, "It's a bad idea to use your ID as your password." elif regex.match('^[A-Za-z_0-9]+$', userID) == -1: raise Error, "User IDs can only contain letters, numbers, and underscores." # It looks like it's OK to create the user, so create a new User # object and call its save() method, telling the object to store itself # in the database. userObj = User(userID, password) userObj.save() token = makeToken(userID) headers['Set-Cookie'] = userObj.getCookie() return userObj def loginUser(userID, password, headers): """Log in a user; this requires checking the password, and handing out a cookie containing the authentication token.""" db = _openDB() import pickle if not db.has_key(userID): raise Error, "There's either no such user, or the password is incorrect." userObj = pickle.loads( db[userID] ) if password != userObj.password: raise Error, "There's either no such user, or the password is incorrect." # It's OK, so drop a cookie on the user headers['Set-Cookie'] = userObj.getCookie() return userObj def getCurrentUser(environ): """Read the cookie to determine the user's ID, and returns their User object. Returns None if no ID could be determined.""" try: cookie = environ['HTTP_COOKIE'] except KeyError: return None # No cookie present # There's a complete cookie module available for Python, but we'll # just use a regex to get the cookie's value. The following # regular expression looks kind of ugly because the regex module # uses an Emacs-like regex syntax. In Python 1.5, the new re # module will provide regular expressions that support almost all # the features of Perl's regexes. import regex IDpat = regex.compile('userID[ \t]*=[ \t]*\([^ \t;]*\)') if IDpat.search(cookie) == -1: # No match for the regex could be found return None # Get the token, and verify that it's correct. If it is, token = IDpat.group(1) userID = checkToken(token) if userID: db = _openDB() import pickle if not db.has_key(userID): return None return pickle.loads( db[userID] ) LISTING THREE # register.py : Create a new user object import User if not (webvars.has_key('userID') and webvars.has_key('password') ): print "
You must fill in both the user ID and the password fields." else: userID, password = webvars['userID'].value, webvars['password'].value try: userObj = User.createUser(userID, password, headers) except User.Error, message: print "
", message else: print "
Thank you for joining." LISTING FOUR # login.py: Log in as an existing user. This file is almost identical # to register.py, except that it calls User.loginUser() and # not User.createUser(), and the success message is different. import User if not (webvars.has_key('userID') and webvars.has_key('password') ): print "
You must fill in both the user ID and the password fields." else: userID, password = webvars['userID'].value, webvars['password'].value try: userObj = User.loginUser(userID, password, headers) except User.Error, message: print "
", message else: print "
Welcome back, %s!" % (userObj.userID,) LISTING FIVE # info.py : A sample page that demonstrates how to find out # who the current user is. import User userObj = User.getCurrentUser(environ) if userObj != None: print "
This page would be customized for user '%s'." % (userObj.userID,) else: print """
You're not logged in as a registered user, so you'd see this page without any customizations."""