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]);
}
}