python: Initial porting of the Python code to Python 3.
[ashd.git] / python / ashd / proto.py
CommitLineData
2baf419b
FT
1"""Low-level protocol module for ashd(7)
2
3This module provides primitive functions that speak the raw ashd(7)
4protocol. Primarily, it implements the `req' class that is used to
5represent ashd requests. The functions it provides can also be used to
6create ashd handlers, but unless you require very precise control, the
7ashd.util module provides an easier-to-use interface.
8"""
9
c270f222 10import os, socket
55fa3f63 11from . import htlib
c270f222 12
e66efe5f
FT
13__all__ = ["req", "recvreq", "sendreq"]
14
c270f222
FT
15class protoerr(Exception):
16 pass
17
18class req(object):
2baf419b
FT
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
c270f222
FT
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
55fa3f63
FT
49 self.bsk = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
50 self.sk = self.bsk.makefile('rwb')
c270f222
FT
51 os.close(fd)
52
53 def close(self):
2baf419b 54 "Close this request's response socket."
c270f222 55 self.sk.close()
55fa3f63 56 self.bsk.close()
c270f222
FT
57
58 def __getitem__(self, header):
2baf419b
FT
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 """
55fa3f63
FT
64 if isinstance(header, str):
65 header = header.encode("ascii")
c270f222
FT
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):
2baf419b
FT
73 """Works analogously to the __getitem__ method for checking
74 header presence case-insensitively.
75 """
55fa3f63
FT
76 if isinstance(header, str):
77 header = header.encode("ascii")
c270f222
FT
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):
2baf419b
FT
85 """Creates a duplicate of this request, referring to a
86 duplicate of the response socket.
87 """
55fa3f63 88 return req(self.method, self.url, self.ver, self.rest, self.headers, os.dup(self.bsk.fileno()))
c270f222
FT
89
90 def match(self, match):
2baf419b
FT
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 """
55fa3f63
FT
104 if isinstance(match, str):
105 match = match.encode("utf-8")
c270f222
FT
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):
55fa3f63
FT
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))
c270f222
FT
115
116 def __enter__(self):
117 return self
118
119 def __exit__(self, *excinfo):
120 self.sk.close()
121 return False
122
123def recvreq(sock = 0):
2baf419b
FT
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
b0ac3ba1
FT
129 done, to avoid leaking response sockets. If end-of-file is
130 received on the socket, None is returned.
2baf419b
FT
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 """
c270f222
FT
136 data, fd = htlib.recvfd(sock)
137 if fd is None:
138 return None
139 try:
55fa3f63 140 parts = data.split(b'\0')[:-1]
c270f222
FT
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:
55fa3f63 147 if parts[i] == b"": break
c270f222
FT
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
156def sendreq(sock, req):
2baf419b
FT
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 """
55fa3f63
FT
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'
c270f222 169 for key, val in req.headers:
55fa3f63
FT
170 data += key + b'\0'
171 data += val + b'\0'
172 data += b'\0'
c270f222 173 htlib.sendfd(sock, req.sk.fileno(), data)