diff options
author | Tobias Brox <tobias@redpill-linpro.com> | 2021-11-30 00:09:17 +0100 |
---|---|---|
committer | Tobias Brox <tobias@redpill-linpro.com> | 2021-11-30 00:53:55 +0100 |
commit | 30e6629d4e5343f7f117b37d5598259c27793eb9 (patch) | |
tree | 5e535afc22811d476a29feda5d9cc918da2a323c | |
parent | 541daa244ef426bf9cb5587f8a53e3ffba6b1421 (diff) | |
download | python-caldav-30e6629d4e5343f7f117b37d5598259c27793eb9.zip |
not happy with added code complexity, but this commit seems to solve https://github.com/python-caldav/caldav/issues/158
-rw-r--r-- | caldav/davclient.py | 95 | ||||
-rw-r--r-- | caldav/lib/error.py | 24 |
2 files changed, 96 insertions, 23 deletions
diff --git a/caldav/davclient.py b/caldav/davclient.py index eea0530..e5f9e51 100644 --- a/caldav/davclient.py +++ b/caldav/davclient.py @@ -486,36 +486,105 @@ class DAVClient: def options(self, url): return self.request(url, "OPTIONS") - def request(self, url, method="GET", body="", headers={}): + def _pre_request(self, url, body, headers): """ - Actually sends the request + refacored out some stuff from requests, to be reused in verify_login """ - - # objectify the url - url = URL.objectify(url) + combined_headers = self.headers.copy() + combined_headers.update(headers) + if (body is None or body == "") and "Content-Type" in combined_headers: + del combined_headers["Content-Type"] proxies = None if self.proxy is not None: proxies = {url.scheme: self.proxy} log.debug("using proxy - %s" % (proxies)) + return (combined_headers, proxies) + + def verify_login(self, url=None, method="PROPFIND", body="", headers={}): + """ + Will do the following: + * run a test request without auth towards the server. + * assert it returns 401 + * read the WWW-Authenticate header and decide what kind of auth object to create + * run a test query with auth + * assert it returns 2xx or 3xx + In 0.9, it should return True or raise an exception + In 0.8.2, it may log an error and return False + """ + if not url: + url=self.url + + (combined_headers, proxies) = self._pre_request(url, body, headers) + + if not self.auth: + ## Try a test request w/o auth + resp = self.session.request( + method, url, data=to_wire(body), + headers=combined_headers, proxies=proxies, + verify=self.ssl_verify_cert, cert=self.ssl_cert) + + if resp.status_code > 399 and not self.password and not self.username: + return True + + ## I'd like to raise an AuthorizationError here, but "assert_" and + ## return is safer for a minor release. + error.assert_(resp.status_code == 401) + error.assert_('WWW-Authenticate' in resp.headers) + if resp.status_code != 401: + return True + if not 'WWW-Authenticate' in resp.headers: + return False + + auth_type = resp.headers['WWW-Authenticate'] + auth_type = auth_type[0:auth_type.find(" ")] + + error.assert_(auth_type in ('Basic', 'Digest')) + + if auth_type == 'Basic': + self.auth = requests.auth.HTTPBasicAuth(self.username, self.password) + elif auth_type == 'Digest': + self.auth = requests.auth.HTTPDigestAuth(self.username, self.password) + else: + ## I'm a bit concerned ... don't want to raise new exceptions in a minor release + #raise NotImplementedError(f"Auth method {auth_type} not supported yet") + return False + + resp = self.session.request( + method, url, data=to_wire(body), + headers=combined_headers, proxies=proxies, auth=self.auth, + verify=self.ssl_verify_cert, cert=self.ssl_cert) + + if resp.status_code > 399: + raise error.AuthorizationError(url=url, reason=resp.reason) + + def request(self, url, method="GET", body="", headers={}): + """ + Actually sends the request + """ + (combined_headers, proxies) = self._pre_request(url, body, headers) + + # objectify the url + url = URL.objectify(url) + # ensure that url is a normal string url = str(url) - combined_headers = dict(self.headers) - combined_headers.update(headers) - if body is None or body == "" and "Content-Type" in combined_headers: - del combined_headers["Content-Type"] - - log.debug( - "sending request - method={0}, url={1}, headers={2}\nbody:\n{3}" - .format(method, url, combined_headers, to_normal_str(body))) auth = None + + if self.auth is None: + self.verify_login() + if self.auth is None and self.username is not None: auth = requests.auth.HTTPDigestAuth(self.username, self.password) else: auth = self.auth + log.debug( + "sending request - method={0}, url={1}, headers={2}\nbody:\n{3}" + .format(method, url, combined_headers, to_normal_str(body))) + r = self.session.request( method, url, data=to_wire(body), headers=combined_headers, proxies=proxies, auth=auth, diff --git a/caldav/lib/error.py b/caldav/lib/error.py index 3ec55f8..593f349 100644 --- a/caldav/lib/error.py +++ b/caldav/lib/error.py @@ -34,24 +34,28 @@ def assert_(condition): ERR_FRAGMENT="Please raise an issue at https://github.com/python-caldav/caldav/issues or reach out to t-caldav@tobixen.no, include this error and the traceback and tell what server you are using" -class AuthorizationError(Exception): - """ - The client encountered an HTTP 403 error and is passing it on - to the user. The url property will contain the url in question, - the reason property will contain the excuse the server sent. - """ +class DAVError(Exception): url = None - reason = "PHP at work[tm]" + reason = "no reason" + + def __init__(self, url=None, reason=None): + if url: + self.url = url + if reason: + self.reason = reason def __str__(self): return "AuthorizationError at '%s', reason '%s'" % \ (self.url, self.reason) - -class DAVError(Exception): +class AuthorizationError(DAVError): + """ + The client encountered an HTTP 403 error and is passing it on + to the user. The url property will contain the url in question, + the reason property will contain the excuse the server sent. + """ pass - class PropsetError(DAVError): pass |