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