1 """Low-level protocol module for ashd(7)
3 This module provides primitive functions that speak the raw ashd(7)
4 protocol. Primarily, it implements the `req' class that is used to
5 represent ashd requests. The functions it provides can also be used to
6 create ashd handlers, but unless you require very precise control, the
7 ashd.util module provides an easier-to-use interface.
13 __all__ = ["req", "recvreq", "sendreq"]
15 class protoerr(Exception):
19 """Represents a single ashd request. Normally, you would not
20 create instances of this class manually, but receive them from the
23 For the abstract structure of ashd requests, please see the
24 ashd(7) manual page. This class provides access to the HTTP
25 method, raw URL, HTTP version and rest string via the `method',
26 `url', `ver' and `rest' variables respectively. It also implements
27 a dict-like interface for case-independent access to the HTTP
28 headers. The raw headers are available as a list of (name, value)
29 tuples in the `headers' variable.
31 For responding, the response socket is available as a standard
32 Python stream object in the `sk' variable. Again, see the ashd(7)
33 manpage for what to receive and transmit on the response socket.
35 Note that instances of this class contain a reference to the live
36 socket used for responding to requests, which should be closed
37 when you are done with the request. The socket can be closed
38 manually by calling the close() method on this
39 object. Alternatively, this class implements the resource-manager
40 interface, so that it can be used in `with' statements.
43 def __init__(self, method, url, ver, rest, headers, fd):
48 self.headers = headers
49 self.bsk = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
50 self.sk = self.bsk.makefile('rwb')
54 "Close this request's response socket."
58 def __getitem__(self, header):
59 """Find a HTTP header case-insensitively. For example,
60 req["Content-Type"] returns the value of the content-type
61 header regardlessly of whether the client specified it as
62 "Content-Type", "content-type" or "Content-type".
64 if isinstance(header, str):
65 header = header.encode("ascii")
66 header = header.lower()
67 for key, val in self.headers:
68 if key.lower() == header:
70 raise KeyError(header)
72 def __contains__(self, header):
73 """Works analogously to the __getitem__ method for checking
74 header presence case-insensitively.
76 if isinstance(header, str):
77 header = header.encode("ascii")
78 header = header.lower()
79 for key, val in self.headers:
80 if key.lower() == header:
85 """Creates a duplicate of this request, referring to a
86 duplicate of the response socket.
88 return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.bsk.fileno()))
90 def match(self, match):
91 """If the `match' argument matches exactly the leading part of
92 the rest string, this method strips that part of the rest
93 string off and returns True. Otherwise, it returns False
94 without doing anything.
96 This can be used for simple dispatching. For example:
99 elif req.match("bar/"):
100 handle_otherwise(req)
102 util.respond(req, "Not found", status = "404 Not Found", ctype = "text/plain")
104 if isinstance(match, str):
105 match = match.encode("utf-8")
106 if self.rest[:len(match)] == match:
107 self.rest = self.rest[len(match):]
113 return b.decode("ascii", errors="replace")
114 return "\"%s %s %s\"" % (dec(self.method), dec(self.url), dec(self.ver))
119 def __exit__(self, *excinfo):
123 def recvreq(sock = 0):
124 """Receive a single ashd request on the specified socket file
125 descriptor (or standard input if unspecified).
127 The returned value is an instance of the `req' class. As per its
128 description, care should be taken to close() the request when
129 done, to avoid leaking response sockets. If end-of-file is
130 received on the socket, None is returned.
132 This function may either raise on OSError if an error occurs on
133 the socket, or a ashd.proto.protoerr if the incoming request is
136 data, fd = htlib.recvfd(sock)
140 parts = data.split(b'\0')[:-1]
142 raise protoerr("Truncated request")
143 method, url, ver, rest = parts[:4]
147 if parts[i] == b"": break
148 if len(parts) - i < 3:
149 raise protoerr("Truncated request")
150 headers.append((parts[i], parts[i + 1]))
152 return req(method, url, ver, rest, headers, os.dup(fd))
156 def sendreq(sock, req):
157 """Encode and send a single request to the specified socket file
158 descriptor using the ashd protocol. The request should be an
159 instance of the `req' class.
161 This function may raise an OSError if an error occurs on the
165 data += req.method + b'\0'
166 data += req.url + b'\0'
167 data += req.ver + b'\0'
168 data += req.rest + b'\0'
169 for key, val in req.headers:
173 htlib.sendfd(sock, req.sk.fileno(), data)