+++ /dev/null
-#!/usr/bin/python3
-
-import sys, os, getopt, threading, time, locale, collections
-import ashd.proto, ashd.util
-
-def usage(out):
- out.write("usage: ashd-wsgi [-hA] [-p MODPATH] [-l REQLIMIT] HANDLER-MODULE [ARGS...]\n")
-
-reqlimit = 0
-modwsgi_compat = False
-opts, args = getopt.getopt(sys.argv[1:], "+hAp:l:")
-for o, a in opts:
- if o == "-h":
- usage(sys.stdout)
- sys.exit(0)
- elif o == "-p":
- sys.path.insert(0, a)
- elif o == "-A":
- modwsgi_compat = True
- elif o == "-l":
- reqlimit = int(a)
-if len(args) < 1:
- usage(sys.stderr)
- sys.exit(1)
-
-try:
- handlermod = __import__(args[0], fromlist = ["dummy"])
-except ImportError as exc:
- sys.stderr.write("ashd-wsgi: handler %s not found: %s\n" % (args[0], exc.args[0]))
- sys.exit(1)
-if not modwsgi_compat:
- if not hasattr(handlermod, "wmain"):
- sys.stderr.write("ashd-wsgi: handler %s has no `wmain' function\n" % args[0])
- sys.exit(1)
- handler = handlermod.wmain(*args[1:])
-else:
- if not hasattr(handlermod, "application"):
- sys.stderr.write("ashd-wsgi: handler %s has no `application' object\n" % args[0])
- sys.exit(1)
- handler = handlermod.application
-
-class closed(IOError):
- def __init__(self):
- super().__init__("The client has closed the connection.")
-
-cwd = os.getcwd()
-def absolutify(path):
- if path[0] != '/':
- return os.path.join(cwd, path)
- return path
-
-def unquoteurl(url):
- buf = bytearray()
- i = 0
- while i < len(url):
- c = url[i]
- i += 1
- if c == ord(b'%'):
- if len(url) >= i + 2:
- c = 0
- if ord(b'0') <= url[i] <= ord(b'9'):
- c |= (url[i] - ord(b'0')) << 4
- elif ord(b'a') <= url[i] <= ord(b'f'):
- c |= (url[i] - ord(b'a') + 10) << 4
- elif ord(b'A') <= url[i] <= ord(b'F'):
- c |= (url[i] - ord(b'A') + 10) << 4
- else:
- raise ValueError("Illegal URL escape character")
- if ord(b'0') <= url[i + 1] <= ord(b'9'):
- c |= url[i + 1] - ord('0')
- elif ord(b'a') <= url[i + 1] <= ord(b'f'):
- c |= url[i + 1] - ord(b'a') + 10
- elif ord(b'A') <= url[i + 1] <= ord(b'F'):
- c |= url[i + 1] - ord(b'A') + 10
- else:
- raise ValueError("Illegal URL escape character")
- buf.append(c)
- i += 2
- else:
- raise ValueError("Incomplete URL escape character")
- else:
- buf.append(c)
- return buf
-
-def dowsgi(req):
- env = {}
- env["wsgi.version"] = 1, 0
- for key, val in req.headers:
- env["HTTP_" + key.upper().replace(b"-", b"_").decode("latin-1")] = val.decode("latin-1")
- env["SERVER_SOFTWARE"] = "ashd-wsgi/1"
- env["GATEWAY_INTERFACE"] = "CGI/1.1"
- env["SERVER_PROTOCOL"] = req.ver.decode("latin-1")
- env["REQUEST_METHOD"] = req.method.decode("latin-1")
- try:
- rawpi = unquoteurl(req.rest)
- except:
- rawpi = req.rest
- try:
- name, rest, pi = (v.decode("utf-8") for v in (req.url, req.rest, rawpi))
- env["wsgi.uri_encoding"] = "utf-8"
- except UnicodeError as exc:
- name, rest, pi = (v.decode("latin-1") for v in (req.url, req.rest, rawpi))
- env["wsgi.uri_encoding"] = "latin-1"
- env["REQUEST_URI"] = name
- p = name.find('?')
- if p >= 0:
- env["QUERY_STRING"] = name[p + 1:]
- name = name[:p]
- else:
- env["QUERY_STRING"] = ""
- if name[-len(rest):] == rest:
- # This is the same hack used in call*cgi.
- name = name[:-len(rest)]
- if name == "/":
- # This seems to be normal CGI behavior, but see callcgi.c for
- # details.
- pi = "/" + pi
- name = ""
- env["SCRIPT_NAME"] = name
- env["PATH_INFO"] = pi
- for src, tgt in [("HTTP_HOST", "SERVER_NAME"), ("HTTP_X_ASH_SERVER_PORT", "SERVER_PORT"),
- ("HTTP_X_ASH_ADDRESS", "REMOTE_ADDR"), ("HTTP_CONTENT_TYPE", "CONTENT_TYPE"),
- ("HTTP_CONTENT_LENGTH", "CONTENT_LENGTH"), ("HTTP_X_ASH_PROTOCOL", "wsgi.url_scheme")]:
- if src in env: env[tgt] = env[src]
- if "X-Ash-Protocol" in req and req["X-Ash-Protocol"] == b"https": env["HTTPS"] = "on"
- if "X-Ash-File" in req: env["SCRIPT_FILENAME"] = absolutify(req["X-Ash-File"].decode(locale.getpreferredencoding()))
- env["wsgi.input"] = req.sk
- env["wsgi.errors"] = sys.stderr
- env["wsgi.multithread"] = True
- env["wsgi.multiprocess"] = False
- env["wsgi.run_once"] = False
-
- resp = []
- respsent = []
-
- def recode(thing):
- if isinstance(thing, collections.ByteString):
- return thing
- else:
- return str(thing).encode("latin-1")
-
- def flushreq():
- if not respsent:
- if not resp:
- raise Exception("Trying to write data before starting response.")
- status, headers = resp
- respsent[:] = [True]
- buf = bytearray()
- buf += b"HTTP/1.1 " + recode(status) + b"\n"
- for nm, val in headers:
- buf += recode(nm) + b": " + recode(val) + b"\n"
- buf += b"\n"
- try:
- req.sk.write(buf)
- except IOError:
- raise closed()
-
- def write(data):
- if not data:
- return
- flushreq()
- try:
- req.sk.write(data)
- req.sk.flush()
- except IOError:
- raise closed()
-
- def startreq(status, headers, exc_info = None):
- if resp:
- if exc_info: # Interesting, this...
- try:
- if respsent:
- raise exc_info[1]
- finally:
- exc_info = None # CPython GC bug?
- else:
- raise Exception("Can only start responding once.")
- resp[:] = status, headers
- return write
-
- respiter = handler(env, startreq)
- try:
- try:
- for data in respiter:
- write(data)
- if resp:
- flushreq()
- except closed:
- pass
- finally:
- if hasattr(respiter, "close"):
- respiter.close()
-
-flightlock = threading.Condition()
-inflight = 0
-
-class reqthread(threading.Thread):
- def __init__(self, req):
- super().__init__(name = "Request handler")
- self.req = req.dup()
-
- def run(self):
- global inflight
- try:
- with flightlock:
- if reqlimit != 0:
- start = time.time()
- while inflight >= reqlimit:
- flightlock.wait(10)
- if time.time() - start > 10:
- os.abort()
- inflight += 1
- try:
- dowsgi(self.req)
- finally:
- with flightlock:
- inflight -= 1
- flightlock.notify()
- finally:
- self.req.close()
-
-def handle(req):
- reqthread(req).start()
-
-ashd.util.serveloop(handle)