Skip to content

Conversation

@BKPepe
Copy link
Member

@BKPepe BKPepe commented Dec 29, 2025

This package is being added only because the radicale package was removed in the past (#28020), and since the same issue also applies to the radicale2 package, it will likely be removed soon as well.

I only kept the files that were part of radicale2 and slightly adjusted them for radicale3. Unfortunately, the package currently has no maintainer, and I will not be taking that role either — I already maintain too many packages.

The package has not been tested or runtime-tested yet.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds the radicale3 package (version 3.5.10) as a replacement for the deprecated radicale2 package, along with a python-pika dependency package. The implementation is adapted from radicale2 files with adjustments for version 3.

Key changes:

  • New radicale3 package with CalDAV/CardDAV server functionality
  • Python-pika library added as a dependency
  • Init script and configuration files adapted from radicale2

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 12 comments.

File Description
net/radicale3/Makefile Package definition for radicale3 with metadata, dependencies, and installation rules
net/radicale3/files/radicale3.init Init script for managing the radicale3 service with procd integration
net/radicale3/files/radicale3.config Sample UCI configuration file for radicale3 service
lang/python/python-pika/Makefile Package definition for the python-pika library (AMQP client)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 69 to 125
conf_line "$tmpfile" hosts "$hostlist"
conf_getline "$cfg" $tmpfile max_connections
conf_getline "$cfg" $tmpfile max_content_length
conf_getline "$cfg" $tmpfile timeout

conf_getline "$cfg" $tmpfile ssl 0 1
if [ "$value" -eq 1 ]; then
conf_getline "$cfg" $tmpfile certificate
conf_getline "$cfg" $tmpfile key
conf_getline "$cfg" $tmpfile certificate_authority
conf_getline "$cfg" $tmpfile protocol
conf_getline "$cfg" $tmpfile ciphers
fi

conf_getline "$cfg" $tmpfile dns_lookup 1 1
conf_getline "$cfg" $tmpfile realm
;;
encoding)
conf_getline "$cfg" $tmpfile request
conf_getline "$cfg" $tmpfile stock
;;
auth)
conf_getline "$cfg" $tmpfile "type" htpasswd
if [ "$value" = "htpasswd" ]; then
conf_getline "$cfg" $tmpfile htpasswd_filename $CFGDIR/users
conf_getline "$cfg" "$tmpfile" htpasswd_encryption plain
fi

conf_getline "$cfg" "$tmpfile" delay
;;
rights)
conf_getline "$cfg" "$tmpfile" "type"
if [ "$value" = "from_file" ]; then
conf_getline "$cfg" "$tmpfile" "file"
fi
;;
storage)
conf_getline "$cfg" $tmpfile filesystem_folder "$DATADIR"
DATADIR="$value"
conf_getline "$cfg" $tmpfile filesystem_locking 1 1
conf_getline "$cfg" $tmpfile max_sync_token_age
conf_getline "$cfg" $tmpfile filesystem_close_lock_file 0 1
conf_getline "$cfg" $tmpfile hook
;;
web)
conf_getline "$cfg" $tmpfile "type"
;;
logging)
conf_getline "$cfg" "$tmpfile" config
conf_getline "$cfg" "$tmpfile" debug 0 1
conf_getline "$cfg" "$tmpfile" full_environment 0 1
conf_getline "$cfg" "$tmpfile" mask_passwords 1 1
;;
headers)
config_get "$cfg" "$tmpfile" cors
if [ -n "$cors" ]; then
echo "Access-Control-Allow-Origin = $cors" >>$tmpfile
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable $tmpfile is used here but the function parameter is $cfgfile. This should be $cfgfile to match the function's parameter at line 59, otherwise the configuration won't be written to the correct file.

Suggested change
conf_line "$tmpfile" hosts "$hostlist"
conf_getline "$cfg" $tmpfile max_connections
conf_getline "$cfg" $tmpfile max_content_length
conf_getline "$cfg" $tmpfile timeout
conf_getline "$cfg" $tmpfile ssl 0 1
if [ "$value" -eq 1 ]; then
conf_getline "$cfg" $tmpfile certificate
conf_getline "$cfg" $tmpfile key
conf_getline "$cfg" $tmpfile certificate_authority
conf_getline "$cfg" $tmpfile protocol
conf_getline "$cfg" $tmpfile ciphers
fi
conf_getline "$cfg" $tmpfile dns_lookup 1 1
conf_getline "$cfg" $tmpfile realm
;;
encoding)
conf_getline "$cfg" $tmpfile request
conf_getline "$cfg" $tmpfile stock
;;
auth)
conf_getline "$cfg" $tmpfile "type" htpasswd
if [ "$value" = "htpasswd" ]; then
conf_getline "$cfg" $tmpfile htpasswd_filename $CFGDIR/users
conf_getline "$cfg" "$tmpfile" htpasswd_encryption plain
fi
conf_getline "$cfg" "$tmpfile" delay
;;
rights)
conf_getline "$cfg" "$tmpfile" "type"
if [ "$value" = "from_file" ]; then
conf_getline "$cfg" "$tmpfile" "file"
fi
;;
storage)
conf_getline "$cfg" $tmpfile filesystem_folder "$DATADIR"
DATADIR="$value"
conf_getline "$cfg" $tmpfile filesystem_locking 1 1
conf_getline "$cfg" $tmpfile max_sync_token_age
conf_getline "$cfg" $tmpfile filesystem_close_lock_file 0 1
conf_getline "$cfg" $tmpfile hook
;;
web)
conf_getline "$cfg" $tmpfile "type"
;;
logging)
conf_getline "$cfg" "$tmpfile" config
conf_getline "$cfg" "$tmpfile" debug 0 1
conf_getline "$cfg" "$tmpfile" full_environment 0 1
conf_getline "$cfg" "$tmpfile" mask_passwords 1 1
;;
headers)
config_get "$cfg" "$tmpfile" cors
if [ -n "$cors" ]; then
echo "Access-Control-Allow-Origin = $cors" >>$tmpfile
conf_line "$cfgfile" hosts "$hostlist"
conf_getline "$cfg" $cfgfile max_connections
conf_getline "$cfg" $cfgfile max_content_length
conf_getline "$cfg" $cfgfile timeout
conf_getline "$cfg" $cfgfile ssl 0 1
if [ "$value" -eq 1 ]; then
conf_getline "$cfg" $cfgfile certificate
conf_getline "$cfg" $cfgfile key
conf_getline "$cfg" $cfgfile certificate_authority
conf_getline "$cfg" $cfgfile protocol
conf_getline "$cfg" $cfgfile ciphers
fi
conf_getline "$cfg" $cfgfile dns_lookup 1 1
conf_getline "$cfg" $cfgfile realm
;;
encoding)
conf_getline "$cfg" $cfgfile request
conf_getline "$cfg" $cfgfile stock
;;
auth)
conf_getline "$cfg" $cfgfile "type" htpasswd
if [ "$value" = "htpasswd" ]; then
conf_getline "$cfg" $cfgfile htpasswd_filename $CFGDIR/users
conf_getline "$cfg" "$cfgfile" htpasswd_encryption plain
fi
conf_getline "$cfg" "$cfgfile" delay
;;
rights)
conf_getline "$cfg" "$cfgfile" "type"
if [ "$value" = "from_file" ]; then
conf_getline "$cfg" "$cfgfile" "file"
fi
;;
storage)
conf_getline "$cfg" $cfgfile filesystem_folder "$DATADIR"
DATADIR="$value"
conf_getline "$cfg" $cfgfile filesystem_locking 1 1
conf_getline "$cfg" $cfgfile max_sync_token_age
conf_getline "$cfg" $cfgfile filesystem_close_lock_file 0 1
conf_getline "$cfg" $cfgfile hook
;;
web)
conf_getline "$cfg" $cfgfile "type"
;;
logging)
conf_getline "$cfg" "$cfgfile" config
conf_getline "$cfg" "$cfgfile" debug 0 1
conf_getline "$cfg" "$cfgfile" full_environment 0 1
conf_getline "$cfg" "$cfgfile" mask_passwords 1 1
;;
headers)
config_get "$cfg" "$cfgfile" cors
if [ -n "$cors" ]; then
echo "Access-Control-Allow-Origin = $cors" >>$cfgfile

Copilot uses AI. Check for mistakes.
Comment on lines 69 to 125
conf_line "$tmpfile" hosts "$hostlist"
conf_getline "$cfg" $tmpfile max_connections
conf_getline "$cfg" $tmpfile max_content_length
conf_getline "$cfg" $tmpfile timeout

conf_getline "$cfg" $tmpfile ssl 0 1
if [ "$value" -eq 1 ]; then
conf_getline "$cfg" $tmpfile certificate
conf_getline "$cfg" $tmpfile key
conf_getline "$cfg" $tmpfile certificate_authority
conf_getline "$cfg" $tmpfile protocol
conf_getline "$cfg" $tmpfile ciphers
fi

conf_getline "$cfg" $tmpfile dns_lookup 1 1
conf_getline "$cfg" $tmpfile realm
;;
encoding)
conf_getline "$cfg" $tmpfile request
conf_getline "$cfg" $tmpfile stock
;;
auth)
conf_getline "$cfg" $tmpfile "type" htpasswd
if [ "$value" = "htpasswd" ]; then
conf_getline "$cfg" $tmpfile htpasswd_filename $CFGDIR/users
conf_getline "$cfg" "$tmpfile" htpasswd_encryption plain
fi

conf_getline "$cfg" "$tmpfile" delay
;;
rights)
conf_getline "$cfg" "$tmpfile" "type"
if [ "$value" = "from_file" ]; then
conf_getline "$cfg" "$tmpfile" "file"
fi
;;
storage)
conf_getline "$cfg" $tmpfile filesystem_folder "$DATADIR"
DATADIR="$value"
conf_getline "$cfg" $tmpfile filesystem_locking 1 1
conf_getline "$cfg" $tmpfile max_sync_token_age
conf_getline "$cfg" $tmpfile filesystem_close_lock_file 0 1
conf_getline "$cfg" $tmpfile hook
;;
web)
conf_getline "$cfg" $tmpfile "type"
;;
logging)
conf_getline "$cfg" "$tmpfile" config
conf_getline "$cfg" "$tmpfile" debug 0 1
conf_getline "$cfg" "$tmpfile" full_environment 0 1
conf_getline "$cfg" "$tmpfile" mask_passwords 1 1
;;
headers)
config_get "$cfg" "$tmpfile" cors
if [ -n "$cors" ]; then
echo "Access-Control-Allow-Origin = $cors" >>$tmpfile
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All references to $tmpfile in this function should be $cfgfile to match the function's parameter. The variable $tmpfile is not defined in this function's scope and these lines won't write to the correct configuration file.

Suggested change
conf_line "$tmpfile" hosts "$hostlist"
conf_getline "$cfg" $tmpfile max_connections
conf_getline "$cfg" $tmpfile max_content_length
conf_getline "$cfg" $tmpfile timeout
conf_getline "$cfg" $tmpfile ssl 0 1
if [ "$value" -eq 1 ]; then
conf_getline "$cfg" $tmpfile certificate
conf_getline "$cfg" $tmpfile key
conf_getline "$cfg" $tmpfile certificate_authority
conf_getline "$cfg" $tmpfile protocol
conf_getline "$cfg" $tmpfile ciphers
fi
conf_getline "$cfg" $tmpfile dns_lookup 1 1
conf_getline "$cfg" $tmpfile realm
;;
encoding)
conf_getline "$cfg" $tmpfile request
conf_getline "$cfg" $tmpfile stock
;;
auth)
conf_getline "$cfg" $tmpfile "type" htpasswd
if [ "$value" = "htpasswd" ]; then
conf_getline "$cfg" $tmpfile htpasswd_filename $CFGDIR/users
conf_getline "$cfg" "$tmpfile" htpasswd_encryption plain
fi
conf_getline "$cfg" "$tmpfile" delay
;;
rights)
conf_getline "$cfg" "$tmpfile" "type"
if [ "$value" = "from_file" ]; then
conf_getline "$cfg" "$tmpfile" "file"
fi
;;
storage)
conf_getline "$cfg" $tmpfile filesystem_folder "$DATADIR"
DATADIR="$value"
conf_getline "$cfg" $tmpfile filesystem_locking 1 1
conf_getline "$cfg" $tmpfile max_sync_token_age
conf_getline "$cfg" $tmpfile filesystem_close_lock_file 0 1
conf_getline "$cfg" $tmpfile hook
;;
web)
conf_getline "$cfg" $tmpfile "type"
;;
logging)
conf_getline "$cfg" "$tmpfile" config
conf_getline "$cfg" "$tmpfile" debug 0 1
conf_getline "$cfg" "$tmpfile" full_environment 0 1
conf_getline "$cfg" "$tmpfile" mask_passwords 1 1
;;
headers)
config_get "$cfg" "$tmpfile" cors
if [ -n "$cors" ]; then
echo "Access-Control-Allow-Origin = $cors" >>$tmpfile
conf_line "$cfgfile" hosts "$hostlist"
conf_getline "$cfg" $cfgfile max_connections
conf_getline "$cfg" $cfgfile max_content_length
conf_getline "$cfg" $cfgfile timeout
conf_getline "$cfg" $cfgfile ssl 0 1
if [ "$value" -eq 1 ]; then
conf_getline "$cfg" $cfgfile certificate
conf_getline "$cfg" $cfgfile key
conf_getline "$cfg" $cfgfile certificate_authority
conf_getline "$cfg" $cfgfile protocol
conf_getline "$cfg" $cfgfile ciphers
fi
conf_getline "$cfg" $cfgfile dns_lookup 1 1
conf_getline "$cfg" $cfgfile realm
;;
encoding)
conf_getline "$cfg" $cfgfile request
conf_getline "$cfg" $cfgfile stock
;;
auth)
conf_getline "$cfg" $cfgfile "type" htpasswd
if [ "$value" = "htpasswd" ]; then
conf_getline "$cfg" $cfgfile htpasswd_filename $CFGDIR/users
conf_getline "$cfg" "$cfgfile" htpasswd_encryption plain
fi
conf_getline "$cfg" "$cfgfile" delay
;;
rights)
conf_getline "$cfg" "$cfgfile" "type"
if [ "$value" = "from_file" ]; then
conf_getline "$cfg" "$cfgfile" "file"
fi
;;
storage)
conf_getline "$cfg" $cfgfile filesystem_folder "$DATADIR"
DATADIR="$value"
conf_getline "$cfg" $cfgfile filesystem_locking 1 1
conf_getline "$cfg" $cfgfile max_sync_token_age
conf_getline "$cfg" $cfgfile filesystem_close_lock_file 0 1
conf_getline "$cfg" $cfgfile hook
;;
web)
conf_getline "$cfg" $cfgfile "type"
;;
logging)
conf_getline "$cfg" "$cfgfile" config
conf_getline "$cfg" "$cfgfile" debug 0 1
conf_getline "$cfg" "$cfgfile" full_environment 0 1
conf_getline "$cfg" "$cfgfile" mask_passwords 1 1
;;
headers)
config_get "$cfg" "$cfgfile" cors
if [ -n "$cors" ]; then
echo "Access-Control-Allow-Origin = $cors" >>$cfgfile

Copilot uses AI. Check for mistakes.
Comment on lines 253 to 268
if [ ! -r /etc/radicale3/config ]; then
build_config
set_permission
fi

interface_triggers "check_interface_up" || return

procd_open_instance "radicale3"
procd_set_param respawn
procd_set_param stderr 1
procd_set_param stdout 1
if [ ! -r /etc/radicale3/config ]; then
procd_set_param command /usr/bin/radicale3 --config="$SYSCFG"
else
procd_set_param command /usr/bin/radicale3 --config="/etc/radicale3/config"
fi
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks if the config doesn't exist to build it, but then the subsequent code at line 264 checks the same condition again to decide which config file to use. This logic is problematic because after the config is built at line 254, the condition at line 264 will still be false (since the file now exists), but it will use the newly built config path. The condition at line 264 should be inverted or the logic restructured.

Copilot uses AI. Check for mistakes.
}

start_service() {
local haveinterface
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable haveinterface is declared but never used in the function. This should either be removed or the code should be updated to use it properly.

Suggested change
local haveinterface

Copilot uses AI. Check for mistakes.
local tmpfile2=$(mktemp)

# temporary config file
# radicale3 need read access
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a grammatical error in the comment. It should be "radicale3 needs read access" (plural form of 'need').

Suggested change
# radicale3 need read access
# radicale3 needs read access

Copilot uses AI. Check for mistakes.
conf_getline "$cfg" $tmpfile "type" htpasswd
if [ "$value" = "htpasswd" ]; then
conf_getline "$cfg" $tmpfile htpasswd_filename $CFGDIR/users
conf_getline "$cfg" "$tmpfile" htpasswd_encryption plain
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auth configuration defaults htpasswd_encryption to plain, which causes Radicale to store user passwords in clear text in the htpasswd file. An attacker who gains read access to this file (or to backups/logs containing it) can directly recover reusable user credentials instead of having to crack password hashes. Configure htpasswd_encryption to use a strong password hashing algorithm and avoid storing cleartext passwords in configuration files or on disk.

Suggested change
conf_getline "$cfg" "$tmpfile" htpasswd_encryption plain
conf_getline "$cfg" "$tmpfile" htpasswd_encryption bcrypt

Copilot uses AI. Check for mistakes.
[$section]
type = htpasswd
htpasswd_filename = $CFGDIR/users
htpasswd_encryption = plain
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the default section creation, htpasswd_encryption is again set to plain, reinforcing the use of cleartext passwords in the Radicale htpasswd file. This means any compromise of that file (including backups or misconfigured permissions) yields users’ actual passwords rather than slow-to-crack hashes. Prefer a strong hashing scheme here (e.g., Radicale’s supported salted hash algorithms) instead of plain to mitigate credential disclosure.

Suggested change
htpasswd_encryption = plain
htpasswd_encryption = bcrypt

Copilot uses AI. Check for mistakes.
Comment on lines 170 to 175
local name password

config_get name "$cfg" name
config_get password "$cfg" password

[ -n "$name" ] && echo "$name:$password" >>$tmpfile
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The add_user helper writes name:password directly into the users file, which in combination with htpasswd_encryption = plain results in passwords being stored in clear text. If an attacker obtains read access to this file, they immediately recover valid user credentials that are likely reused elsewhere. Use a configuration and user-management flow that stores only strong password hashes (never raw passwords) in the Radicale users file.

Suggested change
local name password
config_get name "$cfg" name
config_get password "$cfg" password
[ -n "$name" ] && echo "$name:$password" >>$tmpfile
local name password hash
config_get name "$cfg" name
config_get password "$cfg" password
# Hash the password before writing it to the users file to avoid storing clear-text credentials.
# Use a strong salted hash (SHA-512 based crypt via openssl). If hashing fails, skip the user.
if [ -n "$name" ] && [ -n "$password" ]; then
if command -v openssl >/dev/null 2>&1; then
hash="$(printf '%s' "$password" | openssl passwd -6 -stdin 2>/dev/null)"
else
hash=""
fi
if [ -n "$hash" ]; then
echo "$name:$hash" >>"$tmpfile"
fi
fi

Copilot uses AI. Check for mistakes.
@BKPepe
Copy link
Member Author

BKPepe commented Dec 29, 2025

Seriously? I haven't done anything to it. 🤦 😠
Those are existing files - https://github.com/openwrt/packages/blob/84f4bb9515e4d12ebcc83dc9ba75d80cdfa21e18/net/radicale2/files/radicale2.init, ach...

@BKPepe BKPepe force-pushed the radicale3 branch 4 times, most recently from 8231656 to 2e28c39 Compare December 30, 2025 09:30
@danielfdickinson
Copy link
Contributor

@BKPepe I'll take a look at this. I think this is my mess from many years ago, so I should at least fix it (not that I plan on become its maintainer, though).

@danielfdickinson
Copy link
Contributor

  1. The basic launch of radicale3 with the defaults (no actual configuration), works.
  2. Adding a server address and a user:password pair, works.

In both cases, works means I can access :5232 and see the web interface.
In addition with a user configured I am able to login to the collection creation and editing pages.

I also have the beginnings of a translation of the luci-app-radicale2 as luci-app-radicale3, which is javascript based instead of lua based.

@BKPepe
Copy link
Member Author

BKPepe commented Jan 4, 2026

Great! 😊

BKPepe added 2 commits January 4, 2026 11:03
Pika is a pure-Python implementation of the AMQP 0-9-1 protocol that
tries to stay fairly independent of the underlying network support
library.

Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
Radicale is a small but powerful CalDAV (calendars, to-do lists) and
CardDAV (contacts) server.
This package provides the latest 3.x series, which succeeds radicale2.

This is replacament for recently dropped radicale2 and radicale1.

Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
@danielfdickinson
Copy link
Contributor

danielfdickinson commented Jan 5, 2026

FYI passlib and bcrypt no longer play together nicely: https://forum.ansible.com/t/python-passlib-and-bcrypt-issue-and-patch/44640

This issue is that passlib is (apparently) unmaintained. pyca/bcrypt#684 (comment) https://stackoverflow.com/questions/79469355/error-attributeerror-problem-with-passlibbcrypt-module

So at least for the LuCI app my plan is to simply not make bcrypt available as an option for luci-app-radicale3.

@danielfdickinson
Copy link
Contributor

FYI Radicale upstream has found a possible replacement for passlib, which I will be testing at some point (hopefully this week). So bcrypt for radicale3 is not dead (and upstream is responsive, which is good).

@BKPepe BKPepe marked this pull request as ready for review January 6, 2026 02:06
@BKPepe
Copy link
Member Author

BKPepe commented Jan 6, 2026

This issue is that passlib is (apparently) unmaintained

We should remove the python-passlib from our repository as you found out and later on, we can replace it by libpass as said here Kozea/Radicale#1952 (comment)

@BKPepe BKPepe requested a review from Copilot January 6, 2026 02:10
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@danielfdickinson
Copy link
Contributor

@BKPepe Since removing python-passlib would break radicale2 and preventing adding radicale3, are you thinking of removing passlib and radicale2 and having no radicale until radicale3 using libpass can be packaged, or keeping passlib around until a transition can be made (which shouldn't be that long either way).

@danielfdickinson
Copy link
Contributor

I have a test branch at https://gitlab.com/dfd-web/firmware/openwrt-packages/-/tree/radicale3-libpass?ref_type=heads which adds python-libpass and uses the dev version of radicale v3 from Kozea/Radicale#1953 . It works well.

@danielfdickinson
Copy link
Contributor

My work in progress LuCi app for radicale3 is here: https://gitlab.com/dfd-web/firmware/openwrt-luci/-/tree/add-app-radicale3?ref_type=heads - there is quite a bit to do yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants