python: Configure logging in ashd-wsgi{,3}.
[ashd.git] / python3 / ashd-wsgi3
1 #!/usr/bin/python3
2
3 import sys, os, getopt, threading, logging, time, locale, collections
4 import ashd.proto, ashd.util, ashd.perf
5 try:
6     import pdm.srv
7 except:
8     pdm = None
9
10 def usage(out):
11     out.write("usage: ashd-wsgi3 [-hAL] [-m PDM-SPEC] [-p MODPATH] [-l REQLIMIT] HANDLER-MODULE [ARGS...]\n")
12
13 reqlimit = 0
14 modwsgi_compat = False
15 setlog = True
16 opts, args = getopt.getopt(sys.argv[1:], "+hALp:l:m:")
17 for o, a in opts:
18     if o == "-h":
19         usage(sys.stdout)
20         sys.exit(0)
21     elif o == "-p":
22         sys.path.insert(0, a)
23     elif o == "-L":
24         setlog = False
25     elif o == "-A":
26         modwsgi_compat = True
27     elif o == "-l":
28         reqlimit = int(a)
29     elif o == "-m":
30         if pdm is not None:
31             pdm.srv.listen(a)
32 if len(args) < 1:
33     usage(sys.stderr)
34     sys.exit(1)
35 if setlog:
36     logging.basicConfig(format="ashd-wsgi3(%(name)s): %(levelname)s: %(message)s")
37
38 try:
39     handlermod = __import__(args[0], fromlist = ["dummy"])
40 except ImportError as exc:
41     sys.stderr.write("ashd-wsgi3: handler %s not found: %s\n" % (args[0], exc.args[0]))
42     sys.exit(1)
43 if not modwsgi_compat:
44     if not hasattr(handlermod, "wmain"):
45         sys.stderr.write("ashd-wsgi3: handler %s has no `wmain' function\n" % args[0])
46         sys.exit(1)
47     handler = handlermod.wmain(*args[1:])
48 else:
49     if not hasattr(handlermod, "application"):
50         sys.stderr.write("ashd-wsgi3: handler %s has no `application' object\n" % args[0])
51         sys.exit(1)
52     handler = handlermod.application
53
54 class closed(IOError):
55     def __init__(self):
56         super().__init__("The client has closed the connection.")
57
58 cwd = os.getcwd()
59 def absolutify(path):
60     if path[0] != '/':
61         return os.path.join(cwd, path)
62     return path
63
64 def unquoteurl(url):
65     buf = bytearray()
66     i = 0
67     while i < len(url):
68         c = url[i]
69         i += 1
70         if c == ord(b'%'):
71             if len(url) >= i + 2:
72                 c = 0
73                 if ord(b'0') <= url[i] <= ord(b'9'):
74                     c |= (url[i] - ord(b'0')) << 4
75                 elif ord(b'a') <= url[i] <= ord(b'f'):
76                     c |= (url[i] - ord(b'a') + 10) << 4
77                 elif ord(b'A') <= url[i] <= ord(b'F'):
78                     c |= (url[i] - ord(b'A') + 10) << 4
79                 else:
80                     raise ValueError("Illegal URL escape character")
81                 if ord(b'0') <= url[i + 1] <= ord(b'9'):
82                     c |= url[i + 1] - ord('0')
83                 elif ord(b'a') <= url[i + 1] <= ord(b'f'):
84                     c |= url[i + 1] - ord(b'a') + 10
85                 elif ord(b'A') <= url[i + 1] <= ord(b'F'):
86                     c |= url[i + 1] - ord(b'A') + 10
87                 else:
88                     raise ValueError("Illegal URL escape character")
89                 buf.append(c)
90                 i += 2
91             else:
92                 raise ValueError("Incomplete URL escape character")
93         else:
94             buf.append(c)
95     return buf
96
97 def dowsgi(req):
98     env = {}
99     env["wsgi.version"] = 1, 0
100     for key, val in req.headers:
101         env["HTTP_" + key.upper().replace(b"-", b"_").decode("latin-1")] = val.decode("latin-1")
102     env["SERVER_SOFTWARE"] = "ashd-wsgi/1"
103     env["GATEWAY_INTERFACE"] = "CGI/1.1"
104     env["SERVER_PROTOCOL"] = req.ver.decode("latin-1")
105     env["REQUEST_METHOD"] = req.method.decode("latin-1")
106     try:
107         rawpi = unquoteurl(req.rest)
108     except:
109         rawpi = req.rest
110     try:
111         name, rest, pi = (v.decode("utf-8") for v in (req.url, req.rest, rawpi))
112         env["wsgi.uri_encoding"] = "utf-8"
113     except UnicodeError as exc:
114         name, rest, pi = (v.decode("latin-1") for v in (req.url, req.rest, rawpi))
115         env["wsgi.uri_encoding"] = "latin-1"
116     env["REQUEST_URI"] = name
117     p = name.find('?')
118     if p >= 0:
119         env["QUERY_STRING"] = name[p + 1:]
120         name = name[:p]
121     else:
122         env["QUERY_STRING"] = ""
123     if name[-len(rest):] == rest:
124         # This is the same hack used in call*cgi.
125         name = name[:-len(rest)]
126     if name == "/":
127         # This seems to be normal CGI behavior, but see callcgi.c for
128         # details.
129         pi = "/" + pi
130         name = ""
131     env["SCRIPT_NAME"] = name
132     env["PATH_INFO"] = pi
133     for src, tgt in [("HTTP_HOST", "SERVER_NAME"), ("HTTP_X_ASH_SERVER_PORT", "SERVER_PORT"),
134                      ("HTTP_X_ASH_ADDRESS", "REMOTE_ADDR"), ("HTTP_CONTENT_TYPE", "CONTENT_TYPE"),
135                      ("HTTP_CONTENT_LENGTH", "CONTENT_LENGTH"), ("HTTP_X_ASH_PROTOCOL", "wsgi.url_scheme")]:
136         if src in env: env[tgt] = env[src]
137     if "X-Ash-Protocol" in req and req["X-Ash-Protocol"] == b"https": env["HTTPS"] = "on"
138     if "X-Ash-File" in req: env["SCRIPT_FILENAME"] = absolutify(req["X-Ash-File"].decode(locale.getpreferredencoding()))
139     env["wsgi.input"] = req.sk
140     env["wsgi.errors"] = sys.stderr
141     env["wsgi.multithread"] = True
142     env["wsgi.multiprocess"] = False
143     env["wsgi.run_once"] = False
144
145     resp = []
146     respsent = []
147
148     def recode(thing):
149         if isinstance(thing, collections.ByteString):
150             return thing
151         else:
152             return str(thing).encode("latin-1")
153
154     def flushreq():
155         if not respsent:
156             if not resp:
157                 raise Exception("Trying to write data before starting response.")
158             status, headers = resp
159             respsent[:] = [True]
160             buf = bytearray()
161             buf += b"HTTP/1.1 " + recode(status) + b"\n"
162             for nm, val in headers:
163                 buf += recode(nm) + b": " + recode(val) + b"\n"
164             buf += b"\n"
165             try:
166                 req.sk.write(buf)
167             except IOError:
168                 raise closed()
169
170     def write(data):
171         if not data:
172             return
173         flushreq()
174         try:
175             req.sk.write(data)
176             req.sk.flush()
177         except IOError:
178             raise closed()
179
180     def startreq(status, headers, exc_info = None):
181         if resp:
182             if exc_info:                # Interesting, this...
183                 try:
184                     if respsent:
185                         raise exc_info[1]
186                 finally:
187                     exc_info = None     # CPython GC bug?
188             else:
189                 raise Exception("Can only start responding once.")
190         resp[:] = status, headers
191         return write
192
193     with ashd.perf.request(env) as reqevent:
194         respiter = handler(env, startreq)
195         try:
196             try:
197                 for data in respiter:
198                     write(data)
199                 if resp:
200                     flushreq()
201             except closed:
202                 pass
203         finally:
204             if hasattr(respiter, "close"):
205                 respiter.close()
206         if resp:
207             reqevent.response(resp)
208
209 flightlock = threading.Condition()
210 inflight = 0
211
212 class reqthread(threading.Thread):
213     def __init__(self, req):
214         super().__init__(name = "Request handler")
215         self.req = req.dup()
216     
217     def run(self):
218         global inflight
219         try:
220             with flightlock:
221                 if reqlimit != 0:
222                     start = time.time()
223                     while inflight >= reqlimit:
224                         flightlock.wait(10)
225                         if time.time() - start > 10:
226                             os.abort()
227                 inflight += 1
228             try:
229                 dowsgi(self.req)
230             finally:
231                 with flightlock:
232                     inflight -= 1
233                     flightlock.notify()
234         finally:
235             self.req.close()
236             sys.stderr.flush()
237     
238 def handle(req):
239     reqthread(req).start()
240
241 ashd.util.serveloop(handle)