From 3f74ab1cd3e5d2d14beb2ccc3c6645079ff6ffda Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 15:46:50 +0100 Subject: [PATCH 01/13] do not log problematic sync token twice --- radicale/app/report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/app/report.py b/radicale/app/report.py index b63681f7..023e6fe4 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -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: %s", + 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")) From 4c0d216cb970865ce206a60a29f1eafb6d190d56 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 15:50:05 +0100 Subject: [PATCH 02/13] log path on invalid sync token --- radicale/app/report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/app/report.py b/radicale/app/report.py index 023e6fe4..42ce1ae0 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -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: %s", - e, exc_info=True) + logger.warning("Client provided invalid sync token for path %r: %s", + path, 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")) From ed849a727136ea29269c5dfff65dce749dc8cee0 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 16:06:26 +0100 Subject: [PATCH 03/13] add support for optional arguments --- radicale/tests/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index c13c8725..944243e2 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -1687,7 +1687,7 @@ permissions: RrWw""") """, 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 @@ -1699,7 +1699,7 @@ permissions: RrWw""") %s -""" % sync_token_xml) +""" % sync_token_xml, **kwargs) xml = DefusedET.fromstring(answer) if status in (403, 409): assert xml.tag == xmlutils.make_clark("D:error") From f9697eeda1d1295aa36b7528638fdb6ecc121d7e Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 16:06:53 +0100 Subject: [PATCH 04/13] log user on invalid sync token --- radicale/app/report.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/radicale/app/report.py b/radicale/app/report.py index 42ce1ae0..e6dc45c0 100644 --- a/radicale/app/report.py +++ b/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 = "" ) -> 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 for path %r: %s", - path, e, exc_info=True) + logger.warning("Client provided invalid sync token for path %r (user %r): %s", + path, user, 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")) @@ -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) except ValueError as e: logger.warning( "Bad REPORT request on %r: %s", path, e, exc_info=True) From b2320c607d77faabc04be770e2c74998dce22bde Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 16:07:13 +0100 Subject: [PATCH 05/13] test case for invalid-sync token with user --- radicale/tests/test_base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 944243e2..453bf045 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -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+client""" + 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:") + assert not sync_token + def test_propfind_sync_token(self) -> None: """Retrieve the sync-token with a propfind request""" calendar_path = "/calendar.ics/" From 3fecba62e7f7c166a5dd0d6dc8e831385709bfc0 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 16:22:20 +0100 Subject: [PATCH 06/13] fix comment --- CHANGELOG.md | 1 + radicale/tests/test_base.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff94443b..99cdc931 100644 --- a/CHANGELOG.md +++ b/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 and path ## 3.5.8 * Extend: [auth] re-factor & overhaul LDAP authentication, especially for Python's ldap module diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 453bf045..5512ef9d 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -1848,7 +1848,7 @@ permissions: RrWw""") 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+client""" + """Test sync-collection report with an invalid sync token and user""" self.configure({"auth": {"type": "none"}}) calendar_path = "/calendar.ics/" self.mkcalendar(calendar_path) From 4980172defef4977c9bcd48477aa32814ccf1b94 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 16:57:31 +0100 Subject: [PATCH 07/13] extend copyright --- radicale/app/get.py | 3 ++- radicale/app/head.py | 3 ++- radicale/app/options.py | 3 ++- radicale/app/post.py | 5 +++-- radicale/app/propfind.py | 3 ++- radicale/tests/__init__.py | 3 ++- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/radicale/app/get.py b/radicale/app/get.py index edd29b75..d170d380 100644 --- a/radicale/app/get.py +++ b/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 +# Copyright © 2017-2023 Unrud +# Copyright © 2025-2025 Peter Bieringer # # 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 diff --git a/radicale/app/head.py b/radicale/app/head.py index 5166db2d..718861e6 100644 --- a/radicale/app/head.py +++ b/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 +# Copyright © 2017-2022 Unrud +# Copyright © 2025-2025 Peter Bieringer # # 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 diff --git a/radicale/app/options.py b/radicale/app/options.py index 6e9053a3..159584bf 100644 --- a/radicale/app/options.py +++ b/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 +# Copyright © 2017-2021 Unrud +# Copyright © 2025-2025 Peter Bieringer # # 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 diff --git a/radicale/app/post.py b/radicale/app/post.py index f5367b86..1bb68a12 100644 --- a/radicale/app/post.py +++ b/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 -# Copyright © 2020 Tom Hacohen +# Copyright © 2017-2021 Unrud +# Copyright © 2020-2020 Tom Hacohen +# Copyright © 2025-2025 Peter Bieringer # # 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 diff --git a/radicale/app/propfind.py b/radicale/app/propfind.py index 6a3cea6d..2ba4b5d1 100644 --- a/radicale/app/propfind.py +++ b/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 +# Copyright © 2017-2021 Unrud +# Copyright © 2025-2025 Peter Bieringer # # 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 diff --git a/radicale/tests/__init__.py b/radicale/tests/__init__.py index c1a2aab2..73179efe 100644 --- a/radicale/tests/__init__.py +++ b/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 +# Copyright © 2017-2023 Unrud +# Copyright © 2024-2025 Peter Bieringer # # 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 From 03481a11840d57e6e8d089099b70a1bd5446c8b4 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 17:02:04 +0100 Subject: [PATCH 08/13] add support for remote host+useragent --- radicale/tests/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/radicale/tests/__init__.py b/radicale/tests/__init__.py index 73179efe..9a57e6fd 100644 --- a/radicale/tests/__init__.py +++ b/radicale/tests/__init__.py @@ -80,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): @@ -91,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: From 91cee5e5146b43723bf547f959211a52b3df6419 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 17:02:31 +0100 Subject: [PATCH 09/13] extend test with remote host+useragent --- radicale/tests/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 5512ef9d..8eff78d5 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -1848,12 +1848,12 @@ permissions: RrWw""") 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""" + """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:") + 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: From 85f1850b198ec790bdbff1289a3d572fd772e225 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 17:03:59 +0100 Subject: [PATCH 10/13] make flake8 happy --- radicale/tests/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 8eff78d5..0f33a4fc 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -1853,7 +1853,7 @@ permissions: RrWw""") 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") + 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: From 1c7ff414a96a6024616f1ece1d473bebd0455206 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 17:06:00 +0100 Subject: [PATCH 11/13] extend app calls with remote host+useragent --- radicale/app/__init__.py | 2 +- radicale/app/delete.py | 2 +- radicale/app/get.py | 2 +- radicale/app/head.py | 4 ++-- radicale/app/mkcalendar.py | 2 +- radicale/app/mkcol.py | 2 +- radicale/app/move.py | 2 +- radicale/app/options.py | 2 +- radicale/app/post.py | 2 +- radicale/app/propfind.py | 2 +- radicale/app/proppatch.py | 2 +- radicale/app/put.py | 2 +- radicale/app/report.py | 6 +++--- 13 files changed, 16 insertions(+), 16 deletions(-) diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index 0b895820..940d15b5 100644 --- a/radicale/app/__init__.py +++ b/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") diff --git a/radicale/app/delete.py b/radicale/app/delete.py index 060abb18..695de45f 100644 --- a/radicale/app/delete.py +++ b/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"): diff --git a/radicale/app/get.py b/radicale/app/get.py index d170d380..b8adb39a 100644 --- a/radicale/app/get.py +++ b/radicale/app/get.py @@ -59,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): diff --git a/radicale/app/head.py b/radicale/app/head.py index 718861e6..eec68bb5 100644 --- a/radicale/app/head.py +++ b/radicale/app/head.py @@ -26,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) diff --git a/radicale/app/mkcalendar.py b/radicale/app/mkcalendar.py index 632d3c38..db14bfdc 100644 --- a/radicale/app/mkcalendar.py +++ b/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 diff --git a/radicale/app/mkcol.py b/radicale/app/mkcol.py index 169cb62c..72d5aa2b 100644 --- a/radicale/app/mkcol.py +++ b/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"): diff --git a/radicale/app/move.py b/radicale/app/move.py index 77e56f3e..ba346762 100644 --- a/radicale/app/move.py +++ b/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) diff --git a/radicale/app/options.py b/radicale/app/options.py index 159584bf..a869e2ab 100644 --- a/radicale/app/options.py +++ b/radicale/app/options.py @@ -27,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( diff --git a/radicale/app/post.py b/radicale/app/post.py index 1bb68a12..df944499 100644 --- a/radicale/app/post.py +++ b/radicale/app/post.py @@ -26,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) diff --git a/radicale/app/propfind.py b/radicale/app/propfind.py index 2ba4b5d1..b546c5e1 100644 --- a/radicale/app/propfind.py +++ b/radicale/app/propfind.py @@ -377,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"): diff --git a/radicale/app/proppatch.py b/radicale/app/proppatch.py index 2e8eed47..9d6dc221 100644 --- a/radicale/app/proppatch.py +++ b/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"): diff --git a/radicale/app/put.py b/radicale/app/put.py index 6cfed1eb..de11589b 100644 --- a/radicale/app/put.py +++ b/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"): diff --git a/radicale/app/report.py b/radicale/app/report.py index e6dc45c0..dd59c373 100644 --- a/radicale/app/report.py +++ b/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, user: str = "" + max_occurrence: int = 0, user: str = "", remote_addr: str = "", remote_useragent: str = "" ) -> Tuple[int, ET.Element]: """Read and answer REPORT requests that return XML. @@ -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, user) + 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) From 7ebe5703acf07714ba6a5f51ef888feb53615449 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 17:06:21 +0100 Subject: [PATCH 12/13] log remote host+useragent on invalid sync token --- radicale/app/report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/app/report.py b/radicale/app/report.py index dd59c373..dda4ba10 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -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 for path %r (user %r): %s", - path, user, 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")) From fb991798e843600cf52bbd2d186f8658d1c36fe5 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 16 Nov 2025 17:10:45 +0100 Subject: [PATCH 13/13] extend changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99cdc931..25129ef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 3.5.9.dev * Extend: [auth] add support for type http_remote_user -* Extend: logging of invalid sync-token with user and path +* 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