3 commits - config/config.ini.sample lib/Kolab web/index.php

Thomas Brüderli bruederli at kolabsys.com
Thu Jan 17 14:50:55 CET 2013


 config/config.ini.sample             |   38 +++++++----
 lib/Kolab/FreeBusy/Config.php        |    7 +-
 lib/Kolab/FreeBusy/Directory.php     |    2 
 lib/Kolab/FreeBusy/DirectoryLDAP.php |    2 
 lib/Kolab/FreeBusy/HTTPAuth.php      |  117 ++++++++++++++++++++++++++++++++++
 lib/Kolab/FreeBusy/Logger.php        |   27 +++-----
 lib/Kolab/FreeBusy/Source.php        |    2 
 lib/Kolab/FreeBusy/SourceURL.php     |   11 ++-
 lib/Kolab/FreeBusy/Utils.php         |  118 +++++++++++++++++++++++++++++++++++
 web/index.php                        |   40 +++++++----
 10 files changed, 312 insertions(+), 52 deletions(-)

New commits:
commit ae5792a9e1ba9576eee49b515119b80616be9567
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jan 17 14:50:42 2013 +0100

    Add examples to sample config

diff --git a/config/config.ini.sample b/config/config.ini.sample
index d7668d9..5eb6607 100644
--- a/config/config.ini.sample
+++ b/config/config.ini.sample
@@ -1,33 +1,45 @@
-; Kolab Free/Busy Service configuration
+;; Kolab Free/Busy Service configuration
 
-; require HTTP authentication to access this service
+;; Require HTTP authentication to access this service
 [httpauth]
-type = static
-username = <user>
-password = <pass>
 
-; allow privileged access from these IPs
+;; 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
-allow[] = 192.168.0.
-allow[] = 10.10.
+allow[] = 192.168.0.0/16
+allow[] = 10.10.*
 allow[] = ::1
 
-; logging configuration
+;; Logging configuration
 [log]
-driver = file
+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
+;; Directories to resolve email addresses and their f/b source locations
 
-; try local filesystem first
+;; try local filesystem first
 [directory "local"]
 type = static
 filter = "@yourdomain"
 fbsource = file:/var/lib/kolab-freebusy/%u.ifb
 
-; local Kolab directory server
+;; local Kolab directory server
 [directory "kolab-ldap"]
 type = ldap
 host = ldap://localhost:389


commit f507d75cb6761e42eba846a7cc4b6a34d736150b
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jan 17 14:50:12 2013 +0100

    Improve logging setup; support syslog driver

diff --git a/lib/Kolab/FreeBusy/Config.php b/lib/Kolab/FreeBusy/Config.php
index d80ef7f..205a438 100644
--- a/lib/Kolab/FreeBusy/Config.php
+++ b/lib/Kolab/FreeBusy/Config.php
@@ -48,13 +48,16 @@ class Config
 	private function load($inifile)
 	{
 		if ($raw = parse_ini_file($inifile, true)) {
+			$config['directories'] = array();
 			foreach ($raw as $section => $values) {
 				// check for known sections
 				if (in_array($section, array('httpauth','trustednetworks','log'))) {
 					$config[$section] = $values;
 				}
-				else if (strpos($section, 'directory') === 0 || isset($values['fbsource'])){
-					$config['directories'][] = $values;
+				else if (strpos($section, 'directory') === 0 || isset($values['fbsource'])) {
+					$sect = preg_replace('/^directory\s*/', '', $section);
+					$key = strlen($sect) ? $sect : count($config['directories']);
+					$config['directories'][$key] = $values;
 				}
 			}
 
diff --git a/lib/Kolab/FreeBusy/Directory.php b/lib/Kolab/FreeBusy/Directory.php
index a68e5bb..67e6d8d 100644
--- a/lib/Kolab/FreeBusy/Directory.php
+++ b/lib/Kolab/FreeBusy/Directory.php
@@ -25,7 +25,7 @@ abstract class Directory
 				return new DirectoryStatic($config);
 
 			default:
-				trigger_error("Invalid directory type '" . $config['type'] . "'!", E_USER_ERROR);
+				Logger::get('directory')->addError("Invalid directory type '" . $config['type'] . "'!");
 		}
 
 		return null;
diff --git a/lib/Kolab/FreeBusy/DirectoryLDAP.php b/lib/Kolab/FreeBusy/DirectoryLDAP.php
index 1c675f9..54709f2 100644
--- a/lib/Kolab/FreeBusy/DirectoryLDAP.php
+++ b/lib/Kolab/FreeBusy/DirectoryLDAP.php
@@ -34,7 +34,7 @@ class DirectoryLDAP extends Directory
 		) + $config;
 
 		// instantiate Net_LDAP3 and connect with logger
-		$this->logger = Logger::get('ldap', $config['loglevel']);
+		$this->logger = Logger::get('ldap', intval($config['loglevel']));
 		$this->ldap = new Net_LDAP3($ldap_config);
 		$this->ldap->config_set('log_hook', array($this, 'log'));
 		$this->ldap->config_set('return_attributes', (array)$config['attributes']);
diff --git a/lib/Kolab/FreeBusy/Logger.php b/lib/Kolab/FreeBusy/Logger.php
index 552c7e0..8af607c 100644
--- a/lib/Kolab/FreeBusy/Logger.php
+++ b/lib/Kolab/FreeBusy/Logger.php
@@ -4,6 +4,7 @@ namespace Kolab\FreeBusy;
 
 use Monolog\Logger as Monologger;
 use Monolog\Handler\StreamHandler;
+use Monolog\Handler\SyslogHandler;
 use Monolog\Handler\NullHandler;
 
 /**
@@ -18,16 +19,22 @@ class Logger
 	 */
 	public static function get($name, $level = 0)
 	{
-		if (!isset(self::$instances[$name])) {
+		if (!isset(self::$instances[$name]) || ($level && !self::$instances[$name]->isHandling($level))) {
 			$logger = new Monologger($name);
 
-			// TODO: support more log drivers
+			// read log config
 			$config = Config::getInstance();
+			$identity = $config->get('log.name', 'freebusy');
+			$loglevel = $level ?: $config->get('log.level', Monologger::INFO);
+
 			switch ($config->get('log.driver')) {
 				case 'file':
-					$logdir = self::realpath($config->get('log.path'));
-					$loglevel = $level ?: $config->get("log.level", Monologger::INFO);
-					$logger->pushHandler(new StreamHandler($logdir . $name. '.log', $loglevel));
+					$logdir = Utils::abspath($config->get('log.path'), '/');
+					$logger->pushHandler(new StreamHandler($logdir . $identity . '.log', $loglevel));
+					break;
+
+				case 'syslog':
+					$logger->pushHahdler(new SyslogHandler($identity, $config->get('log.facility', 'user'), $loglevel));
 					break;
 
 				default:
@@ -41,15 +48,5 @@ class Logger
 		return self::$instances[$name];
 	}
 
-	/**
-	 * Resolve the given directory to a real path ending with a /
-	 */
-	private static function realpath($dirname)
-	{
-		if ($dirname[0] != '/')
-			$dirname = realpath(KOLAB_FREEBUSY_ROOT . '/' . $dirname);
-
-		return rtrim($dirname, '/') . '/';
-	}
 }
 
diff --git a/lib/Kolab/FreeBusy/Source.php b/lib/Kolab/FreeBusy/Source.php
index 53c4d0f..2d12ebc 100644
--- a/lib/Kolab/FreeBusy/Source.php
+++ b/lib/Kolab/FreeBusy/Source.php
@@ -25,7 +25,7 @@ abstract class Source
 			case 'https':	return new SourceURL($config);
 		}
 
-		trigger_error("Invalid source configuration: " . $url, E_USER_ERROR);
+		Logger::get('source')->addError("Invalid source configuration: " . $url);
 		return null;
 	}
 
diff --git a/lib/Kolab/FreeBusy/SourceURL.php b/lib/Kolab/FreeBusy/SourceURL.php
index d165bd5..e838640 100644
--- a/lib/Kolab/FreeBusy/SourceURL.php
+++ b/lib/Kolab/FreeBusy/SourceURL.php
@@ -29,18 +29,21 @@ class SourceURL extends Source
 					'header'  => "Authorization: Basic " . base64_encode($config['user'] . ':' . $config['pass']) . "\r\n",
 				),
 			));
-			$config['url'] = self::getUrl($config);  // re-compose url without user:pass
+			$config['url'] = self::composeUrl($config);  // re-compose url without user:pass
 		}
 
-		// TODO: so some logging
+		$data = file_get_contents($config['url'], false, $context);
 
-		return file_get_contents($config['url'], false, $context);
+		// log this...
+		Logger::get('url')->addInfo("Fetching data from " . $config['url'] . ": " . ($data ? 'OK' : 'FAILED'));
+
+		return $data;
 	}
 
 	/**
 	 * Compose a full url from the given config (previously extracted with parse_url())
 	 */
-	private static function getUrl($config)
+	private static function composeUrl($config)
 	{
 		$scheme = isset($config['scheme']) ?       $config['scheme'] . '://' : ''; 
 		$host   = isset($config['host'])   ?       $config['host']  : ''; 


commit 2b0104f82c9c11789ad81b6f8d1db1e086c13392
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jan 17 14:48:06 2013 +0100

    Implement HTTP authentication and trusted network check

diff --git a/lib/Kolab/FreeBusy/HTTPAuth.php b/lib/Kolab/FreeBusy/HTTPAuth.php
new file mode 100644
index 0000000..ad0c394
--- /dev/null
+++ b/lib/Kolab/FreeBusy/HTTPAuth.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Kolab\FreeBusy;
+
+use \Net_LDAP3;
+use \Monolog\Logger as Monolog;
+
+/**
+ * Static class to process HTTP authentication to this service
+ */
+class HTTPAuth
+{
+	private static $logger;
+
+	/**
+	 * Validate HTTP basic auth against the configured backend
+	 */
+	public static function check($config)
+	{
+		$logger = Logger::get('httpauth');
+
+		// no http auth submitted, abort!
+		if (empty($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
+			$logger->addDebug('No HTTP auth submitted');
+			return false;
+		}
+			
+		switch ($config['type']) {
+			case 'static':
+				return self::checkStatic($config, $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
+
+			case 'ldap':
+				return self::checkLDAP($config, $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
+
+			default:
+				$logger->addWarning('Unsupported auth type ' . $config['type']);
+				break;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Validate static user credentials from config
+	 */
+	private static function checkStatic($config, $user, $pass)
+	{
+		$valid = $user == $config['username'] && $pass == $config['password'];
+		Logger::get('httpauth')->addInfo("Static: authenticating user '$user': " . ($valid ? 'SUCCESS' : 'FAILURE'));
+		return $valid;
+	}
+
+	/**
+	 * Validate user credentials against the configured LDAP backend
+	 */
+	private static function checkLDAP($config, $user, $pass)
+	{
+		self::$logger = Logger::get('httpauth', intval($config['loglevel']));
+
+		$host = parse_url($config['host']);
+		$ldap_config = array(
+			'hosts'   => array($host['host']),
+			'port'    => $host['port'] ?: 389,
+			'use_tls' => $host['scheme'] == 'tls' || $host['scheme'] == 'ldaps',
+			'root_dn' => $config['base_dn'],
+			'filter'  => $config['filter'],
+			'service_bind_dn' => $config['bind_dn'],
+			'service_bind_pw' => $config['bind_pw'],
+			'sizelimit' => 0,
+			'timelimit' => 0,
+		);
+
+		// instantiate Net_LDAP3 and connect with logger
+		$ldap = new Net_LDAP3($ldap_config);
+		$ldap->config_set('log_hook', 'Kolab\FreeBusy\HTTPAuth::ldapLog');
+
+		// connect + bind to LDAP server
+		if ($ldap->connect()) {
+			self::$logger->addDebug("LDAP: connected to $config[host] with '$config[bind_dn]'");
+
+			// extract domain part from base_dn
+			$dn_domain = ldap_explode_dn($config['base_dn'], 1);
+			unset($dn_domain['count']);
+			$domain = join('.', $dn_domain);
+
+			$valid = (bool)$ldap->login($user, $pass, $domain);
+		}
+		else {
+			self::$logger->addWarning("LDAP: connectiion to $config[host] with '$config[bind_dn]' failed!");
+		}
+
+		self::$logger->addInfo("LDAP: authenticating user '$user': " . ($valid ? 'SUCCESS' : 'FAILURE'));
+		return $valid;
+	}
+
+	/**
+	 * Callback for Net_LDAP3 logging
+	 */
+	public static function ldapLog($level, $msg)
+	{
+		// map PHP log levels to Monolog levels
+		static $loglevels = array(
+			LOG_DEBUG   => Monolog::DEBUG,
+			LOG_NOTICE  => Monolog::NOTICE,
+			LOG_INFO    => Monolog::INFO,
+			LOG_WARNING => Monolog::WARNING,
+			LOG_ERR     => Monolog::ERROR,
+			LOG_CRIT    => Monolog::CRITICAL,
+			LOG_ALERT   => Monolog::ALERT,
+			LOG_EMERG   => Monolog::EMERGENCY,
+		);
+
+		$msg = is_array($msg) ? join('; ', $msg) : strval($msg);
+		self::$logger->addRecord($loglevels[$level], $msg);
+	}
+
+}
\ No newline at end of file
diff --git a/lib/Kolab/FreeBusy/Utils.php b/lib/Kolab/FreeBusy/Utils.php
new file mode 100644
index 0000000..ba1e736
--- /dev/null
+++ b/lib/Kolab/FreeBusy/Utils.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Kolab\FreeBusy;
+
+/**
+ * Static calss providing utility functions for the Free/Busy service
+ */
+class Utils
+{
+	/**
+	 * 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;
+	}
+
+}
\ No newline at end of file
diff --git a/web/index.php b/web/index.php
index 41286da..93115f2 100644
--- a/web/index.php
+++ b/web/index.php
@@ -33,25 +33,36 @@ ini_set('error_reporting', E_ALL &~ E_NOTICE);
 // use composer's autoloader for both dependencies and local lib
 require_once KOLAB_FREEBUSY_ROOT . '/vendor/autoload.php';
 
+use Kolab\FreeBusy\Utils;
 use Kolab\FreeBusy\Config;
 use Kolab\FreeBusy\Logger;
 use Kolab\FreeBusy\Directory;
+use Kolab\FreeBusy\HTTPAuth;
 
-#header('Content-type: text/calendar; charset=utf-8', true);
-header('Content-type: text/plain', true);
 
 // load config
 $config = Config::getInstance(KOLAB_FREEBUSY_ROOT . '/config');
 if ($config->isValid()) {
-#	print_r($config);
-	$log = Logger::get('service');
-	$log->addInfo('Request: ' . $_SERVER['REDIRECT_URL'], array('ip' => $_SERVER['REMOTE_ADDR']));
-
-	// check HTTP auth first
-	if ($config->httpauth) {
-		// TODO: implement this
+	// check for trusted IP first
+	$remote_ip = Utils::remoteIP();
+	$trusted_ip = $config->trustednetworks ? Utils::checkIPRange($remote_ip, $config->trustednetworks['allow']) : false;
+
+	$log = Logger::get('web');
+	$log->addDebug('Request: ' . $_SERVER['REDIRECT_URL'], 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', true);
+
 	// analyse request
 	$url = array_filter(explode('/', $_SERVER['REDIRECT_URL']));
 	$user = strtolower(array_pop($url));
@@ -69,20 +80,19 @@ if ($config->isValid()) {
 		$log->addDebug("Trying directory $key", $dirconfig);
 
 		$directory = Directory::factory($dirconfig);
-		if ($fbdata = $directory->getFreeBusyData($user, $extended)) {
-			$log->addInfo("Found valid data for user $user");
+		if ($directory && ($fbdata = $directory->getFreeBusyData($user, $extended))) {
+			$log->addInfo("Found valid data for user $user in directory $key");
 			echo $fbdata;
 			exit;
 		}
 	}
 
-/*
-	if ($_SERVER['REMOTE_ADDR'] is in $config->trustednetworks['allow]) {
+	// 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");
 
 		$now = time();
@@ -110,7 +120,7 @@ if ($config->isValid()) {
 		print "FREEBUSY:19700101T000000Z/19700101T000000Z\n";
 		print "END:VFREEBUSY\n";
 		print "END:VCALENDAR\n";
-//	}
+	}
 }
 
 // exit with error





More information about the commits mailing list