Commit | Line | Data |
---|---|---|
68c0eaa7 | 1 | import time, calendar, collections.abc, binascii, base64 |
3ba5e96c | 2 | |
5ef9e488 | 3 | statusinfo = { |
001ec99e FT |
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."), | |
5ef9e488 | 7 | 404: ("Not Found", "The requested resource was not found."), |
001ec99e | 8 | 405: ("Method Not Allowed", "The request method is not recognized or permitted by the requested resource."), |
d54dfe26 FT |
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."), | |
8ad65294 | 28 | 429: ("Too Many Requests", "Your client is sending more frequent requests than are accepted."), |
d54dfe26 FT |
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."), | |
001ec99e FT |
31 | 500: ("Server Error", "An internal error occurred."), |
32 | 501: ("Not Implemented", "The requested functionality has not been implemented."), | |
d54dfe26 | 33 | 502: ("Bad Gateway", "The backing server indicated an error."), |
001ec99e | 34 | 503: ("Service Unavailable", "Service is being denied at this time."), |
d54dfe26 FT |
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."), | |
5ef9e488 FT |
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 | |
87f2697c | 54 | return calendar.timegm(time.strptime(dstr, "%a, %d %b %Y %H:%M:%S")) - tz |
5ef9e488 | 55 | |
eed3cf12 FT |
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, ';') | |
ea18777a | 102 | pars[k.lower()] = v |
eed3cf12 FT |
103 | return val, pars |
104 | ||
5ef9e488 FT |
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 | ||
d740aa68 FT |
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)) | |
cb8bb5ed | 131 | buf = buf.encode("us-ascii") |
d740aa68 FT |
132 | startreq("%i %s" % (code, title), [("Content-Type", "text/html"), ("Content-Length", str(len(buf)))]) |
133 | return [buf] | |
134 | ||
5ef9e488 | 135 | def urlq(url): |
11c9628e | 136 | if isinstance(url, str): |
cbb90a8d | 137 | url = url.encode("utf-8") |
5ef9e488 | 138 | ret = "" |
f6f55f13 | 139 | invalid = b"%;&=+#?/\"'" |
5ef9e488 | 140 | for c in url: |
11c9628e FT |
141 | if c in invalid or (c <= 32) or (c >= 128): |
142 | ret += "%%%02X" % c | |
5ef9e488 | 143 | else: |
11c9628e | 144 | ret += chr(c) |
5ef9e488 FT |
145 | return ret |
146 | ||
bb80acbd FT |
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 | ||
e21c4382 | 168 | def consurl(proto, host, local, query=""): |
bb80acbd FT |
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 | ||
31d48e69 | 186 | def siteurl(req): |
bb80acbd FT |
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" | |
31d48e69 FT |
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 | ||
51a13716 | 201 | def requrl(req, qs=True): |
31d48e69 | 202 | s = siteurl(req) |
bb80acbd FT |
203 | if req.uri[0] != '/': |
204 | raise Exception("Malformed local part when reconstructing URL") | |
51a13716 FT |
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 | |
bb80acbd | 211 | |
e21c4382 | 212 | def parstring(pars={}, **augment): |
5ef9e488 FT |
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] | |
0bf1ac2f FT |
220 | if val is None: |
221 | continue | |
5ef9e488 FT |
222 | if buf != "": buf += "&" |
223 | buf += urlq(key) + "=" + urlq(str(val)) | |
0bf1ac2f FT |
224 | for key, val in augment.items(): |
225 | if val is None: | |
226 | continue | |
5ef9e488 | 227 | if buf != "": buf += "&" |
0bf1ac2f | 228 | buf += urlq(key) + "=" + urlq(str(val)) |
5ef9e488 | 229 | return buf |
08a4b741 | 230 | |
e21c4382 | 231 | def parurl(url, pars={}, **augment): |
08a4b741 FT |
232 | qs = parstring(pars, **augment) |
233 | if qs != "": | |
d6d4c944 | 234 | return url + ("&" if "?" in url else "?") + qs |
08a4b741 FT |
235 | else: |
236 | return url | |
c4b97e16 FT |
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): | |
68c0eaa7 | 242 | if not isinstance(es, collections.abc.ByteString): |
c4b97e16 FT |
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): | |
68c0eaa7 | 251 | if not isinstance(es, collections.abc.ByteString): |
c4b97e16 FT |
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): | |
68c0eaa7 | 263 | if not isinstance(es, collections.abc.ByteString): |
c4b97e16 FT |
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) | |
c62c64aa FT |
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()) |