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 FT |
10 | import os, socket |
11 | import htlib | |
12 | ||
13 | class protoerr(Exception): | |
14 | pass | |
15 | ||
16 | class req(object): | |
2baf419b FT |
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 | ||
c270f222 FT |
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): | |
2baf419b | 51 | "Close this request's response socket." |
c270f222 FT |
52 | self.sk.close() |
53 | ||
54 | def __getitem__(self, header): | |
2baf419b FT |
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 | """ | |
c270f222 FT |
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): | |
2baf419b FT |
67 | """Works analogously to the __getitem__ method for checking |
68 | header presence case-insensitively. | |
69 | """ | |
c270f222 FT |
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): | |
2baf419b FT |
77 | """Creates a duplicate of this request, referring to a |
78 | duplicate of the response socket. | |
79 | """ | |
c270f222 FT |
80 | return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.sk.fileno())) |
81 | ||
82 | def match(self, match): | |
2baf419b FT |
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 | """ | |
c270f222 FT |
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): | |
2baf419b FT |
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 | |
b0ac3ba1 FT |
117 | done, to avoid leaking response sockets. If end-of-file is |
118 | received on the socket, None is returned. | |
2baf419b FT |
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 | """ | |
c270f222 FT |
124 | data, fd = htlib.recvfd(sock) |
125 | if fd is None: | |
126 | return None | |
127 | try: | |
c270f222 FT |
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): | |
2baf419b FT |
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 | """ | |
c270f222 FT |
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) |