python: Fixed WSGI wrapper to handle empty responses.
[ashd.git] / python / ashd / scgi.py
1 import sys
2 import threading
3
4 class protoerr(Exception):
5     pass
6
7 def readns(sk):
8     hln = 0
9     while True:
10         c = sk.read(1)
11         if c == ':':
12             break
13         elif c >= '0' or c <= '9':
14             hln = (hln * 10) + (ord(c) - ord('0'))
15         else:
16             raise protoerr, "Invalid netstring length byte: " + c
17     ret = sk.read(hln)
18     if sk.read(1) != ',':
19         raise protoerr, "Non-terminated netstring"
20     return ret
21
22 def readhead(sk):
23     parts = readns(sk).split('\0')[:-1]
24     if len(parts) % 2 != 0:
25         raise protoerr, "Malformed headers"
26     ret = {}
27     i = 0
28     while i < len(parts):
29         ret[parts[i]] = parts[i + 1]
30         i += 2
31     return ret
32
33 class reqthread(threading.Thread):
34     def __init__(self, sk, handler):
35         super(reqthread, self).__init__(name = "SCGI request handler")
36         self.sk = sk.dup().makefile("r+")
37         self.handler = handler
38
39     def run(self):
40         try:
41             head = readhead(self.sk)
42             self.handler(head, self.sk)
43         finally:
44             self.sk.close()
45
46 def handlescgi(sk, handler):
47     t = reqthread(sk, handler)
48     t.start()
49
50 def servescgi(socket, handler):
51     while True:
52         nsk, addr = socket.accept()
53         try:
54             handlescgi(nsk, handler)
55         finally:
56             nsk.close()
57
58 def wrapwsgi(handler):
59     def handle(head, sk):
60         env = dict(head)
61         env["wsgi.version"] = 1, 0
62         if "HTTP_X_ASH_PROTOCOL" in env:
63             env["wsgi.url_scheme"] = env["HTTP_X_ASH_PROTOCOL"]
64         elif "HTTPS" in env:
65             env["wsgi.url_scheme"] = "https"
66         else:
67             env["wsgi.url_scheme"] = "http"
68         env["wsgi.input"] = sk
69         env["wsgi.errors"] = sys.stderr
70         env["wsgi.multithread"] = True
71         env["wsgi.multiprocess"] = False
72         env["wsgi.run_once"] = False
73
74         resp = []
75         respsent = []
76
77         def flushreq():
78             if not respsent:
79                 if not resp:
80                     raise Exception, "Trying to write data before starting response."
81                 status, headers = resp
82                 respsent[:] = [True]
83                 sk.write("Status: %s\n" % status)
84                 for nm, val in headers:
85                     sk.write("%s: %s\n" % (nm, val))
86                 sk.write("\n")
87
88         def write(data):
89             if not data:
90                 return
91             flushreq()
92             sk.write(data)
93             sk.flush()
94
95         def startreq(status, headers, exc_info = None):
96             if resp:
97                 if exc_info:                # Interesting, this...
98                     try:
99                         if respsent:
100                             raise exc_info[0], exc_info[1], exc_info[2]
101                     finally:
102                         exc_info = None     # CPython GC bug?
103                 else:
104                     raise Exception, "Can only start responding once."
105             resp[:] = status, headers
106             return write
107
108         respiter = handler(env, startreq)
109         try:
110             for data in respiter:
111                 write(data)
112             if resp:
113                 flushresp()
114         finally:
115             if hasattr(respiter, "close"):
116                 respiter.close()
117     return handle