python: Clarified recvreq documentation.
[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 class protoerr(Exception):
14     pass
15
16 class req(object):
17     """Represents a single ashd request. Normally, you would not
18     create instances of this class manually, but receive them from the
19     recvreq function.
20
21     For the abstract structure of ashd requests, please see the
22     ashd(7) manual page. This class provides access to the HTTP
23     method, raw URL, HTTP version and rest string via the `method',
24     `url', `ver' and `rest' variables respectively. It also implements
25     a dict-like interface for case-independent access to the HTTP
26     headers. The raw headers are available as a list of (name, value)
27     tuples in the `headers' variable.
28
29     For responding, the response socket is available as a standard
30     Python stream object in the `sk' variable. Again, see the ashd(7)
31     manpage for what to receive and transmit on the response socket.
32
33     Note that instances of this class contain a reference to the live
34     socket used for responding to requests, which should be closed
35     when you are done with the request. The socket can be closed
36     manually by calling the close() method on this
37     object. Alternatively, this class implements the resource-manager
38     interface, so that it can be used in `with' statements.
39     """
40     
41     def __init__(self, method, url, ver, rest, headers, fd):
42         self.method = method
43         self.url = url
44         self.ver = ver
45         self.rest = rest
46         self.headers = headers
47         self.sk = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM).makefile('r+')
48         os.close(fd)
49
50     def close(self):
51         "Close this request's response socket."
52         self.sk.close()
53
54     def __getitem__(self, header):
55         """Find a HTTP header case-insensitively. For example,
56         req["Content-Type"] returns the value of the content-type
57         header regardlessly of whether the client specified it as
58         "Content-Type", "content-type" or "Content-type".
59         """
60         header = header.lower()
61         for key, val in self.headers:
62             if key.lower() == header:
63                 return val
64         raise KeyError(header)
65
66     def __contains__(self, header):
67         """Works analogously to the __getitem__ method for checking
68         header presence case-insensitively.
69         """
70         header = header.lower()
71         for key, val in self.headers:
72             if key.lower() == header:
73                 return True
74         return False
75
76     def dup(self):
77         """Creates a duplicate of this request, referring to a
78         duplicate of the response socket.
79         """
80         return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.sk.fileno()))
81
82     def match(self, match):
83         """If the `match' argument matches exactly the leading part of
84         the rest string, this method strips that part of the rest
85         string off and returns True. Otherwise, it returns False
86         without doing anything.
87
88         This can be used for simple dispatching. For example:
89         if req.match("foo/"):
90             handle(req)
91         elif req.match("bar/"):
92             handle_otherwise(req)
93         else:
94             util.respond(req, "Not found", status = "404 Not Found", ctype = "text/plain")
95         """
96         if self.rest[:len(match)] == match:
97             self.rest = self.rest[len(match):]
98             return True
99         return False
100
101     def __str__(self):
102         return "\"%s %s %s\"" % (self.method, self.url, self.ver)
103
104     def __enter__(self):
105         return self
106
107     def __exit__(self, *excinfo):
108         self.sk.close()
109         return False
110
111 def recvreq(sock = 0):
112     """Receive a single ashd request on the specified socket file
113     descriptor (or standard input if unspecified).
114
115     The returned value is an instance of the `req' class. As per its
116     description, care should be taken to close() the request when
117     done, to avoid leaking response sockets. If end-of-file is
118     received on the socket, None is returned.
119
120     This function may either raise on OSError if an error occurs on
121     the socket, or a ashd.proto.protoerr if the incoming request is
122     invalidly encoded.
123     """
124     data, fd = htlib.recvfd(sock)
125     if fd is None:
126         return None
127     try:
128         parts = data.split('\0')[:-1]
129         if len(parts) < 5:
130             raise protoerr("Truncated request")
131         method, url, ver, rest = parts[:4]
132         headers = []
133         i = 4
134         while True:
135             if parts[i] == "": break
136             if len(parts) - i < 3:
137                 raise protoerr("Truncated request")
138             headers.append((parts[i], parts[i + 1]))
139             i += 2
140         return req(method, url, ver, rest, headers, os.dup(fd))
141     finally:
142         os.close(fd)
143
144 def sendreq(sock, req):
145     """Encode and send a single request to the specified socket file
146     descriptor using the ashd protocol. The request should be an
147     instance of the `req' class.
148
149     This function may raise an OSError if an error occurs on the
150     socket.
151     """
152     data = ""
153     data += req.method + '\0'
154     data += req.url + '\0'
155     data += req.ver + '\0'
156     data += req.rest + '\0'
157     for key, val in req.headers:
158         data += key + '\0'
159         data += val + '\0'
160     data += '\0'
161     htlib.sendfd(sock, req.sk.fileno(), data)