Just van Rossum | 4011723 | 2000-03-28 12:05:13 +0000 | [diff] [blame] | 1 | """PythonCGISlave.py |
| 2 | |
| 3 | This program can be used in two ways: |
| 4 | - As a Python CGI script server for web servers supporting "Actions", like WebStar. |
| 5 | - As a wrapper for a single Python CGI script, for any "compliant" Mac web server. |
| 6 | |
| 7 | See CGI_README.txt for more details. |
| 8 | """ |
| 9 | |
| 10 | # |
| 11 | # Written by Just van Rossum, but partly stolen from example code by Jack. |
| 12 | # |
| 13 | |
| 14 | |
| 15 | LONG_RUNNING = 1 # If true, don't quit after each request. |
| 16 | |
| 17 | |
| 18 | import MacOS |
| 19 | MacOS.SchedParams(0, 0) |
| 20 | from MiniAEFrame import AEServer, MiniApplication |
| 21 | |
| 22 | import os |
| 23 | import string |
| 24 | import cStringIO |
| 25 | import sys |
| 26 | import traceback |
| 27 | import mimetools |
| 28 | |
| 29 | __version__ = '3.2' |
| 30 | |
| 31 | |
| 32 | slave_dir = os.getcwd() |
| 33 | |
| 34 | |
| 35 | # log file for errors |
| 36 | sys.stderr = open(sys.argv[0] + ".errors", "a+") |
| 37 | |
| 38 | def convertFSSpec(fss): |
| 39 | return fss.as_pathname() |
| 40 | |
| 41 | |
| 42 | # AE -> os.environ mappings |
| 43 | ae2environ = { |
| 44 | 'kfor': 'QUERY_STRING', |
| 45 | 'Kcip': 'REMOTE_ADDR', |
| 46 | 'svnm': 'SERVER_NAME', |
| 47 | 'svpt': 'SERVER_PORT', |
| 48 | 'addr': 'REMOTE_HOST', |
| 49 | 'scnm': 'SCRIPT_NAME', |
| 50 | 'meth': 'REQUEST_METHOD', |
| 51 | 'ctyp': 'CONTENT_TYPE', |
| 52 | } |
| 53 | |
| 54 | |
| 55 | ERROR_MESSAGE = """\ |
| 56 | Content-type: text/html |
| 57 | |
| 58 | <html> |
| 59 | <head> |
| 60 | <title>Error response</title> |
| 61 | </head> |
| 62 | <body> |
| 63 | <h1>Error response</h1> |
| 64 | <p>Error code %d. |
| 65 | <p>Message: %s. |
| 66 | </body> |
| 67 | </html> |
| 68 | """ |
| 69 | |
| 70 | |
| 71 | def get_cgi_code(): |
| 72 | # If we're a CGI wrapper, the CGI code resides in a PYC resource. |
| 73 | import Res, marshal |
| 74 | try: |
| 75 | code = Res.GetNamedResource('PYC ', "CGI_MAIN") |
| 76 | except Res.Error: |
| 77 | return None |
| 78 | else: |
| 79 | return marshal.loads(code.data[8:]) |
| 80 | |
| 81 | |
| 82 | |
| 83 | class PythonCGISlave(AEServer, MiniApplication): |
| 84 | |
| 85 | def __init__(self): |
| 86 | self.crumblezone = 100000 * "\0" |
| 87 | MiniApplication.__init__(self) |
| 88 | AEServer.__init__(self) |
| 89 | self.installaehandler('aevt', 'oapp', self.open_app) |
| 90 | self.installaehandler('aevt', 'quit', self.quit) |
| 91 | self.installaehandler('WWW\275', 'sdoc', self.cgihandler) |
| 92 | |
| 93 | self.code = get_cgi_code() |
| 94 | self.long_running = LONG_RUNNING |
| 95 | |
| 96 | if self.code is None: |
| 97 | print "%s version %s, ready to serve." % (self.__class__.__name__, __version__) |
| 98 | else: |
| 99 | print "%s, ready to serve." % os.path.basename(sys.argv[0]) |
| 100 | |
| 101 | try: |
| 102 | self.mainloop() |
| 103 | except: |
| 104 | self.crumblezone = None |
| 105 | sys.stderr.write("- " * 30 + '\n') |
| 106 | self.message("Unexpected exception") |
| 107 | self.dump_environ() |
| 108 | sys.stderr.write("%s: %s\n" % sys.exc_info()[:2]) |
| 109 | |
| 110 | def getabouttext(self): |
| 111 | if self.code is None: |
| 112 | return "PythonCGISlave %s, written by Just van Rossum." % __version__ |
| 113 | else: |
| 114 | return "Python CGI script, wrapped by BuildCGIApplet and " \ |
| 115 | "PythonCGISlave, version %s." % __version__ |
| 116 | |
| 117 | def getaboutmenutext(self): |
| 118 | return "About %s\311" % os.path.basename(sys.argv[0]) |
| 119 | |
| 120 | def message(self, msg): |
| 121 | import time |
| 122 | sys.stderr.write("%s (%s)\n" % (msg, time.asctime(time.localtime(time.time())))) |
| 123 | |
| 124 | def dump_environ(self): |
| 125 | sys.stderr.write("os.environ = {\n") |
| 126 | keys = os.environ.keys() |
| 127 | keys.sort() |
| 128 | for key in keys: |
| 129 | sys.stderr.write(" %s: %s,\n" % (repr(key), repr(os.environ[key]))) |
| 130 | sys.stderr.write("}\n") |
| 131 | |
| 132 | def quit(self, **args): |
| 133 | self.quitting = 1 |
| 134 | |
| 135 | def open_app(self, **args): |
| 136 | pass |
| 137 | |
| 138 | def cgihandler(self, pathargs, **args): |
| 139 | # We emulate the unix way of doing CGI: fill os.environ with stuff. |
| 140 | environ = os.environ |
| 141 | |
| 142 | # First, find the document root. If we don't get a DIRE parameter, |
| 143 | # we take the directory of this program, which may be wrong if |
| 144 | # it doesn't live the actual http document root folder. |
| 145 | if args.has_key('DIRE'): |
| 146 | http_root = args['DIRE'].as_pathname() |
| 147 | del args['DIRE'] |
| 148 | else: |
| 149 | http_root = slave_dir |
| 150 | environ['DOCUMENT_ROOT'] = http_root |
| 151 | |
| 152 | if self.code is None: |
| 153 | # create a Mac pathname to the Python CGI script or applet |
| 154 | script = string.replace(args['scnm'], '/', ':') |
| 155 | script_path = os.path.join(http_root, script) |
| 156 | else: |
| 157 | script_path = sys.argv[0] |
| 158 | |
| 159 | if not os.path.exists(script_path): |
| 160 | rv = "HTTP/1.0 404 Not found\n" |
| 161 | rv = rv + ERROR_MESSAGE % (404, "Not found") |
| 162 | return rv |
| 163 | |
| 164 | # Kfrq is the complete http request. |
| 165 | infile = cStringIO.StringIO(args['Kfrq']) |
| 166 | firstline = infile.readline() |
| 167 | |
| 168 | msg = mimetools.Message(infile, 0) |
| 169 | |
| 170 | uri, protocol = string.split(firstline)[1:3] |
| 171 | environ['REQUEST_URI'] = uri |
| 172 | environ['SERVER_PROTOCOL'] = protocol |
| 173 | |
| 174 | # Make all http headers available as HTTP_* fields. |
| 175 | for key in msg.keys(): |
| 176 | environ['HTTP_' + string.upper(string.replace(key, "-", "_"))] = msg[key] |
| 177 | |
| 178 | # Translate the AE parameters we know of to the appropriate os.environ |
| 179 | # entries. Make the ones we don't know available as AE_* fields. |
| 180 | items = args.items() |
| 181 | items.sort() |
| 182 | for key, value in items: |
| 183 | if key[0] == "_": |
| 184 | continue |
| 185 | if ae2environ.has_key(key): |
| 186 | envkey = ae2environ[key] |
| 187 | environ[envkey] = value |
| 188 | else: |
| 189 | environ['AE_' + string.upper(key)] = str(value) |
| 190 | |
| 191 | # Redirect stdout and stdin. |
| 192 | saveout = sys.stdout |
| 193 | savein = sys.stdin |
| 194 | out = sys.stdout = cStringIO.StringIO() |
| 195 | postdata = args.get('post', "") |
| 196 | if postdata: |
| 197 | environ['CONTENT_LENGTH'] = str(len(postdata)) |
| 198 | sys.stdin = cStringIO.StringIO(postdata) |
| 199 | |
| 200 | # Set up the Python environment |
| 201 | script_dir = os.path.dirname(script_path) |
| 202 | os.chdir(script_dir) |
| 203 | sys.path.insert(0, script_dir) |
| 204 | sys.argv[:] = [script_path] |
| 205 | namespace = {"__name__": "__main__"} |
| 206 | rv = "HTTP/1.0 200 OK\n" |
| 207 | |
| 208 | try: |
| 209 | if self.code is None: |
| 210 | # we're a Python script server |
| 211 | execfile(script_path, namespace) |
| 212 | else: |
| 213 | # we're a CGI wrapper, self.code is the CGI code |
| 214 | exec self.code in namespace |
| 215 | except SystemExit: |
| 216 | # We're not exiting dammit! ;-) |
| 217 | pass |
| 218 | except: |
| 219 | self.crumblezone = None |
| 220 | sys.stderr.write("- " * 30 + '\n') |
| 221 | self.message("CGI exception") |
| 222 | self.dump_environ() |
| 223 | traceback.print_exc() |
| 224 | sys.stderr.flush() |
| 225 | self.quitting = 1 |
| 226 | # XXX we should return an error AE, but I don't know how to :-( |
| 227 | rv = "HTTP/1.0 500 Internal error\n" |
| 228 | |
| 229 | # clean up |
| 230 | namespace.clear() |
| 231 | environ.clear() |
| 232 | sys.path.remove(script_dir) |
| 233 | sys.stdout = saveout |
| 234 | sys.stdin = savein |
| 235 | |
| 236 | if not self.long_running: |
| 237 | # quit after each request |
| 238 | self.quitting = 1 |
| 239 | |
| 240 | return rv + out.getvalue() |
| 241 | |
| 242 | |
| 243 | PythonCGISlave() |