| 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 += "&" |
| 110 | elif c == "<": |
| 111 | ret += "<" |
| 112 | elif c == ">": |
| 113 | ret += ">" |
| 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()) |