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 | ||
1f3d7aa3 FT |
35 | Note that all request parts are stored in byte, rather than |
36 | string, form. The response socket is also opened in binary mode. | |
37 | ||
38 | Note also that instances of this class contain a reference to the | |
39 | live socket used for responding to requests, which should be | |
40 | closed when you are done with the request. The socket can be | |
41 | closed manually by calling the close() method on this | |
173e0e9e FT |
42 | object. Alternatively, this class implements the resource-manager |
43 | interface, so that it can be used in `with' statements. | |
44 | """ | |
45 | ||
46 | def __init__(self, method, url, ver, rest, headers, fd): | |
47 | self.method = method | |
48 | self.url = url | |
49 | self.ver = ver | |
50 | self.rest = rest | |
51 | self.headers = headers | |
52 | self.bsk = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM) | |
53 | self.sk = self.bsk.makefile('rwb') | |
54 | os.close(fd) | |
55 | ||
56 | def close(self): | |
57 | "Close this request's response socket." | |
58 | self.sk.close() | |
59 | self.bsk.close() | |
60 | ||
61 | def __getitem__(self, header): | |
62 | """Find a HTTP header case-insensitively. For example, | |
63 | req["Content-Type"] returns the value of the content-type | |
64 | header regardlessly of whether the client specified it as | |
65 | "Content-Type", "content-type" or "Content-type". | |
1f3d7aa3 FT |
66 | |
67 | If the header is given as a (Unicode) string, it is encoded | |
68 | into Ascii for use in matching. | |
173e0e9e FT |
69 | """ |
70 | if isinstance(header, str): | |
71 | header = header.encode("ascii") | |
72 | header = header.lower() | |
73 | for key, val in self.headers: | |
74 | if key.lower() == header: | |
75 | return val | |
76 | raise KeyError(header) | |
77 | ||
78 | def __contains__(self, header): | |
79 | """Works analogously to the __getitem__ method for checking | |
80 | header presence case-insensitively. | |
81 | """ | |
82 | if isinstance(header, str): | |
83 | header = header.encode("ascii") | |
84 | header = header.lower() | |
85 | for key, val in self.headers: | |
86 | if key.lower() == header: | |
87 | return True | |
88 | return False | |
89 | ||
90 | def dup(self): | |
91 | """Creates a duplicate of this request, referring to a | |
92 | duplicate of the response socket. | |
93 | """ | |
94 | return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.bsk.fileno())) | |
95 | ||
96 | def match(self, match): | |
97 | """If the `match' argument matches exactly the leading part of | |
98 | the rest string, this method strips that part of the rest | |
99 | string off and returns True. Otherwise, it returns False | |
100 | without doing anything. | |
101 | ||
1f3d7aa3 FT |
102 | If the `match' argument is given as a (Unicode) string, it is |
103 | encoded into UTF-8. | |
104 | ||
173e0e9e FT |
105 | This can be used for simple dispatching. For example: |
106 | if req.match("foo/"): | |
107 | handle(req) | |
108 | elif req.match("bar/"): | |
109 | handle_otherwise(req) | |
110 | else: | |
111 | util.respond(req, "Not found", status = "404 Not Found", ctype = "text/plain") | |
112 | """ | |
113 | if isinstance(match, str): | |
114 | match = match.encode("utf-8") | |
115 | if self.rest[:len(match)] == match: | |
116 | self.rest = self.rest[len(match):] | |
117 | return True | |
118 | return False | |
119 | ||
120 | def __str__(self): | |
121 | def dec(b): | |
122 | return b.decode("ascii", errors="replace") | |
123 | return "\"%s %s %s\"" % (dec(self.method), dec(self.url), dec(self.ver)) | |
124 | ||
125 | def __enter__(self): | |
126 | return self | |
127 | ||
128 | def __exit__(self, *excinfo): | |
129 | self.sk.close() | |
130 | return False | |
131 | ||
132 | def recvreq(sock = 0): | |
133 | """Receive a single ashd request on the specified socket file | |
134 | descriptor (or standard input if unspecified). | |
135 | ||
136 | The returned value is an instance of the `req' class. As per its | |
137 | description, care should be taken to close() the request when | |
138 | done, to avoid leaking response sockets. If end-of-file is | |
139 | received on the socket, None is returned. | |
140 | ||
f56b0790 FT |
141 | This function may either raise an OSError if an error occurs on |
142 | the socket, or an ashd.proto.protoerr if the incoming request is | |
173e0e9e FT |
143 | invalidly encoded. |
144 | """ | |
145 | data, fd = htlib.recvfd(sock) | |
146 | if fd is None: | |
147 | return None | |
148 | try: | |
149 | parts = data.split(b'\0')[:-1] | |
150 | if len(parts) < 5: | |
151 | raise protoerr("Truncated request") | |
152 | method, url, ver, rest = parts[:4] | |
153 | headers = [] | |
154 | i = 4 | |
155 | while True: | |
156 | if parts[i] == b"": break | |
157 | if len(parts) - i < 3: | |
158 | raise protoerr("Truncated request") | |
159 | headers.append((parts[i], parts[i + 1])) | |
160 | i += 2 | |
161 | return req(method, url, ver, rest, headers, os.dup(fd)) | |
162 | finally: | |
163 | os.close(fd) | |
164 | ||
165 | def sendreq(sock, req): | |
166 | """Encode and send a single request to the specified socket file | |
167 | descriptor using the ashd protocol. The request should be an | |
168 | instance of the `req' class. | |
169 | ||
170 | This function may raise an OSError if an error occurs on the | |
171 | socket. | |
172 | """ | |
173 | data = b"" | |
174 | data += req.method + b'\0' | |
175 | data += req.url + b'\0' | |
176 | data += req.ver + b'\0' | |
177 | data += req.rest + b'\0' | |
178 | for key, val in req.headers: | |
179 | data += key + b'\0' | |
180 | data += val + b'\0' | |
181 | data += b'\0' | |
182 | htlib.sendfd(sock, req.sk.fileno(), data) |