5a2fee7a02ae1da137e3cbf6f4a8ebe5a26e8b45
[wrw.git] / wrw / proto.py
1 import time, calendar, collections.abc, binascii, base64
2
3 statusinfo = {
4     400: ("Bad Request", "Invalid HTTP request."),
5     401: ("Unauthorized", "Authentication must be provided for the requested resource."),
6     403: ("Forbidden", "You are not authorized to request the requested resource."),
7     404: ("Not Found", "The requested resource was not found."),
8     405: ("Method Not Allowed", "The request method is not recognized or permitted by the requested resource."),
9     406: ("Not Acceptable", "No way was found to satisfy the given content-negotiation criteria."),
10     407: ("Proxy Authentication Required", "Authentication must be provided to proxy the request."),
11     408: ("Request Timeout", "The connection timed out."),
12     409: ("Conflict", "The request conflicts with the live state."),
13     410: ("Gone", "The requested resource has been deleted."),
14     411: ("Length Required", "The requested resource requires the Content-Length header."),
15     412: ("Precondition Failed", "The preconditions specified in the request are not met."),
16     413: ("Payload Too Large", "The request entity is larger than permitted."),
17     414: ("URI Too Long", "The requested URI is too long."),
18     415: ("Unsupported Media Type", "The request entity format is not supported."),
19     416: ("Range Not Satisfiable", "The specified Range cannot be satisfied."),
20     417: ("Expectation Failed", "The expectation specified by the Expect header cannot be met."),
21     421: ("Misdirected Request", "This server cannot handle the request."),
22     422: ("Unprocessable Content", "The requet entity cannot be processed."),
23     423: ("Locked", "The requested resource is locked."),
24     424: ("Failed Dependency", "A previous required request failed."),
25     425: ("TOo Early", "The requested action has already been performed."),
26     426: ("Upgrade Required", "The requested resource is not available over this protocol."),
27     428: ("Precondition Requred", "The requested resource needs to be conditionally requested."),
28     429: ("Too Many Requests", "Your client is sending more frequent requests than are accepted."),
29     431: ("Request Header Fields Too Large", "The request headers are too large."),
30     451: ("Unavilable For Legal Reasons", "The requested resource has been censored."),
31     500: ("Server Error", "An internal error occurred."),
32     501: ("Not Implemented", "The requested functionality has not been implemented."),
33     502: ("Bad Gateway", "The backing server indicated an error."),
34     503: ("Service Unavailable", "Service is being denied at this time."),
35     504: ("Gateway Timeout", "The backing server is not responding."),
36     505: ("Unsupported HTTP Version", "The server does not support the requested HTTP version."),
37     506: ("Variant Also Negotiates", "The server content-negotiation is misconfigured."),
38     507: ("Insufficient Storage", "The server is out of storage to process the request."),
39     508: ("Loop Detected", "An infinite loop was detected while processing the request."),
40     510: ("Not Extended", "The requested extension is not supported."),
41     511: ("Network Authentication Required", "Authentication for network access is required."),
42     }
43
44 def httpdate(ts):
45     return time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(ts))
46
47 def phttpdate(dstr):
48     tz = dstr[-6:]
49     dstr = dstr[:-6]
50     if tz[0] != " " or (tz[1] != "+" and tz[1] != "-") or not tz[2:].isdigit():
51         return None
52     tz = int(tz[1:])
53     tz = (((tz / 100) * 60) + (tz % 100)) * 60
54     return calendar.timegm(time.strptime(dstr, "%a, %d %b %Y %H:%M:%S")) - tz
55
56 def pmimehead(hstr):
57     def pws(p):
58         while p < len(hstr) and hstr[p].isspace():
59             p += 1
60         return p
61     def token(p, sep):
62         buf = ""
63         p = pws(p)
64         if p >= len(hstr):
65             return "", p
66         if hstr[p] == '"':
67             p += 1
68             while p < len(hstr):
69                 if hstr[p] == '\\':
70                     p += 1
71                     if p < len(hstr):
72                         buf += hstr[p]
73                         p += 1
74                     else:
75                         break
76                 elif hstr[p] == '"':
77                     p += 1
78                     break
79                 else:
80                     buf += hstr[p]
81                     p += 1
82             return buf, pws(p)
83         else:
84             while p < len(hstr):
85                 if hstr[p] in sep:
86                     break
87                 buf += hstr[p]
88                 p += 1
89             return buf.strip(), pws(p)
90     p = 0
91     val, p = token(p, ";")
92     pars = {}
93     while p < len(hstr):
94         if hstr[p] != ';':
95             break
96         p += 1
97         k, p = token(p, "=")
98         if k == "" or hstr[p:p + 1] != '=':
99             break
100         p += 1
101         v, p = token(p, ';')
102         pars[k.lower()] = v
103     return val, pars
104
105 def htmlq(html):
106     ret = ""
107     for c in html:
108         if c == "&":
109             ret += "&amp;"
110         elif c == "<":
111             ret += "&lt;"
112         elif c == ">":
113             ret += "&gt;"
114         else:
115             ret += c
116     return ret
117
118 def simpleerror(env, startreq, code, title, msg):
119     buf = """<?xml version="1.0" encoding="US-ASCII"?>
120 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
121 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
122 <head>
123 <title>%s</title>
124 </head>
125 <body>
126 <h1>%s</h1>
127 <p>%s</p>
128 </body>
129 </html>
130 """ % (title, title, htmlq(msg))
131     buf = buf.encode("us-ascii")
132     startreq("%i %s" % (code, title), [("Content-Type", "text/html"), ("Content-Length", str(len(buf)))])
133     return [buf]
134
135 def urlq(url):
136     if isinstance(url, str):
137         url = url.encode("utf-8")
138     ret = ""
139     invalid = b"%;&=+#?/\"'"
140     for c in url:
141         if c in invalid or (c <= 32) or (c >= 128):
142             ret += "%%%02X" % c
143         else:
144             ret += chr(c)
145     return ret
146
147 class urlerror(ValueError):
148     pass
149
150 def parseurl(url):
151     p = url.find("://")
152     if p < 0:
153         raise urlerror("Protocol not found in absolute URL `%s'" % url)
154     proto = url[:p]
155     l = url.find("/", p + 3)
156     if l < 0:
157         raise urlerror("Local part not found in absolute URL `%s'" % url)
158     host = url[p + 3:l]
159     local = url[l:]
160     q = local.find("?")
161     if q < 0:
162         query = ""
163     else:
164         query = local[q + 1:]
165         local = local[:q]
166     return proto, host, local, query
167
168 def consurl(proto, host, local, query=""):
169     if len(local) < 1 and local[0] != '/':
170         raise urlerror("Local part of URL must begin with a slash")
171     ret = "%s://%s%s" % (proto, host, local)
172     if len(query) > 0:
173         ret += "?" + query
174     return ret
175
176 def appendurl(url, other):
177     if "://" in other:
178         return other
179     proto, host, local, query = parseurl(url)
180     if len(other) > 0 and other[0] == '/':
181         return consurl(proto, host, other)
182     else:
183         p = local.rfind('/')
184         return consurl(proto, host, local[:p + 1] + other)
185
186 def siteurl(req):
187     host = req.ihead.get("Host", None)
188     if host is None:
189         raise Exception("Could not reconstruct URL because no Host header was sent")
190     proto = "http"
191     if req.https:
192         proto = "https"
193     return "%s://%s/" % (proto, host)
194
195 def scripturl(req):
196     s = siteurl(req)
197     if req.uriname[0] != '/':
198         raise Exception("Malformed local part when reconstructing URL")
199     return siteurl(req) + req.uriname[1:]
200
201 def requrl(req, qs=True):
202     s = siteurl(req)
203     if req.uri[0] != '/':
204         raise Exception("Malformed local part when reconstructing URL")
205     pf = req.uri[1:]
206     if not qs:
207         p = pf.find('?')
208         if not p < 0:
209             pf = pf[:p]
210     return siteurl(req) + pf
211
212 def parstring(pars={}, **augment):
213     buf = ""
214     for key in pars:
215         if key in augment:
216             val = augment[key]
217             del augment[key]
218         else:
219             val = pars[key]
220         if val is None:
221             continue
222         if buf != "": buf += "&"
223         buf += urlq(key) + "=" + urlq(str(val))
224     for key, val in augment.items():
225         if val is None:
226             continue
227         if buf != "": buf += "&"
228         buf += urlq(key) + "=" + urlq(str(val))
229     return buf
230
231 def parurl(url, pars={}, **augment):
232     qs = parstring(pars, **augment)
233     if qs != "":
234         return url + ("&" if "?" in url else "?") + qs
235     else:
236         return url
237
238 # Wrap these, since binascii is a bit funky. :P
239 def enhex(bs):
240     return base64.b16encode(bs).decode("us-ascii")
241 def unhex(es):
242     if not isinstance(es, collections.abc.ByteString):
243         try:
244             es = es.encode("us-ascii")
245         except UnicodeError:
246             raise binascii.Error("non-ascii character in hex-string")
247     return base64.b16decode(es)
248 def enb32(bs):
249     return base64.b32encode(bs).decode("us-ascii")
250 def unb32(es):
251     if not isinstance(es, collections.abc.ByteString):
252         try:
253             es = es.encode("us-ascii")
254         except UnicodeError:
255             raise binascii.Error("non-ascii character in base32-string")
256     if (len(es) % 8) != 0:
257         es += b"=" * (8 - (len(es) % 8))
258     es = es.upper()             # The whole point of Base32 is that it's case-insensitive :P
259     return base64.b32decode(es)
260 def enb64(bs):
261     return base64.b64encode(bs).decode("us-ascii")
262 def unb64(es):
263     if not isinstance(es, collections.abc.ByteString):
264         try:
265             es = es.encode("us-ascii")
266         except UnicodeError:
267             raise binascii.Error("non-ascii character in base64-string")
268     if (len(es) % 4) != 0:
269         es += b"=" * (4 - (len(es) % 4))
270     return base64.b64decode(es)
271
272 def _quoprisafe():
273     ret = [False] * 256
274     for c in "-!*+/":
275         ret[ord(c)] = True
276     for c in range(ord('0'), ord('9') + 1):
277         ret[c] = True
278     for c in range(ord('A'), ord('Z') + 1):
279         ret[c] = True
280     for c in range(ord('a'), ord('z') + 1):
281         ret[c] = True
282     return ret
283 _quoprisafe = _quoprisafe()
284 def quopri(s, charset="utf-8"):
285     bv = s.encode(charset)
286     qn = sum(not _quoprisafe[b] for b in bv)
287     if qn == 0:
288         return s
289     if qn > len(bv) / 2:
290         return "=?%s?B?%s?=" % (charset, enb64(bv))
291     else:
292         return "=?%s?Q?%s?=" % (charset, "".join(chr(b) if _quoprisafe[b] else "=%02X" % b for b in bv))
293
294 class mimeparam(object):
295     def __init__(self, name, val, fallback=None, charset="utf-8", lang=""):
296         self.name = name
297         self.val = val
298         self.fallback = fallback
299         self.charset = charset
300         self.lang = lang
301
302     def __str__(self):
303         self.name.encode("ascii")
304         try:
305             self.val.encode("ascii")
306         except UnicodeError:
307             pass
308         else:
309             return "%s=%s" % (self.name, self.val)
310         val = self.val.encode(self.charset)
311         self.charset.encode("ascii")
312         self.lang.encode("ascii")
313         ret = ""
314         if self.fallback is not None:
315             self.fallback.encode("ascii")
316             ret += "%s=%s; " % (self.name, self.fallback)
317         ret += "%s*=%s'%s'%s" % (self.name, self.charset, self.lang, urlq(val))
318         return ret
319
320 class mimeheader(object):
321     def __init__(self, name, val, *, mime_charset="utf-8", mime_lang="", **params):
322         self.name = name
323         self.val = val
324         self.params = {}
325         self.charset = mime_charset
326         self.lang = mime_lang
327         for k, v in params.items():
328             self[k] = v
329
330     def __getitem__(self, nm):
331         return self.params[nm.lower()]
332
333     def __setitem__(self, nm, val):
334         if not isinstance(val, mimeparam):
335             val = mimeparam(nm, val, charset=self.charset, lang=self.lang)
336         self.params[nm.lower()] = val
337
338     def __delitem__(self, nm):
339         del self.params[nm.lower()]
340
341     def value(self):
342         parts = []
343         if self.val != None:
344             parts.append(quopri(self.val))
345         parts.extend(str(x) for x in self.params.values())
346         return("; ".join(parts))
347
348     def __str__(self):
349         if self.name is None:
350             return self.value()
351         return "%s: %s" % (self.name, self.value())