diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php index 71cc709..5bc9bfd 100644 --- a/lib/kolab_sync.php +++ b/lib/kolab_sync.php @@ -1,532 +1,532 @@ | | | | 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 | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Main application class (based on Roundcube Framework) */ class kolab_sync extends rcube { /** * Application name * * @var string */ public $app_name = 'ActiveSync for Kolab'; // no double quotes inside /** * Current user * * @var rcube_user */ public $user; public $username; public $password; public $task = null; protected $per_user_log_dir; const CHARSET = 'UTF-8'; - const VERSION = "2.4.0"; + const VERSION = "2.4.1"; /** * This implements the 'singleton' design pattern * * @param int $mode Unused * @param string $env Unused * * @return kolab_sync The one and only instance */ static function get_instance($mode = 0, $env = '') { if (!self::$instance || !is_a(self::$instance, 'kolab_sync')) { self::$instance = new kolab_sync(); self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Initialization of class instance */ public function startup() { // Initialize Syncroton Logger $debug_mode = $this->config->get('activesync_debug') ? kolab_sync_logger::DEBUG : kolab_sync_logger::WARN; $this->logger = new kolab_sync_logger($debug_mode); // Get list of plugins // WARNING: We can use only plugins that are prepared for this // e.g. are not using output or rcmail objects or // doesn't throw errors when using them $plugins = (array)$this->config->get('activesync_plugins', array('kolab_auth')); $plugins = array_unique(array_merge($plugins, array('libkolab', 'libcalendaring'))); // Initialize/load plugins $this->plugins = kolab_sync_plugin_api::get_instance(); $this->plugins->init($this, $this->task); // this way we're compatible with Roundcube Framework 1.2 // we can't use load_plugins() here foreach ($plugins as $plugin) { $this->plugins->load_plugin($plugin, true); } } /** * Application execution (authentication and ActiveSync) */ public function run() { // when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule if (!isset($_SERVER['PHP_AUTH_USER'])) { // "Basic didhfiefdhfu4fjfjdsa34drsdfterrde..." if (isset($_SERVER["REMOTE_USER"])) { $basicAuthData = base64_decode(substr($_SERVER["REMOTE_USER"], 6)); } elseif (isset($_SERVER["REDIRECT_REMOTE_USER"])) { $basicAuthData = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6)); } elseif (isset($_SERVER["Authorization"])) { $basicAuthData = base64_decode(substr($_SERVER["Authorization"], 6)); } elseif (isset($_SERVER["HTTP_AUTHORIZATION"])) { $basicAuthData = base64_decode(substr($_SERVER["HTTP_AUTHORIZATION"], 6)); } if (isset($basicAuthData) && !empty($basicAuthData)) { list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = explode(":", $basicAuthData); } } if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) { // Convert domain.tld\username into username@domain (?) $username = explode("\\", $_SERVER['PHP_AUTH_USER']); if (count($username) == 2) { $_SERVER['PHP_AUTH_USER'] = $username[1]; if (!strpos($_SERVER['PHP_AUTH_USER'], '@') && !empty($username[0])) { $_SERVER['PHP_AUTH_USER'] .= '@' . $username[0]; } } // Authenticate the user $userid = $this->authenticate($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']); } if (empty($userid)) { header('WWW-Authenticate: Basic realm="' . $this->app_name .'"'); header('HTTP/1.1 401 Unauthorized'); exit; } $this->plugins->exec_hook('ready', array('task' => 'syncroton')); // Set log directory per-user $this->set_log_dir(); // Save user password for Roundcube Framework $this->password = $_SERVER['PHP_AUTH_PW']; // Register Syncroton backends/callbacks Syncroton_Registry::set(Syncroton_Registry::LOGGERBACKEND, $this->logger); Syncroton_Registry::set(Syncroton_Registry::DATABASE, $this->get_dbh()); Syncroton_Registry::set(Syncroton_Registry::TRANSACTIONMANAGER, kolab_sync_transaction_manager::getInstance()); Syncroton_Registry::set(Syncroton_Registry::DEVICEBACKEND, new kolab_sync_backend_device); Syncroton_Registry::set(Syncroton_Registry::FOLDERBACKEND, new kolab_sync_backend_folder); Syncroton_Registry::set(Syncroton_Registry::SYNCSTATEBACKEND, new kolab_sync_backend_state); Syncroton_Registry::set(Syncroton_Registry::CONTENTSTATEBACKEND, new kolab_sync_backend_content); Syncroton_Registry::set(Syncroton_Registry::POLICYBACKEND, new kolab_sync_backend_policy); Syncroton_Registry::set(Syncroton_Registry::SLEEP_CALLBACK, array($this, 'sleep')); Syncroton_Registry::setContactsDataClass('kolab_sync_data_contacts'); Syncroton_Registry::setCalendarDataClass('kolab_sync_data_calendar'); Syncroton_Registry::setEmailDataClass('kolab_sync_data_email'); Syncroton_Registry::setNotesDataClass('kolab_sync_data_notes'); Syncroton_Registry::setTasksDataClass('kolab_sync_data_tasks'); Syncroton_Registry::setGALDataClass('kolab_sync_data_gal'); // Configuration Syncroton_Registry::set(Syncroton_Registry::PING_TIMEOUT, (int) $this->config->get('activesync_ping_timeout', 60)); Syncroton_Registry::set(Syncroton_Registry::PING_INTERVAL, (int) $this->config->get('activesync_ping_interval', 15 * 60)); Syncroton_Registry::set(Syncroton_Registry::QUIET_TIME, (int) $this->config->get('activesync_quiet_time', 3 * 60)); Syncroton_Registry::set(Syncroton_Registry::MAX_COLLECTIONS, (int) $this->config->get('activesync_max_folders', 100)); // Run Syncroton $syncroton = new Syncroton_Server($userid); $syncroton->handle(); } /** * Authenticates a user * * @param string $username User name * @param string $password User password * * @param int User ID */ public function authenticate($username, $password) { // use shared cache for kolab_auth plugin result (username canonification) $cache = $this->get_cache_shared('activesync_auth'); $host = $this->select_host($username); $cache_key = sha1($username . '::' . $host); if (!$cache || !($auth = $cache->get($cache_key))) { $auth = $this->plugins->exec_hook('authenticate', array( 'host' => $host, 'user' => $username, 'pass' => $password, )); if (!$auth['abort'] && $cache) { $cache->set($cache_key, array( 'user' => $auth['user'], 'host' => $auth['host'], )); } // LDAP server failure... send 503 error if ($auth['kolab_ldap_error'] ?? null) { self::server_error(); } // Close LDAP connection from kolab_auth plugin if (class_exists('kolab_auth', false)) { if (method_exists('kolab_auth', 'ldap_close')) { kolab_auth::ldap_close(); } } } else { $auth['pass'] = $password; } $err = null; // Authenticate - get Roundcube user ID if (!($auth['abort'] ?? false) && ($userid = $this->login($auth['user'], $auth['pass'], $auth['host'], $err))) { // set real username $this->username = $auth['user']; return $userid; } else if ($err) { $err_str = $this->get_storage()->get_error_str(); } if (class_exists('kolab_auth', false)) { kolab_auth::log_login_error($auth['user'], !empty($err_str) ? $err_str : $err); } $this->plugins->exec_hook('login_failed', array( 'host' => $auth['host'], 'user' => $auth['user'], )); // IMAP server failure... send 503 error if ($err == rcube_imap_generic::ERROR_BAD) { self::server_error(); } } /** * Storage host selection */ private function select_host($username) { // Get IMAP host $host = $this->config->get('default_host'); if (is_array($host)) { list($user, $domain) = explode('@', $username); // try to select host by mail domain if (!empty($domain)) { foreach ($host as $storage_host => $mail_domains) { if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) { $host = $storage_host; break; } else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) { $host = is_numeric($storage_host) ? $mail_domains : $storage_host; break; } } } // take the first entry if $host is not found if (is_array($host)) { list($key, $val) = each($host); $host = is_numeric($key) ? $val : $key; } } return rcube_utils::parse_host($host); } /** * Authenticates a user in IMAP and returns Roundcube user ID. */ private function login($username, $password, $host, &$error = null) { if (empty($username)) { return null; } $login_lc = $this->config->get('login_lc'); $default_port = $this->config->get('default_port', 143); // parse $host $a_host = parse_url($host); $port = null; $ssl = null; if (!empty($a_host['host'])) { $host = $a_host['host']; $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; if (!empty($a_host['port'])) { $port = $a_host['port']; } else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) { $port = 993; } } if (!$port) { $port = $default_port; } // Convert username to lowercase. If storage backend // is case-insensitive we need to store always the same username if ($login_lc) { if ($login_lc == 2 || $login_lc === true) { $username = mb_strtolower($username); } else if (strpos($username, '@')) { // lowercase domain name list($local, $domain) = explode('@', $username); $username = $local . '@' . mb_strtolower($domain); } } // Here we need IDNA ASCII // Only rcube_contacts class is using domain names in Unicode $host = rcube_utils::idn_to_ascii($host); $username = rcube_utils::idn_to_ascii($username); // user already registered? if ($user = rcube_user::query($username, $host)) { $username = $user->data['username']; } // authenticate user in IMAP $storage = $this->get_storage(); if (!$storage->connect($host, $username, $password, $port, $ssl)) { $error = $storage->get_error_code(); return null; } // No user in database, but IMAP auth works if (!is_object($user)) { if ($this->config->get('auto_create_user')) { // create a new user record $user = rcube_user::create($username, $host); if (!$user) { self::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to create a user record", ), true, false); return null; } } else { self::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Access denied for new user $username. 'auto_create_user' is disabled", ), true, false); return null; } } // overwrite config with user preferences $this->user = $user; $this->config->set_user_prefs((array)$this->user->get_prefs()); $this->set_storage_prop(); // required by rcube_utils::parse_host() later $_SESSION['storage_host'] = $host; setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); // force reloading of mailboxes list/data //$storage->clear_cache('mailboxes', true); return $user->ID; } /** * Set logging directory per-user */ protected function set_log_dir() { if (empty($this->username)) { return; } $this->logger->set_username($this->username); $user_debug = (bool) $this->config->get('per_user_logging'); if (!$user_debug) { return; } $log_dir = $this->config->get('log_dir'); $log_dir .= DIRECTORY_SEPARATOR . $this->username; // No automatically creating any log directories if (!is_dir($log_dir)) { $this->logger->set_log_dir(null); return; } $deviceId = null; if (!empty($_GET['DeviceId'])) { $deviceId = $_GET['DeviceId']; } else if ( !empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], '&') == false && ($query = base64_decode($_SERVER['QUERY_STRING'])) && strlen($query) > 8 ) { // unpack the first 5 bytes, the last one is a length of the device id $unpacked = unpack('Cversion/Ccommand/vlocale/Clength', substr($query, 0, 5)); // unpack the deviceId, with some input sanity checks if ( !empty($unpacked['version']) && !empty($unpacked['length']) && $unpacked['version'] >= 121 && ($length = $unpacked['length']) > 0 && $length <= 32 ) { $unpacked = unpack("H" . ($length * 2) . "string", $query, 5); $deviceId = $unpacked['string']; } } if (!empty($deviceId)) { $dev_dir = $log_dir . DIRECTORY_SEPARATOR . $deviceId; if (is_dir($dev_dir) || mkdir($dev_dir, 0770)) { $log_dir = $dev_dir; } } $this->per_user_log_dir = $log_dir; $this->logger->set_log_dir($log_dir); $this->config->set('log_dir', $log_dir); } /** * Get the per-user log directory */ public function get_user_log_dir() { return $this->per_user_log_dir; } /** * Send HTTP 503 response. * We send it on LDAP/IMAP server error instead of 401 (Unauth), * so devices will not ask for new password. */ public static function server_error() { header("HTTP/1.1 503 Service Temporarily Unavailable"); header("Retry-After: 120"); exit; } /** * Function to be executed in script shutdown */ public function shutdown() { parent::shutdown(); // cache garbage collector $this->gc_run(); // write performance stats to logs/console if ($this->config->get('devel_mode') || $this->config->get('performance_stats')) { // we have to disable per_user_logging to make sure stats end up in the main console log $this->config->set('per_user_logging', false); // make sure logged numbers use unified format setlocale(LC_NUMERIC, 'en_US.utf8', 'en_US.UTF-8', 'en_US', 'C'); if (function_exists('memory_get_usage')) $mem = round(memory_get_usage() / 1048576, 1); if (function_exists('memory_get_peak_usage')) $mem .= '/' . round(memory_get_peak_usage() / 1048576, 1); $query = $_SERVER['QUERY_STRING']; $log = $query . ($mem ? ($query ? ' ' : '') . "[$mem]" : ''); if (defined('KOLAB_SYNC_START')) self::print_timer(KOLAB_SYNC_START, $log); else self::console($log); } } /** * When you're going to sleep the script execution for a longer time * it is good to close all external connections (sql, memcache, SMTP, IMAP). * * No action is required on wake up, all connections will be * re-established automatically. */ public function sleep() { parent::sleep(); // We'll have LDAP addressbooks here if using activesync_gal_sync if ($this->config->get('activesync_gal_sync')) { foreach (kolab_sync_data_gal::$address_books as $book) { $book->close(); } kolab_sync_data_gal::$address_books = array(); } } }