Commit 4011723d authored by Just van Rossum's avatar Just van Rossum

- new version of PythonCGISlave

- new script/applet BuildCGIApplet
This largely supercedes :Mac:Demos:cgi, except for the html doc file. Should it move here? Merged with CGI_README.txt?
Todo: fullbuild support.
(jvr)
parent b7a40ba8
"""BuildCGIApplet.py -- Create a CGI applet from a Python script.
Specilized version of BuildApplet, enabling Python CGI scripts to be
used under Mac web servers like WebStar. The __main__ program is
PythonCGISlave.py, which provides a compatibility layer, emulating
Unix-style CGI scripts. See CGI_README.txt for details.
"""
import sys
import os
import macfs
import MacOS
import Res
import EasyDialogs
import buildtools
import py_resource
def main():
try:
buildcgiapplet()
except buildtools.BuildError, detail:
EasyDialogs.Message(detail)
def buildcgiapplet():
buildtools.DEBUG=1
# Find the template
# (there's no point in proceeding if we can't find it)
template = buildtools.findtemplate()
wrapper = os.path.join(sys.exec_prefix, ":Mac:Tools:CGI:PythonCGISlave.py")
# Ask for source text if not specified in sys.argv[1:]
if not sys.argv[1:]:
srcfss, ok = macfs.PromptGetFile('Select a CGI script:', 'TEXT', 'APPL')
if not ok:
return
filename = srcfss.as_pathname()
dstfilename = mkcgifilename(filename)
dstfss, ok = macfs.StandardPutFile('Save application as:',
os.path.basename(dstfilename))
if not ok:
return
dstfilename = dstfss.as_pathname()
buildone(template, wrapper, filename, dstfilename)
else:
# Loop over all files to be processed
for filename in sys.argv[1:]:
dstfilename = mkcgifilename(filename)
buildone(template, wrapper, filename, dstfilename)
def mkcgifilename(filename):
if filename[-3:] == '.py':
filename = filename[:-3]
filename = filename + ".cgi"
return filename
def buildone(template, wrapper, src, dst):
buildtools.process(template, wrapper, dst, 1)
# write source as a PYC resource into dst
ref = Res.OpenResFile(dst)
try:
Res.UseResFile(ref)
py_resource.frompyfile(src, "CGI_MAIN", preload=1)
finally:
Res.CloseResFile(ref)
if __name__ == '__main__':
main()
Python CGI under MacOS
This folder contains two tools that enable Python CGI scripts under
Mac based web servers, like WebStar, Quid Quo Pro, NetPresentz or
Apple's Personal Webserver.
Both tools emulate Unix style CGI's, allowing for cross platform
CGI scripts. In short, this happens by converting an AppleEvent sent
by the web server into os.environ dictionary entries. See below for more
details.
Both tools serve slightly different purposes:
- PythonCGISlave enables execution of Python scripts as plain *.py
text files. The web server must be configured to handle .py requests
over to PythonCGISlave. Not all web servers support that. Eg. WebStar
does, but NetPresentz does not.
- BuildCGIApplet wraps a Python CGI script in a compatibility layer, and
creates a CGI Applet which can be executed by any web server.
The pros and cons of using PythonCGISlave are (+ is good, - is bad):
+ support plain .py files, no need to wrap each script
- not supported b all servers, requires more complicated configuration
The pros and cons of using BuildCGIApplet are:
+ supported by more servers
+ less configuration troubles
- must wrap each script
Using BuildCGIApplet
Drop your CGI script onto BuildCGIApplet. An applet called <script name>.cgi
will be created. Move it to the appropriate location in the HTTP document tree.
Make sure your web server is configured to handle .cgi applet files. Usually
it is configured correctly by default, since .cgi is a standard extension.
If your CGI applet starts up for the first time, a file <applet name>.errors
is created. If your CGI script causes an exception, debug info will be written
to that file.
Using PythonCGISlave
Place the PythonCGISlave applet somewhere in the HTTP document tree. Configure
your web server so it'll pass requests for .py files to PythonCGISlave. For
Webstar, this goes roughly like this:
- in the WebStar Admin app, create a new "action", call it PYTHON, click the
"Choose" button and select our applet. Save the settings.
- go to Suffix Mappings, create a new suffix .PY, type TEXT, creator *, and
choose PYTHON in the actions popup. Save the settings.
How it works
For each Python CGI request, the web server will send an AppleEvent to the
CGI applet. Most relevant CGI parameters are taken from the AppleEvent and
get stuffed into the os.environ dictionary. Then the script gets executed.
This emulates Unix-style CGI as much as possible, so CGI scripts that are
written portably should now also work under a Mac web server.
Since the applet does not quit after each request by default, there is hardly
any startup overhead except the first time it starts up. If an exception occurs
in the CGI script, the applet will write a traceback to a file called
<applet name>.errors, and then quit. The latter seems a good idea, just in case
we leak memory. The applet will be restarted upon the next request.
Please direct feedback to <just@letterror.com> and/or <pythonmac-sig@python.org>.
"""PythonCGISlave.py
This program can be used in two ways:
- As a Python CGI script server for web servers supporting "Actions", like WebStar.
- As a wrapper for a single Python CGI script, for any "compliant" Mac web server.
See CGI_README.txt for more details.
"""
#
# Written by Just van Rossum, but partly stolen from example code by Jack.
#
LONG_RUNNING = 1 # If true, don't quit after each request.
import MacOS
MacOS.SchedParams(0, 0)
from MiniAEFrame import AEServer, MiniApplication
import os
import string
import cStringIO
import sys
import traceback
import mimetools
__version__ = '3.2'
slave_dir = os.getcwd()
# log file for errors
sys.stderr = open(sys.argv[0] + ".errors", "a+")
def convertFSSpec(fss):
return fss.as_pathname()
# AE -> os.environ mappings
ae2environ = {
'kfor': 'QUERY_STRING',
'Kcip': 'REMOTE_ADDR',
'svnm': 'SERVER_NAME',
'svpt': 'SERVER_PORT',
'addr': 'REMOTE_HOST',
'scnm': 'SCRIPT_NAME',
'meth': 'REQUEST_METHOD',
'ctyp': 'CONTENT_TYPE',
}
ERROR_MESSAGE = """\
Content-type: text/html
<html>
<head>
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code %d.
<p>Message: %s.
</body>
</html>
"""
def get_cgi_code():
# If we're a CGI wrapper, the CGI code resides in a PYC resource.
import Res, marshal
try:
code = Res.GetNamedResource('PYC ', "CGI_MAIN")
except Res.Error:
return None
else:
return marshal.loads(code.data[8:])
class PythonCGISlave(AEServer, MiniApplication):
def __init__(self):
self.crumblezone = 100000 * "\0"
MiniApplication.__init__(self)
AEServer.__init__(self)
self.installaehandler('aevt', 'oapp', self.open_app)
self.installaehandler('aevt', 'quit', self.quit)
self.installaehandler('WWW\275', 'sdoc', self.cgihandler)
self.code = get_cgi_code()
self.long_running = LONG_RUNNING
if self.code is None:
print "%s version %s, ready to serve." % (self.__class__.__name__, __version__)
else:
print "%s, ready to serve." % os.path.basename(sys.argv[0])
try:
self.mainloop()
except:
self.crumblezone = None
sys.stderr.write("- " * 30 + '\n')
self.message("Unexpected exception")
self.dump_environ()
sys.stderr.write("%s: %s\n" % sys.exc_info()[:2])
def getabouttext(self):
if self.code is None:
return "PythonCGISlave %s, written by Just van Rossum." % __version__
else:
return "Python CGI script, wrapped by BuildCGIApplet and " \
"PythonCGISlave, version %s." % __version__
def getaboutmenutext(self):
return "About %s\311" % os.path.basename(sys.argv[0])
def message(self, msg):
import time
sys.stderr.write("%s (%s)\n" % (msg, time.asctime(time.localtime(time.time()))))
def dump_environ(self):
sys.stderr.write("os.environ = {\n")
keys = os.environ.keys()
keys.sort()
for key in keys:
sys.stderr.write(" %s: %s,\n" % (repr(key), repr(os.environ[key])))
sys.stderr.write("}\n")
def quit(self, **args):
self.quitting = 1
def open_app(self, **args):
pass
def cgihandler(self, pathargs, **args):
# We emulate the unix way of doing CGI: fill os.environ with stuff.
environ = os.environ
# First, find the document root. If we don't get a DIRE parameter,
# we take the directory of this program, which may be wrong if
# it doesn't live the actual http document root folder.
if args.has_key('DIRE'):
http_root = args['DIRE'].as_pathname()
del args['DIRE']
else:
http_root = slave_dir
environ['DOCUMENT_ROOT'] = http_root
if self.code is None:
# create a Mac pathname to the Python CGI script or applet
script = string.replace(args['scnm'], '/', ':')
script_path = os.path.join(http_root, script)
else:
script_path = sys.argv[0]
if not os.path.exists(script_path):
rv = "HTTP/1.0 404 Not found\n"
rv = rv + ERROR_MESSAGE % (404, "Not found")
return rv
# Kfrq is the complete http request.
infile = cStringIO.StringIO(args['Kfrq'])
firstline = infile.readline()
msg = mimetools.Message(infile, 0)
uri, protocol = string.split(firstline)[1:3]
environ['REQUEST_URI'] = uri
environ['SERVER_PROTOCOL'] = protocol
# Make all http headers available as HTTP_* fields.
for key in msg.keys():
environ['HTTP_' + string.upper(string.replace(key, "-", "_"))] = msg[key]
# Translate the AE parameters we know of to the appropriate os.environ
# entries. Make the ones we don't know available as AE_* fields.
items = args.items()
items.sort()
for key, value in items:
if key[0] == "_":
continue
if ae2environ.has_key(key):
envkey = ae2environ[key]
environ[envkey] = value
else:
environ['AE_' + string.upper(key)] = str(value)
# Redirect stdout and stdin.
saveout = sys.stdout
savein = sys.stdin
out = sys.stdout = cStringIO.StringIO()
postdata = args.get('post', "")
if postdata:
environ['CONTENT_LENGTH'] = str(len(postdata))
sys.stdin = cStringIO.StringIO(postdata)
# Set up the Python environment
script_dir = os.path.dirname(script_path)
os.chdir(script_dir)
sys.path.insert(0, script_dir)
sys.argv[:] = [script_path]
namespace = {"__name__": "__main__"}
rv = "HTTP/1.0 200 OK\n"
try:
if self.code is None:
# we're a Python script server
execfile(script_path, namespace)
else:
# we're a CGI wrapper, self.code is the CGI code
exec self.code in namespace
except SystemExit:
# We're not exiting dammit! ;-)
pass
except:
self.crumblezone = None
sys.stderr.write("- " * 30 + '\n')
self.message("CGI exception")
self.dump_environ()
traceback.print_exc()
sys.stderr.flush()
self.quitting = 1
# XXX we should return an error AE, but I don't know how to :-(
rv = "HTTP/1.0 500 Internal error\n"
# clean up
namespace.clear()
environ.clear()
sys.path.remove(script_dir)
sys.stdout = saveout
sys.stdin = savein
if not self.long_running:
# quit after each request
self.quitting = 1
return rv + out.getvalue()
PythonCGISlave()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment