17 require_once(
"XMLDocument.php");
32 private $try_anyway =
false;
33 private $failed =
false;
34 private $failOnError =
true;
35 private $subdomainsOK =
true;
36 private $remote_public_key ;
37 private $required_headers = Array (
'host',
41 private $disallowed_headers = Array (
'connection',
45 'proxy-authorization',
51 function __construct ( )
54 $this->selector =
'cal';
55 if ( is_object ( $c ) && isset ( $c->scheduling_dkim_selector ) )
57 $this->scheduling_dkim_domain = $c->scheduling_dkim_domain ;
58 $this->scheduling_dkim_selector = $c->scheduling_dkim_selector ;
59 $this->schedule_private_key = $c->schedule_private_key ;
60 if ( ! preg_match (
'/BEGIN RSA PRIVATE KEY/', $this->schedule_private_key ) )
62 $key = file_get_contents ( $this->schedule_private_key );
64 $this->schedule_private_key = $key;
66 if ( isset ( $c->scheduling_dkim_algo ) )
67 $this->scheduling_dkim_algo = $c->scheduling_dkim_algo;
69 $this->scheduling_dkim_algo =
'sha256';
70 if ( isset ( $c->scheduling_dkim_valid_time ) )
71 $this->valid_time = $c->scheduling_dkim_valid_time;
82 if ( $icfg [ $this->remote_selector .
'._domainkey.' . $this->remote_server ] )
84 $this->dk = $icfg [ $this->remote_selector .
'._domainkey.' . $this->remote_server ];
88 $dkim = dns_get_record ( $this->remote_selector .
'._domainkey.' . $this->remote_server , DNS_TXT );
89 if ( count ( $dkim ) > 0 )
91 $this->dk = $dkim [ 0 ] [
'txt' ];
92 if ( $dkim [ 0 ] [
'entries' ] )
95 foreach ( $dkim [ 0 ] [
'entries' ] as $v )
97 $this->dk .= trim ( $v );
100 dbg_error_log(
'ischedule',
'getTxt '. $this->dk .
' XX');
104 dbg_error_log(
'ischedule',
'getTxt FAILED '. print_r ( $dkim ) );
105 $this->failed =
true;
124 if ( $this->failed ==
true )
126 $clean = preg_replace (
'/\s?([;=])\s?/',
'$1', $this->dk );
127 $pairs = preg_split (
'/;/', $clean );
128 $this->parsed = array();
129 foreach ( $pairs as $v )
131 list($key,$value) = preg_split (
'/=/', $v, 2 );
132 $value = trim ( $value,
'\\' );
133 if ( preg_match (
'/(g|k|n|p|s|t|v)/', $key ) )
134 $this->parsed [ $key ] = $value;
136 $this->parsed_ignored [ $key ] = $value;
146 $this->failed =
true;
147 if ( isset ( $this->parsed [
's' ] ) )
149 if ( ! preg_match (
'/(\*|calendar)/', $this->parsed [
's' ] ) ) {
150 dbg_error_log(
'ischedule',
'validateKey ERROR: bad selector' );
154 if ( isset ( $this->parsed [
'k' ] ) && $this->parsed [
'k' ] !=
'rsa' ) {
155 dbg_error_log(
'ischedule',
'validateKey ERROR: bad key algorythm, algo was:' . $this->parsed [
'k' ] );
158 if ( isset ( $this->parsed [
't' ] ) && ! preg_match (
'/^[y:s]+$/', $this->parsed [
't' ] ) ) {
159 dbg_error_log(
'ischedule',
'validateKey ERROR: type mismatch' );
164 if ( preg_match (
'/y/', $this->parsed [
't' ] ) )
165 $this->failOnError =
false;
166 if ( preg_match (
'/s/', $this->parsed [
't' ] ) )
167 $this->subdomainsOK =
false;
169 if ( isset ( $this->parsed [
'g' ] ) )
170 $this->remote_user_rule = $this->parsed [
'g' ];
172 $this->remote_user_rule =
'*';
173 if ( isset ( $this->parsed [
'p' ] ) )
175 if ( preg_match (
'/[^A-Za-z0-9_=+\/]/', $this->parsed [
'p' ] ) )
177 $data =
"-----BEGIN PUBLIC KEY-----\n" . implode (
"\n",str_split ( $this->parsed [
'p' ], 64 )) .
"\n-----END PUBLIC KEY-----";
178 if ( $data ===
false )
180 $this->remote_public_key = $data;
183 dbg_error_log(
'ischedule',
'validateKey ERROR: no key in dns record' . $this->parsed [
'p' ] );
186 $this->failed =
false;
196 if ( $icfg [ $this->domain ] )
198 $this->remote_server = $icfg [ $this->domain ] [
'server' ];
199 $this->remote_port = $icfg [ $this->domain ] [
'port' ];
200 $this->remote_ssl = $icfg [ $this->domain ] [
'ssl' ];
203 $this->remote_ssl =
false;
204 $parts = explode (
'.', $this->domain );
205 $tld = $parts [ count ( $parts ) - 1 ];
207 if ( strlen ( $tld ) == 2 && in_array ( $tld, Array (
'uk',
'nz' ) ) )
209 if ( $this->domain ==
'mycaldav' || $this->domain ==
'altcaldav' )
211 while ( count ( $parts ) >= $len )
213 $r = dns_get_record (
'_ischedules._tcp.' . implode (
'.', $parts ) , DNS_SRV );
214 if ( 0 < count ( $r ) )
216 $remote_server = $r [ 0 ] [
'target' ];
217 $remote_port = $r [ 0 ] [
'port' ];
218 $this->remote_ssl =
true;
221 if ( ! isset ( $remote_server ) )
223 $r = dns_get_record (
'_ischedule._tcp.' . implode (
'.', $parts ) , DNS_SRV );
224 if ( 0 < count ( $r ) )
226 $remote_server = $r [ 0 ] [
'target' ];
227 $remote_port = $r [ 0 ] [
'port' ];
231 array_shift ( $parts );
233 if ( ! isset ( $remote_server ) )
235 if ( $this->try_anyway ==
true )
237 if ( ! isset ( $remote_server ) )
238 $remote_server = $this->domain;
239 if ( ! isset ( $remote_port ) )
243 dbg_error_log(
'ischedule',
'Domain %s did not have srv records for iSchedule', $this->domain );
247 dbg_error_log(
'ischedule', $this->domain .
' found srv records for ' . $remote_server .
':' . $remote_port );
248 $this->remote_server = $remote_server;
249 $this->remote_port = $remote_port;
258 if ( $domain != null && $this->domain != $domain )
259 $this->domain = $domain;
260 if ( ! isset ( $this->remote_server ) && isset ( $this->domain ) && ! $this->
getServer ( ) )
262 $this->remote_url =
'http'. ( $this->remote_ssl ?
's' :
'' ) .
'://' .
263 $this->remote_server .
':' . $this->remote_port .
'/.well-known/ischedule';
264 $remote_capabilities = file_get_contents ( $this->remote_url .
'?query=capabilities' );
265 if ( $remote_capabilities ===
false )
267 $xml_parser = xml_parser_create_ns(
'UTF-8');
268 $this->xml_tags = array();
269 xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
270 xml_parser_set_option ( $xml_parser, XML_OPTION_CASE_FOLDING, 0 );
271 $rc = xml_parse_into_struct( $xml_parser, $remote_capabilities, $this->xml_tags );
272 if ( $rc ==
false ) {
273 dbg_error_log(
'ERROR',
'XML parsing error: %s at line %d, column %d',
274 xml_error_string(xml_get_error_code($xml_parser)),
275 xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser) );
276 dbg_error_log(
'ischedule', $this->domain .
' iSchedule error parsing remote xml' );
279 xml_parser_free($xml_parser);
280 $xmltree = BuildXMLTree( $this->xml_tags );
281 if ( !is_object($xmltree) ) {
282 dbg_error_log(
'ischedule', $this->domain .
' iSchedule error in remote xml' );
283 $request->DoResponse( 406, translate(
"REPORT body is not valid XML data!") );
286 dbg_error_log(
'ischedule', $this->domain .
' got capabilites' );
287 $this->capabilities_xml = $xmltree;
296 if ( ! isset ( $this->capabilities_xml ) )
298 dbg_error_log(
'ischedule', $this->domain .
' capabilities not set, quering for capability:' . $capability );
299 if ( $domain == null )
301 if ( $this->domain != $domain )
302 $this->domain = $domain;
306 switch ( $capability )
311 $comp = $this->capabilities_xml->GetPath (
'urn:ietf:params:xml:ns:ischedule:supported-scheduling-message-set/urn:ietf:params:xml:ns:ischedule:comp' );
312 foreach ( $comp as $c )
314 if ( $c->GetAttribute (
'name' ) == $capability )
318 case 'VFREEBUSY/REQUEST':
320 case 'VTODO/REQUEST':
324 case 'VEVENT/REQUEST':
326 case 'VEVENT/CANCEL':
327 case 'VEVENT/PUBLISH':
328 case 'VEVENT/COUNTER':
329 case 'VEVENT/DECLINECOUNTER':
330 dbg_error_log(
'ischedule', $this->domain .
' xml query' );
331 $comp = $this->capabilities_xml->GetPath (
'urn:ietf:params:xml:ns:ischedule:supported-scheduling-message-set/urn:ietf:params:xml:ns:ischedule:comp' );
332 list ( $component, $method ) = explode (
'/', $capability );
333 dbg_error_log(
'ischedule', $this->domain .
' quering for capability:' . count ( $comp ) .
' ' . $component );
334 foreach ( $comp as $c )
336 dbg_error_log(
'ischedule', $this->domain .
' quering for capability:' . $c->GetAttribute (
'name' ) .
' == ' . $component );
337 if ( $c->GetAttribute (
'name' ) == $component )
339 $methods = $c->GetElements (
'urn:ietf:params:xml:ns:ischedule:method' );
340 if ( count ( $methods ) == 0 )
342 foreach ( $methods as $m )
344 if ( $m->GetAttribute (
'name' ) == $method )
363 if ( $this->scheduling_dkim_domain == null )
366 if ( is_array ( $headers ) !==
true )
368 foreach ( $headers as $key => $value )
370 $b .= $key .
': ' . $value .
"\r\n";
373 $dk[
'a'] =
'rsa-' . $this->scheduling_dkim_algo;
374 $dk[
's'] = $this->selector;
375 $dk[
'd'] = $this->scheduling_dkim_domain;
376 $dk[
'c'] =
'simple-http';
377 if ( isset ( $_SERVER[
'SERVER_NAME'] ) && strstr ( $_SERVER[
'SERVER_NAME'], $this->domain ) !==
false )
378 $dk[
'i'] =
'@' . $_SERVER[
'SERVER_NAME'];
379 $dk[
'q'] =
'dns/txt';
380 $dk[
'l'] = strlen ( $body );
382 if ( isset ( $this->valid_time ) )
383 $dk[
'x'] = $this->valid_time;
384 $dk[
'h'] = implode (
':', array_keys ( $headers ) );
385 $dk[
'bh'] = base64_encode ( hash (
'sha256', $body ,
true ) );
387 foreach ( $dk as $key => $val )
388 $value .=
"$key=$val; ";
390 $tosign = $b .
'DKIM-Signature: ' . $value;
391 openssl_sign ( $tosign, $sig, $this->schedule_private_key, $this->scheduling_dkim_algo );
392 $this->tosign = $tosign;
393 $value .= base64_encode ( $sig );
406 if ( empty($this->scheduling_dkim_domain) )
408 if ( is_array ( $address ) )
409 list ( $user, $domain ) = explode (
'@', $address[0] );
411 list ( $user, $domain ) = explode (
'@', $address );
414 dbg_error_log(
'ischedule', $domain .
' did not have iSchedule capabilities for ' . $type );
417 dbg_error_log(
'ischedule', $domain .
' trying with iSchedule capabilities for ' . $type );
420 dbg_error_log(
'ischedule', $domain .
' trying with iSchedule capabilities for ' . $type .
' OK');
421 list ( $component, $method ) = explode (
'/', $type );
422 $headers = array ( );
423 $headers[
'iSchedule-Version'] =
'1.0';
424 $headers[
'Originator'] =
'mailto:' . $session->email;
425 if ( is_array ( $address ) )
426 $headers[
'Recipient'] = implode (
', ' , $address );
428 $headers[
'Recipient'] = $address;
429 $headers[
'Content-Type'] =
'text/calendar; component=' . $component ;
431 $headers[
'Content-Type'] .=
'; method=' . $method;
432 $headers[
'DKIM-Signature'] = $this->
signDKIM ( $headers, $body );
433 if ( $headers[
'DKIM-Signature'] ==
false )
435 $request_headers = array ( );
436 foreach ( $headers as $k => $v )
437 $request_headers[] = $k .
': ' . $v;
438 $curl = curl_init ( $this->remote_url );
439 curl_setopt ( $curl, CURLOPT_RETURNTRANSFER,
true );
440 curl_setopt ( $curl, CURLOPT_HTTPHEADER, array() );
441 curl_setopt ( $curl, CURLOPT_HTTPHEADER, $request_headers );
442 curl_setopt ( $curl, CURLOPT_SSL_VERIFYPEER,
false);
443 curl_setopt ( $curl, CURLOPT_SSL_VERIFYHOST,
false);
444 curl_setopt ( $curl, CURLOPT_POST, 1);
445 curl_setopt ( $curl, CURLOPT_POSTFIELDS, $data);
446 curl_setopt ( $curl, CURLOPT_CUSTOMREQUEST,
'POST' );
447 $xmlresponse = curl_exec ( $curl );
448 $info = curl_getinfo ( $curl );
449 curl_close ( $curl );
450 if ( $info[
'http_code'] >= 400 )
452 dbg_error_log (
'ischedule',
'remote server returned error (%s)', $info[
'http_code'] );
456 error_log (
'remote response '. $xmlresponse . print_r ( $info,
true ) );
457 $xml_parser = xml_parser_create_ns(
'UTF-8');
459 xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
460 xml_parser_set_option ( $xml_parser, XML_OPTION_CASE_FOLDING, 0 );
461 $rc = xml_parse_into_struct( $xml_parser, $xmlresponse, $xml_tags );
462 if ( $rc ==
false ) {
463 dbg_error_log(
'ERROR',
'XML parsing error: %s at line %d, column %d',
464 xml_error_string(xml_get_error_code($xml_parser)),
465 xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser) );
468 $xmltree = BuildXMLTree( $xml_tags );
469 xml_parser_free($xml_parser);
470 if ( !is_object($xmltree) ) {
471 dbg_error_log(
'ERROR',
'iSchedule RESPONSE body is not valid XML data!' );
474 $resp = $xmltree->GetPath (
'/*/urn:ietf:params:xml:ns:ischedule:response' );
476 foreach ( $resp as $r )
478 $recipient = $r->GetElements (
'urn:ietf:params:xml:ns:ischedule:recipient' );
479 $status = $r->GetElements (
'urn:ietf:params:xml:ns:ischedule:request-status' );
480 $calendardata = $r->GetElements (
'urn:ietf:params:xml:ns:ischedule:calendar-data' );
481 if ( count ( $recipient ) < 1 )
483 if ( count ( $calendardata ) > 0 )
485 $result [ $recipient[0]->GetContent() ] = $calendardata[0]->GetContent();
489 $result [ $recipient[0]->GetContent() ] = $status[0]->GetContent();
492 if ( count ( $result ) < 1 )
509 $this->failed =
true;
510 $tags = preg_split (
'/;[\s\t]/', $sig );
511 foreach ( $tags as $v )
513 list($key,$value) = preg_split (
'/=/', $v, 2 );
514 $dkim[$key] = $value;
520 if ( ! preg_match (
'{(simple|simple-http|relaxed)(/(simple|simple-http|relaxed))?}', $dkim[
'c'], $matches ) )
521 return 'bad canonicalization:' . $dkim[
'c'] ;
522 if ( count ( $matches ) > 2 )
523 $this->body_cannon = $matches[2];
525 $this->body_cannon = $matches[1];
526 $this->header_cannon = $matches[1];
528 if ( $dkim[
'a'] !=
'rsa-sha1' && $dkim[
'a'] !=
'rsa-sha256' )
529 return 'bad signing algorythm:' . $dkim[
'a'] ;
531 if ( $dkim[
'q'] !=
'dns/txt' )
532 return 'bad query method';
534 if ( ! isset ( $dkim[
'd'] ) )
535 return 'missing signing domain';
536 $this->remote_server = $dkim[
'd'];
538 if ( isset ( $dkim[
'i'] ) )
541 if ( ! stristr ( $dkim[
'i'], $dkim[
'd'] ) )
542 return 'signing domain mismatch';
544 if ( strstr ( $dkim [
'i' ],
'@' ) )
545 $this->remote_user = substr ( $dkim [
'i' ], 0, strpos ( $dkim [
'i' ],
'@' ) - 1 );
548 if ( ! isset ( $dkim[
's'] ) )
549 return 'missing selector';
550 $this->remote_selector = $dkim[
's'];
552 if ( ! isset ( $dkim[
'h'] ) )
553 return 'missing list of signed headers';
554 $this->signed_headers = preg_split (
'/:/', $dkim[
'h'] );
557 foreach ( $this->signed_headers as $h )
559 $sh[] = strtolower ( $h );
560 if ( in_array ( strtolower ( $h ), $this->disallowed_headers ) )
561 return "$h is NOT allowed in signed header fields per RFC4871 or iSchedule";
563 foreach ( $this->required_headers as $h )
564 if ( ! in_array ( strtolower ( $h ), $sh ) )
565 return "$h is REQUIRED but missing in signed header fields per iSchedule";
567 if ( ! isset ( $dkim[
'bh'] ) )
568 return 'missing body signature';
570 if ( ! isset ( $dkim[
'b'] ) )
571 return 'missing signature in b field';
573 if ( isset ( $dkim[
'l'] ) )
574 $this->signed_length = $dkim[
'l'];
575 $this->failed =
false;
576 $this->DKSig = $dkim;
586 if ( preg_match (
'/^mailto:([^@]+)@([^\s\t\n]+)/', $uri, $matches ) )
588 $this->remote_user = $matches[1];
589 $this->domain = $matches[2];
602 $this->failed =
true;
604 foreach ( $this->signed_headers as $h )
605 if ( isset ( $_SERVER[
'HTTP_' . strtoupper ( strtr ( $h,
'-',
'_' ) ) ] ) )
606 $signed .=
"$h: " . $_SERVER[
'HTTP_' . strtoupper ( strtr ( $h,
'-',
'_' ) ) ] .
"\r\n";
608 $signed .=
"$h: " . $_SERVER[ strtoupper ( strtr ( $h,
'-',
'_' ) ) ] .
"\r\n";
609 if ( ! isset ( $_SERVER[
'HTTP_ORIGINATOR'] ) || stripos ( $signed,
'Originator' ) ===
false )
610 return "missing Originator";
611 if ( ! isset ( $_SERVER[
'HTTP_RECIPIENT'] ) || stripos ( $signed,
'Recipient' ) ===
false )
612 return "missing Recipient";
613 if ( ! isset ( $_SERVER[
'HTTP_ISCHEDULE_VERSION'] ) || $_SERVER[
'HTTP_ISCHEDULE_VERSION'] !=
'1' )
614 return "missing or mismatch ischedule-version header";
615 $body = $request->raw_post;
616 if ( ! isset ( $this->signed_length ) )
617 $this->signed_length = strlen ( $body );
619 $body = substr ( $body, 0, $this->signed_length );
620 if ( isset ( $this->remote_user_rule ) )
621 if ( $this->remote_user_rule !=
'*' && ! stristr ( $this->remote_user, $this->remote_user_rule ) )
622 return "remote user rule failure";
623 $hash_algo = preg_replace (
'/^.*(sha1|sha256).*/',
'$1', $this->DKSig[
'a'] );
624 $body_hash = base64_encode ( hash ( $hash_algo, $body ,
true ) );
625 if ( $this->DKSig[
'bh'] != $body_hash )
626 return "body hash mismatch";
627 $sig = $_SERVER[
'HTTP_DKIM_SIGNATURE'];
628 $sig = preg_replace (
'/ b=[^;\s\r\n\t]+/',
' b=', $sig );
629 $signed .=
'DKIM-Signature: ' . $sig;
630 $verify = openssl_verify ( $signed, base64_decode ( $this->DKSig[
'b'] ), $this->remote_public_key, $hash_algo );
633 openssl_sign ( $signed, $sigb, $this->schedule_private_key, $hash_algo );
634 $sigc = base64_encode ( $sigb );
635 $verify1 = openssl_verify ( $signed, $sigc, $this->remote_public_key, $hash_algo );
636 return "signature verification failed " . $this->remote_public_key .
" \n\n". $sig .
" \n" . $hash_algo .
"\n". print_r ($verify,1) .
" XX " . $verify1 .
"\n";
638 $this->failed =
false;
648 if ( isset ( $_SERVER[
'HTTP_DKIM_SIGNATURE'] ) )
649 $sig = $_SERVER[
'HTTP_DKIM_SIGNATURE'];
652 $request->DoResponse( 403, translate(
'DKIM signature missing') );
655 if ( isset ( $_SERVER[
'HTTP_ORGANIZER'] ) )
656 $request->DoResponse( 403, translate(
'Organizer Missing') );
658 dbg_error_log (
'ischedule',
'beginning validation');
660 if ( $err !==
true || $this->failed )
661 $request->DoResponse( 412,
'DKIM signature invalid ' .
"\n" . $err .
"\n" );
662 if ( ! $this->
getTxt () || $this->failed )
663 $request->DoResponse( 400, translate(
'DKIM signature validation failed(DNS ERROR)') );
664 if ( ! $this->
parseTxt () || $this->failed )
665 $request->DoResponse( 400, translate(
'DKIM signature validation failed(KEY Parse ERROR)') );
667 $request->DoResponse( 400, translate(
'DKIM signature validation failed(KEY Validation ERROR)') );
669 if ( $err !==
true || $this->failed )
670 $request->DoResponse( 412, translate(
'DKIM signature validation failed(Signature verification ERROR)') .
'\n' . $err );
671 dbg_error_log (
'ischedule',
'signature ok');
sendRequest($address, $type, $data)
getCapabilities($domain=null)
signDKIM($headers, $body)
queryCapabilities($capability, $domain=null)