Remove a couple of debug messages.
[wrw.git] / wrw / proto.py
... / ...
CommitLineData
1import time, calendar, collections.abc, binascii, base64
2
3statusinfo = {
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
44def httpdate(ts):
45 return time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(ts))
46
47def 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
56def 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
105def 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
118def 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
135def 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
147class urlerror(ValueError):
148 pass
149
150def 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
168def 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
176def 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
186def 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
195def 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
201def 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
212def 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
231def 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
239def enhex(bs):
240 return base64.b16encode(bs).decode("us-ascii")
241def 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)
248def enb32(bs):
249 return base64.b32encode(bs).decode("us-ascii")
250def 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)
260def enb64(bs):
261 return base64.b64encode(bs).decode("us-ascii")
262def 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
272def _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()
284def 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
294class 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
320class 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())