radicale: [NEW] Python-based CalDAV/CardDAV Server

Inspired by OpenWrt Ticket System Ticket 9119
Python3 package currently marked as @BROKEN because no time for testing.

Signed-off-by: Christian Schoenebeck <christian.schoenebeck@gmail.com>
This commit is contained in:
Christian Schoenebeck 2015-04-20 21:14:03 +02:00
parent 3828a7096d
commit f98dbf5aab
9 changed files with 728 additions and 0 deletions

138
net/radicale/Makefile Normal file
View File

@ -0,0 +1,138 @@
#
# Copyright (C) 2008-2015 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=radicale
PKG_VERSION:=0.10
PKG_RELEASE:=1
PKG_MAINTAINER:=Christian Schoenebeck <chris5560@web.de>
PKG_LICENSE:=GPL-3.0
PKG_LICENSE_FILES:=COPYING
PKG_SOURCE:=Radicale-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=http://pypi.python.org/packages/source/R/Radicale/
PKG_MD5SUM:=32655d8893962956ead0ad690cca6044
# needed for "r"adicale <-> "R"adicale
PKG_BUILD_DIR:=$(BUILD_DIR)/Radicale-$(PKG_VERSION)
include $(INCLUDE_DIR)/package.mk
# no default dependencies
PKG_DEFAULT_DEPENDS=
define Package/$(PKG_NAME)/Default
SECTION:=net
CATEGORY:=Network
SUBMENU:=Web Servers/Proxies
URL:=http://radicale.org/
PKGARCH:=all
USERID:=radicale=5232:radicale=5232
endef
define Package/$(PKG_NAME)-py2
$(call Package/$(PKG_NAME)/Default)
PYTHON_VERSION:=2.7
TITLE:=Radicale CalDAV/CardDAV server (Python 2)
VARIANT:=python2
DEPENDS:=+python-logging +python-openssl +python-xml +python-codecs
endef
define Package/$(PKG_NAME)-py3
$(call Package/$(PKG_NAME)/Default)
PYTHON_VERSION:=3.4
TITLE:=Radicale CalDAV/CardDAV server (Python 3)
VARIANT:=python3
DEPENDS:=+python3-logging +python3-openssl +python3-xml +python3-codecs +python3-email @BROKEN
endef
# shown in LuCI package description
define Package/$(PKG_NAME)-py2/description
Radicale CalDAV/CardDAV server (Python 2) - Homepage: http://radicale.org/
endef
define Package/$(PKG_NAME)-py3/description
Radicale CalDAV/CardDAV server (Python 3) - Homepage: http://radicale.org/
endef
# shown in make menuconfig <Help>
define Package/$(PKG_NAME)-py2/config
help
The Radicale Project is a CalDAV (calendar) and CardDAV (contact) server.
It aims to be a light solution, easy to use, easy to install, easy to configure.
As a consequence, it requires few software dependances and is pre-configured to work out-of-the-box.
!!! Will install and use Python $(PYTHON_VERSION) !!!
.
Version : $(PKG_VERSION)
Homepage: http://radicale.org/
.
$(PKG_MAINTAINER)
endef
Package/$(PKG_NAME)-py3/config = $(Package/$(PKG_NAME)-py2/config)
define Package/$(PKG_NAME)-py2/conffiles
/etc/config/radicale
/etc/radicale/users
/etc/radicale/rights
endef
Package/$(PKG_NAME)-py3/conffiles = $(Package/$(PKG_NAME)-py2/conffiles)
define Build/Configure
endef
define Build/Compile
endef
define Package/$(PKG_NAME)-py2/preinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] && exit 0 # if run within buildroot exit
# stop service if PKG_UPGRADE
[ "$${PKG_UPGRADE}" = "1" ] && /etc/init.d/$(PKG_NAME) stop >/dev/null 2>&1
exit 0 # supress errors from stop command
endef
define Package/$(PKG_NAME)-py3/preinst
$(call Package/$(PKG_NAME)-py2/preinst)
endef
define Package/$(PKG_NAME)-py2/install
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/radicale.init $(1)/etc/init.d/radicale
$(INSTALL_DIR) $(1)/etc/hotplug.d/iface
$(INSTALL_BIN) ./files/radicale.hotplug $(1)/etc/hotplug.d/iface/80-radicale
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/radicale.config $(1)/etc/config/radicale
$(INSTALL_DIR) $(1)/etc/radicale/ssl
$(INSTALL_DATA) ./files/config.template $(1)/etc/radicale/
$(INSTALL_DATA) ./files/logging.template $(1)/etc/radicale/
$(INSTALL_DATA) ./files/radicale.users $(1)/etc/radicale/users
$(INSTALL_DATA) ./files/radicale.rights $(1)/etc/radicale/rights
$(INSTALL_DIR) $(1)/usr/lib/python$(PYTHON_VERSION)/site-packages/radicale
$(CP) \
$(PKG_BUILD_DIR)/radicale/* \
$(1)/usr/lib/python$(PYTHON_VERSION)/site-packages/radicale
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/bin/radicale $(1)/usr/bin/
endef
define Package/$(PKG_NAME)-py3/install
$(call Package/$(PKG_NAME)-py2/install, $(1))
endef
define Package/$(PKG_NAME)-py2/postinst
#!/bin/sh
# patch /usr/bin/radicale force run using python2
/bin/sed -i 's/python/python2/' $${IPKG_INSTROOT}/usr/bin/radicale
endef
define Package/$(PKG_NAME)-py3/postinst
#!/bin/sh
# patch /usr/bin/radicale force run using python3
/bin/sed -i 's/python/python3/' $${IPKG_INSTROOT}/usr/bin/radicale
endef
$(eval $(call BuildPackage,$(PKG_NAME)-py2))
$(eval $(call BuildPackage,$(PKG_NAME)-py3))

View File

@ -0,0 +1,30 @@
# -*- mode: conf -*-
# vim:ft=cfg
### AUTO-GENERATED CONFIGURATION
### USED BY RADICALE
### DO NOT EDIT
### SEE /etc/config/radicale INSTEAD
[server]
# daemon # handled by /etc/init.d/radicale
# pid # handled by /etc/init.d/radicale
[encoding]
[well-known]
[auth]
# htpasswd_filename # hard-coded /etc/radicale/users
[git]
[rights]
# file # hard-coded /etc/radicale/rights
[storage]
[logging]
# config # hard-coded /var/etc/radicale/logging
[headers]

View File

@ -0,0 +1,47 @@
# -*- mode: conf -*-
# vim:ft=cfg
### AUTO-GENERATED CONFIGURATION
### USED BY RADICALE
### DO NOT EDIT
### SEE /etc/config/radicale INSTEAD
[loggers]
keys = root
[handlers]
keys = console,file,syslog
[formatters]
keys = simple,full,syslog
[logger_root]
level = DEBUG
handlers = console,file,syslog
[handler_console]
class = StreamHandler
args = (sys.stdout,)
formatter = simple
# level = WARNING # set via /etc/config/radicale
[handler_file]
class = handlers.RotatingFileHandler
formatter = full
# level = INFO # set via /etc/config/radicale
# args = ('[filename]','a',[maxbytes],[backupcount]) # set via /etc/config/radicale
[handler_syslog]
class = handlers.SysLogHandler
args = ('/dev/log', handlers.SysLogHandler.LOG_DAEMON)
formatter = syslog
# level = WARNING # set via /etc/config/radicale
[formatter_simple]
format = %(message)s
[formatter_full]
format = %(asctime)s - %(levelname)s: %(message)s
[formatter_syslog]
format = radicale [%(process)d]: %(message)s

View File

@ -0,0 +1,192 @@
#
# You find additional information on Radicale Homepage
# http://radicale.org
#
# OpenWrt's wiki needs to be setup/updated ;-)
#
# if setting additional options please remember that UCI does not support
# section names and option names with "-" (Dash) inside their name
# to use them anyway replace "-" with "_" (Underscore)
# Each Radicale's config [section] is setup as UCI config setting 'section'
#
####################################################
# Server options
#
config setting 'server'
# hostname:port
# IPv4 syntax: address:port
# IPv6 syntax: [address]:port
# ATTENTION:
# only use ports > 1024 (non-privileged Ports)
# because this implementation is running as non-root user
# Default: 0.0.0.0:5232
# list hosts '0.0.0.0:5232'
# list hosts 'localhost:5232'
# SSL flag, enable HTTPS protocol
# Default: 0 (disabled)
# option ssl '1'
# SSL Protocol used. See python's ssl module for available values
# Default: PROTOCOL_SSLv23
# option protocol 'PROTOCOL_SSLv23'
# Ciphers available. See python's ssl module for available ciphers
# option ciphers ''
# SSL certificate path and file
# option certificate '/etc/radicale/ssl/server.crt'
# SSL private key path and file
# option key '/etc/radicale/ssl/server.key'
# Reverse DNS to resolve client address in logs
# Default: 0 (disabled)
# option dns_lookup '1'
# Message displayed in the client when a password is needed
# option realm 'Radicale - Password Required'
####################################################
# Encoding options
#
config setting 'encoding'
# Encoding for responding requests
# option request 'utf-8'
# Encoding for storing local collections
# option stock 'utf-8'
####################################################
# Authentication options
#
config setting 'auth'
# Authentication method
# Value: None | htpasswd | IMAP | LDAP | PAM | courier | http | remote_user | custom
# Default: None
# if setting 'htpasswd' the file /etc/radicale/users is used (hardcoded)
# option type 'htpasswd'
# Htpasswd encryption method
# Value: plain | sha1 | ssha | crypt
# option htpasswd_encryption 'crypt'
# for other authenication methods consult Radicale documentation
# and set options here
####################################################
# Git default options
#
config setting 'git'
# Git default options
# option committer 'Radicale <radicale@example.com>'
####################################################
# Rights backend
#
config setting 'rights'
# Value: None | authenticated | owner_only | owner_write | from_file | custom
# Default: None
# if setting 'from_file' the file /etc/radicale/rights is used (hardcoded)
# option type 'from_file'
# Custom rights handler
# option custom_handler ''
####################################################
# Storage backend
# -------
# WARNING: ONLY "filesystem" IS DOCUMENTED AND TESTED,
# OTHER BACKENDS ARE NOT READY FOR PRODUCTION.
# -------
#
config setting 'storage'
# Value: filesystem | multifilesystem | database | custom
option type 'filesystem'
option filesystem_folder '/srv/radicale'
####################################################
# Additional HTTP headers
#
config setting 'headers'
# enable all if using CardDavMATE-, CalDavZAP- or InfCloud- WEBclient
# list Access_Control_Allow_Origin '*'
# list Access_Control_Allow_Methods 'GET'
# list Access_Control_Allow_Methods 'POST'
# list Access_Control_Allow_Methods 'OPTIONS'
# list Access_Control_Allow_Methods 'PROPFIND'
# list Access_Control_Allow_Methods 'PROPPATCH'
# list Access_Control_Allow_Methods 'REPORT'
# list Access_Control_Allow_Methods 'PUT'
# list Access_Control_Allow_Methods 'MOVE'
# list Access_Control_Allow_Methods 'DELETE'
# list Access_Control_Allow_Methods 'LOCK'
# list Access_Control_Allow_Methods 'UNLOCK'
# list Access_Control_Allow_Headers 'User-Agent'
# list Access_Control_Allow_Headers 'Authorization'
# list Access_Control_Allow_Headers 'Content-type'
# list Access_Control_Allow_Headers 'Depth'
# list Access_Control_Allow_Headers 'If-match'
# list Access_Control_Allow_Headers 'If-None-Match'
# list Access_Control_Allow_Headers 'Lock-Token'
# list Access_Control_Allow_Headers 'Timeout'
# list Access_Control_Allow_Headers 'Destination'
# list Access_Control_Allow_Headers 'Overwrite'
# list Access_Control_Allow_Headers 'X-client'
# list Access_Control_Allow_Headers 'X-Requested-With'
# list Access_Control_Expose_Headers 'Etag'
####################################################
# Global logging options
#
config setting 'logging'
# Set the default logging level to debug for all outputs (ignore output level settings)
# Default: 0 (disabled)
# option debug '1'
# Log all environment variables (including those set in the shell) when starting
# Default: 0 (disabled)
# option full_environment '1'
####################################################
# Spezial logging options
# !!! not documented in Radicale documentation
# !!! special settings for this implementation
#
config logging 'logger'
# Level: DEBUG | INFO | WARNING | ERROR | CRITICAL
# To nearly disable logging set level to critical
# log level on console
# option console_level 'ERROR'
# Here we use Rotating Logfiles in this implementation
# !!! if maxbytes and/or backupcount is set to 0 !!!
# !!! file rotation is disabled and logfile grows endless !!!
# log level
# option file_level 'INFO'
# directory where log files are written
# option file_path '/var/log/radicale'
# max size of each logfile (see warning above)
# option file_maxbytes '8196'
# number of backup files to create (see warning above)
# option file_backupcount '1'
# log level for syslog logging
# option syslog_level 'WARNING'

View File

@ -0,0 +1,16 @@
#!/bin/sh
# only (re-)start on ifup
[ "$ACTION" = "ifup" ] || exit 0
_PID=$(ps | grep '[p]ython.*[r]adicale' 2>/dev/null | awk '{print \$1}')
kill -1 $_PID 2>/dev/null
if [ $? -eq 0 ]; then
# only restart if already running
logger -p user.info -t "radicale[$_PID]" \
"Restart request due to '$ACTION' of interface '$INTERFACE'"
/etc/init.d/radicale restart
else
# only start if enabled
/etc/init.d/radicale enabled && /etc/init.d/radicale start
fi

220
net/radicale/files/radicale.init Executable file
View File

@ -0,0 +1,220 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2006-2015 OpenWrt.org
START=80
STOP=10
CFGDIR=/var/etc/radicale
SYSCFG=$CFGDIR/config
LOGCFG=$CFGDIR/logging
DATADIR="/srv/radicale"
LOGDIR=""
PGREP="ps | grep '[p]ython.*[r]adicale' 2>/dev/null | awk '{print \$1}' "
# we could start with empty configuration file using defaults
[ -f /etc/config/radicale ] || touch /etc/config/radicale
_uci2radicale() {
local _SYSTMP="$SYSCFG.tmp"
local _LOGTMP="$LOGCFG.tmp"
local _LOPT # list option name
local _LVAL # list option value
local _STYPE # section type
local _SNAME # section name
local _console_level="ERROR" # logging console level
local _file_level="INFO" # logging file level
local _file_path="/var/log/radicale" # logging file path
local _file_maxbytes="8196" # logging file maxBytes
local _file_backupcount="1" # logging file backupCount
local _syslog_level="WARNING" # logging syslog level
# write list values to config
_write_list() {
_write_value "$_LOPT" "$_LVAL" # there might be spaces in _LVAL
_LOPT=""
_LVAL=""
}
_write_value() {
# $1 option
# $2 value
local __OPT=$1
local __VAL=$2
# section "server" ignore option "daemon" and "pid"
[ "$_SNAME" = "server" -a "$__OPT" = "daemon" ] && return 0
[ "$_SNAME" = "server" -a "$__OPT" = "pid" ] && return 0
# section "logging" ignore option "config" (logging config file)
[ "$_SNAME" = "logging" -a "$__OPT" = "config" ] && return 0
# section "auth" ignore option "htpasswd_filename" (htpasswd file)
[ "$_SNAME" = "auth" -a "$__OPT" = "htpasswd_filename" ] && return 0
# section "rights" ignore option "file" (reg-based rights file)
[ "$_SNAME" = "rights" -a "$__OPT" = "file" ] && return 0
# section "headers" replace "_" with "-" in option (UCI problem)
[ "$_SNAME" = "headers" ] && __OPT=$(echo "$__OPT" | sed -e "s#_#-#g")
# save data driectory
[ "$_SNAME" = "storage" -a "$__OPT" = "filesystem_folder" ] && DATADIR="$__VAL"
# special handling for well-known, value needs single quotes
[ "$_SNAME" = "well-known" -a "${__VAL#*\%\(}" != "$__VAL" ] && __VAL="'$__VAL'"
# handling of log settings
if [ "$_STYPE" = "logging" -a "$_SNAME" = "logger" ]; then
eval "_$__OPT='$__VAL'" # set to environment for later use
else
# handle bool
[ "$__VAL" = "0" ] && __VAL="False"
[ "$__VAL" = "1" ] && __VAL="True"
# append data to the corresponding section
sed -i "/\[$_SNAME\]/a $__OPT = $__VAL" $_SYSTMP
fi
}
# redefined callback for sections when calling config_load
config_cb() {
# $1 "Type"
# $2 "Name"
# write out last list option
[ -n "$_LOPT" ] && _write_list
# mark invalid
_STYPE=""
_SNAME=""
# check section type
[ "$1" = "setting" -o "$1" = "logging" ] && {
_STYPE="$1"
_SNAME="$2"
}
# translate section name
[ "$2" = "well_known" ] && _SNAME="well-known"
return 0
}
# redefined callback for lists when calling config_load
list_cb() {
# $1 name of variable
# $2 value
# invalid section type then ignore
[ -z "$_STYPE" -o -z "$_SNAME" ] && return 0
# write out last list option if new list starts
[ -n "$_LOPT" -a "$_LOPT" != "$1" ] && _write_list
# new list option
if [ -z "$_LOPT" ]; then
_LOPT="$1"
_LVAL="$2"
else
_LVAL="$_LVAL, $2"
fi
return 0
}
# redefined callback for options when calling config_load
option_cb() {
# $1 name of variable
# $2 value
local __OPT="$1"
local __VAL="$2"
# invalid section type then ignore
[ -z "$_STYPE" -o -z "$_SNAME" ] && return 0
# ignore list entrys will be handled by list_cb()
[ "${__OPT#*_ITEM}" != "$__OPT" ] && return 0 # ignore lists *_ITEM*
[ "${__OPT#*_LENGTH}" != "$__OPT" ] && return 0 # ignore lists *_LENGTH
# write out last list option and clear
[ -n "$_LOPT" ] && _write_list
# write to file
_write_value "$__OPT" "$__VAL" # there might be spaces in __VAL
return 0
}
# temporary config file
# radicale need read access
mkdir -m0755 -p $CFGDIR
cp /etc/radicale/config.template $_SYSTMP
config_load radicale # calling above config_cb()/option_cb()/list_cb() and write into $_SYSTMP
sed -i "/\[logging\]/a config = /var/etc/radicale/logging" $_SYSTMP # hard-code logging config
sed -i "/\[auth\]/a htpasswd_filename = /etc/radicale/users" $_SYSTMP # hard-code htpasswd
sed -i "/\[rights\]/a file = /etc/radicale/rights" $_SYSTMP # hard-code regexp-based rights
# temporary logging config file
cp /etc/radicale/logging.template $_LOGTMP
LOGDIR="$_file_path"
sed -i "/\[handler_console\]/a level = $_console_level" $_LOGTMP
sed -i "/\[handler_file\]/a level = $_file_level" $_LOGTMP
sed -i "/\[handler_file\]/a args = ('$_file_path/radicale','a',$_file_maxbytes,$_file_backupcount)" $_LOGTMP
sed -i "/\[handler_syslog\]/a level = $_syslog_level" $_LOGTMP
# move tmp to final
mv -f $_SYSTMP $SYSCFG
mv -f $_LOGTMP $LOGCFG
}
_set_permission() {
# config file permissions (read access for group)
chmod 644 $SYSCFG $LOGCFG
chgrp -R radicale $CFGDIR
# log directory (full access and owner)
[ -d $LOGDIR ] || mkdir -m0755 -p $LOGDIR
chown -R radicale:radicale $LOGDIR
# data directory does not exist
[ -d $DATADIR ] || {
logger -p user.error -t "radicale[----]" "Data directory '$DATADIR' does not exists. Startup failed !!!"
exit 1
}
chgrp -R radicale $DATADIR
}
boot() {
return 0 # will be started by "iface" hotplug events
}
start() {
_running() {
sleep 2 # give radicale time to completely come up
local _PID=$(eval "$PGREP")
kill -1 $_PID 2>/dev/null
[ $? -eq 0 ] \
&& logger -p user.notice -t "radicale[$_PID]" "Service started successfully"\
|| logger -p user.warn -t "radicale[----]" "Service failed to start"
}
# if already running do nothing
local _PID=$(eval "$PGREP")
kill -1 $_PID 2>/dev/null && return 0
_uci2radicale
_set_permission
radicale --daemon --config=$SYSCFG
_running & # check if running and syslog
return 0
}
reload() {
# reload is also used by luci
local _PID=$(eval "$PGREP")
kill -1 $_PID 2>/dev/null
if [ $? -eq 0 ]; then
# only restart if already running
restart
else
# only start if enabled
enabled && start
fi
return 0
}
stop() {
local _PID=$(eval "$PGREP")
[ -z "$_PID" ] && return 0 # not running
kill -15 $_PID 2>/dev/null
sleep 1 # give time to shutdown
local _tmp=$(eval "$PGREP")
if [ -z "$_tmp" ]; then
logger -p user.notice -t "radicale[$_PID]" "Service shutdown successfully"
else
kill -9 $_tmp # Normally never come here
logger -p user.warn -t "radicale[----]" "Service shutdown FORCED"
fi
return 0
}

View File

@ -0,0 +1,49 @@
#
# Authentication login is matched against the "user" key, and collection's path is matched against the "collection" key.
# You can use Python's ConfigParser interpolation values %(login)s and %(path)s.
# You can also get groups from the user regex in the collection with {0}, {1}, etc.
#
# For example, for the "user" key, ".+" means "authenticated user" and ".*" means "anybody" (including anonymous users).
#
# Section names are only used for naming the rule.
# Leading or ending slashes are trimmed from collection's path.
#
# This means all users starting with "admin" may read any collection
[admin]
user: ^admin.*$
collection: .*
permission: r
# This means all users may read and write any collection starting with public.
# We do so by just not testing against the user string.
[public]
user: .*
collection: ^public(/.+)?$
permission: rw
# A little more complex: give read access to users from a domain for all
# collections of all the users (ie. user@domain.tld can read domain/\*).
[domain-wide-access]
user: ^.+@(.+)\..+$
collection: ^{0}/.+$
permission: r
# Allow authenticated user to read all collections
[allow-everyone-read]
user: .+
collection: .*
permission: r
# Give write access to owners
[owner-write]
user: .+
collection: ^%(login)s(/.+)?$
permission: rw
# Allow CardDavMATE-, CalDavZAP- or InfCloud- WEBclient to work
# anonymous users have read access to "/" but no files or subdir
[infcloud]
user: .*
collection: /
permission: r

View File

@ -0,0 +1,6 @@
#
# Sample File
#
user1:password1
user2:password2

View File

@ -0,0 +1,30 @@
Subject: [PATCH] Run as user radicale and group radicale
Patch to run Radicale service as radicale:radicale non root user
Signed-off-by: Christian Schoenebeck <christian.schoenebeck@gmail.com>
---
bin/radicale | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/bin/radicale b/bin/radicale
index 619aca5..7466020 100755
--- a/bin/radicale
+++ b/bin/radicale
@@ -26,6 +26,13 @@ Launch the server according to configuration and command-line options.
"""
+# inserted to run as user radicale
+import pwd, grp, os
+uid = pwd.getpwnam('radicale').pw_uid
+gid = grp.getgrnam('radicale').gr_gid
+os.setegid(gid)
+os.seteuid(uid)
+
import radicale.__main__
--
2.1.0