From d97da6d309033d406872c76b68b797b87da5d623 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 28 Jan 2013 22:00:33 +0000 Subject: [PATCH] modifications for new iSchedule draft: - ischedule-relaxed header cannonisation - error xml response - modified capabilities with serial and iSchedule-Capabilities header in every response - using urls with mailto: schema for Originator and Recipient headers --- phpgwapi/inc/class.ischedule_client.inc.php | 18 ++-- phpgwapi/inc/class.ischedule_server.inc.php | 95 ++++++++++++++------- phpgwapi/ischedule-cli.php | 7 +- phpgwapi/ischedule.php | 2 +- 4 files changed, 83 insertions(+), 39 deletions(-) diff --git a/phpgwapi/inc/class.ischedule_client.inc.php b/phpgwapi/inc/class.ischedule_client.inc.php index 6d2a2dd064..fd279c2312 100644 --- a/phpgwapi/inc/class.ischedule_client.inc.php +++ b/phpgwapi/inc/class.ischedule_client.inc.php @@ -66,7 +66,7 @@ class ischedule_client public function __construct($recipients, $url=null) { $this->recipients = (array)$recipients; - $this->originator = $GLOBALS['egw_info']['user']['account_email']; + $this->originator = 'mailto:'.$GLOBALS['egw_info']['user']['account_email']; if (is_null($url)) { @@ -122,7 +122,7 @@ class ischedule_client { throw new Exception("Invalid orginator '$originator'!"); } - $this->originator = $originator; + $this->originator = 'mailto:'.$originator; if (!is_null($dkim_private_key)) { @@ -221,15 +221,17 @@ class ischedule_client //'follow_location' => 1, // default 1=follow, but only for GET, not POST! //'timeout' => $timeout, // max timeout in seconds (float) 'content' => $content, + 'ignore_errors' => true, // return response, even for http-status != 2xx ) ); if ($debug) echo "POST $this->url HTTP/1.1\n$header_string\n$content\n"; // need to suppress warning, if http-status not 2xx - if (($response = @file_get_contents($this->url, false, stream_context_create($opts))) === false) + $response = @file_get_contents($this->url, false, stream_context_create($opts)); + list(, $code, $message) = explode(' ', $http_response_header[0], 3); + if ($code[0] !== '2') { - list(, $code, $message) = explode(' ', $http_response_header[0], 3); if ($max_redirect && $code[0] === '3') { foreach($http_response_header as $header) @@ -248,6 +250,10 @@ class ischedule_client } } } + if ($debug) echo implode("\r\n", $http_response_header)."\r\n\r\n".$response; + + if (preg_match('|(.*)|', $response, $matches)) $message .= ': '.$matches[1]; + throw new Exception($message, $code); } return $response; @@ -278,12 +284,10 @@ class ischedule_client $header_names[] = $header; } } -error_log(__METHOD__."(".array2string($headers).", \$body, '$selector', '$sign_headers') header_names=".array2string($header_names).', header_values='.array2string($header_values)); - include_once EGW_API_INC.'/php-mail-domain-signer/lib/class.mailDomainSigner.php'; list(,$domain) = explode('@', $this->originator); $mds = new mailDomainSigner($this->dkim_private_key, $domain, $selector); // generate DKIM signature according to iSchedule spec - $dkim = $mds->getDKIM(implode(':', $header_names), $header_values, $body, 'relaxed/simple', 'rsa-sha256', + $dkim = $mds->getDKIM(implode(':', $header_names), $header_values, $body, 'ischedule-relaxed/simple', 'rsa-sha256', "DKIM-Signature: ". "v=1; ". // DKIM Version "a=\$a; ". // The algorithm used to generate the signature "rsa-sha1" diff --git a/phpgwapi/inc/class.ischedule_server.inc.php b/phpgwapi/inc/class.ischedule_server.inc.php index c14f51cd37..131bcfaf9b 100644 --- a/phpgwapi/inc/class.ischedule_server.inc.php +++ b/phpgwapi/inc/class.ischedule_server.inc.php @@ -63,7 +63,6 @@ class ischedule_server extends groupdav // get raw request body $this->request = file_get_contents('php://input'); - switch($_SERVER['REQUEST_METHOD']) { case 'GET': @@ -124,7 +123,19 @@ class ischedule_server extends groupdav if (($missing = array_diff(explode(':', strtolower(self::REQUIRED_DKIM_HEADERS.':DKIM-Signature')), array_keys($headers)))) { //error_log('headers='.array2string(array_keys($headers)).', required='.self::REQUIRED_DKIM_HEADERS.', missing='.array($missing)); - throw new Exception ('Bad Request: missing required headers: '.implode(', ', $missing), 400); + if (in_array('originator', $missing)) + { + $error = 'originator-missing'; + } + elseif(in_array('recipient', $missing)) + { + $error = 'recipient-missing'; + } + else + { + $error = 'invalid-scheduling-message'; + } + throw new Exception ("Bad Request: $error: missing required headers: ".implode(', ', $missing), 400); } // validate dkim signature @@ -133,13 +144,17 @@ class ischedule_server extends groupdav // It will fail if multiple recipients in a single header are also ", " separated (just comma works fine) if (!self::dkim_validate($headers, $this->request, $error)) { - throw new Exception('Bad Request: DKIM signature invalid: '.$error, 400); + throw new Exception('Bad Request: verification-failed: DKIM signature invalid: '.$error, 400); } // check if recipient is a user - // ToDo: multiple recipients - if (!($account_id = $GLOBALS['egw']->accounts->name2id($headers['recipient'], 'account_email'))) + // todo: multiple recipients, currently we use last recipient for EGroupware enviroment + foreach(preg_split('/, */', $headers['recipient']) as $recipient) { - throw new Exception('Bad Request: unknown recipient', 400); + if (!stripos($recipient, 'mailto:') === 0 || + !($account_id = $GLOBALS['egw']->accounts->name2id(substr($recipient, 7), 'account_email'))) + { + throw new Exception("Bad Request: recipient-missing: unknown recipient '$recipient'", 400); + } } // create enviroment for recipient user, as we act on his behalf $GLOBALS['egw']->session->account_id = $account_id; @@ -162,7 +177,7 @@ class ischedule_server extends groupdav if (!preg_match('/component=([^;]+)/i', $headers['content-type'], $matches) || (!in_array($component=strtoupper($matches[1]), self::$supported_components))) { - throw new Exception ('Bad Request: missing or unsupported component in Content-Type header', 400); + throw new Exception ('Bad Request: invalid-calendar-data-type: missing or unsupported component in Content-Type header', 400); } if (!preg_match('/method=([^;]+)/i', $headers['content-type'], $matches) || (!isset(self::$supported_method2origin_requirement[$method=strtoupper($matches[1])])) || @@ -196,12 +211,15 @@ class ischedule_server extends groupdav $matches = false; foreach($originator_requirement as $requirement) { + $originator = $headers['originator']; + if (stripos($originator, 'mailto:') === 0) $originator = substr($originator, 7); + if ($requirement == 'ORGANIZER' && - ($event['organizer'] == $headers['originator'] || strpos($event['organizer'], '<'.$headers['originator'].'>') !== false) || + ($event['organizer'] == $originator || strpos($event['organizer'], '<'.$originator.'>') !== false) || $requirement == 'ATTENDEE' && - (in_array('e'.$headers['originator'], $event['participants']) || + (in_array('e'.$originator, $event['participants']) || // ToDO: Participant could have CN, need to check that too - $originator_account_id = $GLOBALS['egw']->accounts->name2id($headers['originator'], 'account_email') && + $originator_account_id = $GLOBALS['egw']->accounts->name2id($originator, 'account_email') && in_array($originator_account_id, $event['participants']))) { $matches = true; @@ -210,7 +228,7 @@ class ischedule_server extends groupdav } if (!$matches) { - throw new Exception('Bad Request: originator invalid for given '.$component.'!', 400); + throw new Exception("Bad Request: originator-invalid: originator '$originator' invalid for given $component component!", 400); } } @@ -239,6 +257,7 @@ class ischedule_server extends groupdav header('Content-Type: text/xml; charset=UTF-8'); header('iSchedule-Version: '.self::VERSION); + header('iSchedule-Capabilities: '.self::SERIAL); echo $xml->outputMemory(); } @@ -337,11 +356,10 @@ class ischedule_server extends groupdav * @param array $headers header-name in lowercase(!) as key * @param string $body * @param string &$error=null error if false returned - * @param boolean $split_recipients=null true=split recpients in multiple headers, false dont, default null try both * @return boolean true if signature could be validated, false otherwise * @todo other dkim q= methods: http/well-known bzw private-exchange */ - public static function dkim_validate(array $headers, $body, &$error=null, $split_recipients=null) + public static function dkim_validate(array $headers, $body, &$error=null) { // parse dkim signature if (!isset($headers['dkim-signature']) || @@ -377,14 +395,6 @@ class ischedule_server extends groupdav $value = $check[$header]; unset($check[$header]); - // special handling of multivalued recipient header - if ($header == 'recipient' && (!isset($split_recipients) || $split_recipients)) - { - if (!is_array($value)) $value = explode(', ', $value); - $v = array_pop($value); // dkim uses reverse order! - if ($value) $check[$header] = $value; - $value = $v; - } $dkim_headers[] = $header.': '.$value; } // dkim signature is obvious without content of signature, but must not necessarly be last tag @@ -392,7 +402,6 @@ class ischedule_server extends groupdav // c defaults to 'simple/simple', check on valid canonicalization methods is performed further down list($header_canon, $body_canon) = explode('/', isset($dkim['c']) ? $dkim['c'] : 'simple/simple'); - require_once EGW_API_INC.'/php-mail-domain-signer/lib/class.mailDomainSigner.php'; // Canonicalization for Body switch($body_canon) @@ -436,6 +445,10 @@ class ischedule_server extends groupdav $_unsigned = mailDomainSigner::headRelaxCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned); break; + case 'ischedule-relaxed': + $_unsigned = mailDomainSigner::headIScheduleRelaxCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned); + break; + case 'simple': $_unsigned = mailDomainSigner::headSimpleCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned); break; @@ -477,7 +490,7 @@ class ischedule_server extends groupdav return false; } $ok = openssl_verify($_unsigned, base64_decode($dkim['b']), $public_key, $hash_algo); - error_log(__METHOD__."() openssl_verify('$_unsigned', ..., '$public_key', '$hash_algo') returned ".array2string($ok)); + if ($ok != 1) error_log(__METHOD__."() openssl_verify('$_unsigned', ..., '$public_key', '$hash_algo') returned ".array2string($ok)); switch($ok) { @@ -487,11 +500,6 @@ class ischedule_server extends groupdav case 0: $error = 'DKIM signature does NOT verify'; - // if dkim did not validate, try not splitting Recipient header - if (!isset($split_recipients)) - { - return self::dkim_validate($headers, $body, $error, false); - } return false; } @@ -526,6 +534,14 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCiawhLuTSVhnl1zz5pXs1A748y N3aNE181dni8nsYqIQB1h4H32J4dZurEiAnP9nflQRjCmmg1NTvFcNz11Bem4zo1 K4r4mcfbjlheorK2Mwoh445HR3fo/pP7uV6CcXTNboBJLTxs6ZHswmQjxyuKBKmx yXUKsIQVi3qPyPdB3QIDAQAB +-----END PUBLIC KEY-----', + ), + 'outdoor-training.de' => array( + 'calendar' => '-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCiawhLuTSVhnl1zz5pXs1A748y +N3aNE181dni8nsYqIQB1h4H32J4dZurEiAnP9nflQRjCmmg1NTvFcNz11Bem4zo1 +K4r4mcfbjlheorK2Mwoh445HR3fo/pP7uV6CcXTNboBJLTxs6ZHswmQjxyuKBKmx +yXUKsIQVi3qPyPdB3QIDAQAB -----END PUBLIC KEY-----', ), ); @@ -745,8 +761,29 @@ yXUKsIQVi3qPyPdB3QIDAQAB // exception handler sending message back to the client as http status $code = $e->getCode(); $msg = $e->getMessage(); + list($http_status, $error, $description) = explode(': ', $msg, 3); + // check if we have a valid iSchedule error element, if not we use invalid-scheduling-message + if (!empty($error) && strpos($error, ' ') !== false) + { + $description = $error.($description ? ': '.$description : ''); + $error = 'invalid-scheduling-message'; + } if (!in_array($code, array(400, 403, 407, 503))) $code = 500; - header('HTTP/1.1 '.$code.' '.$msg, true, $code); + + header('HTTP/1.1 '.$code.' '.$http_status, true, $code); + header('Content-Type: text/xml; charset=UTF-8'); + header('iSchedule-Version: '.self::VERSION); + header('iSchedule-Capabilities: '.self::SERIAL); + + if ($error) + { + echo ' + + <'.$error.' /> + '.htmlspecialchars($description).' + +'; + } // if our groupdav logging is active, log the request plus a trace, if enabled in server-config if (groupdav::$request_starttime && isset(self::$instance)) diff --git a/phpgwapi/ischedule-cli.php b/phpgwapi/ischedule-cli.php index 5de39a9802..01d2f89b56 100755 --- a/phpgwapi/ischedule-cli.php +++ b/phpgwapi/ischedule-cli.php @@ -17,7 +17,7 @@ if (isset($_SERVER['HTTP_HOST'])) die("This is a commandline ONLY tool!\n"); /** * iSchedule command line client, primary for testing and development purpose * - * @link https://tools.ietf.org/html/draft-desruisseaux-ischedule-01 iSchedule draft from 2010 + * @link https://tools.ietf.org/html/draft-desruisseaux-ischedule-03 iSchedule draft from 2013-01-22 */ function usage($err=null) { @@ -40,6 +40,9 @@ $GLOBALS['egw_info'] = array( 'currentapp' => 'login', ) ); +// set a domain for mserver +$_REQUEST['domain'] = 'ralfsmacbook.local'; + // if you move this file somewhere else, you need to adapt the path to the header! $egw_dir = dirname(dirname(__FILE__)); include($egw_dir.'/header.inc.php'); @@ -122,5 +125,5 @@ try { } } catch(Exception $e) { - echo "\n".($e->getCode() ? $e->getCode().' ' : '').$e->getMessage()."\n\n"; + if (!$verbose) echo "\n".($e->getCode() ? $e->getCode().' ' : '').$e->getMessage()."\n\n"; } \ No newline at end of file diff --git a/phpgwapi/ischedule.php b/phpgwapi/ischedule.php index 27f7352e39..442320a91e 100644 --- a/phpgwapi/ischedule.php +++ b/phpgwapi/ischedule.php @@ -14,7 +14,7 @@ /** * iSchedule server: serverside of iSchedule * - * @link https://tools.ietf.org/html/draft-desruisseaux-ischedule-01 iSchedule draft from 2010 + * @link https://tools.ietf.org/html/draft-desruisseaux-ischedule-03 iSchedule draft from 2013-01-22 */ $GLOBALS['egw_info'] = array(