lib: Reimplemented mtio-epoll timeout checking as a bin-heap.
[ashd.git] / python / ashd / proto.py
CommitLineData
2baf419b
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
c270f222 10import os, socket
173e0e9e 11import htlib
c270f222 12
e66efe5f
FT
13__all__ = ["req", "recvreq", "sendreq"]
14
c270f222
FT
15class protoerr(Exception):
16 pass
17
18class req(object):
2baf419b
FT
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
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.
41 """
42
c270f222
FT
43 def __init__(self, method, url, ver, rest, headers, fd):
44 self.method = method
45 self.url = url
46 self.ver = ver
47 self.rest = rest
48 self.headers = headers
173e0e9e 49 self.sk = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM).makefile('r+')
c270f222
FT
50 os.close(fd)
51
52 def close(self):
2baf419b 53 "Close this request's response socket."
c270f222
FT
54 self.sk.close()
55
56 def __getitem__(self, header):
2baf419b
FT
57 """Find a HTTP header case-insensitively. For example,
58 req["Content-Type"] returns the value of the content-type
59 header regardlessly of whether the client specified it as
60 "Content-Type", "content-type" or "Content-type".
61 """
c270f222
FT
62 header = header.lower()
63 for key, val in self.headers:
64 if key.lower() == header:
65 return val
66 raise KeyError(header)
67
68 def __contains__(self, header):
2baf419b
FT
69 """Works analogously to the __getitem__ method for checking
70 header presence case-insensitively.
71 """
c270f222
FT
72 header = header.lower()
73 for key, val in self.headers:
74 if key.lower() == header:
75 return True
76 return False
77
78 def dup(self):
2baf419b
FT
79 """Creates a duplicate of this request, referring to a
80 duplicate of the response socket.
81 """
173e0e9e 82 return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.sk.fileno()))
c270f222
FT
83
84 def match(self, match):
2baf419b
FT
85 """If the `match' argument matches exactly the leading part of
86 the rest string, this method strips that part of the rest
87 string off and returns True. Otherwise, it returns False
88 without doing anything.
89
90 This can be used for simple dispatching. For example:
91 if req.match("foo/"):
92 handle(req)
93 elif req.match("bar/"):
94 handle_otherwise(req)
95 else:
96 util.respond(req, "Not found", status = "404 Not Found", ctype = "text/plain")
97 """
c270f222
FT
98 if self.rest[:len(match)] == match:
99 self.rest = self.rest[len(match):]
100 return True
101 return False
102
103 def __str__(self):
173e0e9e 104 return "\"%s %s %s\"" % (self.method, self.url, self.ver)
c270f222
FT
105
106 def __enter__(self):
107 return self
108
109 def __exit__(self, *excinfo):
110 self.sk.close()
111 return False
112
113def recvreq(sock = 0):
2baf419b
FT
114 """Receive a single ashd request on the specified socket file
115 descriptor (or standard input if unspecified).
116
117 The returned value is an instance of the `req' class. As per its
118 description, care should be taken to close() the request when
b0ac3ba1
FT
119 done, to avoid leaking response sockets. If end-of-file is
120 received on the socket, None is returned.
2baf419b 121
f56b0790
FT
122 This function may either raise an OSError if an error occurs on
123 the socket, or an ashd.proto.protoerr if the incoming request is
2baf419b
FT
124 invalidly encoded.
125 """
c270f222
FT
126 data, fd = htlib.recvfd(sock)
127 if fd is None:
128 return None
129 try:
173e0e9e 130 parts = data.split('\0')[:-1]
c270f222
FT
131 if len(parts) < 5:
132 raise protoerr("Truncated request")
133 method, url, ver, rest = parts[:4]
134 headers = []
135 i = 4
136 while True:
173e0e9e 137 if parts[i] == "": break
c270f222
FT
138 if len(parts) - i < 3:
139 raise protoerr("Truncated request")
140 headers.append((parts[i], parts[i + 1]))
141 i += 2
142 return req(method, url, ver, rest, headers, os.dup(fd))
143 finally:
144 os.close(fd)
145
146def sendreq(sock, req):
2baf419b
FT
147 """Encode and send a single request to the specified socket file
148 descriptor using the ashd protocol. The request should be an
149 instance of the `req' class.
150
151 This function may raise an OSError if an error occurs on the
152 socket.
153 """
173e0e9e
FT
154 data = ""
155 data += req.method + '\0'
156 data += req.url + '\0'
157 data += req.ver + '\0'
158 data += req.rest + '\0'
c270f222 159 for key, val in req.headers:
173e0e9e
FT
160 data += key + '\0'
161 data += val + '\0'
162 data += '\0'
c270f222 163 htlib.sendfd(sock, req.sk.fileno(), data)