Logo Search packages:      
Sourcecode: karrigell version File versions  Download package

KarrigellRequestHandler.py

00001 """Karrigell, a web programming framework in Python

Written by Pierre Quentel quentel.pierre@wanadoo.fr

Published under the GPL licence - no warranty, used at user's risk etc. See
http://www.gnu.org/copyleft/gpl.html and the file LICENCE.txt

Request handler built upon SimpleHTTPRequestHandler, except for the following
extensions :
- .py : executes the Python file in the global namespace ; sys.stdout is
redirected for writing on the HTTP output stream
- .pih : Python Inside HTML (see the PythonInsideHTML module for syntax) :
files mixing HTML and Python code, like PHP, ASP or JSP. The .pih file is
first translated into Python code, then this script is run
- .hip : HTML Inside Python : see the HIP module
- .ks : Karrigell Service (a service with different pages in one Python script)

Python programs run in a namespace including the following variables :
- QUERY : an dictionnary made out of the Query string (GET requests) or
from the request body (POST requests). The keys are the names of the form
items and the values are the item's value, as a string or as a list if the
field name ends with [] : for an <input type="text" name="info"> form item
you get the value in QUERY["info"] as a string ; for
<select multiple name="foo[]"> QUERY["foo"] will be a list
- the same form field QUERY["info"] is also available with the
shortcut _info (underscore + field name)
- HEADERS : dictionnary with the HTTP headers sent by the client
- RESPONSE : dictionnary with the HTTP headers to send in the response
- SET_COOKIE : a SimpleCookie objet (in Python Lib's Cookie module) used in
the response if set
- AUTH_USER, AUTH_PASSWORD : user identifier and password submitted in the
Authorization header if it's provided, None and None if not
- ACCEPTED_LANGUAGES : a list of languages accepted by user (accept-language
header field) ordered by preference
- REQUEST_HANDLER : the current instance of KarrigellRequestHandler
- THIS : the current instance of the Script class (see Template.py)

Python programs can also raise exceptions handled by the Karrigell
engine :
- SCRIPT_END : to terminate a script (added for readability reasons in the
.pih and .hip scripts, see examples)
- HTTP_REDIRECTION : raise HTTP_REDIRECTION,url causes the server to redirect
the client towards the given url

Session management by cookies is supported :
- in a script, a call to the Session() function returns a session object :
sessionObject=Session()
- you can then set and read attributes of this object :
    sessionObject.name=name
    name=sessionObject.name
- to end the session : sessionObject.close() erases the cookie

A maximum of 1000 simultaneous sessions is supported
"""

import __builtin__
import sys, os, string, cStringIO, traceback, base64, time, gettext, copy, imp
import Cookie, urlparse, mimetypes, BaseHTTPServer, cgi, urllib, threading

import Template, URLResolution, k_utils, debugger.k_debugger, k_session
import k_config
import modify_request
from InternationalRequestHandler import InternationalRequestHandler

__version__ = "2.2.1"

os.chdir(k_config.serverDir)

class ReloadError(Exception):
    pass

class HTTP_REDIRECTION(Exception):
    pass

class AUTH_ABORT(Exception):
    pass

class HTTP_ERROR(Exception):

    def __init__(self,code,message):
        self.code=code
        self.message=message

    def __str__(self):
        return self.code+self.message

# update mime types
mimetypes.init()
mimetypes.types_map.update({
    '': 'application/octet-stream', # Default
    '.py' : 'text/html',
    '.pih': 'text/html',
    '.hip': 'text/html',
    '.pyk': 'text/html',
    '.ks' : 'text/html'
    })
mimetypes.types_map.update(k_config.extensions_map)

# adds current directory and databases directory to sys.path,
# for imports
paths=[os.getcwd(), os.path.join(k_config.serverDir,"debugger")]

for p in paths:
    if not p in sys.path:
        sys.path.append(p)

class KarrigellRequestHandler(InternationalRequestHandler):

    server_version = "Karrigell/" + __version__
    cachemanaged   = 1      # httpd optimization
    imported = {}

    if k_config.language:
        accepted_languages.append(k_config.language)

    # dictionnary holding already loaded code
    # key=name of the file's full path
    # value=[time of last modification when loaded, Script object]
    # when a Python, pih, hip or ks file is required, if the file exists in
    # loadedScripts and has not been modified since it was loaded,
    # use the content in loadedScripts instead of loading from the file
    # system and parse the code
    loadedScripts={}

    def prepare_env(self):
        """Prepare environment for dynamic scripts"""

        # initialize the cookie
        if self.HEADERS.has_key("cookie"):
            self.SET_COOKIE=Cookie.SimpleCookie(self.HEADERS["cookie"])

        # if there is a Query String, decode it in a QUERY dictionary
        # handle the query string, if any
        self.QUERY = cgi.parse_qs(self.qs,1)

        # if the method was POST the request body is in self.body
        if self.command == 'POST':
            for key in self.body.keys():
                self.QUERY[key] = self.body[key]
                if not isinstance(self.body[key],list):
                    self.QUERY[key] = [self.body[key]]
                for i,elt in enumerate(self.QUERY[key]):
                    if not elt.filename:
                        # convert FieldStorage to string if not file upload
                        self.QUERY[key][i] = elt.value 

        self.QUERY=k_utils.applyQueryConvention(self.QUERY)
        
        self.ctype='text/html'  # default content-type

        # replace standard output by a StringIO
        # the "print" statements in scripts will write to this StringIO
        self.outputStream=cStringIO.StringIO()

        # select language
        self.get_language()
        #self.translateClassAttributes()

        # read an Authorization header if any
        # and decode it in AUTH_USER and AUTH_PASSWORD
        self.AUTH_USER,self.AUTH_PASSWORD=None,None
        if self.HEADERS.has_key('authorization'):
            basic_credentials=self.HEADERS["authorization"]
            basic_cookie=basic_credentials.split()[1]
            self.AUTH_USER,self.AUTH_PASSWORD=\
                base64.decodestring(basic_cookie).split(":")

        # initialize session object
        self.setSessionObject()

        # create the namespace in which the script is going to run
        self.nameSpace.update({
            "RESPONSE":self.RESPONSE,"HEADERS":self.HEADERS,
            "AUTH_USER":self.AUTH_USER,"AUTH_PASSWORD":self.AUTH_PASSWORD,
            "QUERY":self.QUERY,"SET_COOKIE":self.SET_COOKIE,
            "ACCEPTED_LANGUAGES":self.accepted_languages,
            "HTTP_REDIRECTION":HTTP_REDIRECTION,
            "HTTP_ERROR":HTTP_ERROR,
            "AUTH_ABORT":AUTH_ABORT,
            "SERVER_DIR":k_config.serverDir,
            "Session":self.Session,"Authentication":self.Authentication,
            "os":os,"Cookie":Cookie,"string":string})
            # frequently used modules needn't be imported in scripts

    def handle_data(self):
        """Wrap in a try/except clause for uncaught exceptions 
        to prevent crashing the server"""
        try:
            self.try_handle_data()
        except AUTH_ABORT:
            pass
        except:
            traceback.print_exc(file=sys.stderr)

    def try_handle_data(self):
        """Handle a GET or POST request"""
        sys.stdout = k_utils.Stdout()
        self.HEADERS = k_utils.CI_dict(self.headers)
            
        # a hook to modify the path and request headers
        self.path = modify_request.modify_path(self.path)
        self.HEADERS = modify_request.modify_headers(self.HEADERS)

        self.RESPONSE=k_utils.CI_dict({'Content-Type':"text/html"})  # default value
        parsed_path = urlparse.urlparse(self.path)
        self.qs = parsed_path[4]
        self.path_without_qs = parsed_path[2]
        
        # virtual hosts
        self.host = self.HEADERS.get('host',0)
        if not k_config.virtual_hosts.has_key(self.host):
            self.host = 0
        for k in k_config.virtual_hosts[self.host].keys():
            setattr(k_config,k,k_config.virtual_hosts[self.host][k])

        fileName=URLResolution.translate_path(self.path_without_qs)
        self.SET_COOKIE=Cookie.SimpleCookie()
        self.nameSpace={"REQUEST_HANDLER":self}
        self.outputStream = cStringIO.StringIO()

        # save current directory (it may be modified inside scripts)
        saveDir=os.getcwd()

        # if file doesn't exist, search for a file with same name and
        # an extension .py, .pih, .hip, .ks
        if not k_utils.exists(fileName):
            # I don't use os.path.exists() because on Windows, trailing dots
            # at the end of a file name are ignored
            if self.path in k_config.ignore:
                self.karrigellSendResponse(204,'No content')
                return
            try:
                ext=URLResolution.search(self.path_without_qs,fileName)
                fileName+=ext
                self.path_without_qs+=ext
            except IOError:
                self.send_error(404,_("No file matching url ")+self.path_without_qs)
                return
            except URLResolution.DuplicateExtensionError,msg:
                self.send_error(300,msg)
                return

        # if fileName is a directory, search an index file and redirect
        elif os.path.isdir(fileName):
            try:
                indexFile=URLResolution.indexFile(fileName)
            except URLResolution.DuplicateIndexError,msg:
                self.send_error(300, "More than one index file : %s" %msg)
                return
            except URLResolution.NoIndexError:
                # no index found : print directory listing
                if k_config.allow_directory_listing == 'none':
                    self.send_error(403,"Directory listing not allowed")
                    return
                try:
                    f = cStringIO.StringIO(
                        Template.list_directory(self.path_without_qs,fileName))
                    self.ctype = 'text/html'
                except:
                    f = cStringIO.StringIO()
                    traceback.print_exc(file = f)
                    self.ctype = 'text/plain'
                self.outputStream = f
                self.outputStream.read()    # so that tell() returns the length
                self.karrigellSendResponse(200,"Ok")
                return
            if not self.path_without_qs.endswith('/'):
                self.path=urlparse.urljoin(self.path_without_qs+'/',indexFile)
            else:
                self.path=urlparse.urljoin(self.path_without_qs,indexFile)

            # make http redirection to destination file
            self.redirect(self.path)
            return

        # if file is a ks script with no method, redirect to method 'index'
        if os.path.isfile(fileName) and fileName.lower().endswith('.ks'):
            function=URLResolution.getKsFunction(self.path)
            if not function:
                if not self.path_without_qs.endswith('/'):
                    self.path=self.path_without_qs+'/index'
                else:
                    self.path=self.path_without_qs+'index'

                # make http redirection to index file
                self.redirect(self.path)
                return

        # file exists, now work on it
        base,extension=os.path.splitext(fileName)
        extension=extension[1:]
        
        # check if extension is authorized
        # hidden extensions are specified in [Directories] hidden_extensions
        # in the configuration file
        if extension in k_config.hide_extensions:
            self.send_error(403,"You are not allowed to see " \
                "the files with this extension")
            return
            
        self.ctype, self.encoding = mimetypes.guess_type(fileName)
        self.RESPONSE['Content-Type']=self.ctype

        os.chdir(os.path.dirname(fileName))

        script = None
        # cache
        if self.cache(fileName):
            script = self.loadedScripts[fileName][1]
        # process the script according to its extension
        elif extension.lower() in Template.handled_extensions:
            # Python, pih or hip script : prepare environment and namespace
            try:
                script=Template.getScript(fileName)
                self.loadedScripts[fileName]=(os.path.getmtime(fileName),
                    script)
            except Template.ParseError,error:
                # parse error : show traceback and file content
                tb=cStringIO.StringIO()
                traceback.print_exc(file=tb)
                error_info = [self.path,fileName,tb.getvalue(),
                    error.errorLine]
                errorFileName = os.path.join(k_config.serverDir,
                    "debugger/parseErrorShow.pih")
                try:
                    err_script = Template.getScript(errorFileName)
                    out = err_script.render({'error_info':error_info}).value
                except:
                    out = cStringIO.StringIO()
                    traceback.print_exc(file=out)
                    out = out.getvalue()
                self.outputStream = cStringIO.StringIO()
                self.outputStream.write(out)
                k_utils.trace(out)
                self.karrigellSendResponse(200,'Ok')
                return
        
        if script:
            self.prepare_env()
            # add attributes to the script object
            script.url=self.path_without_qs
            script.path=self.path
            script.parent=None
            # execution
            self.execute(script)
        elif k_utils.pathInDirs(fileName,k_config.protectedDirs)[0]:
            # static file in a protected directory
            self.prepare_env()
            self.nameSpace['fileName'] = fileName
            depth = k_utils.pathInDirs(fileName,k_config.protectedDirs)[1]
            script = Template.getScript(
                os.path.join(k_config.serverDir,'dump.py'))
            script.code = 'Include("'+'../'*depth + 'AuthentScript.py");' \
                + script.code
            self.execute(script)
        else:
            # for all other extensions, just read data and send it as is
            self.testGzip()
            if not self.send_static(fileName):
                self.send_response(200,"Ok")
                f = open(fileName,'rb')
                if self.testGzip():
                    f = self.doGzip(f.read())
                    self.RESPONSE['Content-Length'] = f.tell()
                    f.seek(0)
                # send response headers
                for item in self.RESPONSE.keys():
                    self.send_header(item,self.RESPONSE[item])
                self.end_headers()
                self.copyfile(f, self.wfile)

        # in any case, restore current directory
        os.chdir(saveDir)

    def cache(self,fileName):
        # cache :
        # if file was already loaded and source has not been modified
        # since it was loaded, uses loaded code (prevents from reading
        # and parsing again)
        if self.loadedScripts.has_key(fileName):
            try:
                fileMTime=os.path.getmtime(fileName)
            except:
                return
            if self.loadedScripts[fileName][0]==fileMTime:
                return True
            elif fileName.lower().endswith('.ks'):
                # for ks script, if source has changed, remove the module
                moduleName=os.path.splitext(os.path.basename(fileName))[0]
                try:
                    del sys.modules[moduleName]
                except KeyError:
                    pass

    def send_static(self,fileName):
        """
        25/01/2005 Luca Montecchian <l.montecchiani@teamsystem.com>
        Http optimization, cache and headers for static files
        """
        # Disabled by configuration ?
        if self.cachemanaged == 0:
            return False

        s = os.stat(fileName)
        mdt = time.gmtime(s.st_mtime)
        lastModified = time.strftime("%a, %d %b %Y %H:%M:%S GMT", mdt)
        size = str(s.st_size)
        ims = self.HEADERS.get('if-modified-since',None)

        if lastModified and ims == lastModified :
            self.send_response(304)
            return True
        else:
            # populate the header  ;) 
            self.RESPONSE["Last-Modified"] = lastModified
            self.RESPONSE["Content-Length"] = size
        return False

    def execute(self,script):
        """Create an output stream and execute the script. Handle exceptions"""

        # add script directory in sys.path, so that the script can
        # import modules in the same directory
        dirname=os.path.dirname(script.name)
        if not dirname in sys.path:
            sys.path.append(dirname)    # for imports

        if not self.imported.has_key(self.host):
            self.imported[self.host] = {}
        else:
            for m in self.imported[self.host].keys():
                sys.modules[m] = self.imported[self.host][m][0]

        # self.execution is used in Include()
        self.execution=Template.ExecContext(script,self.nameSpace,
            self.path, self)

        # try to reload the imported modules
        try:
            self.reload_modules()
        except ReloadError:
            return

        try:
            self.outputStream.write(self.execution())
        # catches defined exceptions
        except HTTP_REDIRECTION,url:    # HTTP redirection towards a given URL
            self.redirect(url)
        except HTTP_ERROR,error:
            self.translateClassAttributes()
            self.send_error(error.code,error.message)
        except AUTH_ABORT:
            pass
        else:
            # if everything's ok, send response
            self.karrigellSendResponse(200,"Ok")

        # save session object
        k_session.store(self.sessionId,self.sessionObject)
        
        # find new modules (those in sys.modules
        # that were not in the initial modules)
        newModules = k_utils.new_items(sys.modules.keys(),
            initialModules)

        # complete module dictionary
        for m in newModules:
            if m =='__main__':
                pass
            elif not m in self.imported[self.host].keys() and \
                hasattr(sys.modules[m],'__file__'):
                pyfile = sys.modules[m].__file__
                if pyfile.endswith('.pyc'):
                    pyfile = pyfile[:-1]
                if pyfile.endswith('.py'):
                    self.imported[self.host][m] = [sys.modules[m],
                        pyfile,os.stat(pyfile)[8]]

        for m in newModules:
            # may seem strange, but *must* set the module to None first
            sys.modules[m] = None
            del sys.modules[m]
        sys.path = copy.copy(initial_path)

    def reload_modules(self):
        # if debug mode, force reload of the imported modules
        # whose source code has changed since last request
        if k_config.debug:
            to_reload = []
            deleted = []
            for m in sys.modules:
                if m == '__main__':
                    continue
                if m in self.imported[self.host].keys():
                    try:
                        mtime = os.stat(self.imported[self.host][m][1])[8]
                        if mtime != self.imported[self.host][m][2]:
                            to_reload.append((m,mtime))
                    except OSError:
                        deleted.append(m)
            # reload the modified modules
            for d in deleted:
                del sys.modules[d]
            for (module,mtime) in to_reload:
                try:
                    reload(sys.modules[module])
                    self.imported[self.host][module][2] = mtime
                except ImportError:
                    pass
                except:
                    # print traceback
                    self.outputStream = cStringIO.StringIO()
                    self.outputStream.write('<font face="verdana" color="red">')
                    self.outputStream.write('<b>Error reloading module %s</b>'
                        % module)
                    self.outputStream.write('</font>\n<pre>')
                    traceback.print_exc(file=self.outputStream)
                    self.outputStream.write('</pre>')
                    self.karrigellSendResponse(200,"Ok")
                    raise ReloadError

    def redirect(self,url):
        """HTTP redirection to url"""
        self.send_response(302,"Found")
        # don't forget cookies !
        for morsel in self.SET_COOKIE.values():
            self.send_header('Set-Cookie', morsel.output(header='').lstrip())
        self.send_header('Location',url)
        self.end_headers()

    def Authentication(self,testFunction,realm="Protected zone",
        errorMessage="Authentication error"):
        """Utility function for authentication
        testFunction is a user-defined function taking no argument and
        returning true if the couple (AUTH_USER,AUTH_PASSWORD) is allowed,
        false otherwise
        Example :
            def testFunction():
                return AUTH_USER=="holden" and AUTH_PASSWORD=="caulfield"

        errorMessage is the message displayed if user cancels authentication
        """
        if self.AUTH_USER:
            if not testFunction():
                self.authenticate(realm,errorMessage)
        else:
            self.authenticate(realm,errorMessage)

    def authenticate(self,realm,errorMessage):
        self.send_response(401,"Authorization")
        self.send_header("WWW-Authenticate",'Basic realm="%s"' %realm)
        self.send_header("Content-type","text/html")
        self.end_headers()
        self.wfile.write(errorMessage)
        # message if user cancels authentication request
        raise AUTH_ABORT

    def setSessionObject(self):
        """Internal method, initializes the session object
        If the client has sent a cookie named sessionId, takes its value and
        returns the corresponding SessionElement objet, stored in
        k_session.sessionDict
        Otherwise creates a new SessionElement objet and generates a random
        8-letters value sent back to the client as the value for a cookie
        called sessionId
        """
        if self.HEADERS.has_key("cookie"):
            ck=Cookie.SimpleCookie(self.HEADERS["cookie"])
            if ck.has_key("sessionId"):
                sessionId=ck["sessionId"].value
            else:
                self.SET_COOKIE=Cookie.BaseCookie()
                sessionId=k_utils.generateRandom(8)
                self.SET_COOKIE["sessionId"]=sessionId
        else:
            self.SET_COOKIE=Cookie.BaseCookie()
            sessionId=k_utils.generateRandom(8)
            self.SET_COOKIE["sessionId"]=sessionId
        self.sessionObject=k_session.getSessionObject(sessionId)
        self.sessionId=sessionId

    def Session(self):
        """Function called in scripts, retrieves the session object"""
        return self.sessionObject

    def send_error(self, code, message=None):
        """Overrides BaseHTTPServer.BaseHTTPRequestHandler's send_error with
        additional output : server version and date/time"""
        try:
            short, long = self.responses[code]
        except KeyError:
            short, long = '???', '???'
        if not message:
            message = short
        explain = long
        self.log_error("code %d, message %s", code, message)
        self.send_response(code, message)
        self.send_header("Content-Type", "text/html")
        self.end_headers()
        # translation
        gettext._translations={}
        try:
            t=gettext.translation("messages",
                os.path.join(URLResolution.serverDir,"translations"),
                self.accepted_languages)
            t.install()
        except:
            t=gettext.NullTranslations()
        self.wfile.write(self.error_message_format %
                         {'code': code,
                          'message': _(message),
                          'explain': _(explain),
                          'version': __version__,
                          'time':self.date_time_string()})

    def date_time_string(self):
        """Overrides BaseHTTPServer.BaseHTTPRequestHandler's date_time_string
        with localtime instead of GMT"""
        now = time.time()
        year, month, day, hh, mm, ss, wd, y, z = time.localtime(now)
        s = "%s, %02d %3s %4d %02d:%02d:%02d" % (
                _(self.weekdayname[wd]),
                day, self.monthname[month], year,
                hh, mm, ss)
        return s

    def log_message(self, format, *args):
        if not k_config.silent:
            BaseHTTPServer.BaseHTTPRequestHandler.log_message(self, format, *args)

    def testGzip(self):
        """Test if content should be gzipped"""
        if not k_config.gzip:
            return False
        if not gzip_support:
            return False
        accept_encoding = self.HEADERS.get('accept-encoding','').split(',')
        # if gzip is supported by the user agent,
        # and if the option gzip in the [Server] section of the
        # configuration file is set, 
        # and content type is text/ or javascript, 
        # set Content-Encoding to 'gzip' and return True
        if 'gzip' in accept_encoding and \
            self.ctype and (self.ctype.startswith('text/') or 
            self.ctype=='application/x-javascript'):
            self.RESPONSE['Content-Encoding']='gzip'
            return True
        return False

    def doGzip(self,data):
        """gzip data and return a StringIO holding the gzipped data"""
        sio = cStringIO.StringIO()
        gzf = gzip.GzipFile(fileobj = sio, mode = "wb")
        gzf.write(data)
        gzf.close()
        return sio

    def karrigellSendResponse(self,code,message):
        self.send_response(code,message)
        # sends the cookie before the other headers, seems to be required ?
        if self.SET_COOKIE.has_key("sessionId") \
            and not k_session.sessionDict.has_key(self.sessionId):
            # if session was closed, set expiration time of cookie to 0
            self.SET_COOKIE["sessionId"]=self.sessionId
            self.SET_COOKIE["sessionId"]["expires"]=0
        if self.SET_COOKIE:
            # Cookies should be header items, rather than pushed out to the
            # main stream.
            for morsel in self.SET_COOKIE.values():
                self.send_header('Set-Cookie', morsel.output(header='').lstrip())
        # test if content should be gzipped
        if self.testGzip():
            self.outputStream = self.doGzip(self.outputStream.getvalue())
        # set Content-Length header
        self.RESPONSE['Content-Length']=self.outputStream.tell()
        # send response headers
        for item in self.RESPONSE.keys():
            self.send_header(item,self.RESPONSE[item])
        self.end_headers()
        # output stream is written on the socket fileobject
        self.outputStream.seek(0)
        self.copyfile(self.outputStream, self.wfile)

# for internationalization
t=gettext.NullTranslations()
t.install()

# Python may not have gzip support
gzip_support = False
try:
    import gzip
    gzip_support = True
except ImportError:
    print "Warning - gzip is not supported"
    pass

initial_path = copy.copy(sys.path)
initialModules = copy.copy(sys.modules.keys())
initialModules.sort()

Generated by  Doxygen 1.6.0   Back to index