python: Initial porting of the Python code to Python 3.
[ashd.git] / python / 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 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)