callcgi: Don't mass-close child FDs.
[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.sk = sk.dup().makefile("rwb")
41         self.handler = handler
42
43     def run(self):
44         try:
45             head = readhead(self.sk)
46             self.handler(head, self.sk)
47         finally:
48             self.sk.close()
49
50 def handlescgi(sk, handler):
51     t = reqthread(sk, handler)
52     t.start()
53
54 def servescgi(socket, handler):
55     while True:
56         nsk, addr = socket.accept()
57         try:
58             handlescgi(nsk, handler)
59         finally:
60             nsk.close()
61
62 def decodehead(head, coding):
63     return {k.decode(coding): v.decode(coding) for k, v in head.items()}
64
65 def wrapwsgi(handler):
66     def handle(head, sk):
67         try:
68             env = decodehead(head, "utf-8")
69             env["wsgi.uri_encoding"] = "utf-8"
70         except UnicodeError:
71             env = decodehead(head, "latin-1")
72             env["wsgi.uri_encoding"] = "latin-1"
73         env["wsgi.version"] = 1, 0
74         if "HTTP_X_ASH_PROTOCOL" in env:
75             env["wsgi.url_scheme"] = env["HTTP_X_ASH_PROTOCOL"]
76         elif "HTTPS" in env:
77             env["wsgi.url_scheme"] = "https"
78         else:
79             env["wsgi.url_scheme"] = "http"
80         env["wsgi.input"] = sk
81         env["wsgi.errors"] = sys.stderr
82         env["wsgi.multithread"] = True
83         env["wsgi.multiprocess"] = False
84         env["wsgi.run_once"] = False
85
86         resp = []
87         respsent = []
88
89         def recode(thing):
90             if isinstance(thing, collections.ByteString):
91                 return thing
92             else:
93                 return str(thing).encode("latin-1")
94
95         def flushreq():
96             if not respsent:
97                 if not resp:
98                     raise Exception("Trying to write data before starting response.")
99                 status, headers = resp
100                 respsent[:] = [True]
101                 buf = bytearray()
102                 buf += b"Status: " + recode(status) + b"\n"
103                 for nm, val in headers:
104                     buf += recode(nm) + b": " + recode(val) + b"\n"
105                 buf += b"\n"
106                 try:
107                     sk.write(buf)
108                 except IOError:
109                     raise closed()
110
111         def write(data):
112             if not data:
113                 return
114             flushreq()
115             try:
116                 sk.write(data)
117                 sk.flush()
118             except IOError:
119                 raise closed()
120
121         def startreq(status, headers, exc_info = None):
122             if resp:
123                 if exc_info:                # Interesting, this...
124                     try:
125                         if respsent:
126                             raise exc_info[1]
127                     finally:
128                         exc_info = None     # CPython GC bug?
129                 else:
130                     raise Exception("Can only start responding once.")
131             resp[:] = status, headers
132             return write
133
134         respiter = handler(env, startreq)
135         try:
136             try:
137                 for data in respiter:
138                     write(data)
139                 if resp:
140                     flushreq()
141             except closed:
142                 pass
143         finally:
144             if hasattr(respiter, "close"):
145                 respiter.close()
146     return handle