Page MenuHomePhorge

No OneTemporary

diff --git a/config/config.ini.sample b/config/config.ini.sample
index 4439cb7..79b31eb 100644
--- a/config/config.ini.sample
+++ b/config/config.ini.sample
@@ -1,87 +1,96 @@
;; Kolab Free/Busy Service configuration
;; Require HTTP authentication to access this service
[httpauth]
;; Example for static auth credentials
; type = static
; username = "<user>"
; password = "<pass>"
;; Example for LDAP-based authentication
; type = ldap
; host = ldap://localhost:389
; bind_dn = "uid=kolab-service,ou=Special Users,dc=yourdomain,dc=com"
; bind_pw = "<service-bind-pw>"
; base_dn = "dc=yourdomain,dc=com"
; filter = "(&(|(mail=%s)(alias=%s)(uid=%s))(objectclass=inetorgperson))" ; optional, %s is replaced by the username
;; Allow privileged access from these IPs
[trustednetworks]
allow = 127.0.0.1,
192.168.0.0/16,
10.10.*,
::1
;; Logging configuration
[log]
driver = file ; supported drivers: file, syslog
path = ./log
name = freebusy
level = 300 ; (100 = Debug, 200 = Info, 300 = Warn, 400 = Error, 500 = Critical)
;; Directories to resolve email addresses and their f/b source locations
;; try local filesystem first
[directory "local"]
type = static
filter = "@yourdomain"
fbsource = file:/var/lib/kolab-freebusy/%s.ifb
;; check if primary email address hits a cache file (saves LDAP lookups)
[directory "local-cache"]
type = static
fbsource = file:/var/cache/kolab-freebusy/%s.ifb
expires = 10m
;; local Kolab directory server
[directory "kolab-ldap"]
type = ldap
host = ldap://localhost:389
bind_dn = "uid=kolab-service,ou=Special Users,dc=yourdomain,dc=com"
bind_pw = "<service-bind-pw>"
base_dn = "ou=People,dc=yourdomain,dc=com" ; use %dc as placeholder for the domain part extracted from the request string
filter = "(&(objectClass=kolabInetOrgPerson)(|(uid=%s)(mail=%s)(alias=%s)))"
attributes = mail, sn, alias
lc_attributes = sn
mail_attributes = mail, alias
fbsource = file:/var/lib/kolab-freebusy/%mail.ifb
loglevel = 200 ; Info
;; resolve Kolab resources from LDAP and fetch calendar from IMAP
[directory "kolab-resources"]
type = ldap
host = ldap://localhost:389
bind_dn = "uid=kolab-service,ou=Special Users,dc=yourdomain,dc=com"
bind_pw = "<service-bind-pw>"
base_dn = "ou=Resources,dc=yourdomain,dc=com"
filter = "(&(objectClass=kolabsharedfolder)(mail=%s))"
attributes = mail, kolabtargetfolder
-fbsource = "imap://cyrus-admin:<admin-pass>@localhost/%kolabtargetfolder?acl=lrs"
+fbsource = "fbdaemon://localhost:<port>?folder=%kolabtargetfolder"
+timeout = 10 ; abort after 10 seconds
cacheto = /var/cache/kolab-freebusy/%mail.ifb
expires = 10m
loglevel = 100 ; Debug
;; external MS Exchange 2010 server
[directory "exchange"]
type = static
filter = "@microsoft.com$"
fbsource = https://externalhost/free-busy/%s.ics
format = Exchange2010
;; further examples of fbsource URIs
-; - fetch data from another server by HTTP(s)
+;; - fetch data from another server by HTTP(s)
; fbsource = "https://fb-service-user:imap-password@kolab-server/freebusy/%mail.ifb"
-; - read directoy from a users calendars (all) using IMAP proxy authentication
+
+;; - read data from a users calendars (all) using IMAP proxy authentication
; fbsource = "imap://%mail:<admin-pass>@localhost/?proxy_auth=cyrus-admin"
+
+;; - read data from a shared IMAP folder with cyrus-admin privileges
+; fbsource = "imap://cyrus-admin:<admin-pass>@localhost/%kolabtargetfolder?acl=lrs"
+
+;; - trigger kolab-freebusyd daemon (folder= for shared folders, user= for user mailboxes)
+; fbsource = "fbdaemon://localhost:<port>?folder=%kolabtargetfolder&user=%mail"
+
diff --git a/lib/Kolab/FreeBusy/Source.php b/lib/Kolab/FreeBusy/Source.php
index a0a915d..def290f 100644
--- a/lib/Kolab/FreeBusy/Source.php
+++ b/lib/Kolab/FreeBusy/Source.php
@@ -1,116 +1,121 @@
<?php
/**
* This file is part of the Kolab Server Free/Busy Service
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\FreeBusy;
/**
* Abstract class to fetch free/busy data from a specific source
*/
abstract class Source
{
protected $config = array();
protected $cached = false;
/**
* Factory method creating an instace of Source according to config
*
* @param string Source URI
* @param array Hash array with config
*/
public static function factory($url, $conf)
{
$config = parse_url($url);
$config['url'] = $url;
switch ($config['scheme']) {
case 'file': return new SourceFile($config + $conf);
case 'imap':
case 'imaps': return new SourceIMAP($config + $conf);
case 'http':
case 'https': return new SourceURL($config + $conf);
+ case 'fbd':
+ case 'fbdaemon': return new SourceFBDaemon($config + $conf);
}
Logger::get('source')->addError("Invalid source configuration: " . $url);
return null;
}
/**
* Default constructor
*/
public function __construct($config)
{
$this->config = $config;
}
/**
* Retrieve free/busy data for the given user
*
* @param array Hash array with user attributes
*/
abstract public function getFreeBusyData($user, $extended);
/**
* Replace all %varname strings in config with values from $user
*/
protected function getUserConfig($user)
{
$config = array();
foreach ($this->config as $k => $val) {
if (is_string($val) && strpos($val, '%') !== false) {
$val = preg_replace_callback(
'/%\{?([a-z0-9]+)\}?/',
- function($m) use ($user) { return $user[$m[1]]; },
+ function($m) use ($k, $user) {
+ $enc = $k == 'url' || $k == 'query' || $k == 'fbsource';
+ return $enc ? urlencode($user[$m[1]]) : $user[$m[1]];
+ },
$val);
}
$config[$k] = $val;
}
return $config;
}
/**
* Helper method to check if a cached file exists and is still valid
*
* @param array Hash array with (replaced) config properties
* @return string Cached free-busy data or false if cache file doesn't exist or is expired
*/
protected function getCached($config)
{
if ($config['cacheto'] && file_exists($config['cacheto'])) {
if (empty($config['expires']) || filemtime($config['cacheto']) + Utils::getOffsetSec($config['expires']) >= time()) {
$this->cached = true;
return file_get_contents($config['cacheto']);
}
}
return false;
}
/**
* Return the value of the 'cached' flag
*/
public function isCached()
{
return $this->cached;
}
}
\ No newline at end of file
diff --git a/lib/Kolab/FreeBusy/SourceFBDaemon.php b/lib/Kolab/FreeBusy/SourceFBDaemon.php
new file mode 100644
index 0000000..13a0fb5
--- /dev/null
+++ b/lib/Kolab/FreeBusy/SourceFBDaemon.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * This file is part of the Kolab Server Free/Busy Service
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Kolab\FreeBusy;
+
+/**
+ * Implementation of a Free/Busy data source reading from kolab-freebusyd service
+ */
+class SourceFBDaemon extends Source
+{
+ /**
+ * @see Source::getFreeBusyData()
+ */
+ public function getFreeBusyData($user, $extended)
+ {
+ $log = Logger::get('fbdaemon', intval($this->config['loglevel']));
+
+ $config = $this->getUserConfig($user);
+ parse_str(strval($config['query']), $param);
+ $config += $param;
+
+ // log this...
+ $log->addInfo("Fetching data for ", $config);
+
+ // caching is enabled
+ if (!empty($config['cacheto'])) {
+ // check for cached data
+ if ($cached = $this->getCached($config)) {
+ $log->addInfo("Deliver cached data from " . $config['cacheto']);
+ return $cached;
+ }
+ // touch cache file to avoid multiple requests generating the same data
+ if (file_exists($config['cacheto'])) {
+ touch($config['cacheto']);
+ }
+ else {
+ file_put_contents($config['cacheto'], Utils::dummyVFreebusy($user['mail']));
+ $tempfile = $config['cacheto'];
+ }
+ }
+
+ // compose command for freebusyd
+ if (!empty($config['folder'])) {
+ $cmd = 'FOLDER';
+ $target = $config['folder'];
+ }
+ else if (!empty($config['user'])) {
+ $cmd = 'USER';
+ $target = $config['user'];
+ }
+ else {
+ $log->addWarning("No valid target user/name specified", $config);
+ }
+
+ // open connection to fbdaemon and execute IFB command
+ if (!empty($cmd) && ($fp = fsockopen($config['host'], $config['port'], $errno, $errstr, 5))) {
+ $timeout = $config['timeout'] ? intval($config['timeout']) : max(10, ini_get('max_execution_time')) - 5;
+ stream_set_timeout($fp , $timeout);
+
+ $start = Utils::periodStart();
+ $end = Utils::periodEnd();
+
+ $send = sprintf('IFB %s "%s" slot:%d-%d'."\n", $cmd, $target, $start, $end);
+ $log->debug('C: ' . $send, array('start' => gmdate('Y-m-d\TH:i:s\Z', $start), 'end' => gmdate('Y-m-d\TH:i:s\Z', $end), 'timeout' => $timeout));
+ $fbdata = false;
+
+ fwrite($fp, $send);
+ while (!feof($fp)) {
+ $line = fgets($fp, 128);
+ $log->debug('S: ' . $line);
+ $len = 0;
+
+ // detect response header
+ if (preg_match('/^\*\s+\(\{(\d+)\}/', $line, $m)) {
+ $len = intval($m[1]);
+ if ($len > 0) {
+ $fbdata = fread($fp, $len);
+ $log->debug("S: " . $fbdata);
+ }
+ }
+
+ // exit loop if result complete
+ if (preg_match('/^(OK|BAD)\s+/', $line, $m)) {
+ if ($m[1] == 'BAD') {
+ $log->addWarning("BAD response from kolab-freebusyd", array('request' => $send, 'response' => $line));
+ }
+ break;
+ }
+ }
+
+ fclose($fp);
+ }
+
+ // log daemon connection errors
+ if ($errno || $errstr) {
+ $log->addError("Cannot connect to kolab-freebusyd", array(
+ 'errno' => $errno,
+ 'error' => $errstr,
+ 'host' => $config['host'],
+ 'port' => $config['port'],
+ ));
+ }
+
+ if (!empty($fbdata)) {
+ // post-process fbdata (replace ORGANIZER: property)
+ if (!empty($user['mail'])) {
+ $fbdata = preg_replace('/(ORGANIZER:mailto:)(.+)/i', '\1' . $user['mail'], $fbdata);
+ }
+ return $fbdata;
+ }
+ // remove (temporary) cache file again
+ else if ($tempfile) {
+ unlink($tempfile);
+ }
+
+ // not found
+ return false;
+ }
+}
diff --git a/lib/Kolab/FreeBusy/Utils.php b/lib/Kolab/FreeBusy/Utils.php
index 0d76ab6..942341e 100644
--- a/lib/Kolab/FreeBusy/Utils.php
+++ b/lib/Kolab/FreeBusy/Utils.php
@@ -1,204 +1,218 @@
<?php
/**
* This file is part of the Kolab Server Free/Busy Service
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013-2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\FreeBusy;
/**
* Static calss providing utility functions for the Free/Busy service
*/
class Utils
{
const PRODID = '-//kolab.org//NONSGML Kolab Free-Busy Service 3.2//EN';
/**
* Resolve the given directory to a real path ending with $append
*
* @param string Arbitrary directory directory path
* @param string Make path end with this string/character
* @return string Absolute file system path
*/
public static function abspath($dirname, $append = '')
{
if ($dirname[0] != '/')
$dirname = realpath(KOLAB_FREEBUSY_ROOT . '/' . $dirname);
return rtrim($dirname, '/') . $append;
}
/**
* Returns remote IP address and forwarded addresses if found
*
* @return string Remote IP address(es)
*/
public static function remoteIP()
{
$address = $_SERVER['REMOTE_ADDR'];
// use the NGINX X-Real-IP header, if set
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
$address = $_SERVER['HTTP_X_REAL_IP'];
}
// use the X-Forwarded-For header, if set
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$address = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
return $address;
}
/**
* Checks if the given IP address is in one of the provided ranges
*
* @param string IP address
* @param array List of IP ranges/subnets to check against
* @return boolean True if in range, False if not
*/
public static function checkIPRange($ip, $ranges)
{
$ipv6 = strpos($ip, ':') !== false;
$ipbin = $ipv6 ? self::ip6net2bits($ip) : ip2long($ip);
foreach ((array)$ranges as $range) {
// don't compare IPv4 and IPv6 addresses/ranges
$rangev6 = strpos($range, ':') !== false;
if ($ipv6 != $rangev6) {
continue;
}
// quick substring check (e.g. 192.168.0.)
if (( $ipv6 && strpos($ipbin, self::ip6net2bits($range)) === 0) ||
(!$ipv6 && strpos($ip, rtrim($range, '*')) === 0)) {
return true;
}
// range from-to specified (IPv4 only)
list($lower, $upper) = explode('-', $range);
if (strlen($upper) && !$ipv6) {
if ($ipbin >= ip2long(trim($lower)) && $ipbin <= ip2long(trim($upper))) {
return true;
}
}
// subnet/length is given
list($subnet, $bits) = explode('/', $range);
// IPv6 subnet
if (strlen($bits) && $ipv6) {
$subnetbin = self::ip6net2bits($subnet);
if (substr($ipbin, 0, $bits) === substr($subnetbin, 0, $bits)) {
return true;
}
}
// IPv4 subnet
else if (strlen($bits)) {
$subnet = ip2long($subnet);
$mask = -1 << $bits;
$subnet &= $mask; // just in case the supplied subnet wasn't correctly aligned
if (($ipbin & $mask) == $subnet) {
return true;
}
}
}
return false;
}
/**
* Convert the given IPv6 address to a binary string representation.
* (from http://stackoverflow.com/questions/7951061/matching-ipv6-address-to-a-cidr-subnet)
*/
public static function ip6net2bits($inet)
{
$binaryip = '';
$unpacked = @unpack('A16', inet_pton($inet));
foreach (str_split($unpacked[1]) as $char) {
$binaryip .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
}
return $binaryip;
}
/**
* Returns number of seconds for a specified offset string.
*
* @param string String representation of the offset (e.g. 20min, 5h, 2days, 1week)
* @return int Number of seconds
*/
public static function getOffsetSec($str)
{
if (preg_match('/^([0-9]+)\s*([smhdw])/i', $str, $regs)) {
$amount = (int) $regs[1];
$unit = strtolower($regs[2]);
}
else {
$amount = (int) $str;
$unit = 's';
}
switch ($unit) {
case 'w':
$amount *= 7;
case 'd':
$amount *= 24;
case 'h':
$amount *= 60;
case 'm':
$amount *= 60;
}
return $amount;
}
+ /**
+ * Getter for the free/busy period start time
+ *
+ * @return int Unix timestamp
+ */
+ public static function periodStart()
+ {
+ // Should probably be a setting. For now, do 8 weeks in the past
+ return time() - (60 * 60 * 24 * 7 * 8);
+ }
+
+ /**
+ * Getter for the free/busy period end time
+ *
+ * @return int Unix timestamp
+ */
+ public static function periodEnd()
+ {
+ // Should probably be a setting. For now, do 16 weeks into the future
+ return time() + (60 * 60 * 24 * 7 * 16);
+ }
+
/**
* Returns an apparent empty Free/Busy list for the given user
*/
public static function dummyVFreebusy($user)
{
$now = time();
$dtformat = 'Ymd\THis\Z';
- // NOTE: The following settings should probably correspond with
- // whatever period of time kolab-freebusyd thinks it should use.
-
- // Should probably be a setting. For now, do 8 weeks in the past
- $start = $now - (60 * 60 * 24 * 7 * 8);
- // Should probably be a setting. For now, do 16 weeks into the future
- $end = $now + (60 * 60 * 24 * 7 * 16);
-
$dummy = "BEGIN:VCALENDAR\n";
$dummy .= "VERSION:2.0\n";
$dummy .= "PRODID:" . self::PRODID . "\n";
$dummy .= "METHOD:PUBLISH\n";
$dummy .= "BEGIN:VFREEBUSY\n";
$dummy .= "ORGANIZER:MAILTO:" . $user . "\n";
$dummy .= "DTSTAMP:" . gmdate($dtformat) . "\n";
- $dummy .= "DTSTART:" . gmdate($dtformat, $start) . "\n";
- $dummy .= "DTEND:" . gmdate($dtformat, $end) . "\n";
+ $dummy .= "DTSTART:" . gmdate($dtformat, self::periodStart()) . "\n";
+ $dummy .= "DTEND:" . gmdate($dtformat, self::periodEnd()) . "\n";
$dummy .= "COMMENT:This is a dummy vfreebusy that indicates an empty calendar\n";
$dummy .= "FREEBUSY:19700101T000000Z/19700101T000000Z\n";
$dummy .= "END:VFREEBUSY\n";
$dummy .= "END:VCALENDAR\n";
return $dummy;
}
}
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Mon, Sep 15, 3:19 AM (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
287461
Default Alt Text
(17 KB)

Event Timeline