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())