From c06db49a3a4bfbf14b1661b667e1ed1cbab2bcd0 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Fri, 3 Sep 2010 07:38:03 +0200 Subject: [PATCH] Added a SCGI-WSGI gateway for Python. --- python/.gitignore | 1 + python/ashd/__init__.py | 0 python/ashd/scgi.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++++ python/ashd/wsgidir.py | 62 ++++++++++++++++++++++++++ python/ashd/wsgiutil.py | 29 +++++++++++++ python/scgi-wsgi | 58 +++++++++++++++++++++++++ 6 files changed, 263 insertions(+) create mode 100644 python/.gitignore create mode 100644 python/ashd/__init__.py create mode 100644 python/ashd/scgi.py create mode 100644 python/ashd/wsgidir.py create mode 100644 python/ashd/wsgiutil.py create mode 100755 python/scgi-wsgi diff --git a/python/.gitignore b/python/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/python/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/python/ashd/__init__.py b/python/ashd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/ashd/scgi.py b/python/ashd/scgi.py new file mode 100644 index 0000000..9357e9d --- /dev/null +++ b/python/ashd/scgi.py @@ -0,0 +1,113 @@ +import sys +import threading + +class protoerr(Exception): + pass + +def readns(sk): + hln = 0 + while True: + c = sk.read(1) + if c == ':': + break + elif c >= '0' or c <= '9': + hln = (hln * 10) + (ord(c) - ord('0')) + else: + raise protoerr, "Invalid netstring length byte: " + c + ret = sk.read(hln) + if sk.read(1) != ',': + raise protoerr, "Non-terminated netstring" + return ret + +def readhead(sk): + parts = readns(sk).split('\0')[:-1] + if len(parts) % 2 != 0: + raise protoerr, "Malformed headers" + ret = {} + i = 0 + while i < len(parts): + ret[parts[i]] = parts[i + 1] + i += 2 + return ret + +class reqthread(threading.Thread): + def __init__(self, sk, handler): + super(reqthread, self).__init__(name = "SCGI request handler") + self.sk = sk.dup().makefile("r+") + self.handler = handler + + def run(self): + try: + head = readhead(self.sk) + self.handler(head, self.sk) + finally: + self.sk.close() + +def handlescgi(sk, handler): + t = reqthread(sk, handler) + t.start() + +def servescgi(socket, handler): + while True: + nsk, addr = socket.accept() + try: + handlescgi(nsk, handler) + finally: + nsk.close() + +def wrapwsgi(handler): + def handle(head, sk): + env = dict(head) + env["wsgi.version"] = 1, 0 + if "HTTP_X_ASH_PROTOCOL" in env: + env["wsgi.url_scheme"] = env["HTTP_X_ASH_PROTOCOL"] + elif "HTTPS" in env: + env["wsgi.url_scheme"] = "https" + else: + env["wsgi.url_scheme"] = "http" + env["wsgi.input"] = sk + env["wsgi.errors"] = sys.stderr + env["wsgi.multithread"] = True + env["wsgi.multiprocess"] = False + env["wsgi.run_once"] = False + + resp = [] + respsent = [] + + def write(data): + if not data: + return + if not respsent: + if not resp: + raise Exception, "Trying to write data before starting response." + status, headers = resp + respsent[:] = [True] + sk.write("Status: %s\n" % status) + for nm, val in headers: + sk.write("%s: %s\n" % (nm, val)) + sk.write("\n") + sk.write(data) + sk.flush() + + def startreq(status, headers, exc_info = None): + if resp: + if exc_info: # Interesting, this... + try: + if respsent: + raise exc_info[0], exc_info[1], exc_info[2] + finally: + exc_info = None # CPython GC bug? + else: + raise Exception, "Can only start responding once." + resp[:] = status, headers + return write + + respiter = handler(env, startreq) + try: + for data in respiter: + write(data) + write("") + finally: + if hasattr(respiter, "close"): + respiter.close() + return handle diff --git a/python/ashd/wsgidir.py b/python/ashd/wsgidir.py new file mode 100644 index 0000000..037544d --- /dev/null +++ b/python/ashd/wsgidir.py @@ -0,0 +1,62 @@ +import os, threading, types +import wsgiutil + +exts = {} +modcache = {} +cachelock = threading.Lock() + +def mangle(path): + ret = "" + for c in path: + if c.isalnum(): + ret += c + else: + ret += "_" + return ret + +def getmod(path): + sb = os.stat(path) + cachelock.acquire() + try: + if path in modcache: + mod, mtime = modcache[path] + if sb.st_mtime <= mtime: + return mod + f = open(path) + try: + text = f.read() + finally: + f.close() + code = compile(text, path, "exec") + mod = types.ModuleType(mangle(path)) + mod.__file__ = path + exec code in mod.__dict__ + modcache[path] = mod, sb.st_mtime + return mod + finally: + cachelock.release() + +def chain(path, env, startreq): + mod = getmod(path) + if hasattr(mod, "wmain"): + return (mod.wmain())(env, startreq) + elif hasattr(mod, "application"): + return mod.application(env, startreq) + return wsgi.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.") +exts["wsgi"] = chain + +def application(env, startreq): + if not "SCRIPT_FILENAME" in env: + return wsgiutil.simplerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.") + path = env["SCRIPT_FILENAME"] + base = os.path.basename(path) + p = base.rfind('.') + if p < 0 or not os.access(path, os.R_OK): + return wsgiutil.simplerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.") + ext = base[p + 1:] + if not ext in exts: + return wsgiutil.simplerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.") + return(exts[ext](path, env, startreq)) + +def wmain(argv): + return application diff --git a/python/ashd/wsgiutil.py b/python/ashd/wsgiutil.py new file mode 100644 index 0000000..b947407 --- /dev/null +++ b/python/ashd/wsgiutil.py @@ -0,0 +1,29 @@ +def htmlquote(text): + ret = "" + for c in text: + if c == '&': + ret += "&" + elif c == '<': + ret += "<" + elif c == '>': + ret += ">" + elif c == '"': + ret += """ + else: + ret += c + return ret + +def simpleerror(env, startreq, code, title, msg): + buf = """ + + + +%s + + +

%s

+

%s

+ +""" % (title, title, htmlquote(msg)) + startreq("%i %s" % (code, title), [("Content-Type", "text/html"), ("Content-Length", str(len(buf)))]) + return [buf] diff --git a/python/scgi-wsgi b/python/scgi-wsgi new file mode 100755 index 0000000..944ed39 --- /dev/null +++ b/python/scgi-wsgi @@ -0,0 +1,58 @@ +#!/usr/bin/python + +import sys, os, getopt +import socket +import ashd.scgi + +def usage(out): + out.write("usage: scgi-wsgi [-hA] [-p MODPATH] [-T [HOST:]PORT] HANDLER-MODULE [ARGS...]\n") + +sk = None +modwsgi_compat = False +opts, args = getopt.getopt(sys.argv[1:], "+hAp:T:") +for o, a in opts: + if o == "-h": + usage(sys.stdout) + sys.exit(0) + elif o == "-p": + sys.path.append(0, a) + elif o == "-T": + sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + p = a.rfind(":") + if p < 0: + bindhost = "hostname" + bindport = int(a) + else: + bindhost = a[:p] + bindport = int(a[p + 1:]) + sk.bind((bindhost, bindport)) + sk.listen(32) + elif o == "-A": + modwsgi_compat = True +if len(args) < 1: + usage(sys.stderr) + sys.exit(1) + +if sk is None: + # This is suboptimal, since the socket on stdin is not necessarily + # AF_UNIX, but Python does not seem to offer any way around it, + # that I can find. + sk = socket.fromfd(0, socket.AF_UNIX, socket.SOCK_STREAM) + +try: + handlermod = __import__(args[0], fromlist = ["dummy"]) +except ImportError, exc: + sys.stderr.write("scgi-wsgi: handler %s not found: %s\n" % (args[0], exc.message)) + sys.exit(1) +if not modwsgi_compat: + if not hasattr(handlermod, "wmain"): + sys.stderr.write("scgi-wsgi: handler %s has no `wmain' function\n" % args[0]) + sys.exit(1) + handler = handlermod.wmain(args[1:]) +else: + if not hasattr(handlermod, "application"): + sys.stderr.write("scgi-wsgi: handler %s has no `application' object\n" % args[0]) + sys.exit(1) + handler = handlermod.application + +ashd.scgi.servescgi(sk, ashd.scgi.wrapwsgi(handler)) -- 2.11.0