Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F1974821
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R28 freebusy
Attached
Detach File
Event Timeline
Log In to Comment