Skip to content

Commit

Permalink
Merge pull request #1695 from pbiering/issue-1693-fix-http-return-codes
Browse files Browse the repository at this point in the history
Issue 1693 fix http return codes
  • Loading branch information
pbiering authored Feb 11, 2025
2 parents d79abc2 + 19a4715 commit aa35c67
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 42 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 20 additions & 4 deletions radicale/app/mkcalendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2018 Unrud <[email protected]>
# Copyright © 2017-2021 Unrud <[email protected]>
# Copyright © 2024-2025 Peter Bieringer <[email protected]>
#
# 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
Expand All @@ -17,7 +18,9 @@
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.

import errno
import posixpath
import re
import socket
from http import client

Expand Down Expand Up @@ -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
24 changes: 20 additions & 4 deletions radicale/app/mkcol.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2018 Unrud <[email protected]>
# Copyright © 2017-2021 Unrud <[email protected]>
# Copyright © 2024-2025 Peter Bieringer <[email protected]>
#
# 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
Expand All @@ -17,7 +18,9 @@
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.

import errno
import posixpath
import re
import socket
from http import client

Expand Down Expand Up @@ -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
23 changes: 19 additions & 4 deletions radicale/app/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2018 Unrud <[email protected]>
# Copyright © 2017-2023 Unrud <[email protected]>
# Copyright © 2023-2025 Peter Bieringer <[email protected]>
#
# 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
Expand All @@ -17,6 +18,7 @@
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.

import errno
import posixpath
import re
from http import client
Expand Down Expand Up @@ -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
25 changes: 21 additions & 4 deletions radicale/app/proppatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2018 Unrud <[email protected]>
# Copyright © 2017-2020 Unrud <[email protected]>
# Copyright © 2020-2020 Tuna Celik <[email protected]>
# Copyright © 2025-2025 Peter Bieringer <[email protected]>
#
# 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
Expand All @@ -17,6 +19,8 @@
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.

import errno
import re
import socket
import xml.etree.ElementTree as ET
from http import client
Expand Down Expand Up @@ -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)
23 changes: 19 additions & 4 deletions radicale/app/put.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2020 Unrud <[email protected]>
# Copyright © 2020-2023 Tuna Celik <[email protected]>
# Copyright © 2024-2024 Peter Bieringer <[email protected]>
# Copyright © 2024-2025 Peter Bieringer <[email protected]>
#
# 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
Expand All @@ -19,8 +19,10 @@
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.

import errno
import itertools
import posixpath
import re
import socket
import sys
from http import client
Expand Down Expand Up @@ -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
5 changes: 4 additions & 1 deletion radicale/httputils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2022 Unrud <[email protected]>
# Copyright © 2024-2024 Peter Bieringer <[email protected]>
# Copyright © 2024-2025 Peter Bieringer <[email protected]>
#
# 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
Expand Down Expand Up @@ -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.")
Expand Down
46 changes: 25 additions & 21 deletions radicale/storage/multifilesystem/create_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Copyright © 2014 Jean-Marc Martins
# Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2021 Unrud <[email protected]>
# Copyright © 2024-2024 Peter Bieringer <[email protected]>
# Copyright © 2024-2025 Peter Bieringer <[email protected]>
#
# 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
Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit aa35c67

Please sign in to comment.