diff --git a/lib/ext/Syncroton/Server.php b/lib/ext/Syncroton/Server.php index 8bb3160..e04bcd2 100644 --- a/lib/ext/Syncroton/Server.php +++ b/lib/ext/Syncroton/Server.php @@ -1,472 +1,482 @@ */ /** * class to handle incoming http ActiveSync requests * * @package Syncroton */ class Syncroton_Server { public const PARAMETER_ATTACHMENTNAME = 0; public const PARAMETER_COLLECTIONID = 1; public const PARAMETER_ITEMID = 3; public const PARAMETER_OPTIONS = 7; public const MAX_HEARTBEAT_INTERVAL = 3540; // 59 minutes protected $_body; /** * informations about the currently device * * @var Syncroton_Backend_IDevice */ protected $_deviceBackend; /** * @var Zend_Log */ protected $_logger; /** * @var Zend_Controller_Request_Http */ protected $_request; protected $_userId; public function __construct($userId, Zend_Controller_Request_Http $request = null, $body = null) { if (Syncroton_Registry::isRegistered('loggerBackend')) { $this->_logger = Syncroton_Registry::get('loggerBackend'); } $this->_userId = $userId; $this->_request = $request instanceof Zend_Controller_Request_Http ? $request : new Zend_Controller_Request_Http(); $this->_body = $body !== null ? $body : fopen('php://input', 'r'); - $this->_deviceBackend = Syncroton_Registry::getDeviceBackend(); + // Not available on unauthenticated OPTIONS request + if (!empty($this->_userId)) { + $this->_deviceBackend = Syncroton_Registry::getDeviceBackend(); + } } public function handle() { if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST METHOD: ' . $this->_request->getMethod()); } + if ($this->_request->getMethod() != "OPTIONS" && empty($this->_userId)) { + $this->_logger->warn(__METHOD__ . '::' . __LINE__ . ' Not authenticated'); + header('WWW-Authenticate: Basic realm="ActiveSync for Kolab"'); + header('HTTP/1.1 401 Unauthorized'); + exit; + } + switch($this->_request->getMethod()) { case 'OPTIONS': $this->_handleOptions(); break; case 'POST': $this->_handlePost(); break; case 'GET': echo "It works!
Your userid is: {$this->_userId} and your IP address is: {$_SERVER['REMOTE_ADDR']}."; break; } } /** * handle options request */ protected function _handleOptions() { $command = new Syncroton_Command_Options(); $this->_sendHeaders($command->getHeaders()); } protected function _sendHeaders(array $headers) { foreach ($headers as $name => $value) { header($name . ': ' . $value); } } /** * handle post request */ protected function _handlePost() { $requestParameters = $this->_getRequestParameters($this->_request); if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST ' . print_r($requestParameters, true)); } $className = 'Syncroton_Command_' . $requestParameters['command']; if (!class_exists($className)) { if ($this->_logger instanceof Zend_Log) { $this->_logger->notice(__METHOD__ . '::' . __LINE__ . " command not supported: " . $requestParameters['command']); } header("HTTP/1.1 501 not implemented"); return; } // get user device $device = $this->_getUserDevice($this->_userId, $requestParameters); if ($requestParameters['contentType'] == 'application/vnd.ms-sync.wbxml' || $requestParameters['contentType'] == 'application/vnd.ms-sync') { // decode wbxml request try { $decoder = new Syncroton_Wbxml_Decoder($this->_body); $requestBody = $decoder->decode(); if ($this->_logger instanceof Zend_Log) { $this->_logDomDocument($requestBody, 'request', __METHOD__, __LINE__); } } catch(Syncroton_Wbxml_Exception_UnexpectedEndOfFile $e) { if ($this->_logger instanceof Zend_Log) { $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unexpected end of file."); } $requestBody = null; } } else { $requestBody = $this->_body; } header("MS-Server-ActiveSync: 14.00.0536.000"); // avoid sending HTTP header "Content-Type: text/html" for empty sync responses ini_set('default_mimetype', 'application/vnd.ms-sync.wbxml'); try { $command = new $className($requestBody, $device, $requestParameters); $response = $command->handle(); if (!$response) { $response = $command->getResponse(); } } catch (Syncroton_Exception_ProvisioningNeeded $sepn) { if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " provisioning needed"); } header("HTTP/1.1 449 Retry after sending a PROVISION command"); if (version_compare($device->acsversion, '14.0', '>=')) { $response = $sepn->domDocument; } else { // pre 14.0 method return; } } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) { $this->_logger->err(__METHOD__ . '::' . __LINE__ . " unexpected exception occured: " . get_class($e)); } if ($this->_logger instanceof Zend_Log) { $this->_logger->err(__METHOD__ . '::' . __LINE__ . " exception message: " . $e->getMessage()); } if ($this->_logger instanceof Zend_Log) { $this->_logger->err(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString()); } header("HTTP/1.1 500 Internal server error"); return; } if ($response instanceof DOMDocument) { if ($this->_logger instanceof Zend_Log) { $this->_logDomDocument($response, 'response', __METHOD__, __LINE__); } if (isset($command) && $command instanceof Syncroton_Command_ICommand) { $this->_sendHeaders($command->getHeaders()); } $outputStream = fopen("php://temp", 'r+'); $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3); try { $encoder->encode($response); } catch (Syncroton_Wbxml_Exception $swe) { if ($this->_logger instanceof Zend_Log) { $this->_logger->err(__METHOD__ . '::' . __LINE__ . " Could not encode output: " . $swe); } header("HTTP/1.1 500 Internal server error"); return; } if ($requestParameters['acceptMultipart'] == true && isset($command)) { $parts = $command->getParts(); // output multipartheader $bodyPartCount = 1 + count($parts); // number of parts (4 bytes) $header = pack('i', $bodyPartCount); $partOffset = 4 + (($bodyPartCount * 2) * 4); // wbxml body start and length $streamStat = fstat($outputStream); $header .= pack('ii', $partOffset, $streamStat['size']); $partOffset += $streamStat['size']; // calculate start and length of parts foreach ($parts as $partId => $partStream) { rewind($partStream); $streamStat = fstat($partStream); // part start and length $header .= pack('ii', $partOffset, $streamStat['size']); $partOffset += $streamStat['size']; } echo $header; } // output body rewind($outputStream); fpassthru($outputStream); // output multiparts if (isset($parts)) { foreach ($parts as $partStream) { rewind($partStream); fpassthru($partStream); } } } } /** * write (possible big) DOMDocument in smaller chunks to log file * * @param DOMDocument $dom * @param string $action * @param string $method * @param int $line */ protected function _logDomDocument(DOMDocument $dom, $action, $method, $line) { if (method_exists($this->_logger, 'hasDebug') && !$this->_logger->hasDebug()) { return; } $tempStream = tmpfile(); $meta_data = stream_get_meta_data($tempStream); $filename = $meta_data["uri"]; $dom->formatOutput = true; $dom->save($filename); $dom->formatOutput = false; rewind($tempStream); $loops = 0; while (!feof($tempStream)) { $this->_logger->debug("{$method}::{$line} xml {$action} ({$loops}):\n" . fread($tempStream, 1048576)); $loops++; } fclose($tempStream); } /** * return request params * * @return array */ protected function _getRequestParameters(Zend_Controller_Request_Http $request) { if (strpos($request->getRequestUri(), '&') === false) { $commands = [ 0 => 'Sync', 1 => 'SendMail', 2 => 'SmartForward', 3 => 'SmartReply', 4 => 'GetAttachment', 9 => 'FolderSync', 10 => 'FolderCreate', 11 => 'FolderDelete', 12 => 'FolderUpdate', 13 => 'MoveItems', 14 => 'GetItemEstimate', 15 => 'MeetingResponse', 16 => 'Search', 17 => 'Settings', 18 => 'Ping', 19 => 'ItemOperations', 20 => 'Provision', 21 => 'ResolveRecipients', 22 => 'ValidateCert', ]; $requestParameters = substr($request->getRequestUri(), strpos($request->getRequestUri(), '?')); $stream = fopen("php://temp", 'r+'); fwrite($stream, base64_decode($requestParameters)); rewind($stream); // unpack the first 4 bytes $unpacked = unpack('CprotocolVersion/Ccommand/vlocale', fread($stream, 4)); // 140 => 14.0 $protocolVersion = substr($unpacked['protocolVersion'], 0, -1) . '.' . substr($unpacked['protocolVersion'], -1); $command = $commands[$unpacked['command']]; $locale = $unpacked['locale']; $deviceId = null; // unpack deviceId $length = ord(fread($stream, 1)); if ($length > 0) { $toUnpack = fread($stream, $length); $unpacked = unpack("H" . ($length * 2) . "string", $toUnpack); $deviceId = $unpacked['string']; } // unpack policyKey $length = ord(fread($stream, 1)); if ($length > 0) { $unpacked = unpack('Vstring', fread($stream, $length)); $policyKey = $unpacked['string']; } // unpack device type $length = ord(fread($stream, 1)); if ($length > 0) { $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $deviceType = $unpacked['string']; } while (! feof($stream)) { $tag = ord(fread($stream, 1)); $length = ord(fread($stream, 1)); // If the stream is at the end we'll get a 0-length if (!$length) { continue; } switch ($tag) { case self::PARAMETER_ATTACHMENTNAME: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $attachmentName = $unpacked['string']; break; case self::PARAMETER_COLLECTIONID: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $collectionId = $unpacked['string']; break; case self::PARAMETER_ITEMID: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $itemId = $unpacked['string']; break; case self::PARAMETER_OPTIONS: $options = ord(fread($stream, 1)); $saveInSent = !!($options & 0x01); $acceptMultiPart = !!($options & 0x02); break; default: if ($this->_logger instanceof Zend_Log) { $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " found unhandled command parameters"); } } } $result = [ 'protocolVersion' => $protocolVersion, 'command' => $command, 'deviceId' => $deviceId, 'deviceType' => $deviceType ?? null, 'policyKey' => $policyKey ?? null, 'saveInSent' => $saveInSent ?? false, 'collectionId' => $collectionId ?? null, 'itemId' => $itemId ?? null, 'attachmentName' => $attachmentName ?? null, 'acceptMultipart' => $acceptMultiPart ?? false, ]; } else { $result = [ 'protocolVersion' => $request->getServer('HTTP_MS_ASPROTOCOLVERSION'), 'command' => $request->getQuery('Cmd'), 'deviceId' => $request->getQuery('DeviceId'), 'deviceType' => $request->getQuery('DeviceType'), 'policyKey' => $request->getServer('HTTP_X_MS_POLICYKEY'), 'saveInSent' => $request->getQuery('SaveInSent') == 'T', 'collectionId' => $request->getQuery('CollectionId'), 'itemId' => $request->getQuery('ItemId'), 'attachmentName' => $request->getQuery('AttachmentName'), 'acceptMultipart' => $request->getServer('HTTP_MS_ASACCEPTMULTIPART') == 'T', ]; } $result['userAgent'] = $request->getServer('HTTP_USER_AGENT', $result['deviceType']); $result['contentType'] = $request->getServer('CONTENT_TYPE'); return $result; } /** * get existing device of owner or create new device for owner * * @param string $ownerId * @param array $requestParameters * * @return Syncroton_Model_IDevice */ protected function _getUserDevice($ownerId, $requestParameters) { try { $device = $this->_deviceBackend->getUserDevice($ownerId, $requestParameters['deviceId']); $device->useragent = $requestParameters['userAgent']; $device->acsversion = $requestParameters['protocolVersion']; $device->devicetype = $requestParameters['deviceType']; if ($device->isDirty()) { $device = $this->_deviceBackend->update($device); } } catch (Syncroton_Exception_NotFound $senf) { $device = $this->_deviceBackend->create(new Syncroton_Model_Device([ 'owner_id' => $ownerId, 'deviceid' => $requestParameters['deviceId'], 'devicetype' => $requestParameters['deviceType'], 'useragent' => $requestParameters['userAgent'], 'acsversion' => $requestParameters['protocolVersion'], 'policyId' => Syncroton_Registry::isRegistered(Syncroton_Registry::DEFAULT_POLICY) ? Syncroton_Registry::get(Syncroton_Registry::DEFAULT_POLICY) : null, ])); } /** @var Syncroton_Model_Device $device */ return $device; } public static function validateSession() { $validatorFunction = Syncroton_Registry::getSessionValidator(); return $validatorFunction(); } } diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php index b4543ce..93a29ee 100644 --- a/lib/kolab_sync.php +++ b/lib/kolab_sync.php @@ -1,556 +1,553 @@ | | | | 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 { /** @var string Application name */ public $app_name = 'ActiveSync for Kolab'; // no double quotes inside /** @var string|null Request user name */ public $username; /** @var string|null Request user password */ public $password; public $task; protected $per_user_log_dir; protected $log_dir; protected $logger; public const CHARSET = 'UTF-8'; public const VERSION = "2.4.2"; /** * This implements the 'singleton' design pattern * * @param int $mode Unused * @param string $env Unused * * @return kolab_sync The one and only instance */ public 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); $this->log_dir = $this->config->get('log_dir'); // Get list of plugins // WARNING: We can use only plugins that are prepared for this // e.g. are not using output or rcmail objects and // do not throw errors when using them $plugins = (array)$this->config->get('activesync_plugins', ['kolab_auth']); $plugins = array_unique(array_merge($plugins, ['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)) { [$_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]; } } // Set log directory per-user $this->set_log_dir($_SERVER['PHP_AUTH_USER']); // 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', ['task' => 'syncroton']); // Set log directory per-user (again, in case the username changed above) $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, [$this, 'sleep']); + // The unauthenticated OPTIONS request doesn't require these backends and we can't instantiate them without credentials for the underlying storage backend + if (!empty($userid)) { + 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, [$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 * * @return null|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', [ 'host' => $host, 'user' => $username, 'pass' => $password, ]); if (!$auth['abort'] && $cache) { $cache->set($cache_key, [ 'user' => $auth['user'], 'host' => $auth['host'], ]); } // LDAP server failure... send 503 error if (!empty($auth['kolab_ldap_error'])) { self::server_error(); } // Close LDAP connection from kolab_auth plugin if (class_exists('kolab_auth', false)) { kolab_auth::ldap_close(); } } else { $auth['pass'] = $password; } $err = null; // Authenticate - get Roundcube user ID if (empty($auth['abort']) && ($userid = $this->login($auth['user'], $auth['pass'], $auth['host'], $err))) { // set real username $this->username = $auth['user']; return $userid; } 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', [ 'host' => $auth['host'], 'user' => $auth['user'], ]); // IMAP server failure... send 503 error if ($err == rcube_imap_generic::ERROR_BAD) { self::server_error(); } return null; } /** * Storage host selection */ private function select_host($username) { // Get IMAP host $host = $this->config->get('imap_host', $this->config->get('default_host')); if (is_array($host)) { [$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; } elseif (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)) { $key = key($host); $host = is_numeric($key) ? $host[$key] : $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'], ['ssl','imaps','tls'])) ? $a_host['scheme'] : null; if (!empty($a_host['port'])) { $port = $a_host['port']; } elseif ($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); } elseif (strpos($username, '@')) { // lowercase domain name [$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([ 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to create a user record", ], true, false); return null; } } else { self::raise_error([ '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; } /** * Initializes and returns the storage backend object */ public static function storage() { $class = 'kolab_sync_storage'; $self = self::get_instance(); if (($name = $self->config->get('activesync_storage')) && $name != 'kolab') { $class .= '_' . strtolower($name); } return $class::get_instance(); } /** * Set logging directory per-user */ protected function set_log_dir($username = null) { if (empty($username)) { $username = $this->username; } if (empty($username)) { return; } $this->logger->set_username($username); $user_debug = (bool) $this->config->get('per_user_logging'); if (!$user_debug) { return; } $log_dir = $this->log_dir . DIRECTORY_SEPARATOR . $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']; } elseif ( !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() { if (php_sapi_name() == 'cli') { throw new Exception("LDAP/IMAP error on authentication"); } 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); $this->config->set('log_dir', $this->log_dir); // make sure logged numbers use unified format setlocale(LC_NUMERIC, 'en_US.utf8', 'en_US.UTF-8', 'en_US', 'C'); $mem = ''; 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 = []; } // Reset internal cache of the storage class self::storage()->reset(); } } diff --git a/tests/Sync/OptionsTest.php b/tests/Sync/OptionsTest.php index 3954e7d..06ac2fb 100644 --- a/tests/Sync/OptionsTest.php +++ b/tests/Sync/OptionsTest.php @@ -1,16 +1,16 @@ request('OPTIONS', ''); + $response = self::$client->request('OPTIONS', '', ['auth' => null]); $this->assertEquals(200, $response->getStatusCode()); $this->assertStringContainsString('14', $response->getHeader('MS-Server-ActiveSync')[0]); $this->assertStringContainsString('14.1', $response->getHeader('MS-ASProtocolVersions')[0]); $this->assertStringContainsString('FolderSync', $response->getHeader('MS-ASProtocolCommands')[0]); } }