Online calendars with SabreDAV, PHP-FPM and Nginx

Jean Baptiste FAVRE

2012 december

La version française est disponible ici: Gestion d'agendas avec SabreDAV, PHP-FPM et Nginx.

Introduction

As I practice self-hosting, I manage my own web server. And one of first service which comes in mind is online calendars.

Here is how I set up SabreDAV, a PHP library which implements WebDAV and its CalDAV and CardDAV extensions.

This setup allows you to manage calendars (quite obvious, isn't it ? ;) ), while keeping security as a requirement.

Preparation

I won't detailled basic installation and configuration of PHP-FPM and Nginx since I already wrote about it, for example:

Here is an excerpt of the final application calendar organisation:

tree /var/www/
calendar/
|--config
|   `-- fpm-pool.conf
|--docroot
|   `-- index.php
|-- logs
|   |-- access.log
|   |-- error.log
|   |-- php_err.log
|   `-- php_slow.log
|-- docroot
|   `-- index.php
|-- private
|   |-- data
|   `-- SabreDAV
|-- sessions
`-- tmp

private directory will host SQLite database as weel as SabreDAV code. Theses files won't be reachable through nginx which basically enhance global security level.

docroot directory will host only one file index.php. This file will be called for every requests.

Finally, PHP-FPM dedicated pool will run under a specific user calendar and within a chroot.

PHP-FPM user creation and environment configuration
mkdir -p /var/www/apps/calendar/{config,docroot,logs,private,sessions,tmp}
addgroup --force-badname --system --gid 10001 calendar

adduser --home /var/www/apps/calendar --shel /bin/false --no-create-home \
     --uid 10001 --gid 10001 --force-badname --disabled-password --disabled-login calendar

mkdir -p /var/www/apps/calendar/private/{data,SabreDAV}
Filesystem permissions setup
chown -R admin: /var/www/apps/calendar && \
chmod -R u=rwX,go=rX /var/www/apps/calendar

chown calendars:admin /var/www/apps/calendar/private/data && \
chmod ug=rwX,o= /var/www/apps/calendar/private/data

chown calendars:admin /var/www/apps/calendar/sessions && \
chmod -R u=rwX,g=rX,o= /var/www/apps/calendar/sessions

chown calendars:admin /var/www/apps/calendar/tmp && \
chmod -R 7750 /var/www/apps/calendar/tmp

Once done, we can proceed to next step: SabreDAV.

SabreDAV installation

SabreDAV installation basically consist of downloading it and extracting archive in /var/www/apps/calendar/private/

Setting up index.php file into docroot isn't much complicated:

cat /var/www/apps/calendar/docroot/index.php
<?php
// settings
date_default_timezone_set('Europe/Paris');

/* Database */
$pdo = new \PDO('sqlite:/private/data/calendars.sqlite');
$pdo->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);

//Mapping PHP errors to exceptions
function exception_error_handler($errno, $errstr, $errfile, $errline ) {
    throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler("exception_error_handler");

// Files we need
require_once 'vendor/autoload.php';

// Backends
$authBackend = new \Sabre\DAV\Auth\Backend\PDO($pdo);
$calendarBackend = new \Sabre\CalDAV\Backend\PDO($pdo);
$principalBackend = new \Sabre\DAVACL\PrincipalBackend\PDO($pdo);

// Directory structure 
$tree = array(
    new \Sabre\CalDAV\Principal\Collection($principalBackend),
    new \Sabre\CalDAV\CalendarRootNode($principalBackend, $calendarBackend),
);

$server = new \Sabre\DAV\Server($tree);
$server->setBaseUri('/');

/* Server Plugins */
$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend,'SabreDAV');
$server->addPlugin($authPlugin);

$aclPlugin = new \Sabre\DAVACL\Plugin();
$server->addPlugin($aclPlugin);

$caldavPlugin = new \Sabre\CalDAV\Plugin();
$server->addPlugin($caldavPlugin);

// Support for html frontend
$browser = new \Sabre\DAV\Browser\Plugin();
$server->addPlugin($browser);

// And off we go!
$server->exec();

Before starting, you'll have to create SQLite database:

SQLite database initialisation
sqlite3 /var/www/apps/calendar/private/data/calendar.sqlite < \
        /var/www/apps/calendar/private/SabreDAV/examples/sql/sqlite.addressbooks.sql

sqlite3 /var/www/apps/calendar/private/data/calendar.sqlite < \
        /var/www/apps/calendar/private/SabreDAV/examples/sql/sqlite.calendars.sql

sqlite3 /var/www/apps/calendar/private/data/calendar.sqlite < \
        /var/www/apps/calendar/private/SabreDAV/examples/sql/sqlite.locks.sql

sqlite3 /var/www/apps/calendar/private/data/calendar.sqlite < \
        /var/www/apps/calendar/private/SabreDAV/examples/sql/sqlite.principals.sql

sqlite3 /var/www/apps/calendar/private/data/calendar.sqlite < \
        /var/www/apps/calendar/private/SabreDAV/examples/sql/sqlite.users.sql

Then, you can configure PHP-FPM.

PHP-FPM configuration

cat /var/www/apps/calendar/config/fpm-pool.conf

[calendars]
    listen                 = /var/www/php_socks/$pool.sock
    listen.backlog         = -1
    listen.owner           = www-data
    listen.group           = www-data
    listen.mode            = 0640

    user  = $pool
    group = $pool

    pm                   = dynamic
    pm.max_requests      = 0
    pm.max_children      = 2
    pm.start_servers     = 1
    pm.min_spare_servers = 1
    pm.max_spare_servers = 2

    pm.status_path       = /php_pool_$pool_status
    ping.path            = /$pool_ping
    ping.response        = $pool_pong
 
    request_terminate_timeout = 50
    request_slowlog_timeout   = 5
    slowlog                   = /var/www/apps/$pool/logs/php_slow.log
 
    chroot = /var/www/apps/$pool/
    chdir = /

    catch_workers_output = yes
 
    env[HOSTNAME] = $HOSTNAME
    env[PATH]     = /usr/local/bin:/usr/bin:/bin
    env[TMP]      = /tmp
    env[TMPDIR]   = /tmp
    env[TEMP]     = /tmp

    php_admin_value[sendmail_path]      = /usr/sbin/sendmail -t -i -f webmaster@jbfavre.org
    php_flag[display_errors]            = off
    php_admin_value[error_reporting]    = E_ALL | E_STRICT
    php_admin_value[error_log]          = /logs/php_err.log
    php_admin_flag[log_errors]          = on
    php_admin_value[memory_limit]       = 128M
    php_value[max_execution_time]       = 5
    php_admin_value[session.save_handler] = files
    php_admin_value[session.save_path] = "/sessions"
    php_value[include_path] = ".:/private/SabreDAV"

Of course, do not forget to restart PHP.

Nginx configuration

Nothing strange here, except one thing: since PHP-FPM runs into a chroot, it does not know about /var/www/apps/calendar/. You'll have to rewrite all requests to get things working:

Nginx virtual host configuration
server {
    # Uncomment for maintenance mode
	#return 503;
        server_name calendar.domain.tld;
        listen   [::]:80;

        ########## Log definition ##########
        access_log /var/www/apps/calendar/logs/access.log vhosts;
        error_log /var/www/apps/calendar/logs/error.log info;

        root /var/www/apps/calendar/docroot/;

        # Here's the surprise :)
        rewrite (.*) /docroot/index.php$uri break;

        location ~ ^(.+\.php)(.*)$ {
                fastcgi_connect_timeout           2;
                fastcgi_send_timeout              2;
                fastcgi_read_timeout              2;
                fastcgi_buffer_size               128k;
                fastcgi_buffers                   4 256k;
                fastcgi_split_path_info           ^(.+\.php)([^?]*).*$;
                fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
                fastcgi_param  SERVER_SOFTWARE    nginx;
                fastcgi_param  QUERY_STRING       $query_string;
                fastcgi_param  REQUEST_METHOD     $request_method;
                fastcgi_param  CONTENT_TYPE       $content_type;
                fastcgi_param  CONTENT_LENGTH     $content_length;
                fastcgi_param  SCRIPT_FILENAME    $fastcgi_script_name;
                fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
                fastcgi_param  REQUEST_URI        $request_uri;
                fastcgi_param  DOCUMENT_URI       $document_uri;
                fastcgi_param  DOCUMENT_ROOT      $document_root;
                fastcgi_param  SERVER_PROTOCOL    $server_protocol;
                fastcgi_param  SERVER_ADDR        $server_addr;
                fastcgi_param  SERVER_PORT        $server_port;
                fastcgi_param  SERVER_NAME        $server_name;
                fastcgi_param  REMOTE_ADDR        $remote_addr;
                fastcgi_param  REMOTE_PORT        $remote_port;
                fastcgi_param  PATH_INFO          $fastcgi_path_info;

                fastcgi_pass   unix:/var/www/php_socks/calendar.sock;
        }
}

Nothing strange anyway.

Tests

We are one step away from tests. You must create users and calendars to be able to use the solution:

Managing users and calendars
echo -n 'foo:SabreDAV:foopassword' | md5sum
INSERT INTO users (username,digesta1)
       VALUES('foo', 'hash from above command');

INSERT INTO principals (uri,email,displayname)
       VALUES ('principals/foo', 'foo@domain.tld','My name is foo');

INSERT INTO principals (uri,email,displayname)
       VALUES ('principals/foo/calendar-proxy-read', null, null);

INSERT INTO principals (uri,email,displayname)
       VALUES ('principals/foo/calendar-proxy-write', null, null);
INSERT INTO calendars (principaluri, displayname, uri, description, components, ctag, transparent)
       VALUES ('principals/foo','Foo calendar','foocaluri','','VEVENT,VTODO','1', '0');
echo -n 'bar:SabreDAV:barpassword' | md5sum
INSERT INTO users (username,digesta1)
       VALUES('bar', 'hash from above command');

INSERT INTO principals (uri,email,displayname)
       VALUES ('principals/bar', 'bar@domain.tld','My name is bar');

INSERT INTO principals (uri,email,displayname)
       VALUES ('principals/bar/calendar-proxy-read', null, null);

INSERT INTO principals (uri,email,displayname)
       VALUES ('principals/bar/calendar-proxy-write', null, null);
INSERT INTO calendars (principaluri, displayname, uri, description, components, ctag, transparent)
       VALUES ('principals/bar','Bar calendar','barcaluri','','VEVENT,VTODO','1', '0');
SELECT id from principals where uri='principals/bar/calendar-proxy-read');

SELECT id from users where username='foo');
INSERT INTO groupmembers (principal_id, member_id) VALUES (principals.id from above select, users.id from above select);
SELECT id from principals where uri='principals/foo/calendar-proxy-write');

SELECT id from users where username='bar');
INSERT INTO groupmembers (principal_id, member_id) VALUES (principals.id from above select, users.id from above select);

Now open URL //calendar.domain.tld/, authenticate as one of created users and navigate the website.

Conclusion

Here you have a lightweight solution to manage your online calendars.

You only have to setup your beloved calendar clients. Mine are:

The 2 last ones can automatically discover calendars.

As far as I know, Thunderbird does not support it. You'll need to give it the full URL, as an example //calendar.domain.tld/calendars/username/calendaruri.

Sources et références

SabreDAV

Website
  • //code.google.com/p/sabredav/
  • //code.google.com/p/sabredav/downloads/list
Getting Started
  • //code.google.com/p/sabredav/wiki/Installation
  • //code.google.com/p/sabredav/wiki/GettingStarted
CalDAV & CardDAV
  • //code.google.com/p/sabredav/wiki/CalDAV
  • //code.google.com/p/sabredav/wiki/CardDAV
Client configuration
  • //code.google.com/p/sabredav/wiki/Lightning
  • //code.google.com/p/sabredav/wiki/Evolution
  • //dmfs.org/wiki/index.php?title=Main_Page

About Jean Baptiste FAVRE

I spend most of my free time on the Internet working on GNU/Linux with Debian or CentOS, virtualization with Xen and KVM technology, as well as cluster stacks with corosync and OpenAIS. Particularly interested in Linux, Netfilter, virtualization, monitoring and clusters, most of my personal works are published on this website and others should not delay. By way professional, I manage servers running RedHat or CentOS and VMware ESXi farm.
From time to time, I manage to drop my keyboard and read a book while listening to music, but it never lasts long.

License

Creative Commons License Cette publication est publiée sous contrat Creative Common by-nc-sa

Valid XHTML 1.0 Strict |  Valid CSS |  license Creative Common by-nc-sa

Table des matières

  1. Introduction
  2. Preparation
  3. SabreDAV installation
  4. PHP-FPM installation
  5. Nginx configuration
  6. Tests
  7. Conclusion
  8. Sources and references
  9. About ...
  10. License