summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2013-01-17 13:48:06 (GMT)
committerThomas Bruederli <bruederli@kolabsys.com>2013-01-17 13:48:06 (GMT)
commit2b0104f82c9c11789ad81b6f8d1db1e086c13392 (patch)
treea524898fa902c72153216ebbe62cddb39c15f7d3
parent91d925b304affc82f444535a52d68c78724928b3 (diff)
downloadkolab-freebusy-2b0104f82c9c11789ad81b6f8d1db1e086c13392.tar.gz
Implement HTTP authentication and trusted network check
-rw-r--r--lib/Kolab/FreeBusy/HTTPAuth.php117
-rw-r--r--lib/Kolab/FreeBusy/Utils.php118
-rw-r--r--web/index.php40
3 files changed, 260 insertions, 15 deletions
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