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