--- /dev/null
+#!/usr/bin/python
+
+import sys, os, getopt, threading, time
+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, exc:
+ sys.stderr.write("ashd-wsgi: handler %s not found: %s\n" % (args[0], exc.message))
+ 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(closed, self).__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 = ""
+ i = 0
+ while i < len(url):
+ c = url[i]
+ i += 1
+ if c == '%':
+ if len(url) >= i + 2:
+ c = 0
+ if '0' <= url[i] <= '9':
+ c |= (ord(url[i]) - ord('0')) << 4
+ elif 'a' <= url[i] <= 'f':
+ c |= (ord(url[i]) - ord('a') + 10) << 4
+ elif 'A' <= url[i] <= 'F':
+ c |= (ord(url[i]) - ord('A') + 10) << 4
+ else:
+ raise ValueError("Illegal URL escape character")
+ if '0' <= url[i + 1] <= '9':
+ c |= ord(url[i + 1]) - ord('0')
+ elif 'a' <= url[i + 1] <= 'f':
+ c |= ord(url[i + 1]) - ord('a') + 10
+ elif 'A' <= url[i + 1] <= 'F':
+ c |= ord(url[i + 1]) - ord('A') + 10
+ else:
+ raise ValueError("Illegal URL escape character")
+ buf += chr(c)
+ i += 2
+ else:
+ raise ValueError("Incomplete URL escape character")
+ else:
+ buf += c
+ return buf
+
+def dowsgi(req):
+ env = {}
+ env["wsgi.version"] = 1, 0
+ for key, val in req.headers:
+ env["HTTP_" + key.upper().replace("-", "_")] = val
+ env["SERVER_SOFTWARE"] = "ashd-wsgi/1"
+ env["GATEWAY_INTERFACE"] = "CGI/1.1"
+ env["SERVER_PROTOCOL"] = req.ver
+ env["REQUEST_METHOD"] = req.method
+ env["REQUEST_URI"] = req.url
+ name = req.url
+ p = name.find('?')
+ if p >= 0:
+ env["QUERY_STRING"] = name[p + 1:]
+ name = name[:p]
+ else:
+ env["QUERY_STRING"] = ""
+ if name[-len(req.rest):] == req.rest:
+ # This is the same hack used in call*cgi.
+ name = name[:-len(req.rest)]
+ try:
+ pi = unquoteurl(req.rest)
+ except:
+ pi = req.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
+ if "Host" in req: env["SERVER_NAME"] = req["Host"]
+ if "X-Ash-Server-Port" in req: env["SERVER_PORT"] = req["X-Ash-Server-Port"]
+ if "X-Ash-Protocol" in req and req["X-Ash-Protocol"] == "https": env["HTTPS"] = "on"
+ if "X-Ash-Address" in req: env["REMOTE_ADDR"] = req["X-Ash-Address"]
+ if "Content-Type" in req: env["CONTENT_TYPE"] = req["Content-Type"]
+ if "Content-Length" in req: env["CONTENT_LENGTH"] = req["Content-Length"]
+ if "X-Ash-File" in req: env["SCRIPT_FILENAME"] = absolutify(req["X-Ash-File"])
+ if "X-Ash-Protocol" in req: env["wsgi.url_scheme"] = req["X-Ash-Protocol"]
+ 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 flushreq():
+ if not respsent:
+ if not resp:
+ raise Exception, "Trying to write data before starting response."
+ status, headers = resp
+ respsent[:] = [True]
+ try:
+ req.sk.write("HTTP/1.1 %s\n" % status)
+ for nm, val in headers:
+ req.sk.write("%s: %s\n" % (nm, val))
+ req.sk.write("\n")
+ 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[0], exc_info[1], exc_info[2]
+ 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(reqthread, self).__init__(name = "Request handler")
+ self.req = req.dup()
+
+ def run(self):
+ global inflight
+ try:
+ flightlock.acquire()
+ try:
+ if reqlimit != 0:
+ start = time.time()
+ while inflight >= reqlimit:
+ flightlock.wait(10)
+ if time.time() - start > 10:
+ os.abort()
+ inflight += 1
+ finally:
+ flightlock.release()
+ try:
+ dowsgi(self.req)
+ finally:
+ flightlock.acquire()
+ try:
+ inflight -= 1
+ flightlock.notify()
+ finally:
+ flightlock.release()
+ finally:
+ self.req.close()
+
+def handle(req):
+ reqthread(req).start()
+
+ashd.util.serveloop(handle)
"""
import os, socket
-from . import htlib
+import htlib
__all__ = ["req", "recvreq", "sendreq"]
self.ver = ver
self.rest = rest
self.headers = headers
- self.bsk = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
- self.sk = self.bsk.makefile('rwb')
+ self.sk = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM).makefile('r+')
os.close(fd)
def close(self):
"Close this request's response socket."
self.sk.close()
- self.bsk.close()
def __getitem__(self, header):
"""Find a HTTP header case-insensitively. For example,
header regardlessly of whether the client specified it as
"Content-Type", "content-type" or "Content-type".
"""
- if isinstance(header, str):
- header = header.encode("ascii")
header = header.lower()
for key, val in self.headers:
if key.lower() == header:
"""Works analogously to the __getitem__ method for checking
header presence case-insensitively.
"""
- if isinstance(header, str):
- header = header.encode("ascii")
header = header.lower()
for key, val in self.headers:
if key.lower() == header:
"""Creates a duplicate of this request, referring to a
duplicate of the response socket.
"""
- return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.bsk.fileno()))
+ return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.sk.fileno()))
def match(self, match):
"""If the `match' argument matches exactly the leading part of
else:
util.respond(req, "Not found", status = "404 Not Found", ctype = "text/plain")
"""
- if isinstance(match, str):
- match = match.encode("utf-8")
if self.rest[:len(match)] == match:
self.rest = self.rest[len(match):]
return True
return False
def __str__(self):
- def dec(b):
- return b.decode("ascii", errors="replace")
- return "\"%s %s %s\"" % (dec(self.method), dec(self.url), dec(self.ver))
+ return "\"%s %s %s\"" % (self.method, self.url, self.ver)
def __enter__(self):
return self
if fd is None:
return None
try:
- parts = data.split(b'\0')[:-1]
+ parts = data.split('\0')[:-1]
if len(parts) < 5:
raise protoerr("Truncated request")
method, url, ver, rest = parts[:4]
headers = []
i = 4
while True:
- if parts[i] == b"": break
+ if parts[i] == "": break
if len(parts) - i < 3:
raise protoerr("Truncated request")
headers.append((parts[i], parts[i + 1]))
This function may raise an OSError if an error occurs on the
socket.
"""
- data = b""
- data += req.method + b'\0'
- data += req.url + b'\0'
- data += req.ver + b'\0'
- data += req.rest + b'\0'
+ data = ""
+ data += req.method + '\0'
+ data += req.url + '\0'
+ data += req.ver + '\0'
+ data += req.rest + '\0'
for key, val in req.headers:
- data += key + b'\0'
- data += val + b'\0'
- data += b'\0'
+ data += key + '\0'
+ data += val + '\0'
+ data += '\0'
htlib.sendfd(sock, req.sk.fileno(), data)
-import sys, collections
+import sys
import threading
class protoerr(Exception):
hln = 0
while True:
c = sk.read(1)
- if c == b':':
+ if c == ':':
break
- elif c >= b'0' or c <= b'9':
- hln = (hln * 10) + (ord(c) - ord(b'0'))
+ elif c >= '0' or c <= '9':
+ hln = (hln * 10) + (ord(c) - ord('0'))
else:
- raise protoerr("Invalid netstring length byte: " + c)
+ raise protoerr, "Invalid netstring length byte: " + c
ret = sk.read(hln)
- if sk.read(1) != b',':
- raise protoerr("Non-terminated netstring")
+ if sk.read(1) != ',':
+ raise protoerr, "Non-terminated netstring"
return ret
def readhead(sk):
- parts = readns(sk).split(b'\0')[:-1]
+ parts = readns(sk).split('\0')[:-1]
if len(parts) % 2 != 0:
- raise protoerr("Malformed headers")
+ raise protoerr, "Malformed headers"
ret = {}
i = 0
while i < len(parts):
class reqthread(threading.Thread):
def __init__(self, sk, handler):
super(reqthread, self).__init__(name = "SCGI request handler")
- self.sk = sk.dup().makefile("rwb")
+ self.sk = sk.dup().makefile("r+")
self.handler = handler
def run(self):
finally:
nsk.close()
-def decodehead(head, coding):
- return {k.decode(coding): v.decode(coding) for k, v in head.items()}
-
def wrapwsgi(handler):
def handle(head, sk):
- try:
- env = decodehead(head, "utf-8")
- env["wsgi.uri_encoding"] = "utf-8"
- except UnicodeError:
- env = decodehead(head, "latin-1")
- env["wsgi.uri_encoding"] = "latin-1"
+ env = dict(head)
env["wsgi.version"] = 1, 0
if "HTTP_X_ASH_PROTOCOL" in env:
env["wsgi.url_scheme"] = env["HTTP_X_ASH_PROTOCOL"]
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.")
+ raise Exception, "Trying to write data before starting response."
status, headers = resp
respsent[:] = [True]
- buf = bytearray()
- buf += b"Status: " + recode(status) + b"\n"
- for nm, val in headers:
- buf += recode(nm) + b": " + recode(val) + b"\n"
- buf += b"\n"
try:
- sk.write(buf)
+ sk.write("Status: %s\n" % status)
+ for nm, val in headers:
+ sk.write("%s: %s\n" % (nm, val))
+ sk.write("\n")
except IOError:
raise closed()
if exc_info: # Interesting, this...
try:
if respsent:
- raise exc_info[1]
+ raise exc_info[0], exc_info[1], exc_info[2]
finally:
exc_info = None # CPython GC bug?
else:
- raise Exception("Can only start responding once.")
+ raise Exception, "Can only start responding once."
resp[:] = status, headers
return write
handlers, wrapping the low-level ashd.proto module.
"""
-import os, socket, collections
-from . import proto
+import os, socket
+import proto
__all__ = ["stdfork", "pchild", "respond", "serveloop"]
if pid == 0:
try:
os.dup2(csk.fileno(), 0)
- for fd in range(3, 1024):
+ for fd in xrange(3, 1024):
try:
os.close(fd)
except:
For example:
respond(req, "Not found", status = "404 Not Found", ctype = "text/plain")
"""
- if isinstance(body, collections.ByteString):
- body = bytes(body)
- else:
- body = str(body)
- body = body.encode("utf-8")
+ if type(body) == unicode:
+ body = body.decode("utf-8")
if ctype[:5] == "text/" and ctype.find(';') < 0:
ctype = ctype + "; charset=utf-8"
+ else:
+ body = str(body)
try:
- head = ""
- head += "HTTP/1.1 %s\n" % status
- head += "Content-Type: %s\n" % ctype
- head += "Content-Length: %i\n" % len(body)
- head += "\n"
- req.sk.write(head.encode("ascii"))
+ req.sk.write("HTTP/1.1 %s\n" % status)
+ req.sk.write("Content-Type: %s\n" % ctype)
+ req.sk.write("Content-Length: %i\n" % len(body))
+ req.sk.write("\n")
req.sk.write(body)
finally:
req.close()
"""
import os, threading, types
-from . import wsgiutil
+import wsgiutil
__all__ = ["application", "wmain", "getmod", "cachedmod"]
code = compile(text, path, "exec")
mod = types.ModuleType(mangle(path))
mod.__file__ = path
- exec(code, mod.__dict__)
+ exec code in mod.__dict__
entry = cachedmod(mod, sb.st_mtime)
modcache[path] = entry
return entry
<p>%s</p>
</body>
</html>""" % (title, title, htmlquote(msg))
- buf = buf.encode("ascii")
startreq("%i %s" % (code, title), [("Content-Type", "text/html"), ("Content-Length", str(len(buf)))])
return [buf]
PyErr_SetFromErrno(PyExc_OSError);
return(NULL);
}
- ro = Py_BuildValue("Ni", PyBytes_FromStringAndSize(data, dlen), ret);
+ ro = Py_BuildValue("Ni", PyString_FromStringAndSize(data, dlen), ret);
free(data);
return(ro);
}
static PyObject *p_sendfd(PyObject *self, PyObject *args)
{
int sock, fd, ret;
- Py_buffer data;
+ PyObject *data;
- if(!PyArg_ParseTuple(args, "iiy*", &sock, &fd, &data))
+ if(!PyArg_ParseTuple(args, "iiO", &sock, &fd, &data))
return(NULL);
+ if(!PyString_Check(data)) {
+ PyErr_SetString(PyExc_TypeError, "datagram must be a string");
+ return(NULL);
+ }
Py_BEGIN_ALLOW_THREADS;
- ret = sendfd(sock, fd, data.buf, data.len);
+ ret = sendfd(sock, fd, PyString_AsString(data), PyString_Size(data));
Py_END_ALLOW_THREADS;
- PyBuffer_Release(&data);
if(ret < 0) {
PyErr_SetFromErrno(PyExc_OSError);
return(NULL);
{NULL, NULL, 0, NULL}
};
-static struct PyModuleDef module = {
- PyModuleDef_HEAD_INIT,
- .m_name = "htlib",
- .m_size = -1,
- .m_methods = methods,
-};
-
-PyMODINIT_FUNC PyInit_htlib(void)
+PyMODINIT_FUNC inithtlib(void)
{
- return(PyModule_Create(&module));
+ Py_InitModule("ashd.htlib", methods);
}
--- /dev/null
+#!/usr/bin/python
+
+import sys, os, getopt
+import socket
+import ashd.scgi
+
+def usage(out):
+ out.write("usage: scgi-wsgi [-hA] [-p MODPATH] [-T [HOST:]PORT] HANDLER-MODULE [ARGS...]\n")
+
+sk = None
+modwsgi_compat = False
+opts, args = getopt.getopt(sys.argv[1:], "+hAp:T:")
+for o, a in opts:
+ if o == "-h":
+ usage(sys.stdout)
+ sys.exit(0)
+ elif o == "-p":
+ sys.path.insert(0, a)
+ elif o == "-T":
+ sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ p = a.rfind(":")
+ if p < 0:
+ bindhost = "localhost"
+ bindport = int(a)
+ else:
+ bindhost = a[:p]
+ bindport = int(a[p + 1:])
+ sk.bind((bindhost, bindport))
+ sk.listen(32)
+ elif o == "-A":
+ modwsgi_compat = True
+if len(args) < 1:
+ usage(sys.stderr)
+ sys.exit(1)
+
+if sk is None:
+ # This is suboptimal, since the socket on stdin is not necessarily
+ # AF_UNIX, but Python does not seem to offer any way around it,
+ # that I can find.
+ sk = socket.fromfd(0, socket.AF_UNIX, socket.SOCK_STREAM)
+
+try:
+ handlermod = __import__(args[0], fromlist = ["dummy"])
+except ImportError, exc:
+ sys.stderr.write("scgi-wsgi: handler %s not found: %s\n" % (args[0], exc.message))
+ sys.exit(1)
+if not modwsgi_compat:
+ if not hasattr(handlermod, "wmain"):
+ sys.stderr.write("scgi-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("scgi-wsgi: handler %s has no `application' object\n" % args[0])
+ sys.exit(1)
+ handler = handlermod.application
+
+ashd.scgi.servescgi(sk, ashd.scgi.wrapwsgi(handler))
-#!/usr/bin/python3
+#!/usr/bin/python
from distutils.core import setup, Extension
url = "http://www.dolda2000.com/~fredrik/ashd/",
ext_modules = [htlib],
packages = ["ashd"],
- scripts = ["ashd-wsgi3", "scgi-wsgi3", "serve-ssi", "htredir"],
+ scripts = ["ashd-wsgi", "scgi-wsgi", "serve-ssi", "htredir"],
license = "GPL-3")
--- /dev/null
+*.pyc
+/build
+/ashd/htlib.so
--- /dev/null
+"""Base module for ashd(7)-related fucntions.
+
+This module implements nothing. Please see the ashd.util or ashd.proto
+modules.
+"""
--- /dev/null
+"""Low-level protocol module for ashd(7)
+
+This module provides primitive functions that speak the raw ashd(7)
+protocol. Primarily, it implements the `req' class that is used to
+represent ashd requests. The functions it provides can also be used to
+create ashd handlers, but unless you require very precise control, the
+ashd.util module provides an easier-to-use interface.
+"""
+
+import os, socket
+from . import htlib
+
+__all__ = ["req", "recvreq", "sendreq"]
+
+class protoerr(Exception):
+ pass
+
+class req(object):
+ """Represents a single ashd request. Normally, you would not
+ create instances of this class manually, but receive them from the
+ recvreq function.
+
+ For the abstract structure of ashd requests, please see the
+ ashd(7) manual page. This class provides access to the HTTP
+ method, raw URL, HTTP version and rest string via the `method',
+ `url', `ver' and `rest' variables respectively. It also implements
+ a dict-like interface for case-independent access to the HTTP
+ headers. The raw headers are available as a list of (name, value)
+ tuples in the `headers' variable.
+
+ For responding, the response socket is available as a standard
+ Python stream object in the `sk' variable. Again, see the ashd(7)
+ manpage for what to receive and transmit on the response socket.
+
+ Note that instances of this class contain a reference to the live
+ socket used for responding to requests, which should be closed
+ when you are done with the request. The socket can be closed
+ manually by calling the close() method on this
+ object. Alternatively, this class implements the resource-manager
+ interface, so that it can be used in `with' statements.
+ """
+
+ def __init__(self, method, url, ver, rest, headers, fd):
+ self.method = method
+ self.url = url
+ self.ver = ver
+ self.rest = rest
+ self.headers = headers
+ self.bsk = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
+ self.sk = self.bsk.makefile('rwb')
+ os.close(fd)
+
+ def close(self):
+ "Close this request's response socket."
+ self.sk.close()
+ self.bsk.close()
+
+ def __getitem__(self, header):
+ """Find a HTTP header case-insensitively. For example,
+ req["Content-Type"] returns the value of the content-type
+ header regardlessly of whether the client specified it as
+ "Content-Type", "content-type" or "Content-type".
+ """
+ if isinstance(header, str):
+ header = header.encode("ascii")
+ header = header.lower()
+ for key, val in self.headers:
+ if key.lower() == header:
+ return val
+ raise KeyError(header)
+
+ def __contains__(self, header):
+ """Works analogously to the __getitem__ method for checking
+ header presence case-insensitively.
+ """
+ if isinstance(header, str):
+ header = header.encode("ascii")
+ header = header.lower()
+ for key, val in self.headers:
+ if key.lower() == header:
+ return True
+ return False
+
+ def dup(self):
+ """Creates a duplicate of this request, referring to a
+ duplicate of the response socket.
+ """
+ return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.bsk.fileno()))
+
+ def match(self, match):
+ """If the `match' argument matches exactly the leading part of
+ the rest string, this method strips that part of the rest
+ string off and returns True. Otherwise, it returns False
+ without doing anything.
+
+ This can be used for simple dispatching. For example:
+ if req.match("foo/"):
+ handle(req)
+ elif req.match("bar/"):
+ handle_otherwise(req)
+ else:
+ util.respond(req, "Not found", status = "404 Not Found", ctype = "text/plain")
+ """
+ if isinstance(match, str):
+ match = match.encode("utf-8")
+ if self.rest[:len(match)] == match:
+ self.rest = self.rest[len(match):]
+ return True
+ return False
+
+ def __str__(self):
+ def dec(b):
+ return b.decode("ascii", errors="replace")
+ return "\"%s %s %s\"" % (dec(self.method), dec(self.url), dec(self.ver))
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *excinfo):
+ self.sk.close()
+ return False
+
+def recvreq(sock = 0):
+ """Receive a single ashd request on the specified socket file
+ descriptor (or standard input if unspecified).
+
+ The returned value is an instance of the `req' class. As per its
+ description, care should be taken to close() the request when
+ done, to avoid leaking response sockets. If end-of-file is
+ received on the socket, None is returned.
+
+ This function may either raise on OSError if an error occurs on
+ the socket, or a ashd.proto.protoerr if the incoming request is
+ invalidly encoded.
+ """
+ data, fd = htlib.recvfd(sock)
+ if fd is None:
+ return None
+ try:
+ parts = data.split(b'\0')[:-1]
+ if len(parts) < 5:
+ raise protoerr("Truncated request")
+ method, url, ver, rest = parts[:4]
+ headers = []
+ i = 4
+ while True:
+ if parts[i] == b"": break
+ if len(parts) - i < 3:
+ raise protoerr("Truncated request")
+ headers.append((parts[i], parts[i + 1]))
+ i += 2
+ return req(method, url, ver, rest, headers, os.dup(fd))
+ finally:
+ os.close(fd)
+
+def sendreq(sock, req):
+ """Encode and send a single request to the specified socket file
+ descriptor using the ashd protocol. The request should be an
+ instance of the `req' class.
+
+ This function may raise an OSError if an error occurs on the
+ socket.
+ """
+ data = b""
+ data += req.method + b'\0'
+ data += req.url + b'\0'
+ data += req.ver + b'\0'
+ data += req.rest + b'\0'
+ for key, val in req.headers:
+ data += key + b'\0'
+ data += val + b'\0'
+ data += b'\0'
+ htlib.sendfd(sock, req.sk.fileno(), data)
--- /dev/null
+import sys, collections
+import threading
+
+class protoerr(Exception):
+ pass
+
+class closed(IOError):
+ def __init__(self):
+ super(closed, self).__init__("The client has closed the connection.")
+
+def readns(sk):
+ hln = 0
+ while True:
+ c = sk.read(1)
+ if c == b':':
+ break
+ elif c >= b'0' or c <= b'9':
+ hln = (hln * 10) + (ord(c) - ord(b'0'))
+ else:
+ raise protoerr("Invalid netstring length byte: " + c)
+ ret = sk.read(hln)
+ if sk.read(1) != b',':
+ raise protoerr("Non-terminated netstring")
+ return ret
+
+def readhead(sk):
+ parts = readns(sk).split(b'\0')[:-1]
+ if len(parts) % 2 != 0:
+ raise protoerr("Malformed headers")
+ ret = {}
+ i = 0
+ while i < len(parts):
+ ret[parts[i]] = parts[i + 1]
+ i += 2
+ return ret
+
+class reqthread(threading.Thread):
+ def __init__(self, sk, handler):
+ super(reqthread, self).__init__(name = "SCGI request handler")
+ self.sk = sk.dup().makefile("rwb")
+ self.handler = handler
+
+ def run(self):
+ try:
+ head = readhead(self.sk)
+ self.handler(head, self.sk)
+ finally:
+ self.sk.close()
+
+def handlescgi(sk, handler):
+ t = reqthread(sk, handler)
+ t.start()
+
+def servescgi(socket, handler):
+ while True:
+ nsk, addr = socket.accept()
+ try:
+ handlescgi(nsk, handler)
+ finally:
+ nsk.close()
+
+def decodehead(head, coding):
+ return {k.decode(coding): v.decode(coding) for k, v in head.items()}
+
+def wrapwsgi(handler):
+ def handle(head, sk):
+ try:
+ env = decodehead(head, "utf-8")
+ env["wsgi.uri_encoding"] = "utf-8"
+ except UnicodeError:
+ env = decodehead(head, "latin-1")
+ env["wsgi.uri_encoding"] = "latin-1"
+ env["wsgi.version"] = 1, 0
+ if "HTTP_X_ASH_PROTOCOL" in env:
+ env["wsgi.url_scheme"] = env["HTTP_X_ASH_PROTOCOL"]
+ elif "HTTPS" in env:
+ env["wsgi.url_scheme"] = "https"
+ else:
+ env["wsgi.url_scheme"] = "http"
+ env["wsgi.input"] = 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"Status: " + recode(status) + b"\n"
+ for nm, val in headers:
+ buf += recode(nm) + b": " + recode(val) + b"\n"
+ buf += b"\n"
+ try:
+ sk.write(buf)
+ except IOError:
+ raise closed()
+
+ def write(data):
+ if not data:
+ return
+ flushreq()
+ try:
+ sk.write(data)
+ 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()
+ return handle
--- /dev/null
+"""High-level utility module for ashd(7)
+
+This module implements a rather convenient interface for writing ashd
+handlers, wrapping the low-level ashd.proto module.
+"""
+
+import os, socket, collections
+from . import proto
+
+__all__ = ["stdfork", "pchild", "respond", "serveloop"]
+
+def stdfork(argv, chinit = None):
+ """Fork a persistent handler process using the `argv' argument
+ list, as per the standard ashd(7) calling convention. For an
+ easier-to-use interface, see the `pchild' class.
+
+ If a callable object of no arguments is provided in the `chinit'
+ argument, it will be called in the child process before exec()'ing
+ the handler program, and can be used to set parameters for the new
+ process, such as working directory, nice level or ulimits.
+
+ Returns the file descriptor of the socket for sending requests to
+ the new child.
+ """
+ csk, psk = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+ pid = os.fork()
+ if pid == 0:
+ try:
+ os.dup2(csk.fileno(), 0)
+ for fd in range(3, 1024):
+ try:
+ os.close(fd)
+ except:
+ pass
+ if chinit is not None:
+ chinit()
+ os.execvp(argv[0], argv)
+ finally:
+ os._exit(127)
+ csk.close()
+ fd = os.dup(psk.fileno())
+ psk.close()
+ return fd
+
+class pchild(object):
+ """class pchild(argv, autorespawn=False, chinit=None)
+
+ Represents a persistent child handler process, started as per the
+ standard ashd(7) calling convention. It will be called with the
+ `argv' argument lest, which should be a list (or other iterable)
+ of strings.
+
+ If `autorespawn' is specified as True, the child process will be
+ automatically restarted if a request cannot be successfully sent
+ to it.
+
+ For a description of the `chinit' argument, see `stdfork'.
+
+ When this child handler should be disposed of, care should be
+ taken to call the close() method to release its socket and let it
+ exit. This class also implements the resource-manager interface,
+ so that it can be used in `with' statements.
+ """
+
+ def __init__(self, argv, autorespawn = False, chinit = None):
+ self.argv = argv
+ self.chinit = chinit
+ self.fd = -1
+ self.respawn = autorespawn
+ self.spawn()
+
+ def spawn(self):
+ """Start the child handler, or restart it if it is already
+ running. You should not have to call this method manually
+ unless you explicitly want to manage the process' lifecycle.
+ """
+ self.close()
+ self.fd = stdfork(self.argv, self.chinit)
+
+ def close(self):
+ """Close this child handler's socket. For normal child
+ handlers, this will make the program terminate normally.
+ """
+ if self.fd >= 0:
+ os.close(self.fd)
+ self.fd = -1
+
+ def __del__(self):
+ self.close()
+
+ def passreq(self, req):
+ """Pass the specified request (which should be an instance of
+ the ashd.proto.req class) to this child handler. If the child
+ handler fails for some reason, and `autorespawn' was specified
+ as True when creating this handler, one attempt will be made
+ to restart it.
+
+ Note: You still need to close the request normally.
+
+ This method may raise an OSError if the request fails and
+ autorespawning was either not requested, or if the
+ autorespawning fails.
+ """
+ try:
+ proto.sendreq(self.fd, req)
+ except OSError:
+ if self.respawn:
+ self.spawn()
+ proto.sendreq(self.fd, req)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *excinfo):
+ self.close()
+ return False
+
+def respond(req, body, status = ("200 OK"), ctype = "text/html"):
+ """Simple function for conveniently responding to a request.
+
+ Sends the specified body text to the request's response socket,
+ prepending an HTTP header with the appropriate Content-Type and
+ Content-Length headers, and then closes the response socket.
+
+ The `status' argument can be used to specify a non-200 response,
+ and the `ctype' argument can be used to specify a non-HTML
+ MIME-type.
+
+ If `body' is a Unicode object, it will be encoded as UTF-8.
+
+ For example:
+ respond(req, "Not found", status = "404 Not Found", ctype = "text/plain")
+ """
+ if isinstance(body, collections.ByteString):
+ body = bytes(body)
+ else:
+ body = str(body)
+ body = body.encode("utf-8")
+ if ctype[:5] == "text/" and ctype.find(';') < 0:
+ ctype = ctype + "; charset=utf-8"
+ try:
+ head = ""
+ head += "HTTP/1.1 %s\n" % status
+ head += "Content-Type: %s\n" % ctype
+ head += "Content-Length: %i\n" % len(body)
+ head += "\n"
+ req.sk.write(head.encode("ascii"))
+ req.sk.write(body)
+ finally:
+ req.close()
+
+def serveloop(handler, sock = 0):
+ """Implements a simple loop for serving requests sequentially, by
+ receiving requests from standard input (or the specified socket),
+ passing them to the specified handler function, and finally making
+ sure to close them. Returns when end-of-file is received on the
+ incoming socket.
+
+ The handler function should be a callable object of one argument,
+ and is called once for each received request.
+ """
+ while True:
+ req = proto.recvreq(sock)
+ if req is None:
+ break
+ try:
+ handler(req)
+ finally:
+ req.close()
--- /dev/null
+"""WSGI handler for serving chained WSGI modules from physical files
+
+The WSGI handler in this module examines the SCRIPT_FILENAME variable
+of the requests it handles -- that is, the physical file corresponding
+to the request, as determined by the webserver -- determining what to
+do with the request based on the extension of that file.
+
+By default, it handles files named `.wsgi' by compiling them into
+Python modules and using them, in turn, as chained WSGI handlers, but
+handlers for other extensions can be installed as well.
+
+When handling `.wsgi' files, the compiled modules are cached and
+reused until the file is modified, in which case the previous module
+is discarded and the new file contents are loaded into a new module in
+its place. When chaining such modules, an object named `wmain' is
+first looked for and called with no arguments if found. The object it
+returns is then used as the WSGI application object for that module,
+which is reused until the module is reloaded. If `wmain' is not found,
+an object named `application' is looked for instead. If found, it is
+used directly as the WSGI application object.
+
+This module itself contains both an `application' and a `wmain'
+object. If this module is used by ashd-wsgi(1) or scgi-wsgi(1) so that
+its wmain function is called, arguments can be specified to it to
+install handlers for other file extensions. Such arguments take the
+form `.EXT=MODULE.HANDLER', where EXT is the file extension to be
+handled, and the MODULE.HANDLER string is treated by splitting it
+along its last constituent dot. The part left of the dot is the name
+of a module which is imported, and the part right of the dot is the
+name of an object in that module, which should be a callable adhering
+to the WSGI specification. When called, this module will have made
+sure that the WSGI environment contains the SCRIPT_FILENAME parameter
+and that it is properly working. For example, the argument
+`.fpy=my.module.foohandler' can be given to pass requests for `.fpy'
+files to the function `foohandler' in the module `my.module' (which
+must, of course, be importable). When writing such handler functions,
+you will probably want to use the getmod() function in this module.
+"""
+
+import os, threading, types
+from . import wsgiutil
+
+__all__ = ["application", "wmain", "getmod", "cachedmod"]
+
+class cachedmod(object):
+ """Cache entry for modules loaded by getmod()
+
+ Instances of this class are returned by the getmod()
+ function. They contain three data attributes:
+ * mod - The loaded module
+ * lock - A threading.Lock object, which can be used for
+ manipulating this instance in a thread-safe manner
+ * mtime - The time the file was last modified
+
+ Additional data attributes can be arbitrarily added for recording
+ any meta-data about the module.
+ """
+ def __init__(self, mod, mtime):
+ self.lock = threading.Lock()
+ self.mod = mod
+ self.mtime = mtime
+
+exts = {}
+modcache = {}
+cachelock = threading.Lock()
+
+def mangle(path):
+ ret = ""
+ for c in path:
+ if c.isalnum():
+ ret += c
+ else:
+ ret += "_"
+ return ret
+
+def getmod(path):
+ """Load the given file as a module, caching it appropriately
+
+ The given file is loaded and compiled into a Python module. The
+ compiled module is cached and returned upon subsequent requests
+ for the same file, unless the file has changed (as determined by
+ its mtime), in which case the cached module is discarded and the
+ new file contents are reloaded in its place.
+
+ The return value is an instance of the cachedmod class, which can
+ be used for locking purposes and for storing arbitrary meta-data
+ about the module. See its documentation for details.
+ """
+ sb = os.stat(path)
+ cachelock.acquire()
+ try:
+ if path in modcache:
+ entry = modcache[path]
+ if sb.st_mtime <= entry.mtime:
+ return entry
+
+ f = open(path)
+ try:
+ text = f.read()
+ finally:
+ f.close()
+ code = compile(text, path, "exec")
+ mod = types.ModuleType(mangle(path))
+ mod.__file__ = path
+ exec(code, mod.__dict__)
+ entry = cachedmod(mod, sb.st_mtime)
+ modcache[path] = entry
+ return entry
+ finally:
+ cachelock.release()
+
+def chain(env, startreq):
+ path = env["SCRIPT_FILENAME"]
+ mod = getmod(path)
+ entry = None
+ if mod is not None:
+ mod.lock.acquire()
+ try:
+ if hasattr(mod, "entry"):
+ entry = mod.entry
+ else:
+ if hasattr(mod.mod, "wmain"):
+ entry = mod.mod.wmain()
+ elif hasattr(mod.mod, "application"):
+ entry = mod.mod.application
+ mod.entry = entry
+ finally:
+ mod.lock.release()
+ if entry is not None:
+ return entry(env, startreq)
+ return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
+exts["wsgi"] = chain
+
+def addext(ext, handler):
+ p = handler.rindex('.')
+ mname = handler[:p]
+ hname = handler[p + 1:]
+ mod = __import__(mname, fromlist = ["dummy"])
+ exts[ext] = getattr(mod, hname)
+
+def application(env, startreq):
+ """WSGI handler function
+
+ Handles WSGI requests as per the module documentation.
+ """
+ if not "SCRIPT_FILENAME" in env:
+ return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
+ path = env["SCRIPT_FILENAME"]
+ base = os.path.basename(path)
+ p = base.rfind('.')
+ if p < 0 or not os.access(path, os.R_OK):
+ return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
+ ext = base[p + 1:]
+ if not ext in exts:
+ return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
+ return(exts[ext](env, startreq))
+
+def wmain(*argv):
+ """Main function for ashd(7)-compatible WSGI handlers
+
+ Returns the `application' function. If any arguments are given,
+ they are parsed according to the module documentation.
+ """
+ for arg in argv:
+ if arg[0] == '.':
+ p = arg.index('=')
+ addext(arg[1:p], arg[p + 1:])
+ return application
--- /dev/null
+def htmlquote(text):
+ ret = ""
+ for c in text:
+ if c == '&':
+ ret += "&"
+ elif c == '<':
+ ret += "<"
+ elif c == '>':
+ ret += ">"
+ elif c == '"':
+ ret += """
+ else:
+ ret += c
+ return ret
+
+def simpleerror(env, startreq, code, title, msg):
+ buf = """<?xml version="1.0" encoding="US-ASCII"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
+<head>
+<title>%s</title>
+</head>
+<body>
+<h1>%s</h1>
+<p>%s</p>
+</body>
+</html>""" % (title, title, htmlquote(msg))
+ buf = buf.encode("ascii")
+ startreq("%i %s" % (code, title), [("Content-Type", "text/html"), ("Content-Length", str(len(buf)))])
+ return [buf]
--- /dev/null
+/*.1
+/*.html
+/*.css
--- /dev/null
+ashd-wsgi(1)
+============
+
+NAME
+----
+ashd-wsgi - WSGI adapter for ashd(7)
+
+SYNOPSIS
+--------
+*ashd-wsgi* [*-hA*] [*-p* 'MODPATH'] [*-l* 'LIMIT'] 'HANDLER-MODULE' ['ARGS'...]
+
+DESCRIPTION
+-----------
+
+The *ashd-wsgi* handler translates *ashd*(7) requests to WSGI
+requests, and passes them to a specified Python handler module. The
+precise Python convention for doing so is described in the PROTOCOL
+section, below.
+
+*ashd-wsgi* is a persistent handler, as defined in *ashd*(7). It uses
+multithreaded dispatching in a single Python interpreter, which means
+that WSGI applications that use it need to be thread-safe, but that
+they can also share all Python data structures and global variables
+between requests.
+
+The Python module that *ashd-wsgi* comes with also contains a standard
+handler module, `ashd.wsgidir`, which serves individual WSGI
+applications directly from the files in which they reside and as such
+makes this program useful as a *dirplex*(1) handler. Please see its
+Python documentation for further details.
+
+*ashd-wsgi* requires the `ashd.proto` and `ashd.util` modules, which
+are only available for CPython. If you want to use some other Python
+implementation instead, you may want to use the *scgi-wsgi*(1) program
+instead, along with *callscgi*(1).
+
+OPTIONS
+-------
+
+*-h*::
+
+ Print a brief help message to standard output and exit.
+
+*-A*::
+
+ Use the convention used by Apache's mod_wsgi module to find
+ the WSGI application object. See the PROTOCOL section, below,
+ for details.
+
+*-p* 'MODPATH'::
+
+ Prepend 'MODPATH' to Python's `sys.path`; can be given multiple
+ times. Note that the working directory of *ashd-wsgi* is not
+ on Python's module path by default, so if you want to use a
+ module in that directory, you will need to specify "`-p .`".
+
+*-l* 'LIMIT'::
+
+ Allow at most 'LIMIT' requests to run concurrently. If a new
+ request is made when 'LIMIT' requests are executing, the new
+ request will wait up to ten seconds for one of them to
+ complete; if none does, *ashd-wsgi* will assume that the
+ process is foobar and *abort*(3).
+
+PROTOCOL
+--------
+
+When starting, *ashd-wsgi* will attempt to import the module named by
+'HANDLER-MODULE', look for an object named `wmain` in that module,
+call that object passing the 'ARGS' (as Python strings) as positional
+parameters, and use the returned object as the WSGI application
+object. If the *-A* option was specified, it will look for an object
+named `application` instead of `wmain`, and use that object directly
+as the WSGI application object.
+
+When calling the WSGI application, a new thread is started for each
+request, in which the WSGI application object is called. All requests
+run in the same interpreter, so it is guaranteed that data structures
+and global variables can be shared between requests.
+
+The WSGI environment is the standard CGI environment, including the
+`SCRIPT_FILENAME` variable whenever the `X-Ash-File` header was
+included in the request.
+
+EXAMPLES
+--------
+
+The following *dirplex*(1) configuration can be used for serving WSGI
+modules directly from the filesystem.
+
+--------
+child wsgidir
+ exec ashd-wsgi ashd.wsgidir
+match
+ filename *.wsgi
+ handler wsgidir
+--------
+
+Since *ashd-wsgi* is a persistent handler, it can be used directly as
+a root handler for *htparser*(1). For instance, if the directory
+`/srv/www/foo` contains a `wsgi.py` file, which declares a standard
+WSGI `application` object, it can be served with the following
+command:
+
+--------
+htparser plain:port=8080 -- ashd-wsgi -Ap /srv/www/foo wsgi
+--------
+
+AUTHOR
+------
+Fredrik Tolf <fredrik@dolda2000.com>
+
+SEE ALSO
+--------
+*scgi-wsgi*(1), *ashd*(7), <http://wsgi.org/>
--- /dev/null
+scgi-wsgi(1)
+============
+
+NAME
+----
+scgi-wsgi - WSGI adapter for SCGI
+
+SYNOPSIS
+--------
+*scgi-wsgi* [*-hA*] [*-p* 'MODPATH'] [*-T* \[HOST:]'PORT'] 'HANDLER-MODULE' ['ARGS'...]
+
+DESCRIPTION
+-----------
+
+The *scgi-wsgi* program translates SCGI requests to WSGI requests, and
+passes them to a specified Python module. It is mainly written to
+emulate the behavior of *ashd-wsgi*(1), but over SCGI instead of the
+native *ashd*(7) protocol, so please see its documentation for details
+of Python interoperation. Unlike *ashd-wsgi* which requires CPython,
+however, *scgi-wsgi* is written in pure Python using only the standard
+library, and so should be usable by any Python implementation. If
+using it under *ashd*(7), please see the documentation for
+*callscgi*(1) as well.
+
+Following *callscgi*(1) conventions, *scgi-wsgi* will, by default,
+accept connections on a socket passed on its standard input (a
+behavior which is, obviously, not available on all Python
+implementations). Use the *-T* option to listen to a TCP address
+instead.
+
+OPTIONS
+-------
+
+*-h*::
+
+ Print a brief help message to standard output and exit.
+
+*-A*::
+
+ Use the convention used by Apache's mod_wsgi module to find
+ the WSGI application object. See the PROTOCOL section of
+ *ashd-wsgi*(1) for details.
+
+*-p* 'MODPATH'::
+
+ Prepend 'MODPATH' to Python's `sys.path`; can be given multiple
+ times.
+
+*-T* \[HOST:]'PORT'::
+
+ Instead of using a listening socket passed on standard input
+ to accept SCGI connections, bind a TCP socket to the 'HOST'
+ address listening for connections on 'PORT' instead. If 'HOST'
+ is not given, `localhost` is used by default.
+
+AUTHOR
+------
+Fredrik Tolf <fredrik@dolda2000.com>
+
+SEE ALSO
+--------
+*ashd-wsgi*(1), *callscgi*(1), <http://wsgi.org/>,
+<http://www.python.ca/scgi/>
--- /dev/null
+/*
+ ashd - A Sane HTTP Daemon
+ Copyright (C) 2008 Fredrik Tolf <fredrik@dolda2000.com>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <Python.h>
+#include <errno.h>
+
+#include <ashd/utils.h>
+#include <ashd/proc.h>
+
+static PyObject *p_recvfd(PyObject *self, PyObject *args)
+{
+ int fd, ret;
+ char *data;
+ size_t dlen;
+ PyObject *ro;
+
+ fd = 0;
+ if(!PyArg_ParseTuple(args, "|i", &fd))
+ return(NULL);
+ Py_BEGIN_ALLOW_THREADS;
+ ret = recvfd(fd, &data, &dlen);
+ Py_END_ALLOW_THREADS;
+ if(ret < 0) {
+ if(errno == 0)
+ return(Py_BuildValue("OO", Py_None, Py_None));
+ PyErr_SetFromErrno(PyExc_OSError);
+ return(NULL);
+ }
+ ro = Py_BuildValue("Ni", PyBytes_FromStringAndSize(data, dlen), ret);
+ free(data);
+ return(ro);
+}
+
+static PyObject *p_sendfd(PyObject *self, PyObject *args)
+{
+ int sock, fd, ret;
+ Py_buffer data;
+
+ if(!PyArg_ParseTuple(args, "iiy*", &sock, &fd, &data))
+ return(NULL);
+ Py_BEGIN_ALLOW_THREADS;
+ ret = sendfd(sock, fd, data.buf, data.len);
+ Py_END_ALLOW_THREADS;
+ PyBuffer_Release(&data);
+ if(ret < 0) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ return(NULL);
+ }
+ Py_RETURN_NONE;
+}
+
+static PyMethodDef methods[] = {
+ {"recvfd", p_recvfd, METH_VARARGS, "Receive a datagram and a file descriptor"},
+ {"sendfd", p_sendfd, METH_VARARGS, "Send a datagram and a file descriptor"},
+ {NULL, NULL, 0, NULL}
+};
+
+static struct PyModuleDef module = {
+ PyModuleDef_HEAD_INIT,
+ .m_name = "htlib",
+ .m_size = -1,
+ .m_methods = methods,
+};
+
+PyMODINIT_FUNC PyInit_htlib(void)
+{
+ return(PyModule_Create(&module));
+}
--- /dev/null
+#!/usr/bin/python3
+
+from distutils.core import setup, Extension
+
+htlib = Extension("ashd.htlib", ["htp.c"],
+ libraries = ["ht"])
+
+setup(name = "ashd-py",
+ version = "0.4",
+ description = "Python module for handling ashd requests",
+ author = "Fredrik Tolf",
+ author_email = "fredrik@dolda2000.com",
+ url = "http://www.dolda2000.com/~fredrik/ashd/",
+ ext_modules = [htlib],
+ packages = ["ashd"],
+ scripts = ["ashd-wsgi3", "scgi-wsgi3"],
+ license = "GPL-3")