Commit | Line | Data |
---|---|---|
2baf419b FT |
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 | ||
c270f222 | 10 | import os, socket |
173e0e9e | 11 | import htlib |
c270f222 | 12 | |
e66efe5f FT |
13 | __all__ = ["req", "recvreq", "sendreq"] |
14 | ||
c270f222 FT |
15 | class protoerr(Exception): |
16 | pass | |
17 | ||
18 | class 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 | ||
113 | def 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 | ||
146 | def 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) |