python3: Fixed bug in sendreq.
[ashd.git] / python3 / ashd / proto.py
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 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
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".
66         
67         If the header is given as a (Unicode) string, it is encoded
68         into Ascii for use in matching.
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
102         If the `match' argument is given as a (Unicode) string, it is
103         encoded into UTF-8.
104
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
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
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.bsk.fileno(), data)