diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a1d896e..cd0ab4f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 3.4.2.dev * Add: option [auth] type oauth2 by code migration from https://gitlab.mim-libre.fr/alphabet/radicale_oauth/-/blob/dev/oauth2/ +* Fix: catch OS errors on PUT MKCOL MKCALENDAR MOVE PROPPATCH (insufficient storage, access denied, internal server error) ## 3.4.1 * Add: option [auth] dovecot_connection_type / dovecot_host / dovecot_port diff --git a/radicale/app/mkcalendar.py b/radicale/app/mkcalendar.py index c507ae44..b9f2063a 100644 --- a/radicale/app/mkcalendar.py +++ b/radicale/app/mkcalendar.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 © 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 @@ -17,7 +18,9 @@ # You should have received a copy of the GNU General Public License # along with Radicale. If not, see . +import errno import posixpath +import re import socket from http import client @@ -70,7 +73,20 @@ def do_MKCALENDAR(self, environ: types.WSGIEnviron, base_prefix: str, try: self._storage.create_collection(path, props=props) except ValueError as e: - logger.warning( - "Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) - return httputils.BAD_REQUEST + # return better matching HTTP result in case errno is provided and catched + errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e)) + if errno_match: + logger.error( + "Failed MKCALENDAR request on %r: %s", path, e, exc_info=True) + errno_e = int(errno_match.group(1)) + if errno_e == errno.ENOSPC: + return httputils.INSUFFICIENT_STORAGE + elif errno_e in [errno.EPERM, errno.EACCES]: + return httputils.FORBIDDEN + else: + return httputils.INTERNAL_SERVER_ERROR + else: + logger.warning( + "Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) + return httputils.BAD_REQUEST return client.CREATED, {}, None diff --git a/radicale/app/mkcol.py b/radicale/app/mkcol.py index 5bccc50c..953508ad 100644 --- a/radicale/app/mkcol.py +++ b/radicale/app/mkcol.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 © 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 @@ -17,7 +18,9 @@ # You should have received a copy of the GNU General Public License # along with Radicale. If not, see . +import errno import posixpath +import re import socket from http import client @@ -74,8 +77,21 @@ def do_MKCOL(self, environ: types.WSGIEnviron, base_prefix: str, try: self._storage.create_collection(path, props=props) except ValueError as e: - logger.warning( - "Bad MKCOL request on %r (type:%s): %s", path, collection_type, e, exc_info=True) - return httputils.BAD_REQUEST + # return better matching HTTP result in case errno is provided and catched + errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e)) + if errno_match: + logger.error( + "Failed MKCOL request on %r (type:%s): %s", path, collection_type, e, exc_info=True) + errno_e = int(errno_match.group(1)) + if errno_e == errno.ENOSPC: + return httputils.INSUFFICIENT_STORAGE + elif errno_e in [errno.EPERM, errno.EACCES]: + return httputils.FORBIDDEN + else: + return httputils.INTERNAL_SERVER_ERROR + else: + logger.warning( + "Bad MKCOL request on %r (type:%s): %s", path, collection_type, e, exc_info=True) + return httputils.BAD_REQUEST logger.info("MKCOL request %r (type:%s): %s", path, collection_type, "successful") return client.CREATED, {}, None diff --git a/radicale/app/move.py b/radicale/app/move.py index 5bd8a579..f555e871 100644 --- a/radicale/app/move.py +++ b/radicale/app/move.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 © 2023-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 @@ -17,6 +18,7 @@ # You should have received a copy of the GNU General Public License # along with Radicale. If not, see . +import errno import posixpath import re from http import client @@ -109,7 +111,20 @@ def do_MOVE(self, environ: types.WSGIEnviron, base_prefix: str, try: self._storage.move(item, to_collection, to_href) except ValueError as e: - logger.warning( - "Bad MOVE request on %r: %s", path, e, exc_info=True) - return httputils.BAD_REQUEST + # return better matching HTTP result in case errno is provided and catched + errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e)) + if errno_match: + logger.error( + "Failed MOVE request on %r: %s", path, e, exc_info=True) + errno_e = int(errno_match.group(1)) + if errno_e == errno.ENOSPC: + return httputils.INSUFFICIENT_STORAGE + elif errno_e in [errno.EPERM, errno.EACCES]: + return httputils.FORBIDDEN + else: + return httputils.INTERNAL_SERVER_ERROR + else: + logger.warning( + "Bad MOVE request on %r: %s", path, e, exc_info=True) + return httputils.BAD_REQUEST return client.NO_CONTENT if to_item else client.CREATED, {}, None diff --git a/radicale/app/proppatch.py b/radicale/app/proppatch.py index c15fddfe..76b4a1a1 100644 --- a/radicale/app/proppatch.py +++ b/radicale/app/proppatch.py @@ -2,7 +2,9 @@ # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # Copyright © 2008-2017 Guillaume Ayoub -# Copyright © 2017-2018 Unrud +# Copyright © 2017-2020 Unrud +# Copyright © 2020-2020 Tuna Celik +# 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 @@ -17,6 +19,8 @@ # You should have received a copy of the GNU General Public License # along with Radicale. If not, see . +import errno +import re import socket import xml.etree.ElementTree as ET from http import client @@ -107,7 +111,20 @@ def do_PROPPATCH(self, environ: types.WSGIEnviron, base_prefix: str, ) self._hook.notify(hook_notification_item) except ValueError as e: - logger.warning( - "Bad PROPPATCH request on %r: %s", path, e, exc_info=True) - return httputils.BAD_REQUEST + # return better matching HTTP result in case errno is provided and catched + errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e)) + if errno_match: + logger.error( + "Failed PROPPATCH request on %r: %s", path, e, exc_info=True) + errno_e = int(errno_match.group(1)) + if errno_e == errno.ENOSPC: + return httputils.INSUFFICIENT_STORAGE + elif errno_e in [errno.EPERM, errno.EACCES]: + return httputils.FORBIDDEN + else: + return httputils.INTERNAL_SERVER_ERROR + else: + logger.warning( + "Bad PROPPATCH request on %r: %s", path, e, exc_info=True) + return httputils.BAD_REQUEST return client.MULTI_STATUS, headers, self._xml_response(xml_answer) diff --git a/radicale/app/put.py b/radicale/app/put.py index 6e1ba215..962bf756 100644 --- a/radicale/app/put.py +++ b/radicale/app/put.py @@ -4,7 +4,7 @@ # Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2017-2020 Unrud # Copyright © 2020-2023 Tuna Celik -# Copyright © 2024-2024 Peter Bieringer +# 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 @@ -19,8 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Radicale. If not, see . +import errno import itertools import posixpath +import re import socket import sys from http import client @@ -264,9 +266,22 @@ def do_PUT(self, environ: types.WSGIEnviron, base_prefix: str, ) self._hook.notify(hook_notification_item) except ValueError as e: - logger.warning( - "Bad PUT request on %r (upload): %s", path, e, exc_info=True) - return httputils.BAD_REQUEST + # return better matching HTTP result in case errno is provided and catched + errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e)) + if errno_match: + logger.error( + "Failed PUT request on %r (upload): %s", path, e, exc_info=True) + errno_e = int(errno_match.group(1)) + if errno_e == errno.ENOSPC: + return httputils.INSUFFICIENT_STORAGE + elif errno_e in [errno.EPERM, errno.EACCES]: + return httputils.FORBIDDEN + else: + return httputils.INTERNAL_SERVER_ERROR + else: + logger.warning( + "Bad PUT request on %r (upload): %s", path, e, exc_info=True) + return httputils.BAD_REQUEST headers = {"ETag": etag} return client.CREATED, headers, None diff --git a/radicale/httputils.py b/radicale/httputils.py index 3983d7eb..23cc3677 100644 --- a/radicale/httputils.py +++ b/radicale/httputils.py @@ -3,7 +3,7 @@ # Copyright © 2008 Pascal Halter # Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2017-2022 Unrud -# Copyright © 2024-2024 Peter Bieringer +# 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 @@ -79,6 +79,9 @@ DIRECTORY_LISTING: types.WSGIResponse = ( client.FORBIDDEN, (("Content-Type", "text/plain"),), "Directory listings are not supported.") +INSUFFICIENT_STORAGE: types.WSGIResponse = ( + client.INSUFFICIENT_STORAGE, (("Content-Type", "text/plain"),), + "Insufficient Storage. Please contact the administrator.") INTERNAL_SERVER_ERROR: types.WSGIResponse = ( client.INTERNAL_SERVER_ERROR, (("Content-Type", "text/plain"),), "A server error occurred. Please contact the administrator.") diff --git a/radicale/storage/multifilesystem/create_collection.py b/radicale/storage/multifilesystem/create_collection.py index 2e6e9ce7..cbbdee53 100644 --- a/radicale/storage/multifilesystem/create_collection.py +++ b/radicale/storage/multifilesystem/create_collection.py @@ -2,7 +2,7 @@ # Copyright © 2014 Jean-Marc Martins # Copyright © 2012-2017 Guillaume Ayoub # Copyright © 2017-2021 Unrud -# Copyright © 2024-2024 Peter Bieringer +# 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 @@ -50,27 +50,31 @@ def create_collection(self, href: str, self._makedirs_synced(parent_dir) # Create a temporary directory with an unsafe name - with TemporaryDirectory(prefix=".Radicale.tmp-", dir=parent_dir - ) as tmp_dir: - # The temporary directory itself can't be renamed - tmp_filesystem_path = os.path.join(tmp_dir, "collection") - os.makedirs(tmp_filesystem_path) - col = self._collection_class( - cast(multifilesystem.Storage, self), - pathutils.unstrip_path(sane_path, True), - filesystem_path=tmp_filesystem_path) - col.set_meta(props) - if items is not None: - if props.get("tag") == "VCALENDAR": - col._upload_all_nonatomic(items, suffix=".ics") - elif props.get("tag") == "VADDRESSBOOK": - col._upload_all_nonatomic(items, suffix=".vcf") + try: + with TemporaryDirectory(prefix=".Radicale.tmp-", dir=parent_dir + ) as tmp_dir: + # The temporary directory itself can't be renamed + tmp_filesystem_path = os.path.join(tmp_dir, "collection") + os.makedirs(tmp_filesystem_path) + col = self._collection_class( + cast(multifilesystem.Storage, self), + pathutils.unstrip_path(sane_path, True), + filesystem_path=tmp_filesystem_path) + col.set_meta(props) + if items is not None: + if props.get("tag") == "VCALENDAR": + col._upload_all_nonatomic(items, suffix=".ics") + elif props.get("tag") == "VADDRESSBOOK": + col._upload_all_nonatomic(items, suffix=".vcf") - if os.path.lexists(filesystem_path): - pathutils.rename_exchange(tmp_filesystem_path, filesystem_path) - else: - os.rename(tmp_filesystem_path, filesystem_path) - self._sync_directory(parent_dir) + if os.path.lexists(filesystem_path): + pathutils.rename_exchange(tmp_filesystem_path, filesystem_path) + else: + os.rename(tmp_filesystem_path, filesystem_path) + self._sync_directory(parent_dir) + except Exception as e: + raise ValueError("Failed to create collection %r as %r %s" % + (href, filesystem_path, e)) from e return self._collection_class( cast(multifilesystem.Storage, self),