|
@@ -1,156 +1,76 @@
|
1
|
|
-import os
|
2
|
|
-import re
|
3
|
|
-
|
4
|
1
|
from wsgidav.http_authenticator import HTTPAuthenticator
|
5
|
2
|
from wsgidav import util
|
6
|
|
-import cherrypy
|
7
|
|
-
|
8
|
|
-from tracim.lib.user import CurrentUserGetterApi
|
9
|
|
-from tracim.lib.user import CURRENT_USER_WSGIDAV
|
10
|
3
|
|
11
|
4
|
_logger = util.getModuleLogger(__name__, True)
|
12
|
|
-HOTFIX_WINXP_AcceptRootShareLogin = True
|
13
|
5
|
|
14
|
6
|
|
15
|
7
|
class TracimHTTPAuthenticator(HTTPAuthenticator):
|
16
|
|
- def __init__(self, application, config):
|
17
|
|
- super(TracimHTTPAuthenticator, self).__init__(application, config)
|
18
|
|
- self._headerfixparser = re.compile(r'([\w]+)=("[^"]*,[^"]*"),')
|
19
|
|
-
|
20
|
|
- def authDigestAuthRequest(self, environ, start_response):
|
21
|
|
- realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"], environ)
|
22
|
|
-
|
23
|
|
- isinvalidreq = False
|
24
|
|
-
|
25
|
|
- authheaderdict = dict([])
|
26
|
|
- authheaders = environ["HTTP_AUTHORIZATION"] + ","
|
27
|
|
- if not authheaders.lower().strip().startswith("digest"):
|
28
|
|
- isinvalidreq = True
|
29
|
|
- # Hotfix for Windows file manager and OSX Finder:
|
30
|
|
- # Some clients don't urlencode paths in auth header, so uri value may
|
31
|
|
- # contain commas, which break the usual regex headerparser. Example:
|
32
|
|
- # Digest username="user",realm="/",uri="a,b.txt",nc=00000001, ...
|
33
|
|
- # -> [..., ('uri', '"a'), ('nc', '00000001'), ...]
|
34
|
|
- # Override any such values with carefully extracted ones.
|
35
|
|
- authheaderlist = self._headerparser.findall(authheaders)
|
36
|
|
- authheaderfixlist = self._headerfixparser.findall(authheaders)
|
37
|
|
- if authheaderfixlist:
|
38
|
|
- _logger.info("Fixing authheader comma-parsing: extend %s with %s" \
|
39
|
|
- % (authheaderlist, authheaderfixlist))
|
40
|
|
- authheaderlist += authheaderfixlist
|
41
|
|
- for authheader in authheaderlist:
|
42
|
|
- authheaderkey = authheader[0]
|
43
|
|
- authheadervalue = authheader[1].strip().strip("\"")
|
44
|
|
- authheaderdict[authheaderkey] = authheadervalue
|
45
|
|
-
|
46
|
|
- _logger.debug("authDigestAuthRequest: %s" % environ["HTTP_AUTHORIZATION"])
|
47
|
|
- _logger.debug(" -> %s" % authheaderdict)
|
48
|
|
-
|
49
|
|
- if "username" in authheaderdict:
|
50
|
|
- req_username = authheaderdict["username"]
|
51
|
|
- req_username_org = req_username
|
52
|
|
- # Hotfix for Windows XP:
|
53
|
|
- # net use W: http://127.0.0.1/dav /USER:DOMAIN\tester tester
|
54
|
|
- # will send the name with double backslashes ('DOMAIN\\tester')
|
55
|
|
- # but send the digest for the simple name ('DOMAIN\tester').
|
56
|
|
- if r"\\" in req_username:
|
57
|
|
- req_username = req_username.replace("\\\\", "\\")
|
58
|
|
- _logger.info("Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username))
|
59
|
|
-
|
60
|
|
- if not self._domaincontroller.isRealmUser(realmname, req_username, environ):
|
61
|
|
- isinvalidreq = True
|
62
|
|
- else:
|
63
|
|
- isinvalidreq = True
|
64
|
|
-
|
65
|
|
- # TODO: Chun added this comments, but code was commented out
|
66
|
|
- # Do not do realm checking - a hotfix for WinXP using some other realm's
|
67
|
|
- # auth details for this realm - if user/password match
|
68
|
|
-
|
69
|
|
- if 'realm' in authheaderdict:
|
70
|
|
- if authheaderdict["realm"].upper() != realmname.upper():
|
71
|
|
- if HOTFIX_WINXP_AcceptRootShareLogin:
|
72
|
|
- # Hotfix: also accept '/'
|
73
|
|
- if authheaderdict["realm"].upper() != "/":
|
74
|
|
- isinvalidreq = True
|
75
|
|
- else:
|
76
|
|
- isinvalidreq = True
|
77
|
|
-
|
78
|
|
- if "algorithm" in authheaderdict:
|
79
|
|
- if authheaderdict["algorithm"].upper() != "MD5":
|
80
|
|
- isinvalidreq = True # only MD5 supported
|
81
|
|
-
|
82
|
|
- if "uri" in authheaderdict:
|
83
|
|
- req_uri = authheaderdict["uri"]
|
84
|
|
-
|
85
|
|
- if "nonce" in authheaderdict:
|
86
|
|
- req_nonce = authheaderdict["nonce"]
|
87
|
|
- else:
|
88
|
|
- isinvalidreq = True
|
89
|
|
-
|
90
|
|
- req_hasqop = False
|
91
|
|
- if "qop" in authheaderdict:
|
92
|
|
- req_hasqop = True
|
93
|
|
- req_qop = authheaderdict["qop"]
|
94
|
|
- if req_qop.lower() != "auth":
|
95
|
|
- isinvalidreq = True # only auth supported, auth-int not supported
|
|
8
|
+ def computeDigestResponse(
|
|
9
|
+ self,
|
|
10
|
+ username,
|
|
11
|
+ realm,
|
|
12
|
+ password,
|
|
13
|
+ method,
|
|
14
|
+ uri,
|
|
15
|
+ nonce,
|
|
16
|
+ cnonce,
|
|
17
|
+ qop,
|
|
18
|
+ nc
|
|
19
|
+ ):
|
|
20
|
+ """
|
|
21
|
+ Override standard computeDigestResponse : as user password is already
|
|
22
|
+ hashed in database, we need to use left_digest_response_hash
|
|
23
|
+ to have correctly hashed digest_response.
|
|
24
|
+ """
|
|
25
|
+
|
|
26
|
+ # TODO - G.M - 13-03-2018 Check if environ is useful
|
|
27
|
+ # for get_left_digest_response. If true, find a solution
|
|
28
|
+ # to obtain it here without recopy-paste whole authDigestAuthRequest
|
|
29
|
+ # method.
|
|
30
|
+ left_digest_response_hash = self._domaincontroller.get_left_digest_response_hash(realm, username, None) # nopep8
|
|
31
|
+ if left_digest_response_hash:
|
|
32
|
+ return self.tracim_compute_digest_response(
|
|
33
|
+ left_digest_response_hash=left_digest_response_hash,
|
|
34
|
+ method=method,
|
|
35
|
+ uri=uri,
|
|
36
|
+ nonce=nonce,
|
|
37
|
+ cnonce=cnonce,
|
|
38
|
+ qop=qop,
|
|
39
|
+ nc=nc,
|
|
40
|
+ )
|
96
|
41
|
else:
|
97
|
|
- req_qop = None
|
98
|
|
-
|
99
|
|
- if "cnonce" in authheaderdict:
|
100
|
|
- req_cnonce = authheaderdict["cnonce"]
|
101
|
|
- else:
|
102
|
|
- req_cnonce = None
|
103
|
|
- if req_hasqop:
|
104
|
|
- isinvalidreq = True
|
105
|
|
-
|
106
|
|
- if "nc" in authheaderdict: # is read but nonce-count checking not implemented
|
107
|
|
- req_nc = authheaderdict["nc"]
|
108
|
|
- else:
|
109
|
|
- req_nc = None
|
110
|
|
- if req_hasqop:
|
111
|
|
- isinvalidreq = True
|
112
|
|
-
|
113
|
|
- if "response" in authheaderdict:
|
114
|
|
- req_response = authheaderdict["response"]
|
115
|
|
- else:
|
116
|
|
- isinvalidreq = True
|
117
|
|
-
|
118
|
|
- if not isinvalidreq:
|
119
|
|
- left_digest_response_hash = self._domaincontroller.get_left_digest_response_hash(realmname, req_username, environ)
|
120
|
|
-
|
121
|
|
- req_method = environ["REQUEST_METHOD"]
|
122
|
|
-
|
123
|
|
- required_digest = self.tracim_compute_digest_response(left_digest_response_hash, req_method, req_uri, req_nonce,
|
124
|
|
- req_cnonce, req_qop, req_nc)
|
125
|
|
-
|
126
|
|
- if required_digest != req_response:
|
127
|
|
- _logger.warning("computeDigestResponse('%s', '%s', ...): %s != %s" % (
|
128
|
|
- realmname, req_username, required_digest, req_response))
|
129
|
|
- isinvalidreq = True
|
130
|
|
- else:
|
131
|
|
- _logger.debug("digest succeeded for realm '%s', user '%s'" % (realmname, req_username))
|
132
|
|
- pass
|
133
|
|
-
|
134
|
|
- if isinvalidreq:
|
135
|
|
- _logger.warning("Authentication failed for user '%s', realm '%s'" % (req_username, realmname))
|
136
|
|
- return self.sendDigestAuthResponse(environ, start_response)
|
137
|
|
-
|
138
|
|
- environ["http_authenticator.realm"] = realmname
|
139
|
|
- environ["http_authenticator.username"] = req_username
|
140
|
|
-
|
141
|
|
- # Set request current user email to be able to recognise him later
|
142
|
|
- cherrypy.request.current_user_email = req_username
|
143
|
|
- CurrentUserGetterApi.set_thread_local_getter(CURRENT_USER_WSGIDAV)
|
144
|
|
-
|
145
|
|
- return self._application(environ, start_response)
|
146
|
|
-
|
147
|
|
- def tracim_compute_digest_response(self, left_digest_response_hash, method, uri, nonce, cnonce, qop, nc):
|
148
|
|
- # A1 : username:realm:password
|
149
|
|
- A2 = method + ":" + uri
|
|
42
|
+ return None
|
|
43
|
+
|
|
44
|
+ def tracim_compute_digest_response(
|
|
45
|
+ self,
|
|
46
|
+ left_digest_response_hash,
|
|
47
|
+ method,
|
|
48
|
+ uri,
|
|
49
|
+ nonce,
|
|
50
|
+ cnonce,
|
|
51
|
+ qop,
|
|
52
|
+ nc
|
|
53
|
+ ):
|
|
54
|
+ # TODO : Rename A to something more correct
|
|
55
|
+ A = "{method}:{uri}".format(method=method, uri=uri)
|
150
|
56
|
if qop:
|
151
|
|
- digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + self.md5h(A2))
|
|
57
|
+ right_digest_response_hash = "{nonce}:{nc}:{cnonce}:{qop}:{A}".format( # nopep8
|
|
58
|
+ nonce=nonce,
|
|
59
|
+ nc=nc,
|
|
60
|
+ cnonce=cnonce,
|
|
61
|
+ qop=qop,
|
|
62
|
+ method=method,
|
|
63
|
+ uri=uri,
|
|
64
|
+ A=self.md5h(A),
|
|
65
|
+ )
|
152
|
66
|
else:
|
153
|
|
- digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + self.md5h(A2))
|
154
|
|
- # print(A1, A2)
|
155
|
|
- # print(digestresp)
|
|
67
|
+ right_digest_response_hash = "{nonce}:{A}".format(
|
|
68
|
+ nonce=nonce,
|
|
69
|
+ A=self.md5h(A),
|
|
70
|
+ )
|
|
71
|
+ digestresp = self.md5kd(
|
|
72
|
+ left_digest_response_hash,
|
|
73
|
+ right_digest_response_hash,
|
|
74
|
+ )
|
|
75
|
+
|
156
|
76
|
return digestresp
|