Commit | Line | Data |
---|---|---|
ecbfa279 | 1 | import binascii, hashlib, threading, time |
c4b97e16 | 2 | from . import resp, proto |
ecbfa279 FT |
3 | |
4 | class unauthorized(resp.httperror): | |
5 | def __init__(self, challenge, message=None, detail=None): | |
6d2e09cf | 6 | super().__init__(401, message, detail) |
ecbfa279 FT |
7 | if isinstance(challenge, str): |
8 | challenge = [challenge] | |
9 | self.challenge = challenge | |
10 | ||
11 | def handle(self, req): | |
12 | for challenge in self.challenge: | |
13 | req.ohead.add("WWW-Authenticate", challenge) | |
6d2e09cf | 14 | return super().handle(req) |
ecbfa279 FT |
15 | |
16 | class forbidden(resp.httperror): | |
17 | def __init__(self, message=None, detail=None): | |
6d2e09cf | 18 | super().__init__(403, message, detail) |
ecbfa279 FT |
19 | |
20 | def parsemech(req): | |
21 | h = req.ihead.get("Authorization", None) | |
22 | if h is None: | |
23 | return None, None | |
24 | p = h.find(" ") | |
25 | if p < 0: | |
26 | return None, None | |
27 | return h[:p].strip().lower(), h[p + 1:].strip() | |
28 | ||
29 | def parsebasic(req): | |
30 | mech, data = parsemech(req) | |
31 | if mech != "basic": | |
32 | return None, None | |
33 | try: | |
c4b97e16 | 34 | raw = proto.unb64(data) |
ecbfa279 FT |
35 | except binascii.Error: |
36 | return None, None | |
6d2e09cf FT |
37 | try: |
38 | raw = raw.decode("utf-8") | |
39 | except UnicodeError: | |
40 | raw = raw.decode("latin1") | |
ecbfa279 FT |
41 | p = raw.find(":") |
42 | if p < 0: | |
43 | return None, None | |
44 | return raw[:p], raw[p + 1:] | |
45 | ||
46 | class basiccache(object): | |
47 | cachetime = 300 | |
48 | ||
49 | def __init__(self, realm, authfn=None): | |
50 | self._lock = threading.Lock() | |
51 | self._cache = {} | |
52 | self.realm = realm | |
53 | if authfn is not None: | |
54 | self.auth = authfn | |
55 | ||
56 | def _obscure(self, nm, pw): | |
57 | dig = hashlib.sha256() | |
6d2e09cf FT |
58 | dig.update(self.realm.encode("utf-8")) |
59 | dig.update(nm.encode("utf-8")) | |
60 | dig.update(pw.encode("utf-8")) | |
ecbfa279 FT |
61 | return dig.digest() |
62 | ||
63 | def check(self, req): | |
64 | nm, pw = parsebasic(req) | |
65 | if nm is None: | |
66 | raise unauthorized("Basic Realm=\"%s\"" % self.realm) | |
67 | pwh = self._obscure(nm, pw) | |
68 | now = time.time() | |
69 | with self._lock: | |
70 | if (nm, pwh) in self._cache: | |
71 | lock, atime, res, resob = self._cache[nm, pwh] | |
72 | if now - atime < self.cachetime: | |
73 | if res == "s": | |
74 | return resob | |
75 | elif res == "f": | |
76 | raise resob | |
77 | else: | |
78 | lock = threading.Lock() | |
79 | self._cache[nm, pwh] = (lock, now, None, None) | |
80 | with lock: | |
81 | try: | |
82 | ret = self.auth(req, nm, pw) | |
6d2e09cf | 83 | except forbidden as exc: |
ecbfa279 FT |
84 | with self._lock: |
85 | self._cache[nm, pwh] = (lock, now, "f", exc) | |
86 | raise | |
87 | if ret is None: | |
88 | raise forbidden() | |
89 | with self._lock: | |
90 | self._cache[nm, pwh] = (lock, now, "s", ret) | |
91 | return ret | |
92 | ||
93 | def auth(self, req, nm, pw): | |
94 | raise Exception("authentication function neither supplied nor overridden") |