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