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
This commit is contained in:
Ralf Becker 2013-01-28 22:00:33 +00:00
parent 2fadcab928
commit d97da6d309
4 changed files with 83 additions and 39 deletions

View File

@ -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-description>(.*)</response-description>|', $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"

View File

@ -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 '<?xml version="1.0" encoding="utf-8" ?>
<error xmlns="urn:ietf:params:xml:ns:ischedule">
<'.$error.' />
<response-description>'.htmlspecialchars($description).'</response-description>
</error>
';
}
// if our groupdav logging is active, log the request plus a trace, if enabled in server-config
if (groupdav::$request_starttime && isset(self::$instance))

View File

@ -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";
}

View File

@ -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(