diff --git a/lib/Autodiscover.php b/lib/Autodiscover.php index 3326a9a..9031fa9 100644 --- a/lib/Autodiscover.php +++ b/lib/Autodiscover.php @@ -1,338 +1,338 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU 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 General Public License for more details. | | | | You should have received a copy of the GNU General Public License | | along with this program. If not, see http://www.gnu.org/licenses/. | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Main application class */ class Autodiscover { const CHARSET = 'UTF-8'; protected $conf; protected $config = array(); /** * Autodiscover main execution path */ public static function run() { $uris = array($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME']); $type = ''; // Detect request type foreach ($uris as $uri) { // Outlook/Activesync if (stripos($uri, 'autodiscover.xml') !== false) { $type = 'Microsoft'; break; } // Mozilla Thunderbird (Kmail/Kontact/Evolution) else if (strpos($uri, 'config-v1.1.xml') !== false) { $type = 'Mozilla'; break; } } if (!$type) { header("HTTP/1.0 404 Not Found"); exit; } $class = "Autodiscover$type"; require_once __DIR__ . '/' . $class . '.php'; $engine = new $class; $engine->handle(); // fallback to 404 header("HTTP/1.0 404 Not Found"); exit; } /** * Initialization of class instance */ public function __construct() { require_once __DIR__ . '/Conf.php'; require_once __DIR__ . '/Log.php'; $this->conf = Conf::get_instance(); } /** * Handle request */ public function handle() { // read request parameters $this->handle_request(); // validate requested email address if (empty($this->email)) { $this->error("Email address not provided"); } if (!strpos($this->email, '@')) { $this->error("Invalid email address"); } // find/set services parameters $this->configure(); // send response $this->handle_response(); } /** * Send error to the client and exit */ protected function error($msg) { header("HTTP/1.0 500 $msg"); exit; } /** * Get services configuration */ protected function configure() { $pos = strrpos($this->email, '@'); $this->config = array( 'email' => $this->email, 'domain' => strtolower(substr($this->email, $pos + 1)), 'displayName' => $this->conf->get('autodiscover', 'service_name'), 'displayShortName' => $this->conf->get('autodiscover', 'service_short'), ); // get user form LDAP, set domain/login/user in $this->config $user = $this->get_user($this->email, $this->config['domain']); $proto_map = array('tls' => 'STARTTLS', 'ssl' => 'SSL'); foreach (array('imap', 'pop3', 'smtp') as $type) { if ($value = $this->conf->get('autodiscover', $type)) { $params = explode(';', $value); $pass_secure = in_array($params[1], array('CRAM-MD5', 'DIGEST-MD5')); $host = $params[0]; $host = str_replace('%d', $this->config['domain'], $host); $url = parse_url($host); $this->config[$type] = array( 'hostname' => $url['host'], 'port' => $url['port'], 'socketType' => $proto_map[$url['scheme']] ?: 'plain', 'username' => $this->config['login'] ?: $this->config['email'], 'authentication' => 'password-' . ($pass_secure ? 'encrypted' : 'cleartext'), ); } } if ($host = $this->conf->get('autodiscover', 'activesync')) { $host = str_replace('%d', $this->config['domain'], $host); $this->config['activesync'] = $host; } // Log::debug(print_r($this->config, true)); } /** * Get user record from LDAP */ protected function get_user($email, $domain) { // initialize LDAP connection $result = $this->init_ldap(); if (!$result) { $this->config = array_merge( $this->config, Array('mail' => $email, 'domain' => $domain) ); return; } // find domain if (!$this->ldap->find_domain($domain)) { $this->error("Unknown domain"); } // find user $user = $this->find_user($email, $domain); // update config $this->config = array_merge($this->config, (array)$user, array('domain' => $domain)); } /** * Initialize LDAP connection */ protected function init_ldap() { - $ldap_uri = $this->conf->get('ldap_uri', FALSE); + $ldap_uri = $this->conf->get('ldap_uri', false); if (!$ldap_uri) { - return FALSE; + return false; } $uri = parse_url($ldap_uri); $this->_ldap_server = ($uri['scheme'] === 'ldaps' ? 'ldaps://' : '') . $uri['host']; $this->_ldap_port = $uri['port']; $this->_ldap_scheme = $uri['scheme']; $this->_ldap_bind_dn = $this->conf->get('ldap', 'service_bind_dn'); $this->_ldap_bind_pw = $this->conf->get('ldap', 'service_bind_pw'); // Catch cases in which the ldap server port has not been explicitely defined if (!$this->_ldap_port) { $this->_ldap_port = $this->_ldap_scheme == 'ldaps' ? 636 : 389; } require_once 'Net/LDAP3.php'; $this->ldap = new Net_LDAP3(array( 'debug' => in_array(strtolower($this->conf->get('autodiscover', 'debug_mode')), array('trace', 'debug')), 'log_hook' => array($this, 'ldap_log'), 'vlv' => $this->conf->get('ldap', 'vlv', Conf::AUTO), 'config_root_dn' => "cn=config", 'hosts' => array($this->_ldap_server), 'port' => $this->_ldap_port, 'use_tls' => $this->_ldap_scheme == 'tls', 'domain_base_dn' => $this->conf->get('ldap', 'domain_base_dn'), 'domain_filter' => $this->conf->get('ldap', 'domain_filter'), 'domain_name_attribute' => $this->conf->get('ldap', 'domain_name_attribute'), )); $this->_ldap_domain = $this->conf->get('primary_domain'); // connect to LDAP if (!$this->ldap->connect()) { $this->error("Storage connection failed"); } // bind as the service user if (!$this->ldap->bind($this->_ldap_bind_dn, $this->_ldap_bind_pw)) { $this->error("Storage connection failed"); } } /** * Find user in LDAP */ private function find_user($email, $domain) { $filter = $this->conf->get('login_filter'); if (empty($filter)) { $filter = $this->conf->get('filter'); } if (empty($filter)) { $filter = "(&(|(mail=%s)(mail=%U@%d)(alias=%s)(alias=%U@%d)(uid=%s))(objectclass=inetorgperson))"; } $_parts = explode('@', $email); $localpart = $_parts[0]; $replace_patterns = array( '/%s/' => $email, '/%d/' => $domain, '/%U/' => $localpart, '/%r/' => $domain, ); $attributes = array( 'login' => $this->conf->get('autodiscover', 'login_attribute') ?: 'mail', 'username' => $this->conf->get('autodiscover', 'name_attribute') ?: 'cn', ); $filter = preg_replace(array_keys($replace_patterns), array_values($replace_patterns), $filter); $base_dn = $this->ldap->domain_root_dn($domain); $result = $this->ldap->search($base_dn, $filter, 'sub', array_values($attributes)); if (!$result) { Log::debug("Could not search $base_dn with $filter"); return; } if ($result->count() > 1) { Log::debug("Multiple entries found."); return; } else if ($result->count() < 1) { Log::debug("No entries found."); return; } // parse result $entries = $result->entries(true); $dn = key($entries); $entry = $entries[$dn]; $result = array(); foreach ($attributes as $idx => $attr) { $result[$idx] = is_array($entry[$attr]) ? current($entry[$attr]) : $entry[$attr]; } return $result; } /** * LDAP logging handler */ public function ldap_log($level, $msg) { if (is_array($msg)) { $msg = implode("\n", $msg); } switch ($level) { case LOG_DEBUG: Log::debug($str . $msg); break; case LOG_ERR: Log::error($str . $msg); break; case LOG_INFO: Log::info($str . $msg); break; case LOG_WARNING: Log::warning($str . $msg); break; case LOG_ALERT: case LOG_CRIT: case LOG_EMERG: case LOG_NOTICE: default: Log::trace($str . $msg); break; } } } diff --git a/lib/Conf.php b/lib/Conf.php index 8647a49..5ffa502 100644 --- a/lib/Conf.php +++ b/lib/Conf.php @@ -1,240 +1,240 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU 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 General Public License for more details. | | | | You should have received a copy of the GNU General Public License | | along with this program. If not, see http://www.gnu.org/licenses/. | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class Conf { static private $instance; private $_conf = array(); const CONFIG_FILE = '/etc/kolab/kolab.conf'; const STRING = 0; const BOOL = 1; const INT = 2; const FLOAT = 3; const AUTO = 4; /** * This implements the 'singleton' design pattern * * @return Conf The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new Conf(); } return self::$instance; } public function __construct() { // Do some magic configuration loading here. if (!file_exists(self::CONFIG_FILE)) { $this->_conf = Array( 'autodiscover' => Array( 'activesync' => '%d', 'imap' => 'ssl://%d:993', 'smtp' => 'ssl://%d:465', 'service_name' => 'Kolab Groupware', 'service_short' => 'Kolab', ) ); return; } $this->read_config(); } public function get($key1, $key2 = null, $type = null) { $value = $this->expand($this->get_raw($key1, $key2)); if ($value === null) { return $value; } switch ($type) { case self::INT: return intval($value); case self::FLOAT: return floatval($value); case self::BOOL: return (bool) preg_match('/^(true|1|on|enabled|yes)$/i', $value); case self::AUTO: return $this->auto($value); } return (string) $value; } - public function get_list($key1, $key2 = NULL) + public function get_list($key1, $key2 = null) { $list = array(); $value = $this->get($key1, $key2); $value_components = explode(',', $value); foreach ($value_components as $component) { $component = trim($component); if (!empty($component)) { $list[] = $component; } } return $list; } - public function get_raw($key1, $key2 = NULL) + public function get_raw($key1, $key2 = null) { if (isset($this->_conf[$key1])) { if ($key2) { if (isset($this->_conf[$key1][$key2])) { return $this->_conf[$key1][$key2]; } else if (isset($this->_conf['kolab'][$key2])) { return $this->_conf['kolab'][$key2]; } } else { return $this->_conf[$key1]; } } // If section is specified, but requested setting doesn't exist there // we fall back to (global) 'kolab' section if ($key2) { if (isset($this->_conf['kolab'][$key2])) { return $this->_conf['kolab'][$key2]; } return null; } // Fall back to whatever is the equivalent of auth_mechanism as the // section (i.e. 'ldap', or 'sql') $auth_mech = $this->_conf['kolab']['auth_mechanism']; if (isset($this->_conf[$auth_mech])) { if (isset($this->_conf[$auth_mech][$key1])) { return $this->_conf[$auth_mech][$key1]; } } // Fall back to global settings in the 'kolab' section. if (isset($this->_conf['kolab'][$key1])) { return $this->_conf['kolab'][$key1]; } return null; } - public function expand($str, $custom = FALSE) + public function expand($str, $custom = false) { if (preg_match_all('/%\((?P\w+)\)s/', $str, $matches)) { if (isset($matches['variable']) && !empty($matches['variable'])) { if (is_array($matches['variable'])) { foreach ($matches['variable'] as $key => $value) { if (is_array($custom) && array_key_exists($value, $custom)) { $str = str_replace("%(" . $value . ")s", $custom[$value], $str); } $str = str_replace("%(" . $value . ")s", $this->get($value), $str); } return $str; } else { return str_replace("%(" . $matches['variable'] . ")s", $this->get($matches['variable']), $str); } } return $str; } else { return $str; } } private function read_config() { $_ini_raw = file(self::CONFIG_FILE); $this->_conf = array(); foreach ($_ini_raw as $_line) { if (preg_match('/^\[([a-z0-9-_\.]+)\]/', $_line, $matches)) { $_cur_section = $matches[1]; $this->_conf[$_cur_section] = array(); unset($_cur_key); } if (preg_match('/^;/', $_line, $matches)) { } if (preg_match('/^([a-z0-9\.-_]+)\s*=\s*(.*)/', $_line, $matches)) { if (isset($_cur_section) && !empty($_cur_section)) { $_cur_key = $matches[1]; $this->_conf[$_cur_section][$matches[1]] = isset($matches[2]) ? $matches[2] : ''; } } if (preg_match('/^\s+(.*)$/', $_line, $matches)) { if (isset($_cur_key) && !empty($_cur_key)) { $this->_conf[$_cur_section][$_cur_key] .= $matches[1]; } } } } /** * Auto-detection and conversion of config value */ private function auto($value) { if (preg_match('/^(true|on|enabled|yes|false|off|disabled|no)$/i', $value)) { return (bool) preg_match('/^(true|on|enabled|yes)$/i', $value); } if (is_numeric($value)) { if (strpos($value, '.')) { return floatval($value); } else { return intval($value); } } // array in JSON format if ($value[0] == '{' || $value[0] == '[') { return json_decode($value, true); } return (string) $value; } } diff --git a/lib/Log.php b/lib/Log.php index 2c86c2e..c2811a4 100644 --- a/lib/Log.php +++ b/lib/Log.php @@ -1,239 +1,239 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU 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 General Public License for more details. | | | | You should have received a copy of the GNU General Public License | | along with this program. If not, see http://www.gnu.org/licenses/. | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Class for logging debug/info/warning/error messages into log file(s) */ class Log { const TRACE = 16; // use for protocol tracking: sql queries, ldap commands, etc. const DEBUG = 8; const INFO = 4; // use to log entry creation/update/delete etc. const WARNING = 2; const ERROR = 0; private static $mode; /** * Logs tracing message * * @param string $message Log message * @param array $args Additional arguments ('file', 'line') */ static function trace($message, $args = array()) { if (self::mode() >= self::TRACE) { self::log_message(self::TRACE, $message, $args); } } /** * Logs debug message * * @param string $message Log message * @param array $args Additional arguments ('file', 'line') */ static function debug($message, $args = array()) { if (self::mode() >= self::DEBUG) { self::log_message(self::DEBUG, $message, $args); } } /** * Logs information message * * @param string $message Log message * @param array $args Additional arguments ('file', 'line') */ static function info($message, $args = array()) { if (self::mode() >= self::INFO) { self::log_message(self::INFO, $message, $args); } } /** * Logs warning message * * @param string $message Log message * @param array $args Additional arguments ('file', 'line') */ static function warning($message, $args = array()) { if (self::mode() >= self::WARNING) { self::log_message(self::WARNING, $message, $args); } } /** * Logs error message * * @param string $message Log message * @param array $args Additional arguments ('file', 'line') */ static function error($message, $args = array()) { if (self::mode() >= self::ERROR) { self::log_message(self::ERROR, $message, $args); } } /** * Message logger * * @param int $mode Message severity * @param string $message Log message * @param array $args Additional arguments ('file', 'line') */ private static function log_message($mode, $message, $args = array()) { $conf = Conf::get_instance(); $logfile = $conf->get('autodiscover', 'log_file'); // if log_file is configured all logs will go to it // otherwise use separate file for info/debug and warning/error if (!$logfile) { switch ($mode) { case self::TRACE: case self::DEBUG: case self::INFO: $file = 'console'; break; case self::WARNING: case self::ERROR: $file = 'errors'; break; } $logfile = dirname(__FILE__) . '/../logs/' . $file; } switch ($mode) { case self::TRACE: $prefix = 'TRACE'; break; case self::DEBUG: $prefix = 'DEBUG'; break; case self::INFO: $prefix = 'INFO'; break; case self::WARNING: $prefix = 'WARNING'; break; case self::ERROR: $prefix = 'ERROR'; break; } if (!is_string($message)) { $message = var_export($message, true); } $date = date('d-M-Y H:i:s O'); $sess_id = session_id(); $logline = sprintf("[%s]%s: [%s] %s\n", $date, $sess_id ? "($sess_id)" : '', $prefix, $message); if (!empty($args)) { if (is_array($args)) { if (array_key_exists('file', $args)) { $logline .= ' in ' . $args['file']; unset($args['file']); } if (array_key_exists('line', $args)) { $logline .= ' on line ' . intval($args['line']); unset($args['line']); } } if (!empty($args)) { - $logline .= var_export($args, TRUE); + $logline .= var_export($args, true); } } if ($fp = @fopen($logfile, 'a')) { fwrite($fp, $logline); fflush($fp); fclose($fp); return; } if ($mode == self::ERROR) { // send error to PHPs error handler if write to file didn't succeed trigger_error($message, E_USER_ERROR); } } /** * Returns configured logging mode * * @return int Logging mode */ static function mode() { if (isset(self::$mode)) { return self::$mode; } $conf = Conf::get_instance(); $mode = $conf->get('autodiscover', 'debug_mode'); switch ($mode) { case self::TRACE: case 'trace': case 'TRACE': self::$mode = self::TRACE; break; case self::DEBUG: case 'debug': case 'DEBUG': self::$mode = self::DEBUG; break; case self::INFO: case 'info': case 'INFO': self::$mode = self::INFO; break; case self::WARNING: case 'warning': case 'WARNING': self::$mode = self::WARNING; break; case self::ERROR: default: self::$mode = self::ERROR; } return self::$mode; } }