diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87d3fd2b..9676f5c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,18 @@
# Changelog
## 3.dev
+
+* Adjustment: option [auth] htpasswd_encryption change default from "md5" to "autodetect"
+
+## 3.2.3
+* Add: support for Python 3.13
* Fix: Using icalendar's tzinfo on created datetime to fix issue with icalendar
+* Fix: typos in code
* Enhancement: Added free-busy report
* Enhancement: Added 'max_freebusy_occurrences` setting to avoid potential DOS on reports
* Enhancement: remove unexpected control codes from uploaded items
+* Enhancement: add 'strip_domain' setting for username handling
+* Enhancement: add option to toggle debug log of rights rule with doesn't match
* Drop: remove unused requirement "typeguard"
* Improve: Refactored some date parsing code
diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md
index d34afb50..e1145fe4 100644
--- a/DOCUMENTATION.md
+++ b/DOCUMENTATION.md
@@ -122,12 +122,12 @@ The `users` file can be created and managed with
[htpasswd](https://httpd.apache.org/docs/current/programs/htpasswd.html):
```bash
-# Create a new htpasswd file with the user "user1"
-$ htpasswd -c /path/to/users user1
+# Create a new htpasswd file with the user "user1" using SHA-512 as hash method
+$ htpasswd -5 -c /path/to/users user1
New password:
Re-type new password:
# Add another user
-$ htpasswd /path/to/users user2
+$ htpasswd -5 /path/to/users user2
New password:
Re-type new password:
```
@@ -138,8 +138,7 @@ Authentication can be enabled with the following configuration:
[auth]
type = htpasswd
htpasswd_filename = /path/to/users
-# encryption method used in the htpasswd file
-htpasswd_encryption = md5
+htpasswd_encryption = autodetect
```
##### The simple but insecure way
@@ -623,7 +622,7 @@ hosts = 0.0.0.0:5232, [::]:5232
[auth]
type = htpasswd
htpasswd_filename = ~/.config/radicale/users
-htpasswd_encryption = md5
+htpasswd_encryption = autodetect
[storage]
filesystem_folder = ~/.var/lib/radicale/collections
@@ -641,7 +640,7 @@ The same example configuration via command line arguments looks like:
```bash
python3 -m radicale --server-hosts 0.0.0.0:5232,[::]:5232 \
--auth-type htpasswd --auth-htpasswd-filename ~/.config/radicale/users \
- --auth-htpasswd-encryption md5
+ --auth-htpasswd-encryption autodetect
```
Add the argument `--config ""` to stop Radicale from loading the default
@@ -778,7 +777,7 @@ Available methods:
The installation of **bcrypt** is required for this.
`md5`
-: This uses an iterated MD5 digest of the password with a salt.
+: This uses an iterated MD5 digest of the password with a salt (nowadays insecure).
`sha256`
: This uses an iterated SHA-256 digest of the password with a salt.
@@ -789,7 +788,7 @@ Available methods:
`autodetect`
: This selects autodetection of method per entry.
-Default: `md5`
+Default: `autodetect`
##### delay
@@ -1017,6 +1016,12 @@ Log response on level=debug
Default: `False`
+##### rights_rule_doesnt_match_on_debug = True
+
+Log rights rule which doesn't match on level=debug
+
+Default: `False`
+
#### headers
In this section additional HTTP headers that are sent to clients can be
diff --git a/config b/config
index 67ede2b7..f1037a81 100644
--- a/config
+++ b/config
@@ -80,7 +80,7 @@
# Htpasswd encryption method
# Value: plain | bcrypt | md5 | sha256 | sha512 | autodetect
# bcrypt requires the installation of 'bcrypt' module.
-#htpasswd_encryption = md5
+#htpasswd_encryption = autodetect
# Incorrect authentication delay (seconds)
#delay = 1
@@ -176,6 +176,8 @@
# Log response content on level=debug
#response_content_on_debug = False
+# Log rights rule which doesn't match on level=debug
+#rights_rule_doesnt_match_on_debug = False
[headers]
@@ -196,4 +198,3 @@
# When returning a free-busy report, limit the number of returned
# occurences per event to prevent DOS attacks.
#max_freebusy_occurrence = 10000
-
diff --git a/contrib/apache/radicale.conf b/contrib/apache/radicale.conf
index 7499be61..98a25a72 100644
--- a/contrib/apache/radicale.conf
+++ b/contrib/apache/radicale.conf
@@ -57,13 +57,15 @@
Require all granted
- ## You may want to use apache's authentication (config: [auth] type = remote_user)
+ ## You may want to use apache's authentication (config: [auth] type = http_x_remote_user)
+ ## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser
#AuthBasicProvider file
#AuthType Basic
#AuthName "Enter your credentials"
- #AuthUserFile /path/to/httpdfile/
+ #AuthUserFile /etc/httpd/conf/htpasswd-radicale
#AuthGroupFile /dev/null
#Require valid-user
+ #RequestHeader set X-Remote-User expr=%{REMOTE_USER}
@@ -106,13 +108,15 @@
Require all granted
- ## You may want to use apache's authentication (config: [auth] type = remote_user)
+ ## You may want to use apache's authentication (config: [auth] type = http_x_remote_user)
+ ## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser
#AuthBasicProvider file
#AuthType Basic
#AuthName "Enter your credentials"
- #AuthUserFile /path/to/httpdfile/
+ #AuthUserFile /etc/httpd/conf/htpasswd-radicale
#AuthGroupFile /dev/null
#Require valid-user
+ #RequestHeader set X-Remote-User expr=%{REMOTE_USER}
@@ -179,11 +183,12 @@ CustomLog logs/ssl_request_log "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
Require all granted
- ## You may want to use apache's authentication (config: [auth] type = remote_user)
+ ## You may want to use apache's authentication (config: [auth] type = http_x_remote_user)
+ ## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser
#AuthBasicProvider file
#AuthType Basic
#AuthName "Enter your credentials"
- #AuthUserFile /path/to/httpdfile/
+ #AuthUserFile /etc/httpd/conf/htpasswd-radicale
#AuthGroupFile /dev/null
#Require valid-user
@@ -221,11 +226,12 @@ CustomLog logs/ssl_request_log "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
Require all granted
- ## You may want to use apache's authentication (config: [auth] type = remote_user)
+ ## You may want to use apache's authentication (config: [auth] type = http_x_remote_user)
+ ## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser
#AuthBasicProvider file
#AuthType Basic
#AuthName "Enter your credentials"
- #AuthUserFile /path/to/httpdfile/
+ #AuthUserFile /etc/httpd/conf/htpasswd-radicale
#AuthGroupFile /dev/null
#Require valid-user
diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py
index f1b5144f..ea7ec12b 100644
--- a/radicale/app/__init__.py
+++ b/radicale/app/__init__.py
@@ -146,7 +146,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
if self._response_content_on_debug:
logger.debug("Response content:\n%s", answer)
else:
- logger.debug("Response content: suppressed by config/option [auth] response_content_on_debug")
+ logger.debug("Response content: suppressed by config/option [logging] response_content_on_debug")
headers["Content-Type"] += "; charset=%s" % self._encoding
answer = answer.encode(self._encoding)
accept_encoding = [
@@ -196,7 +196,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
logger.debug("Request header:\n%s",
pprint.pformat(self._scrub_headers(environ)))
else:
- logger.debug("Request header: suppressed by config/option [auth] request_header_on_debug")
+ logger.debug("Request header: suppressed by config/option [logging] request_header_on_debug")
# SCRIPT_NAME is already removed from PATH_INFO, according to the
# WSGI specification.
diff --git a/radicale/app/base.py b/radicale/app/base.py
index 15b5a1df..5c8a9355 100644
--- a/radicale/app/base.py
+++ b/radicale/app/base.py
@@ -51,6 +51,7 @@ class ApplicationBase:
self._encoding = configuration.get("encoding", "request")
self._log_bad_put_request_content = configuration.get("logging", "bad_put_request_content")
self._response_content_on_debug = configuration.get("logging", "response_content_on_debug")
+ self._request_content_on_debug = configuration.get("logging", "request_content_on_debug")
self._hook = hook.load(configuration)
def _read_xml_request_body(self, environ: types.WSGIEnviron
@@ -66,17 +67,20 @@ class ApplicationBase:
logger.debug("Request content (Invalid XML):\n%s", content)
raise RuntimeError("Failed to parse XML: %s" % e) from e
if logger.isEnabledFor(logging.DEBUG):
- logger.debug("Request content:\n%s",
- xmlutils.pretty_xml(xml_content))
+ if self._request_content_on_debug:
+ logger.debug("Request content (XML):\n%s",
+ xmlutils.pretty_xml(xml_content))
+ else:
+ logger.debug("Request content (XML): suppressed by config/option [logging] request_content_on_debug")
return xml_content
def _xml_response(self, xml_content: ET.Element) -> bytes:
if logger.isEnabledFor(logging.DEBUG):
if self._response_content_on_debug:
- logger.debug("Response content:\n%s",
+ logger.debug("Response content (XML):\n%s",
xmlutils.pretty_xml(xml_content))
else:
- logger.debug("Response content: suppressed by config/option [auth] response_content_on_debug")
+ logger.debug("Response content (XML): suppressed by config/option [logging] response_content_on_debug")
f = io.BytesIO()
ET.ElementTree(xml_content).write(f, encoding=self._encoding,
xml_declaration=True)
diff --git a/radicale/app/put.py b/radicale/app/put.py
index e30c4e07..15a7e00d 100644
--- a/radicale/app/put.py
+++ b/radicale/app/put.py
@@ -150,7 +150,7 @@ class ApplicationPartPut(ApplicationBase):
if self._log_bad_put_request_content:
logger.warning("Bad PUT request content of %r:\n%s", path, content)
else:
- logger.debug("Bad PUT request content: suppressed by config/option [auth] bad_put_request_content")
+ logger.debug("Bad PUT request content: suppressed by config/option [logging] bad_put_request_content")
return httputils.BAD_REQUEST
(prepared_items, prepared_tag, prepared_write_whole_collection,
prepared_props, prepared_exc_info) = prepare(
diff --git a/radicale/config.py b/radicale/config.py
index 34e7f4e5..8c9965fe 100644
--- a/radicale/config.py
+++ b/radicale/config.py
@@ -180,7 +180,7 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
"help": "htpasswd filename",
"type": filepath}),
("htpasswd_encryption", {
- "value": "md5",
+ "value": "autodetect",
"help": "htpasswd encryption method",
"type": str}),
("realm", {
@@ -316,6 +316,10 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
"value": "False",
"help": "log response content on level=debug",
"type": bool}),
+ ("rights_rule_doesnt_match_on_debug", {
+ "value": "False",
+ "help": "log rights rules which doesn't match on level=debug",
+ "type": bool}),
("mask_passwords", {
"value": "True",
"help": "mask passwords in logs",
diff --git a/radicale/hook/__init__.py b/radicale/hook/__init__.py
index e31befc1..1f39c9e1 100644
--- a/radicale/hook/__init__.py
+++ b/radicale/hook/__init__.py
@@ -14,8 +14,8 @@ def load(configuration):
return utils.load_plugin(
INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration)
except Exception as e:
- logger.warn(e)
- logger.warn("Hook \"%s\" failed to load, falling back to \"none\"." % configuration.get("hook", "type"))
+ logger.warning(e)
+ logger.warning("Hook \"%s\" failed to load, falling back to \"none\"." % configuration.get("hook", "type"))
configuration = configuration.copy()
configuration.update({"hook": {"type": "none"}}, "hook", privileged=True)
return utils.load_plugin(
diff --git a/radicale/httputils.py b/radicale/httputils.py
index a9565293..04898b40 100644
--- a/radicale/httputils.py
+++ b/radicale/httputils.py
@@ -146,7 +146,7 @@ def read_request_body(configuration: "config.Configuration",
if configuration.get("logging", "request_content_on_debug"):
logger.debug("Request content:\n%s", content)
else:
- logger.debug("Request content: suppressed by config/option [auth] request_content_on_debug")
+ logger.debug("Request content: suppressed by config/option [logging] request_content_on_debug")
return content
diff --git a/radicale/rights/from_file.py b/radicale/rights/from_file.py
index d8e287af..810c28e9 100644
--- a/radicale/rights/from_file.py
+++ b/radicale/rights/from_file.py
@@ -48,6 +48,7 @@ class Rights(rights.BaseRights):
def __init__(self, configuration: config.Configuration) -> None:
super().__init__(configuration)
self._filename = configuration.get("rights", "file")
+ self._log_rights_rule_doesnt_match_on_debug = configuration.get("logging", "rights_rule_doesnt_match_on_debug")
def authorization(self, user: str, path: str) -> str:
user = user or ""
@@ -61,6 +62,8 @@ class Rights(rights.BaseRights):
except Exception as e:
raise RuntimeError("Failed to load rights file %r: %s" %
(self._filename, e)) from e
+ if not self._log_rights_rule_doesnt_match_on_debug:
+ logger.debug("logging of rules which doesn't match suppressed by config/option [logging] rights_rule_doesnt_match_on_debug")
for section in rights_config.sections():
group_match = False
try:
@@ -96,5 +99,9 @@ class Rights(rights.BaseRights):
logger.debug("Rule %r:%r doesn't match %r:%r from section %r",
user, sane_path, user_pattern, collection_pattern,
section)
+ if self._log_rights_rule_doesnt_match_on_debug:
+ logger.debug("Rule %r:%r doesn't match %r:%r from section %r",
+ user, sane_path, user_pattern, collection_pattern,
+ section)
logger.info("Rights: %r:%r doesn't match any section", user, sane_path)
return ""