durusmail: quixote-users: Module to map filesystem files into Quixote, revision 3
Module to map filesystem files into Quixote, revision 3
2002-10-08
2002-10-08
Module to map filesystem files into Quixote, revision 3
Hamish Lawson
2002-10-08
Greg Ward wrote:

>Did you also fix it to defend against "../" attacks, as Jon Corbet
>subtly suggested?

I did incorporate Jon's subtle suggestion :-). I've also made generating a
folder listing optional to make it more like Jon's; by default it's not
allowed. I've also changed the default for caching to be off.


Hamish


=== filesystem.py
=============================================================

"""
SUMMARY

This module provides these classes for mapping filesystem files into Quixote:

     FilesystemFile

         Wraps a static filesystem file as a Quixore resource.

     FilesystemFolder

         Wraps a filesystem folder containing static files as a Quixote
         namespace.

     CGIScript

         Wraps a Python CGI script as a Quixote resource.


EXAMPLES

1. Mapping an individual filesystem file and caching its contents. (Because
'stylesheet.css' isn't a valid identifier name, we need to use setattr to set
it as an attribute of the current module.)

     filepath = ("/htdocs/legacy_app/stylesheet.css")
     setattr(
         sys.modules[__name__],
         "stylesheet.css",
         FilesystemFile(filepath, use_cache=1)
     )

2. Mapping a complete filesytem folder containing static files, by default
not caching their contents.

     notes = FilesystemFolder("/htdocs/legacy_app/notes")

3. Mapping a CGI script and caching the compiled script.

     filepath = CGIScript("/htdocs/legacy_app/results.cgi"
     setattr(
         sys.modules[__name__],
         "results.cgi",
         CGIScript(filepath, use_cache=1)
     )
"""

import quixote, os, sys, mimetypes, cgi, email, urllib, time
from cStringIO import StringIO


class FilesystemFile:

     """
     Wrapper for a static file on the filesystem.

     An instance is initialized with the path to the file and optionally a flag
     indicating whether the file's content should be cached. The instance can
     then be called with a request, so behaving like any Quixote object that
     models a resource.
     """

     def __init__(self, path, use_cache=0):
         self.path = path
         self.use_cache = use_cache
         self.cache = None
         # Decide the Content-Type of the file
         self.mimetype = \
                 mimetypes.guess_type(os.path.basename(path), strict=0)[0] \
                 or 'text/plain'

     def __call__(self, request):
         # Set the Content-Type for the response and return the file's
contents;
         # use caching if enabled.
         request.response.set_header('Content-Type', self.mimetype)
         if self.cache:
             contents = self.cache
         else:
             fsfile = open(self.path)
             contents = fsfile.read()
             fsfile.close()
             if self.use_cache:
                 self.cache = contents
         return contents


class FilesystemFolder:

     """
     Wrap a filesystem folder containing static files as a Quixote namespace.

     An instance is initialized with the path to the folder and optionally
a flag
     indicating whether items within the folder should be cached.
     """

     _q_exports = []

     def __init__(self, path, use_cache=0, use_folder_listing=0):
         self.path = path
         self.use_cache = use_cache
         self.cache = {}
         self.use_folder_listing = use_folder_listing

     def _q_index(self, request):
         """
         If folder listing is allowed, generate a simple HTML listing of the
         folder's contents with each item hyperlinked; if the item is a
folder,
         place a '/' after it. If not allowed, return a page to that effect.
         """
         out = StringIO()
         if self.use_folder_listing:
             template = '%s%s'
             print >>out, "

%s

" % request.environ['REQUEST_URI'] print >>out, "
"
             print >>out, template % ('..', '..', '')
             for filename in os.listdir(self.path):
                 filepath = os.path.join(self.path, filename)
                 marker = os.path.isdir(filepath) and "/" or ""
                 print >>out, template % (urllib.quote(filename), filename,
marker)
             print >>out, "
" else: print >>out, "

Folder listing denied

" print >>out, \ "

This folder does not allow its contents to be listed.

" return out.getvalue() def _q_getname(self, request, name): """ Get a file from the filesystem folder and return the FilesystemFile or FilesystemFolder wrapper of it; use caching if that is in use. """ if name in ('.', '..'): raise quixote.errors.TraversalError(name) if self.cache.has_key(name): # Get item from cache item = self.cache[name] else: # Get item from filesystem; cache it if caching is in use item_filepath = os.path.join(self.path, name) if os.path.isdir(item_filepath): item = FilesystemFolder(item_filepath, self.use_cache, \ self.use_folder_listing) elif os.path.isfile(item_filepath): item = FilesystemFile(item_filepath, self.use_cache) else: raise quixote.errors.TraversalError, name if self.use_cache: self.cache[name] = item if isinstance(item, FilesystemFolder): return item else: return item(request) class SimulatedCGIStandardInput: """ Provides a simulated stdin to CGI scripts. The data is obtained from the request.form object already created by Quixote. """ def __init__(self, request): self.request = request def read(self, length): if self.request.environ['REQUEST_METHOD'] == 'POST': return urllib.urlencode(self.request.form, doseq=1) else: return None class CGIScript: """ Wraps a Python CGI script as a Quixote resource. """ def __init__(self, filepath, use_cache=0): self.filepath = filepath self.folder, self.filename = os.path.split(filepath) self.use_cache = use_cache self.cache = None def __call__(self, request): # If the compiled script is cached, get it from there. Otherwise # read the script file and compile it; if caching is being used, # cache the compiled code. if self.cache: code = self.cache else: scriptfile = open(self.filepath) code = compile(scriptfile.read(), self.filepath, 'exec') scriptfile.close() if self.use_cache: self.cache = code # Set up the context a conventional CGI script may expect. # # If the request is a POST, Quixote will already have consumed stdin, # so we provide the CGI script with a simulated stdin that uses # the form object created by Quixote. # # We capture the script's stdout in order to return it to Quixote. # # We update os.environ to cater for the fact that a CGI script will # look for HTTP/CGI environment variables there, but Quixote stores # them in request.environ. # # We provide for two assumptions that a Python CGI script might make # about directories. First, in a conventional CGI context the web # server would set the current directory to the CGI script's location. # Second, this directory would be at the start of Python's module # search path, due to the fact that a new Python interpreter would # be started up to run the script. original_stdin = sys.stdin original_stdout = sys.stdout sys.stdin = SimulatedCGIStandardInput(request) sys.stdout = StringIO() os.environ.update(request.environ) original_cwd = os.getcwd() os.chdir(self.folder) original_sys_path = sys.path sys.path.insert(0, self.folder) # Execute the compiled CGI script and collect its output as a MIME # message (but parse only the headers). exec code parser = email.Parser.HeaderParser() mime_message = parser.parsestr(sys.stdout.getvalue()) # Restore the context that was in effect before running the script. sys.stdout = original_stdout sys.stdin = original_stdin sys.path = original_sys_path os.chdir(original_cwd) # Copy the generated headers to Quixote's response and return the body. for header, value in mime_message.items(): request.response.set_header(header, value) return str(mime_message.get_payload())
reply