python: Added some SCGI fixes apparently necessary for Jython.
[ashd.git] / python3 / ashd / scgi.py
1 import sys, collections
2 import threading
3
4 class protoerr(Exception):
5     pass
6
7 class closed(IOError):
8     def __init__(self):
9         super(closed, self).__init__("The client has closed the connection.")
10
11 def readns(sk):
12     hln = 0
13     while True:
14         c = sk.read(1)
15         if c == b':':
16             break
17         elif c >= b'0' or c <= b'9':
18             hln = (hln * 10) + (ord(c) - ord(b'0'))
19         else:
20             raise protoerr("Invalid netstring length byte: " + c)
21     ret = sk.read(hln)
22     if sk.read(1) != b',':
23         raise protoerr("Non-terminated netstring")
24     return ret
25
26 def readhead(sk):
27     parts = readns(sk).split(b'\0')[:-1]
28     if len(parts) % 2 != 0:
29         raise protoerr("Malformed headers")
30     ret = {}
31     i = 0
32     while i < len(parts):
33         ret[parts[i]] = parts[i + 1]
34         i += 2
35     return ret
36
37 class reqthread(threading.Thread):
38     def __init__(self, sk, handler):
39         super(reqthread, self).__init__(name = "SCGI request handler")
40         self.bsk = sk.dup()
41         self.sk = self.bsk.makefile("rwb")
42         self.handler = handler
43
44     def run(self):
45         try:
46             head = readhead(self.sk)
47             self.handler(head, self.sk)
48         finally:
49             self.sk.close()
50             self.bsk.close()
51
52 def handlescgi(sk, handler):
53     t = reqthread(sk, handler)
54     t.start()
55
56 def servescgi(socket, handler):
57     while True:
58         nsk, addr = socket.accept()
59         try:
60             handlescgi(nsk, handler)
61         finally:
62             nsk.close()
63
64 def decodehead(head, coding):
65     return {k.decode(coding): v.decode(coding) for k, v in head.items()}
66
67 def wrapwsgi(handler):
68     def handle(head, sk):
69         try:
70             env = decodehead(head, "utf-8")
71             env["wsgi.uri_encoding"] = "utf-8"
72         except UnicodeError:
73             env = decodehead(head, "latin-1")
74             env["wsgi.uri_encoding"] = "latin-1"
75         env["wsgi.version"] = 1, 0
76         if "HTTP_X_ASH_PROTOCOL" in env:
77             env["wsgi.url_scheme"] = env["HTTP_X_ASH_PROTOCOL"]
78         elif "HTTPS" in env:
79             env["wsgi.url_scheme"] = "https"
80         else:
81             env["wsgi.url_scheme"] = "http"
82         env["wsgi.input"] = sk
83         env["wsgi.errors"] = sys.stderr
84         env["wsgi.multithread"] = True
85         env["wsgi.multiprocess"] = False
86         env["wsgi.run_once"] = False
87
88         resp = []
89         respsent = []
90
91         def recode(thing):
92             if isinstance(thing, collections.ByteString):
93                 return thing
94             else:
95                 return str(thing).encode("latin-1")
96
97         def flushreq():
98             if not respsent:
99                 if not resp:
100                     raise Exception("Trying to write data before starting response.")
101                 status, headers = resp
102                 respsent[:] = [True]
103                 buf = bytearray()
104                 buf += b"Status: " + recode(status) + b"\n"
105                 for nm, val in headers:
106                     buf += recode(nm) + b": " + recode(val) + b"\n"
107                 buf += b"\n"
108                 try:
109                     sk.write(buf)
110                 except IOError:
111                     raise closed()
112
113         def write(data):
114             if not data:
115                 return
116             flushreq()
117             try:
118                 sk.write(data)
119                 sk.flush()
120             except IOError:
121                 raise closed()
122
123         def startreq(status, headers, exc_info = None):
124             if resp:
125                 if exc_info:                # Interesting, this...
126                     try:
127                         if respsent:
128                             raise exc_info[1]
129                     finally:
130                         exc_info = None     # CPython GC bug?
131                 else:
132                     raise Exception("Can only start responding once.")
133             resp[:] = status, headers
134             return write
135
136         respiter = handler(env, startreq)
137         try:
138             try:
139                 for data in respiter:
140                     write(data)
141                 if resp:
142                     flushreq()
143             except closed:
144                 pass
145         finally:
146             if hasattr(respiter, "close"):
147                 respiter.close()
148     return handle