""" (C) Gregory Trubetskoy May 1998 This file is part of Httpdapy. See COPYRIGHT for Copyright. Original concept and first code by Aaron Watters from "Internet Programming with Python" by Aaron Watters, Guido Van Rossum and James C. Ahlstrom, ISBN 1-55851-484-8 ============================================================== HOW THIS MODULE WORKS 1. At server startup time, it (or rather a server-specific module which imports this module right away) is imported by the Python interpreter embedded in the server and then the init() function is called. Within init(), a Python object of CallBack class is created and a variable holding a reference to it is set by init() using an internal module called httpdapi_hook. This reference is retained for the lifetime of the server and is used by the server to service requests. 2. When an HTTP request comes in the server determines if this is a Python request. This is done differently on different servers (mime types on Netscape or srm configuraition on Apache) but is always based on the file extension. If this is a Python request, then httpd will call the Service() function of the callback object whose refernce it holds from step 1 above. The Service() function will: get the module name from the URI, import that module, instantiate the RequestHandler object and call its Handle() method passing it parameter block, session and request objects. These objects hold various information about the request similar to what you would find in CGI environment variables. To get a better idea of what is where, look at the output of the httpdapitest.py - it shows all the variables. For in-depth documentation, look at developer.netscape.com. For example, http://localhost/home/myscript.pye will result in the equivalent of: >>> import myscript >>> hr = myscript.RequestHandler( pb, sn, rq ) >>> hr.Handle() Handle() in turn calls the following methods in the following sequence: Content() Header() Status() Send() You can override any one of these to provide custom headers, alter the status and send out the text. At the very least, you'll have to override Content(). Here is a minimal module: import httpdapi class RequestHandler( httpdapi.RequestHandler ): def Content( self ): return "

Hello World!

" Here is a more elaborate one: import httpdapi class RequestHAndler( httpdapi.RequestHandler ): def Content( self ): self.redirect = "http://www.python.org" return "Your browser doesn't understand redirects!'" Here is how to get form data (doesn't matter POST or GET): --snip-- method = self.rq.reqpb['method'] try: if method == 'POST': fdlen = atoi( self.rq.request_header( "content-length", self.sn ) ) fd = cgi.parse_qs( self.sn.form_data( fdlen ) ) else: fd = cgi.parse_qs( self.rq.reqpb['query'] ) --snip-- To cause errors, you can raise SERVER_RETURN with a pair (return_code, status) at any point. If status is not None it will serve as the protocol_status, the return_code will be used as the return code returned to the server-interface: # Can't find the file! raise SERVER_RETURN, (REQ_ABORTED, PROTOCOL_NOT_FOUND) or to simply give up (eg, if the response already started): raise SERVER_RETURN, (REQ_ABORTED, None) 3. You can also do authentication in Python. In this case AuthTrans() function of the callback object is called. The AuthTrans function will: get the module name from the configuration, import that module, instantiate the AuthHandler object and call its Handle() method passing it parameter block, session and request objects: Handle() can return any of these: REQ_NOACTION - ask password again REQ_ABORTED - Server Error REQ_PROCEED - OK You can also set the status to give out other responses, This will show "Forbidden" on the browser: self.rq.protocol_status(self.sn, httpdapi.PROTOCOL_FORBIDDEN) return httpdapi.REQ_ABORTED Here is a minimal module that lets grisha/mypassword in: import httpdapi class AuthHandler( httpdapi.AuthHandler ): def Handle( self ): user = self.rq.vars["auth-user"] pw = self.rq.vars["auth-password"] if user == 'grisha' and pw == 'mypassword': return httpdapi.REQ_PROCEED else: return httpapi.REQ_NOACTION That's basically it... """ import sys import string import traceback import time SERVER_RETURN = "SERVER_RETURN" REQ_PROCEED = "REQ_PROCEED" REQ_ABORTED = "REQ_ABORTED" REQ_NOACTION = "REQ_NOACTION" REQ_EXIT = "REQ_EXIT" # Response status codes for use with rq.protocol_status(sn, *) PROTOCOL_OK = "PROTOCOL_OK" PROTOCOL_REDIRECT = "PROTOCOL_REDIRECT" PROTOCOL_NOT_MODIFIED = "PROTOCOL_NOT_MODIFIED" PROTOCOL_BAD_REQUEST = "PROTOCOL_BAD_REQUEST" PROTOCOL_UNAUTHORIZED = "PROTOCOL_UNAUTHORIZED" PROTOCOL_FORBIDDEN = "PROTOCOL_FORBIDDEN" PROTOCOL_NOT_FOUND = "PROTOCOL_NOT_FOUND" PROTOCOL_SERVER_ERROR = "PROTOCOL_SERVER_ERROR" PROTOCOL_NOT_IMPLEMENTED = "PROTOCOL_NOT_IMPLEMENTED" class CallBack: """ A generic callback object. """ def __init__( self, rootpkg=None ): """ Constructor. if self.debug is not 0, the Python error output will be sent to the browser - very useful for debugging. """ self.root = rootpkg # don't change this here self.debug = 0 def Service(self, pb, sn, rq): """ This method is envoked by nsapi module for each request. The return value is one of the REQ constants above. REQ_PROCEED means OK. """ (self.pb, self.sn, self.rq) = (pb, sn, rq) # be pessimistic result = REQ_ABORTED try: handler = self.get_request_handler() result = handler.Handle() except SERVER_RETURN, value: # SERVER_RETURN indicates a non-local abort from below # with value as (result, status) or (result, None) try: (result, status) = value if status: rq.protocol_status(sn, status) except: pass except KeyboardInterrupt: # This is a special case, meaning # protocol_start_response() C function returned # REQ_NOACTION, so we play along, it's OK. result = REQ_PROCEED except: # Any other rerror if self.debug : result = self.ReportError(sys.exc_type, sys.exc_value, sys.exc_traceback) else: result = REQ_ABORTED # lest we waste memory, always clear traceback sys.last_traceback = None return result def AuthTrans(self, pb, sn, rq): """ This method is envoked for each *AuthTrans* request. REQ_PROCEED means OK REQ_NOACTION means Ask Again REQ_ABORTED means Server Error """ (self.pb, self.sn, self.rq) = (pb, sn, rq) # be pessimistic result = REQ_ABORTED # get URI try: module_name = self.pb.findval( "userdb" ) except: raise ValueError, "Failed to locate auth module name" # debugging? if module_name[-5:] == 'DEBUG': self.debug = 1 module_name = module_name[:-5] else: self.debug = 0 # if we're using packages if self.root: module_name = self.root + "." + module_name # try to import the module try: # we could use __import__ but it can't handle packages exec "import " + module_name module = eval( module_name ) # if module extension ends with DEBUG reload it if self.debug : module = reload( module ) # get the Handler class handler = module.AuthHandler( self.pb, self.sn, self.rq ) result = handler.Handle() except: pass # lest we waste memory, always clear traceback sys.last_traceback = None return result def Log(self, str): log( str ) def get_request_handler( self ): """ Get the module and the object to handle the request. This function is called by Service(). The module is extracted from the URI. Depending on the last letter of the extension self.debug is set: .pyd - debugging ON .pye - debugging OFF When envoked with .pyd, the module is *reloaded* for every request, otherwise, it follows normal Python behaviour - "import" loads a module only once. """ # get URI try: uri = self.rq.reqpb.findval( "uri" ) except: raise ValueError, "Failed to locate URI in rq.reqpb" # debugging? if uri[-1:] == 'd' : self.debug = 1 else: self.debug = 0 # find the module name by getting the string between the # last slash and the last dot. slash = string.rfind( uri, "/" ) dot = string.rfind( uri, "." ) module_name = uri[ slash + 1 : dot ] # if we're using packages if self.root: module_name = self.root + "." + module_name # try to import the module try: # we could use __import__ but it can't handle packages exec "import " + module_name module = eval( module_name ) # if module extension ends with a d reload it if self.debug : module = reload( module ) # get the Handler class Class = module.RequestHandler except (ImportError, AttributeError, SyntaxError): if self.debug : # pass it on raise sys.exc_type, sys.exc_value else: # show and HTTP error raise SERVER_RETURN, (REQ_ABORTED, PROTOCOL_FORBIDDEN) # construct and return an instance of the handler class result = Class( self.pb, self.sn, self.rq ) return result def ReportError(self, etype, evalue, etb): """ This function is only used when debugging is on. It sends the output similar to what you'd see when using Python interactively to the browser """ (sn, rq) = (self.sn, self.rq) srvhdrs = self.rq.srvhdrs # replace magnus-internal/X-python-e with text/html srvhdrs.pblock_remove("content-type") srvhdrs.nvinsert("content-type", "text/html") rq.protocol_status(sn, PROTOCOL_OK) rq.start_response(sn) text = "

A Python Error Happened:

"
	for e in traceback.format_exception( etype, evalue, etb ):
	    text = text + e + '\n'
	text = text + "
" sn.net_write( text ) return "REQ_PROCEED" def log( s, kind='info:' ): """ This adds a timestamp and writes to the log file """ if logfile: t = time.strftime( '[%d/%b/%Y:%H:%M:%S]', time.localtime( time.time() ) ) f = open( logfile, 'a' ) f.write( '%s %s %s\n' % ( t, kind, str( s ) ) ) f.close() def init( logname=None, rootpkg=None ): """ This function is called by the server at startup time If you want logging, give a full path to the logfile. """ global logfile logfile = logname # create a callback object obCallBack = CallBack( rootpkg ) import httpdapi_hook # "give it back" to the server httpdapi_hook.SetCallBack( obCallBack ) class RequestHandler: """ A superclass that may be used to create RequestHandlers in other modules, for use with this module. """ def __init__( self, pb, sn, rq ): ( self.pb, self.sn, self.rq ) = ( pb, sn, rq ) # default content-type self.content_type = 'text/html' # no redirect self.redirect = '' def Send( self, content ): self.rq.start_response( self.sn ) self.sn.net_write( str( content ) ) def Header( self ): """ This prepares the headers """ srvhdrs = self.rq.srvhdrs # content-type srvhdrs.pblock_remove("content-type") srvhdrs.nvinsert("content-type", self.content_type) # for redirects, add Location header if self.redirect: srvhdrs.nvinsert("Location", self.redirect) # add a silly header, for fun. srvhdrs.nvinsert("x-grok-this", "Python-psychobabble") def Status( self ): """ The status is set here. """ if self.redirect: self.rq.protocol_status( self.sn, PROTOCOL_REDIRECT ) else: self.rq.protocol_status( self.sn, PROTOCOL_OK ) def Handle( self ): """ This method handles the request. Although, you may be tempted to override this method, you should consider overriding Content() first, it may be all you need. """ try: content = self.Content() self.Header() self.Status() self.Send( content ) except: # debugging ? uri = self.rq.reqpb.findval("uri") if uri[-1:] == 'd': raise sys.exc_type, sys.exc_value return REQ_ABORTED return REQ_PROCEED def Content( self ): """ For testing and reference """ return "Welcome to Httpdapi!" class AuthHandler( RequestHandler ): def Handle( self ): return REQ_PROCEED