lib: Reimplemented mtio-epoll timeout checking as a bin-heap.
[ashd.git] / python / ashd / proto.py
1 """Low-level protocol module for ashd(7)
2
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.
8 """
9
10 import os, socket
11 import htlib
12
13 __all__ = ["req", "recvreq", "sendreq"]
14
15 class protoerr(Exception):
16     pass
17
18 class 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
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     
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
49         self.sk = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM).makefile('r+')
50         os.close(fd)
51
52     def close(self):
53         "Close this request's response socket."
54         self.sk.close()
55
56     def __getitem__(self, header):
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         """
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):
69         """Works analogously to the __getitem__ method for checking
70         header presence case-insensitively.
71         """
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):
79         """Creates a duplicate of this request, referring to a
80         duplicate of the response socket.
81         """
82         return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.sk.fileno()))
83
84     def match(self, match):
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         """
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):
104         return "\"%s %s %s\"" % (self.method, self.url, self.ver)
105
106     def __enter__(self):
107         return self
108
109     def __exit__(self, *excinfo):
110         self.sk.close()
111         return False
112
113 def recvreq(sock = 0):
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
119     done, to avoid leaking response sockets. If end-of-file is
120     received on the socket, None is returned.
121
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
124     invalidly encoded.
125     """
126     data, fd = htlib.recvfd(sock)
127     if fd is None:
128         return None
129     try:
130         parts = data.split('\0')[:-1]
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:
137             if parts[i] == "": break
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
146 def sendreq(sock, req):
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     """
154     data = ""
155     data += req.method + '\0'
156     data += req.url + '\0'
157     data += req.ver + '\0'
158     data += req.rest + '\0'
159     for key, val in req.headers:
160         data += key + '\0'
161         data += val + '\0'
162     data += '\0'
163     htlib.sendfd(sock, req.sk.fileno(), data)