diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b21942a..fc78134d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * Add: [server] delay_on_error option * Add: [logging] limit_content option * Add: [headers] Content-Security-Policy is now set to be strict on new configs +* Improve: [server] max_resource_size is now also checked on storage discover+get to avoid issues with items stored outside using PUT ## 3.6.1 diff --git a/radicale/app/put.py b/radicale/app/put.py index b9b764c8..a9c2d144 100644 --- a/radicale/app/put.py +++ b/radicale/app/put.py @@ -105,11 +105,11 @@ def prepare(vobject_items: List[vobject.base.Component], path: str, raise size = len(item.serialize()) if (size > max_resource_size): - logger.warning("PUT request contains item with UID %r size %d > limit %d: %r", item.uid, size, max_resource_size, path) + logger.warning("PUT request contains item with UID %r size %s > limit %s: %r", item.uid, utils.format_unit(size, binary=True), utils.format_unit(max_resource_size, binary=True), path) # Use OverflowError as flag for max_resource_size raise OverflowError else: - logger.debug("PUT request contains item with UID %r size %d <= limit %d: %r", item.uid, size, max_resource_size, path) + logger.debug("PUT request contains item with UID %r size %s <= limit %s: %r", item.uid, utils.format_unit(size, binary=True), utils.format_unit(max_resource_size, binary=True), path) items.append(item) elif write_whole_collection and tag == "VADDRESSBOOK": for vobject_item in vobject_items: diff --git a/radicale/pathutils.py b/radicale/pathutils.py index b1ddce00..31341203 100644 --- a/radicale/pathutils.py +++ b/radicale/pathutils.py @@ -32,6 +32,7 @@ from tempfile import TemporaryDirectory from typing import Iterator, Type, Union from radicale import storage, types, utils +from radicale.log import logger if sys.platform == "win32": import ctypes @@ -355,3 +356,12 @@ def path_permissions_as_string(path): pp = path_permissions(path) s = "path=%r owner=%s(%s) group=%s(%s) mode=%s" % (path, pp[0], pp[1], pp[2], pp[3], pp[4]) return s + + +def file_check_size(path: str, limit: int): + if os.path.isfile(path): + size = os.stat(path).st_size + if size > limit: + logger.warning("file skipped because size exceeds limit %s > %s: %r", utils.format_unit(size, binary=True), utils.format_unit(limit, binary=True), path) + return False + return True diff --git a/radicale/storage/multifilesystem/base.py b/radicale/storage/multifilesystem/base.py index f0fe3dd5..c5e29b80 100644 --- a/radicale/storage/multifilesystem/base.py +++ b/radicale/storage/multifilesystem/base.py @@ -78,6 +78,7 @@ class StorageBase(storage.BaseStorage): _debug_cache_actions: bool _folder_umask: str _config_umask: int + _max_resource_size: int def __init__(self, configuration: config.Configuration) -> None: super().__init__(configuration) @@ -99,6 +100,8 @@ class StorageBase(storage.BaseStorage): "storage", "folder_umask") self._debug_cache_actions = configuration.get( "logging", "storage_cache_actions_on_debug") + self._max_resource_size = configuration.get( + "server", "max_resource_size") def _get_collection_root_folder(self) -> str: return os.path.join(self._filesystem_folder, "collection-root") diff --git a/radicale/storage/multifilesystem/discover.py b/radicale/storage/multifilesystem/discover.py index a635906a..15bdaaf6 100644 --- a/radicale/storage/multifilesystem/discover.py +++ b/radicale/storage/multifilesystem/discover.py @@ -77,7 +77,8 @@ class StoragePartDiscover(StorageBase): if href: item = collection._get(href) if item is not None: - yield item + if pathutils.file_check_size(filesystem_path, self._max_resource_size): + yield item return yield collection diff --git a/radicale/storage/multifilesystem/get.py b/radicale/storage/multifilesystem/get.py index ce162d3a..52eab809 100644 --- a/radicale/storage/multifilesystem/get.py +++ b/radicale/storage/multifilesystem/get.py @@ -175,7 +175,8 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock, href) yield (href, None) else: - yield (href, self._get(href, verify_href=False)) + if pathutils.file_check_size(path, self._storage._max_resource_size): + yield (href, self._get(href, verify_href=False)) def get_all(self) -> Iterator[radicale_item.Item]: for href in self._list(): @@ -183,4 +184,5 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock, # are from os.listdir. item = self._get(href, verify_href=False) if item is not None: - yield item + if pathutils.file_check_size(os.path.join(self._filesystem_path, href), self._storage._max_resource_size): + yield item diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 7f37542e..787364dc 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -193,6 +193,38 @@ permissions: RrWw""") path = "/calendar.ics/" self.put(path, event, check=412) + def test_get_vcard_exceed_size(self) -> None: + """Add vcards partially exceeding max-resource-size (adjusted after upload).""" + path_base = "/contacts.vcf/" + file1 = "contact1.vcf" + file2 = "contact_photo_with_data_uri.vcf" + self.create_addressbook(path_base) + contact1 = get_file_content(file1) + contact2 = get_file_content(file2) + path1 = path_base + file1 + path2 = path_base + file2 + self.put(path1, contact1) + self.put(path2, contact2) + self.configure({"server": {"max_resource_size": 100}}) + # test "get" + self.get(path1, check=200) + self.get(path2, check=404) + # test "report" + _, responses = self.report(path_base, """\ + + + + + + """ + path_base + """ +""") + logging.info("response: %r", responses) + assert len(responses) == 1 + response = responses[path1] + assert isinstance(response, dict) + status, prop = response["D:getetag"] + assert status == 200 and prop.text + def test_add_event_broken(self) -> None: """Add a broken event.""" self.mkcalendar("/calendar.ics/") diff --git a/radicale/tests/test_server.py b/radicale/tests/test_server.py index b344dddf..1e302449 100644 --- a/radicale/tests/test_server.py +++ b/radicale/tests/test_server.py @@ -1,5 +1,5 @@ # This file is part of Radicale - CalDAV and CardDAV server -# Copyright © 2018-2019 Unrud +# Copyright © 2018-2022 Unrud # # 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/utils.py b/radicale/utils.py index d25e08e0..ca4da132 100644 --- a/radicale/utils.py +++ b/radicale/utils.py @@ -378,7 +378,10 @@ def format_unit(value: float, binary: bool = False) -> str: unit = "k" else: unit = "" - return ("%.1f %s" % (value, unit)) + if unit == "": + return ("%d" % value) + else: + return ("%.1f %s" % (value, unit)) def limit_str(content: str, limit: int) -> str: