diff --git a/lib/Kolab/FreeBusy/Utils.php b/lib/Kolab/FreeBusy/Utils.php index 3bcb599..c045963 100644 --- a/lib/Kolab/FreeBusy/Utils.php +++ b/lib/Kolab/FreeBusy/Utils.php @@ -1,262 +1,274 @@ * * Copyright (C) 2013-2014, Kolab Systems AG * * 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 . */ 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() { // use date from HTTP query if (!empty($_GET['dtstart'])) { - return self::periodStartDT()->format('u'); + return self::periodStartDT()->format('U'); } // 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 start time * * @return object DateTime instance */ public static function periodStartDT() { // use date from HTTP query if (!empty($_GET['dtstart']) && - ($dtstart = \rcube_utils::anytodatetime(filter_input(INPUT_GET, 'dtstart', FILTER_SANITIZE_STRING)))) { - return $dtstart; + ($dtstart = filter_input(INPUT_GET, 'dtstart', FILTER_SANITIZE_STRING)) + ) { + try { + return new \DateTime($dtstart, new \DateTimezone('UTC')); + } + catch (Exception $e) { + // ignore + } } // Should probably be a setting. For now, do 8 weeks in the past return new \DateTime('now - 8 weeks 00:00:00', new \DateTimezone('UTC')); } /** * Getter for the free/busy period end time * * @return int Unix timestamp */ public static function periodEnd() { // use date from HTTP query if (!empty($_GET['dtend'])) { - return self::periodEndDT()->format('u'); + return self::periodEndDT()->format('U'); } // Should probably be a setting. For now, do 16 weeks into the future return time() + (60 * 60 * 24 * 7 * 16); } /** * Getter for the free/busy period end time * * @return object DateTime instance */ public static function periodEndDT() { // use date from HTTP query if (!empty($_GET['dtend']) && - ($dtend = \rcube_utils::anytodatetime(filter_input(INPUT_GET, 'dtend', FILTER_SANITIZE_STRING)))) { - return $dtend; + ($dtend = filter_input(INPUT_GET, 'dtend', FILTER_SANITIZE_STRING)) + ) { + try { + return new \DateTime($dtend, new \DateTimezone('UTC')); + } + catch (Exception $e) { + // ignore + } } // Should probably be a setting. For now, do 8 weeks in the past return new \DateTime('now + 16 weeks 00:00:00', new \DateTimezone('UTC')); } /** * Returns an apparent empty Free/Busy list for the given user */ public static function dummyVFreebusy($user) { $now = time(); $dtformat = 'Ymd\THis\Z'; $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, 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 +} diff --git a/public_html/index.php b/public_html/index.php index d4fcee6..49e67e8 100644 --- a/public_html/index.php +++ b/public_html/index.php @@ -1,116 +1,118 @@ * * Copyright (C) 2014, Kolab Systems AG * * 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 . */ define('KOLAB_FREEBUSY_ROOT', realpath('../')); // suppress error notices ini_set('error_reporting', E_ALL &~ E_NOTICE); // use composer's autoloader for both dependencies and local lib $loader = require_once(KOLAB_FREEBUSY_ROOT . '/vendor/autoload.php'); $loader->set('Kolab', array(KOLAB_FREEBUSY_ROOT . '/lib')); // register Kolab namespace $loader->setUseIncludePath(true); // enable searching the include_path (e.g. for PEAR packages) use Kolab\Config; use Kolab\FreeBusy\Utils; use Kolab\FreeBusy\Logger; use Kolab\FreeBusy\Directory; use Kolab\FreeBusy\HTTPAuth; // load config $config = Config::get_instance(KOLAB_FREEBUSY_ROOT . '/config'); if ($config->valid()) { // check for trusted IP first $remote_ip = Utils::remoteIP(); $trusted_ip = $config->trustednetworks ? Utils::checkIPRange($remote_ip, $config->get('trustednetworks.allow', array(), Config::ARR)) : false; $log = Logger::get('web'); $uri = $_SERVER['REDIRECT_URL']; // we're not always redirected here if (empty($uri)) { $uri = $_SERVER['REQUEST_URI']; $log->addDebug('Request (direct): ' . $uri, array('ip' => $remote_ip, 'trusted' => $trusted_ip)); } else { $log->addDebug('Request (redirect): ' . $uri, array('ip' => $remote_ip, 'trusted' => $trusted_ip)); } // check HTTP authentication if (!$trusted_ip && $config->httpauth) { if (!HTTPAuth::check($config->httpauth)) { $log->addDebug("Abort with 401 Unauthorized"); header('WWW-Authenticate: Basic realm="Kolab Free/Busy Service"'); header($_SERVER['SERVER_PROTOCOL'] . " 401 Unauthorized", true); exit; } } #header('Content-type: text/calendar; charset=utf-8', true); header('Content-type: text/plain; charset=utf-8', true); + list($uri, $args) = explode('?', $uri); + // analyse request $url = array_filter(explode('/', $uri)); $user = strtolower(array_pop($url)); $action = strtolower(array_pop($url)); $extended = false; // remove file extension if (preg_match('/^(.+)\.([ipx]fb)$/i', $user, $m)) { $user = urldecode($m[1]); $extended = $m[2] == 'xfb'; } // iterate over directories foreach ($config->directory as $key => $dirconfig) { $log->addDebug("Trying directory $key", $dirconfig); $directory = Directory::factory($dirconfig); if ($directory && ($fbdata = $directory->getFreeBusyData($user, $extended))) { $log->addInfo("Found valid data for user $user in directory $key"); echo $fbdata; exit; } } // return 404 if request was sent from a trusted IP if ($trusted_ip) { $log->addDebug("Returning '404 Not Found' for user $user"); header($_SERVER['SERVER_PROTOCOL'] . " 404 Not found", true); } else { $log->addInfo("Returning empty Free/Busy list for user $user"); // Return an apparent empty Free/Busy list. print Utils::dummyVFreebusy($user); } } // exit with error # header($_SERVER['SERVER_PROTOCOL'] . " 500 Internal Server Error", true);