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