Bladeren bron

Merge pull request #1918 from pbiering/improve-synctoken-log

Improve synctoken log
Peter Bieringer 3 maanden geleden
bovenliggende
commit
bde2560bf4

+ 1 - 0
CHANGELOG.md

@@ -2,6 +2,7 @@
 
 ## 3.5.9.dev
 * Extend: [auth] add support for type http_remote_user
+* Extend: logging of invalid sync-token with user, path, remote host and useragent
 
 ## 3.5.8
 * Extend: [auth] re-factor & overhaul LDAP authentication, especially for Python's ldap module

+ 1 - 1
radicale/app/__init__.py

@@ -371,7 +371,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
 
         if not login or user:
             status, headers, answer = function(
-                environ, base_prefix, path, user)
+                environ, base_prefix, path, user, remote_host, remote_useragent)
             if (status, headers, answer) == httputils.NOT_ALLOWED:
                 logger.info("Access to %r denied for %s", path,
                             repr(user) if user else "anonymous user")

+ 1 - 1
radicale/app/delete.py

@@ -55,7 +55,7 @@ def xml_delete(base_prefix: str, path: str, collection: storage.BaseCollection,
 class ApplicationPartDelete(ApplicationBase):
 
     def do_DELETE(self, environ: types.WSGIEnviron, base_prefix: str,
-                  path: str, user: str) -> types.WSGIResponse:
+                  path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage DELETE request."""
         access = Access(self._rights, user, path)
         if not access.check("w"):

+ 3 - 2
radicale/app/get.py

@@ -2,7 +2,8 @@
 # Copyright © 2008 Nicolas Kandel
 # Copyright © 2008 Pascal Halter
 # Copyright © 2008-2017 Guillaume Ayoub
-# Copyright © 2017-2018 Unrud <unrud@outlook.com>
+# Copyright © 2017-2023 Unrud <unrud@outlook.com>
+# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
 #
 # This library is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -58,7 +59,7 @@ class ApplicationPartGet(ApplicationBase):
         return value
 
     def do_GET(self, environ: types.WSGIEnviron, base_prefix: str, path: str,
-               user: str) -> types.WSGIResponse:
+               user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage GET request."""
         # Redirect to /.web if the root path is requested
         if not pathutils.strip_path(path):

+ 4 - 3
radicale/app/head.py

@@ -2,7 +2,8 @@
 # Copyright © 2008 Nicolas Kandel
 # Copyright © 2008 Pascal Halter
 # Copyright © 2008-2017 Guillaume Ayoub
-# Copyright © 2017-2018 Unrud <unrud@outlook.com>
+# Copyright © 2017-2022 Unrud <unrud@outlook.com>
+# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
 #
 # This library is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -25,7 +26,7 @@ from radicale.app.get import ApplicationPartGet
 class ApplicationPartHead(ApplicationPartGet, ApplicationBase):
 
     def do_HEAD(self, environ: types.WSGIEnviron, base_prefix: str, path: str,
-                user: str) -> types.WSGIResponse:
+                user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage HEAD request."""
         # Body is dropped in `Application.__call__` for HEAD requests
-        return self.do_GET(environ, base_prefix, path, user)
+        return self.do_GET(environ, base_prefix, path, user, remote_host, remote_useragent)

+ 1 - 1
radicale/app/mkcalendar.py

@@ -33,7 +33,7 @@ from radicale.log import logger
 class ApplicationPartMkcalendar(ApplicationBase):
 
     def do_MKCALENDAR(self, environ: types.WSGIEnviron, base_prefix: str,
-                      path: str, user: str) -> types.WSGIResponse:
+                      path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage MKCALENDAR request."""
         if "w" not in self._rights.authorization(user, path):
             return httputils.NOT_ALLOWED

+ 1 - 1
radicale/app/mkcol.py

@@ -33,7 +33,7 @@ from radicale.log import logger
 class ApplicationPartMkcol(ApplicationBase):
 
     def do_MKCOL(self, environ: types.WSGIEnviron, base_prefix: str,
-                 path: str, user: str) -> types.WSGIResponse:
+                 path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage MKCOL request."""
         permissions = self._rights.authorization(user, path)
         if not rights.intersect(permissions, "Ww"):

+ 1 - 1
radicale/app/move.py

@@ -48,7 +48,7 @@ def get_server_netloc(environ: types.WSGIEnviron, force_port: bool = False):
 class ApplicationPartMove(ApplicationBase):
 
     def do_MOVE(self, environ: types.WSGIEnviron, base_prefix: str,
-                path: str, user: str) -> types.WSGIResponse:
+                path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage MOVE request."""
         raw_dest = environ.get("HTTP_DESTINATION", "")
         to_url = urlparse(raw_dest)

+ 3 - 2
radicale/app/options.py

@@ -2,7 +2,8 @@
 # Copyright © 2008 Nicolas Kandel
 # Copyright © 2008 Pascal Halter
 # Copyright © 2008-2017 Guillaume Ayoub
-# Copyright © 2017-2018 Unrud <unrud@outlook.com>
+# Copyright © 2017-2021 Unrud <unrud@outlook.com>
+# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
 #
 # This library is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -26,7 +27,7 @@ from radicale.app.base import ApplicationBase
 class ApplicationPartOptions(ApplicationBase):
 
     def do_OPTIONS(self, environ: types.WSGIEnviron, base_prefix: str,
-                   path: str, user: str) -> types.WSGIResponse:
+                   path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage OPTIONS request."""
         headers = {
             "Allow": ", ".join(

+ 4 - 3
radicale/app/post.py

@@ -2,8 +2,9 @@
 # Copyright © 2008 Nicolas Kandel
 # Copyright © 2008 Pascal Halter
 # Copyright © 2008-2017 Guillaume Ayoub
-# Copyright © 2017-2018 Unrud <unrud@outlook.com>
-# Copyright © 2020 Tom Hacohen <tom@stosb.com>
+# Copyright © 2017-2021 Unrud <unrud@outlook.com>
+# Copyright © 2020-2020 Tom Hacohen <tom@stosb.com>
+# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
 #
 # This library is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -25,7 +26,7 @@ from radicale.app.base import ApplicationBase
 class ApplicationPartPost(ApplicationBase):
 
     def do_POST(self, environ: types.WSGIEnviron, base_prefix: str,
-                path: str, user: str) -> types.WSGIResponse:
+                path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage POST request."""
         if path == "/.web" or path.startswith("/.web/"):
             return self._web.post(environ, base_prefix, path, user)

+ 3 - 2
radicale/app/propfind.py

@@ -2,7 +2,8 @@
 # Copyright © 2008 Nicolas Kandel
 # Copyright © 2008 Pascal Halter
 # Copyright © 2008-2017 Guillaume Ayoub
-# Copyright © 2017-2018 Unrud <unrud@outlook.com>
+# Copyright © 2017-2021 Unrud <unrud@outlook.com>
+# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
 #
 # This library is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -376,7 +377,7 @@ class ApplicationPartPropfind(ApplicationBase):
                 yield item, permission
 
     def do_PROPFIND(self, environ: types.WSGIEnviron, base_prefix: str,
-                    path: str, user: str) -> types.WSGIResponse:
+                    path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage PROPFIND request."""
         access = Access(self._rights, user, path)
         if not access.check("r"):

+ 1 - 1
radicale/app/proppatch.py

@@ -73,7 +73,7 @@ def xml_proppatch(base_prefix: str, path: str,
 class ApplicationPartProppatch(ApplicationBase):
 
     def do_PROPPATCH(self, environ: types.WSGIEnviron, base_prefix: str,
-                     path: str, user: str) -> types.WSGIResponse:
+                     path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage PROPPATCH request."""
         access = Access(self._rights, user, path)
         if not access.check("w"):

+ 1 - 1
radicale/app/put.py

@@ -142,7 +142,7 @@ def prepare(vobject_items: List[vobject.base.Component], path: str,
 class ApplicationPartPut(ApplicationBase):
 
     def do_PUT(self, environ: types.WSGIEnviron, base_prefix: str,
-               path: str, user: str) -> types.WSGIResponse:
+               path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage PUT request."""
         access = Access(self._rights, user, path)
         if not access.check("w"):

+ 5 - 5
radicale/app/report.py

@@ -149,7 +149,7 @@ def free_busy_report(base_prefix: str, path: str, xml_request: Optional[ET.Eleme
 def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
                collection: storage.BaseCollection, encoding: str,
                unlock_storage_fn: Callable[[], None],
-               max_occurrence: int = 0,
+               max_occurrence: int = 0, user: str = "", remote_addr: str = "", remote_useragent: str = ""
                ) -> Tuple[int, ET.Element]:
     """Read and answer REPORT requests that return XML.
 
@@ -213,8 +213,8 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
             sync_token, names = collection.sync(old_sync_token)
         except ValueError as e:
             # Invalid sync token
-            logger.warning("Client provided invalid sync token %r: %s",
-                           old_sync_token, e, exc_info=True)
+            logger.warning("Client provided invalid sync token for path %r (user %r from %s%s): %s",
+                           path, user, remote_addr, remote_useragent, e, exc_info=True)
             # client.CONFLICT doesn't work with some clients (e.g. InfCloud)
             return (client.FORBIDDEN,
                     xmlutils.webdav_error("D:valid-sync-token"))
@@ -776,7 +776,7 @@ def test_filter(collection_tag: str, item: radicale_item.Item,
 class ApplicationPartReport(ApplicationBase):
 
     def do_REPORT(self, environ: types.WSGIEnviron, base_prefix: str,
-                  path: str, user: str) -> types.WSGIResponse:
+                  path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
         """Manage REPORT request."""
         access = Access(self._rights, user, path)
         if not access.check("r"):
@@ -820,7 +820,7 @@ class ApplicationPartReport(ApplicationBase):
                 try:
                     status, xml_answer = xml_report(
                         base_prefix, path, xml_content, collection, self._encoding,
-                        lock_stack.close, max_occurrence)
+                        lock_stack.close, max_occurrence, user, remote_host, remote_useragent)
                 except ValueError as e:
                     logger.warning(
                         "Bad REPORT request on %r: %s", path, e, exc_info=True)

+ 8 - 1
radicale/tests/__init__.py

@@ -1,6 +1,7 @@
 # This file is part of Radicale - CalDAV and CardDAV server
 # Copyright © 2012-2017 Guillaume Ayoub
-# Copyright © 2017-2018 Unrud <unrud@outlook.com>
+# Copyright © 2017-2023 Unrud <unrud@outlook.com>
+# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
 #
 # This library is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -79,6 +80,8 @@ class BaseTest:
         if http_if_match is not None and not isinstance(http_if_match, str):
             raise TypeError("http_if_match argument must be %r, not %r" %
                             (str, type(http_if_match)))
+        remote_useragent = kwargs.pop("remote_useragent", None)
+        remote_host = kwargs.pop("remote_host", None)
         environ: Dict[str, Any] = {k.upper(): v for k, v in kwargs.items()}
         for k, v in environ.items():
             if not isinstance(v, str):
@@ -90,6 +93,10 @@ class BaseTest:
                     login.encode(encoding)).decode()
         if http_if_match:
             environ["HTTP_IF_MATCH"] = http_if_match
+        if remote_useragent:
+            environ["HTTP_USER_AGENT"] = remote_useragent
+        if remote_host:
+            environ["REMOTE_ADDR"] = remote_host
         environ["REQUEST_METHOD"] = method.upper()
         environ["PATH_INFO"] = path
         if data is not None:

+ 11 - 2
radicale/tests/test_base.py

@@ -1687,7 +1687,7 @@ permissions: RrWw""")
 </C:free-busy-query>""", 400, is_xml=False)
 
     def _report_sync_token(
-            self, calendar_path: str, sync_token: Optional[str] = None
+            self, calendar_path: str, sync_token: Optional[str] = None, **kwargs
             ) -> Tuple[str, RESPONSES]:
         sync_token_xml = (
             "<sync-token><![CDATA[%s]]></sync-token>" % sync_token
@@ -1699,7 +1699,7 @@ permissions: RrWw""")
         <getetag />
     </prop>
     %s
-</sync-collection>""" % sync_token_xml)
+</sync-collection>""" % sync_token_xml, **kwargs)
         xml = DefusedET.fromstring(answer)
         if status in (403, 409):
             assert xml.tag == xmlutils.make_clark("D:error")
@@ -1847,6 +1847,15 @@ permissions: RrWw""")
             calendar_path, "http://radicale.org/ns/sync/INVALID")
         assert not sync_token
 
+    def test_report_sync_collection_invalid_sync_token_with_user(self) -> None:
+        """Test sync-collection report with an invalid sync token and user+host+useragent"""
+        self.configure({"auth": {"type": "none"}})
+        calendar_path = "/calendar.ics/"
+        self.mkcalendar(calendar_path)
+        sync_token, _ = self._report_sync_token(
+            calendar_path, "http://radicale.org/ns/sync/INVALID", login="testuser:", remote_host="192.0.2.1", remote_useragent="Testclient/1.0")
+        assert not sync_token
+
     def test_propfind_sync_token(self) -> None:
         """Retrieve the sync-token with a propfind request"""
         calendar_path = "/calendar.ics/"