1 import time, calendar, collections.abc, binascii, base64
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 429: ("Too Many Requests", "Your client is sending more frequent requests than are accepted."),
10 500: ("Server Error", "An internal error occurred."),
11 501: ("Not Implemented", "The requested functionality has not been implemented."),
12 503: ("Service Unavailable", "Service is being denied at this time."),
16 return time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(ts))
21 if tz[0] != " " or (tz[1] != "+" and tz[1] != "-") or not tz[2:].isdigit():
24 tz = (((tz / 100) * 60) + (tz % 100)) * 60
25 return calendar.timegm(time.strptime(dstr, "%a, %d %b %Y %H:%M:%S")) - tz
29 while p < len(hstr) and hstr[p].isspace():
60 return buf.strip(), pws(p)
62 val, p = token(p, ";")
69 if k == "" or hstr[p:p + 1] != '=':
89 def simpleerror(env, startreq, code, title, msg):
90 buf = """<?xml version="1.0" encoding="US-ASCII"?>
91 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
92 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
101 """ % (title, title, htmlq(msg))
102 buf = buf.encode("us-ascii")
103 startreq("%i %s" % (code, title), [("Content-Type", "text/html"), ("Content-Length", str(len(buf)))])
107 if isinstance(url, str):
108 url = url.encode("utf-8")
110 invalid = b"%;&=+#?/\"'"
112 if c in invalid or (c <= 32) or (c >= 128):
118 class urlerror(ValueError):
124 raise urlerror("Protocol not found in absolute URL `%s'" % url)
126 l = url.find("/", p + 3)
128 raise urlerror("Local part not found in absolute URL `%s'" % url)
135 query = local[q + 1:]
137 return proto, host, local, query
139 def consurl(proto, host, local, query=""):
140 if len(local) < 1 and local[0] != '/':
141 raise urlerror("Local part of URL must begin with a slash")
142 ret = "%s://%s%s" % (proto, host, local)
147 def appendurl(url, other):
150 proto, host, local, query = parseurl(url)
151 if len(other) > 0 and other[0] == '/':
152 return consurl(proto, host, other)
155 return consurl(proto, host, local[:p + 1] + other)
158 host = req.ihead.get("Host", None)
160 raise Exception("Could not reconstruct URL because no Host header was sent")
164 return "%s://%s/" % (proto, host)
168 if req.uriname[0] != '/':
169 raise Exception("Malformed local part when reconstructing URL")
170 return siteurl(req) + req.uriname[1:]
172 def requrl(req, qs=True):
174 if req.uri[0] != '/':
175 raise Exception("Malformed local part when reconstructing URL")
181 return siteurl(req) + pf
183 def parstring(pars={}, **augment):
193 if buf != "": buf += "&"
194 buf += urlq(key) + "=" + urlq(str(val))
195 for key, val in augment.items():
198 if buf != "": buf += "&"
199 buf += urlq(key) + "=" + urlq(str(val))
202 def parurl(url, pars={}, **augment):
203 qs = parstring(pars, **augment)
205 return url + ("&" if "?" in url else "?") + qs
209 # Wrap these, since binascii is a bit funky. :P
211 return base64.b16encode(bs).decode("us-ascii")
213 if not isinstance(es, collections.abc.ByteString):
215 es = es.encode("us-ascii")
217 raise binascii.Error("non-ascii character in hex-string")
218 return base64.b16decode(es)
220 return base64.b32encode(bs).decode("us-ascii")
222 if not isinstance(es, collections.abc.ByteString):
224 es = es.encode("us-ascii")
226 raise binascii.Error("non-ascii character in base32-string")
227 if (len(es) % 8) != 0:
228 es += b"=" * (8 - (len(es) % 8))
229 es = es.upper() # The whole point of Base32 is that it's case-insensitive :P
230 return base64.b32decode(es)
232 return base64.b64encode(bs).decode("us-ascii")
234 if not isinstance(es, collections.abc.ByteString):
236 es = es.encode("us-ascii")
238 raise binascii.Error("non-ascii character in base64-string")
239 if (len(es) % 4) != 0:
240 es += b"=" * (4 - (len(es) % 4))
241 return base64.b64decode(es)
247 for c in range(ord('0'), ord('9') + 1):
249 for c in range(ord('A'), ord('Z') + 1):
251 for c in range(ord('a'), ord('z') + 1):
254 _quoprisafe = _quoprisafe()
255 def quopri(s, charset="utf-8"):
256 bv = s.encode(charset)
257 qn = sum(not _quoprisafe[b] for b in bv)
261 return "=?%s?B?%s?=" % (charset, enb64(bv))
263 return "=?%s?Q?%s?=" % (charset, "".join(chr(b) if _quoprisafe[b] else "=%02X" % b for b in bv))
265 class mimeparam(object):
266 def __init__(self, name, val, fallback=None, charset="utf-8", lang=""):
269 self.fallback = fallback
270 self.charset = charset
274 self.name.encode("ascii")
276 self.val.encode("ascii")
280 return "%s=%s" % (self.name, self.val)
281 val = self.val.encode(self.charset)
282 self.charset.encode("ascii")
283 self.lang.encode("ascii")
285 if self.fallback is not None:
286 self.fallback.encode("ascii")
287 ret += "%s=%s; " % (self.name, self.fallback)
288 ret += "%s*=%s'%s'%s" % (self.name, self.charset, self.lang, urlq(val))
291 class mimeheader(object):
292 def __init__(self, name, val, *, mime_charset="utf-8", mime_lang="", **params):
296 self.charset = mime_charset
297 self.lang = mime_lang
298 for k, v in params.items():
301 def __getitem__(self, nm):
302 return self.params[nm.lower()]
304 def __setitem__(self, nm, val):
305 if not isinstance(val, mimeparam):
306 val = mimeparam(nm, val, charset=self.charset, lang=self.lang)
307 self.params[nm.lower()] = val
309 def __delitem__(self, nm):
310 del self.params[nm.lower()]
315 parts.append(quopri(self.val))
316 parts.extend(str(x) for x in self.params.values())
317 return("; ".join(parts))
320 if self.name is None:
322 return "%s: %s" % (self.name, self.value())