htparser: Put root handler in a new session regardless of daemonization.
[ashd.git] / python3 / ashd / scgi.py
CommitLineData
173e0e9e
FT
1import sys, collections
2import threading
3
4class protoerr(Exception):
5 pass
6
7class closed(IOError):
8 def __init__(self):
9 super(closed, self).__init__("The client has closed the connection.")
10
11def 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
26def 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
37class reqthread(threading.Thread):
38 def __init__(self, sk, handler):
39 super(reqthread, self).__init__(name = "SCGI request handler")
c329061e
FT
40 self.bsk = sk.dup()
41 self.sk = self.bsk.makefile("rwb")
173e0e9e
FT
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()
c329061e 50 self.bsk.close()
173e0e9e
FT
51
52def handlescgi(sk, handler):
53 t = reqthread(sk, handler)
54 t.start()
55
56def servescgi(socket, handler):
57 while True:
58 nsk, addr = socket.accept()
59 try:
60 handlescgi(nsk, handler)
61 finally:
62 nsk.close()
63
64def decodehead(head, coding):
65 return {k.decode(coding): v.decode(coding) for k, v in head.items()}
66
67def 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