diff --git a/lib/AutodiscoverJson.php b/lib/AutodiscoverJson.php index 936c71f..c514284 100644 --- a/lib/AutodiscoverJson.php +++ b/lib/AutodiscoverJson.php @@ -1,98 +1,134 @@ | | | | 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: Daniel Hoffend | +--------------------------------------------------------------------------+ */ /** * Autodiscover Service class for Microsoft Autodiscover V2 */ class AutodiscoverJson extends Autodiscover { + /** + * process incoming request + */ public function handle_request() { Log::debug('Request [json]: ' . $_SERVER['REQUEST_URI']); - $supportedProtocols = array('autodiscoverv1' => 'AutodiscoverV1'); - if ($this->conf->get('autodiscover', 'activesync')) { - $supportedProtocols['activesync'] = 'ActiveSync'; + // check protocol (at this state we don't know if autodiscover is configured) + $allowedProtocols = ['activesync', 'autodiscoverv1']; + if (empty($_GET['Protocol'])) { + $this->error( + "A valid value must be provided for the query parameter 'Protocol'", + 'MandatoryParameterMissing' + ); } - - $protocol = isset($_GET['Protocol']) ? $_GET['Protocol'] : ''; - - // Exit early on unsupported protocol - if (empty($protocol) || !isset($supportedProtocols[strtolower($protocol)])) { - $json = array( - 'ErrorCode' => 'ProtocolNotSupported', - 'ErrorMessage' => 'The given protocol value \u0027' . $protocol . '\u0027 is invalid.' - . ' Supported values are \u0027' . implode(',', $supportedProtocols) . '\u0027' + elseif (!in_array(strtolower($_GET['Protocol']), $allowedProtocols)) { + $this->error( + sprintf( + "The given protocol value '%s' is invalid. Supported values are '%s'", + $_GET['Protocol'], + implode(",", $allowedProtocols) + ), + 'InvalidProtocol' ); - - $response = json_encode($json, JSON_PRETTY_PRINT); - Log::debug('Response [json]: ' . $response); - - http_response_code(400); - header('Content-Type: application/json; charset=' . Autodiscover::CHARSET); - echo $response; - exit; } + // check email if (preg_match('|autodiscover.json/v1.0/([^\?]+)|', $_SERVER['REQUEST_URI'], $regs)) { $this->email = $regs[1]; } - else if (!empty($_GET['Email'])) { + elseif (!empty($_GET['Email'])) { $this->email = $_GET['Email']; } + elseif (!empty($_GET['email'])) { + $this->email = $_GET['email']; + } + + if (empty($this->email) || !strpos($this->email, '@')) { + $this->error( + 'A valid smtp address must be provided', + 'MandatoryParameterMissing' + ); + } } /** * Generates JSON response */ protected function handle_response() { - if (strtolower($_GET['Protocol']) == 'activesync' - && !empty($this->config['activesync']) - ) { + if (strtolower($_GET['Protocol']) == 'activesync') { + // throw error if activesync is disabled + if (empty($this->config['activesync'])) { + $this->error( + sprintf( + "The given protocol value '%s' is invalid. Supported values are '%s'", + $_GET['Protocol'], 'autodiscoverv1' + ), + 'InvalidProtocol' + ); + } + if (!preg_match('/^https?:/i', $this->config['activesync'])) { $this->config['activesync'] = 'https://' . $this->config['activesync'] . '/Microsoft-Server-ActiveSync'; } $json = array( 'Protocol' => 'ActiveSync', 'Url' => $this->config['activesync'] ); } elseif (strtolower($_GET['Protocol']) == 'autodiscoverv1') { $json = array( 'Protocol' => 'AutodiscoverV1', 'Url' => 'https://' . $_SERVER['HTTP_HOST'] . '/Autodiscover/Autodiscover.xml' ); } - $response = json_encode($json, JSON_PRETTY_PRINT); + $response = json_encode($json, JSON_PRETTY_PRINT | JSON_HEX_APOS | JSON_HEX_QUOT); Log::debug('Response [json]: ' . $response); header('Content-Type: application/json; charset=' . Autodiscover::CHARSET); echo $response; exit; } + + /** + * Send error to the client and exit + */ + protected function error($msg, $code="InternalServerError") + { + http_response_code(400); + $json = array( + 'ErrorCode' => $code, + 'ErrorMessage' => $msg + ); + $response = json_encode($json, JSON_PRETTY_PRINT | JSON_HEX_APOS | JSON_HEX_QUOT); + Log::debug('Error [json]: ' . $response); + header('Content-Type: application/json; charset=' . Autodiscover::CHARSET); + echo $response; + exit; + } + } diff --git a/lib/AutodiscoverMicrosoft.php b/lib/AutodiscoverMicrosoft.php index 6167ecc..da2c7b7 100644 --- a/lib/AutodiscoverMicrosoft.php +++ b/lib/AutodiscoverMicrosoft.php @@ -1,273 +1,309 @@ | | | | 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 | +--------------------------------------------------------------------------+ */ /** * Autodiscover Service class for Microsoft Outlook and Activesync devices */ class AutodiscoverMicrosoft extends Autodiscover { const NS = "http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"; const RESPONSE_NS = "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"; const MOBILESYNC_NS = "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006"; private $type = 'outlook'; private $password; /** * Handle request parameters (find email address) */ protected function handle_request() { $post = $_SERVER['REQUEST_METHOD'] == 'POST' ? file_get_contents('php://input') : null; // check for request object Log::debug('Request [microsoft]: ' . $post); if (empty($post)) { $this->error("Invalid input"); } // parse XML try { $xml = new SimpleXMLElement($post); $ns = $xml->getDocNamespaces(); if (empty($ns) || empty($ns[''])) { $this->error("Invalid input. Missing XML request schema"); } $xml->registerXPathNamespace('request', $ns['']); if ($email = $xml->xpath('//request:EMailAddress')) { $this->email = (string) array_shift($email); } if ($schema = $xml->xpath('//request:AcceptableResponseSchema')) { $schema = (string) array_shift($schema); if (strpos($schema, 'mobilesync')) { $this->type = 'mobilesync'; } } } catch (Exception $e) { $this->error("Invalid input"); } if ($this->conf->get('autodiscover', 'mobilesync_only') && $this->type != 'mobilesync') { $this->error("Only mobilesync schema supported"); } // check for basic authentication if ldap is available if ($this->conf->get('ldap_uri', false)) { Log::debug('Request [microsoft]: Basic Auth Username: ' . ($_SERVER['PHP_AUTH_USER'] ?: 'none')); if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) { $this->unauthorized(); } // basic auth username must match with given email address if (strcasecmp($_SERVER['PHP_AUTH_USER'], $this->email) != 0) { Log::debug("The submitted user {$_SERVER['PHP_AUTH_USER']} does not match the email address {$this->email}"); $this->unauthorized(); } $this->password = $_SERVER['PHP_AUTH_PW']; } } /** * Handle response */ public function handle_response() { if (!empty($this->_ldap_server)) { // authenticate the user found during configure() against ldap if (empty($this->config['dn']) || !$this->authenticate($this->config['dn'], $this->password)) { $this->unauthorized(); } } $method = $this->type . '_response'; $xml = $this->$method(); $xml->formatOutput = true; $response = $xml->saveXML(); Log::debug('Response [microsoft]: ' . $response); header('Content-type: text/xml; charset=' . Autodiscover::CHARSET); echo $response; exit; } + /** + * Send error to the client and exit + */ + protected function error($msg) + { + $xml = new DOMDocument('1.0', Autodiscover::CHARSET); + $doc = $xml->createElementNS(self::NS, 'Autodiscover'); + $doc = $xml->appendChild($doc); + + $response = $xml->createElement('Response'); + $response = $doc->appendChild($response); + + $error = $xml->createElement('Error'); + list($usec, $sec) = explode(' ', microtime()); + $error->setAttribute('Time',date('H:i:s',$sec).".".substr($usec, 2, 6)); + $error->setAttribute('Id',sprintf("%u",crc32($_SERVER['HTTP_HOST']))); + $response->appendChild($error); + + $code = $xml->createElement('ErrorCode'); + $code->appendChild($xml->createTextNode(600)); + $error->appendChild($code); + + $message = $xml->createElement('Message'); + $message->appendChild($xml->createTextNode($msg)); + $error->appendChild($message); + + $response->appendChild($xml->createElement('DebugData')); + + $xml->formatOutput = true; + Log::debug('Error [microsoft]: ' . $msg); + + header('Content-type: text/xml; charset=' . Autodiscover::CHARSET); + echo $xml->saveXML(); + exit; + } + /** * Generates XML response for Activesync */ protected function mobilesync_response() { if (empty($this->config['activesync'])) { $this->error("Activesync not supported"); } if (!preg_match('/^https?:/i', $this->config['activesync'])) { $this->config['activesync'] = 'https://' . $this->config['activesync'] . '/Microsoft-Server-ActiveSync'; } $xml = new DOMDocument('1.0', Autodiscover::CHARSET); // create main elements (tree) $doc = $xml->createElementNS(self::NS, 'Autodiscover'); $doc = $xml->appendChild($doc); $response = $xml->createElementNS(self::MOBILESYNC_NS, 'Response'); $response = $doc->appendChild($response); $user = $xml->createElement('User'); $user = $response->appendChild($user); $action = $xml->createElement('Action'); $action = $response->appendChild($action); $settings = $xml->createElement('Settings'); $settings = $action->appendChild($settings); $server = $xml->createElement('Server'); $server = $settings->appendChild($server); // configuration $dispname = $xml->createElement('DisplayName'); $dispname = $user->appendChild($dispname); $dispname->appendChild($xml->createTextNode($this->config['username'] ?? null)); $email = $xml->createElement('EMailAddress'); $email = $user->appendChild($email); $email->appendChild($xml->createTextNode(($this->config['login'] ?? false) ?: $this->config['email'])); $element = $xml->createElement('Type'); $element = $server->appendChild($element); $element->appendChild($xml->createTextNode('MobileSync')); $element = $xml->createElement('Url'); $element = $server->appendChild($element); $element->appendChild($xml->createTextNode($this->config['activesync'])); $element = $xml->createElement('Name'); $element = $server->appendChild($element); $element->appendChild($xml->createTextNode($this->config['activesync'])); return $xml; } /** * Generates XML response for Outlook */ protected function outlook_response() { $xml = new DOMDocument('1.0', Autodiscover::CHARSET); // create main elements (tree) $doc = $xml->createElementNS(self::NS, 'Autodiscover'); $doc = $xml->appendChild($doc); $response = $xml->createElementNS(self::RESPONSE_NS, 'Response'); $response = $doc->appendChild($response); $user = $xml->createElement('User'); $user = $response->appendChild($user); $account = $xml->createElement('Account'); $account = $response->appendChild($account); $accountType = $xml->createElement('AccountType'); $accountType = $account->appendChild($accountType); $accountType->appendChild($xml->createTextNode('email')); $action = $xml->createElement('Action'); $action = $account->appendChild($action); $action->appendChild($xml->createTextNode('settings')); // configuration $dispname = $xml->createElement('DisplayName'); $dispname = $user->appendChild($dispname); $dispname->appendChild($xml->createTextNode($this->config['username'] ?? null)); $email = $xml->createElement('AutoDiscoverSMTPAddress'); $email = $user->appendChild($email); $email->appendChild($xml->createTextNode(($this->config['login'] ?? false) ?: $this->config['email'])); // @TODO: Microsoft supports also DAV protocol here foreach (array('imap', 'pop3', 'smtp') as $type) { if (!empty($this->config[$type])) { $protocol = $this->add_protocol_element($xml, $type, $this->config[$type]); $account->appendChild($protocol); } } return $xml; } /** * Creates Protocol element for XML response */ private function add_protocol_element($xml, $type, $config) { $protocol = $xml->createElement('Protocol'); $element = $xml->createElement('Type'); $element = $protocol->appendChild($element); $element->appendChild($xml->createTextNode(strtoupper($type))); // @TODO: TTL/ExpirationDate tags // server attributes map $server_attributes = array( 'Server' => 'hostname', 'Port' => 'port', 'LoginName' => 'username', ); foreach ($server_attributes as $tag_name => $conf_name) { $value = $this->config[$type][$conf_name]; if (!empty($value)) { $element = $xml->createElement($tag_name); $element->appendChild($xml->createTextNode($value)); $protocol->appendChild($element); } } $spa = $this->config[$type]['authentication'] == 'password-encrypted' ? 'on' : 'off'; $element = $xml->createElement('SPA'); $element->appendChild($xml->createTextNode($spa)); $protocol->appendChild($element); $map = array('STARTTLS' => 'TLS', 'SSL' => 'SSL', 'plain' => 'None'); $element = $xml->createElement('Encryption'); $element->appendChild($xml->createTextNode($map[$this->config[$type]['socketType']] ?: 'Auto')); $protocol->appendChild($element); return $protocol; } }