"""Script server based on SimpleHTTPServer
Handles GET and POST requests, in-memory session management,
HTTP redirection
Python scripts are executed in a namespace made of :
- request : for the data received from the query string or the request body.
Calling 'http://host/myScript.py?foo=bar' will make
request = {'foo':['bar']} available in the namespace of myScript
- headers : the http request headers
- resp_headers : the http response headers
- Session() : a function returning the session object
- HTTP_REDIRECTION : an exception to raise if the script wants to redirect
to a specified URL (raise HTTP_REDIRECTION, url)
A simple templating system is provided, using the Python string substitution
mechanism introduced in Python 2.4 (syntax $name). Template files must have
the extension .tpl
Hello world programs : will print "Hello world !" if called with the query
string ?name=world
- hello.py (Python script) [ http://localhost/hello.py?name=world ]
print "Hello",request['name'][0],"!"
- hello.tpl (template) [ http://localhost/hello.tpl?name=world ]
Hello $name !
Other extensions can be handled by adding methods self.run_(extension)
"""
import sys
import os
import string
import cStringIO
import random
import cgi
import select
import SimpleHTTPServer
import Cookie
chars = string.ascii_letters + string.digits
sessionDict = {} # dictionary mapping session id's to session objects
class SessionElement(object):
"""Arbitrary objects, referenced by the session id"""
pass
def generateRandom(length):
"""Return a random string of specified length (used for session id's)"""
return ''.join([random.choice(chars) for i in range(length)])
class HTTP_REDIRECTION(Exception):
pass
class ScriptRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
"""One instance of this class is created for each HTTP request"""
def do_GET(self):
"""Begin serving a GET request"""
# build self.body from the query string
self.body = {}
if self.path.find('?')>-1:
qs = self.path.split('?',1)[1]
self.body = cgi.parse_qs(qs, keep_blank_values=1)
self.handle_data()
def do_POST(self):
"""Begin serving a POST request. The request data is readable
on a file-like object called self.rfile"""
ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
length = int(self.headers.getheader('content-length'))
if ctype == 'multipart/form-data':
self.body = cgi.parse_multipart(self.rfile, pdict)
elif ctype == 'application/x-www-form-urlencoded':
qs = self.rfile.read(length)
self.body = cgi.parse_qs(qs, keep_blank_values=1)
else:
self.body = {} # Unknown content-type
# some browsers send 2 more bytes...
[ready_to_read,x,y] = select.select([self.connection],[],[],0)
if ready_to_read:
self.rfile.read(2)
self.handle_data()
def handle_data(self):
"""Process the data received"""
self.resp_headers = {"Content-type":'text/html'} # default
self.cookie=Cookie.SimpleCookie()
if self.headers.has_key('cookie'):
self.cookie=Cookie.SimpleCookie(self.headers.getheader("cookie"))
path = self.get_file() # return a file name or None
if os.path.isdir(path):
# list directory
dir_list = self.list_directory(path)
self.copyfile(dir_list, self.wfile)
return
ext = os.path.splitext(path)[1].lower()
if len(ext)>1 and hasattr(self,"run_%s" %ext[1:]):
# if run_some_extension() exists
exec ("self.run_%s(path)" %ext[1:])
else:
# other files
ctype = self.guess_type(path)
if ctype.startswith('text/'):
mode = 'r'
else:
mode = 'rb'
try:
f = open(path,mode)
self.resp_headers['Content-type'] = ctype
self.resp_headers['Content-length'] = str(os.fstat(f.fileno())[6])
self.done(200,f)
except IOError:
self.send_error(404, "File not found")
def done(self, code, infile):
"""Send response, cookies, response headers
and the data read from infile"""
self.send_response(code)
for morsel in self.cookie.values():
self.send_header('Set-Cookie', morsel.output(header='').lstrip())
for (k,v) in self.resp_headers.items():
self.send_header(k,v)
self.end_headers()
infile.seek(0)
self.copyfile(infile, self.wfile)
def get_file(self):
"""Set the Content-type header and return the file open
for reading, or None"""
path = self.path
if path.find('?')>1:
# remove query string, otherwise the file will not be found
path = path.split('?',1)[0]
path = self.translate_path(path)
if os.path.isdir(path):
for index in "index.html", "index.htm":
index = os.path.join(path, index)
if os.path.exists(index):
path = index
break
return path
def run_py(self, script):
"""Run a Python script"""
# redirect standard output so that the "print" statements
# in the script will be sent to the web browser
sys.stdout = cStringIO.StringIO()
# build the namespace in which the script will be run
namespace = {'request':self.body, 'headers' : self.headers,
'resp_headers':self.resp_headers, 'Session':self.Session,
'HTTP_REDIRECTION':HTTP_REDIRECTION}
try:
execfile (script,namespace)
except HTTP_REDIRECTION,url:
self.resp_headers['Location'] = url
self.done(301,cStringIO.StringIO())
except:
# print a traceback
# first reset the output stream
sys.stdout = cStringIO.StringIO()
exc_type,exc_value,tb=sys.exc_info()
msg = exc_value.args[0]
if tb.tb_next is None: # errors (detected by the parser)
line = exc_value.lineno
text = exc_value.text
else: # exceptions
line = tb.tb_next.tb_lineno
text = open(script).readlines()[line-1]
print '%s in file %s : %s' %(exc_type.__name__,
os.path.basename(script), cgi.escape(msg))
print '
Line %s' %line
print '
%s' %cgi.escape(text) self.resp_headers['Content-length'] = sys.stdout.tell() self.done(200,sys.stdout) def run_tpl(self,script): """Templating system with the string substitution syntax introduced in Python 2.4""" # values must be strings, not lists dic = dict([ (k,v[0]) for k,v in self.body.items() ]) # first check if the string.Template class is available if hasattr(string,"Template"): # Python 2.4 or above try: data = string.Template(open(script).read()).substitute(dic) except: exc_type,exc_value,tb=sys.exc_info() msg = exc_value.args[0] data = '%s in file %s : %s' \ %(exc_type.__name__,os.path.basename(script), cgi.escape(msg)) else: data = "Unable to handle this syntax for " + \ "string substitution. Python version must be 2.4 or above" self.resp_headers['Content-length'] = len(data) self.done(200,cStringIO.StringIO(data)) def Session(self): """Session management If the client has sent a cookie named sessionId, take its value and return the corresponding SessionElement objet, stored in sessionDict Otherwise create a new SessionElement objet and generate a random 8-letters value sent back to the client as the value for a cookie called sessionId""" if self.cookie.has_key("sessionId"): sessionId=self.cookie["sessionId"].value else: sessionId=generateRandom(8) self.cookie["sessionId"]=sessionId try: sessionObject = sessionDict[sessionId] except KeyError: sessionObject = SessionElement() sessionDict[sessionId] = sessionObject return sessionObject if __name__=="__main__": # launch the server on the specified port import SocketServer port = 80 s=SocketServer.TCPServer(('',port),ScriptRequestHandler) print "ScriptServer running on port %s" %port s.serve_forever()