lib: Added mblock support for kqueue.
[ashd.git] / python3 / ashd / proto.py
CommitLineData
173e0e9e
FT
1"""Low-level protocol module for ashd(7)
2
3This module provides primitive functions that speak the raw ashd(7)
4protocol. Primarily, it implements the `req' class that is used to
5represent ashd requests. The functions it provides can also be used to
6create ashd handlers, but unless you require very precise control, the
7ashd.util module provides an easier-to-use interface.
8"""
9
10import os, socket
11from . import htlib
12
13__all__ = ["req", "recvreq", "sendreq"]
14
15class protoerr(Exception):
16 pass
17
18class req(object):
19 """Represents a single ashd request. Normally, you would not
20 create instances of this class manually, but receive them from the
21 recvreq function.
22
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.
30
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.
34
1f3d7aa3
FT
35 Note that all request parts are stored in byte, rather than
36 string, form. The response socket is also opened in binary mode.
37
38 Note also that instances of this class contain a reference to the
39 live socket used for responding to requests, which should be
40 closed when you are done with the request. The socket can be
41 closed manually by calling the close() method on this
173e0e9e
FT
42 object. Alternatively, this class implements the resource-manager
43 interface, so that it can be used in `with' statements.
44 """
45
46 def __init__(self, method, url, ver, rest, headers, fd):
47 self.method = method
48 self.url = url
49 self.ver = ver
50 self.rest = rest
51 self.headers = headers
52 self.bsk = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
53 self.sk = self.bsk.makefile('rwb')
54 os.close(fd)
55
56 def close(self):
57 "Close this request's response socket."
58 self.sk.close()
59 self.bsk.close()
60
61 def __getitem__(self, header):
62 """Find a HTTP header case-insensitively. For example,
63 req["Content-Type"] returns the value of the content-type
64 header regardlessly of whether the client specified it as
65 "Content-Type", "content-type" or "Content-type".
1f3d7aa3
FT
66
67 If the header is given as a (Unicode) string, it is encoded
68 into Ascii for use in matching.
173e0e9e
FT
69 """
70 if isinstance(header, str):
71 header = header.encode("ascii")
72 header = header.lower()
73 for key, val in self.headers:
74 if key.lower() == header:
75 return val
76 raise KeyError(header)
77
78 def __contains__(self, header):
79 """Works analogously to the __getitem__ method for checking
80 header presence case-insensitively.
81 """
82 if isinstance(header, str):
83 header = header.encode("ascii")
84 header = header.lower()
85 for key, val in self.headers:
86 if key.lower() == header:
87 return True
88 return False
89
90 def dup(self):
91 """Creates a duplicate of this request, referring to a
92 duplicate of the response socket.
93 """
94 return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.bsk.fileno()))
95
96 def match(self, match):
97 """If the `match' argument matches exactly the leading part of
98 the rest string, this method strips that part of the rest
99 string off and returns True. Otherwise, it returns False
100 without doing anything.
101
1f3d7aa3
FT
102 If the `match' argument is given as a (Unicode) string, it is
103 encoded into UTF-8.
104
173e0e9e
FT
105 This can be used for simple dispatching. For example:
106 if req.match("foo/"):
107 handle(req)
108 elif req.match("bar/"):
109 handle_otherwise(req)
110 else:
111 util.respond(req, "Not found", status = "404 Not Found", ctype = "text/plain")
112 """
113 if isinstance(match, str):
114 match = match.encode("utf-8")
115 if self.rest[:len(match)] == match:
116 self.rest = self.rest[len(match):]
117 return True
118 return False
119
120 def __str__(self):
121 def dec(b):
122 return b.decode("ascii", errors="replace")
123 return "\"%s %s %s\"" % (dec(self.method), dec(self.url), dec(self.ver))
124
125 def __enter__(self):
126 return self
127
128 def __exit__(self, *excinfo):
129 self.sk.close()
130 return False
131
132def recvreq(sock = 0):
133 """Receive a single ashd request on the specified socket file
134 descriptor (or standard input if unspecified).
135
136 The returned value is an instance of the `req' class. As per its
137 description, care should be taken to close() the request when
138 done, to avoid leaking response sockets. If end-of-file is
139 received on the socket, None is returned.
140
f56b0790
FT
141 This function may either raise an OSError if an error occurs on
142 the socket, or an ashd.proto.protoerr if the incoming request is
173e0e9e
FT
143 invalidly encoded.
144 """
145 data, fd = htlib.recvfd(sock)
146 if fd is None:
147 return None
148 try:
149 parts = data.split(b'\0')[:-1]
150 if len(parts) < 5:
151 raise protoerr("Truncated request")
152 method, url, ver, rest = parts[:4]
153 headers = []
154 i = 4
155 while True:
156 if parts[i] == b"": break
157 if len(parts) - i < 3:
158 raise protoerr("Truncated request")
159 headers.append((parts[i], parts[i + 1]))
160 i += 2
161 return req(method, url, ver, rest, headers, os.dup(fd))
162 finally:
163 os.close(fd)
164
165def sendreq(sock, req):
166 """Encode and send a single request to the specified socket file
167 descriptor using the ashd protocol. The request should be an
168 instance of the `req' class.
169
170 This function may raise an OSError if an error occurs on the
171 socket.
172 """
173 data = b""
174 data += req.method + b'\0'
175 data += req.url + b'\0'
176 data += req.ver + b'\0'
177 data += req.rest + b'\0'
178 for key, val in req.headers:
179 data += key + b'\0'
180 data += val + b'\0'
181 data += b'\0'
ae53bd62 182 htlib.sendfd(sock, req.bsk.fileno(), data)