Merge branch 'master' into timeheap
[ashd.git] / python / ashd / proto.py
... / ...
CommitLineData
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
11import 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
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
115def 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
148def 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)