Add options to do colourized, side-by-side & meld reviewing of results.
[davical.git] / inc / iSchedule.php
blob2ab6e979b7b9d5c2c802a9d88f1b6faf2c7233e5
1 <?php
2 /**
3 * Functions that are needed for iScheduling requests
5 * - verifying Domain Key signatures
6 * - delivering remote scheduling requests to local users inboxes
7 * - Utility functions which we can use to decide whether this
8 * is a permitted activity for this user.
10 * @package davical
11 * @subpackage iSchedule
12 * @author Rob Ostensen <rob@boxacle.net>
13 * @copyright Rob Ostensen
14 * @license http://gnu.org/copyleft/gpl.html GNU GPL v3 or later
17 require_once("XMLDocument.php");
19 /**
20 * A class for handling iScheduling requests.
22 * @package davical
23 * @subpackage iSchedule
25 class iSchedule
27 public $parsed;
28 public $selector;
29 public $domain;
30 private $dk;
31 private $DKSig;
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', // draft 01 section 7.1 required headers
38 'originator',
39 'recipient',
40 'content-type' );
41 private $disallowed_headers = Array ( 'connection', // draft 01 section 7.1 disallowed headers
42 'keep-alive',
43 'dkim-signature',
44 'proxy-authenticate',
45 'proxy-authorization',
46 'te',
47 'trailers',
48 'transfer-encoding',
49 'upgrade' );
51 function __construct ( )
53 global $c;
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 );
63 if ( $key !== false )
64 $this->schedule_private_key = $key;
66 if ( isset ( $c->scheduling_dkim_algo ) )
67 $this->scheduling_dkim_algo = $c->scheduling_dkim_algo;
68 else
69 $this->scheduling_dkim_algo = 'sha256';
70 if ( isset ( $c->scheduling_dkim_valid_time ) )
71 $this->valid_time = $c->scheduling_dkim_valid_time;
75 /**
76 * gets the domainkey TXT record from DNS
77 */
78 function getTxt ()
80 global $icfg;
81 // TODO handle parents of subdomains and procuration records
82 if ( $icfg [ $this->remote_selector . '._domainkey.' . $this->remote_server ] )
84 $this->dk = $icfg [ $this->remote_selector . '._domainkey.' . $this->remote_server ];
85 return true;
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' ] )
94 $this->dk = '';
95 foreach ( $dkim [ 0 ] [ 'entries' ] as $v )
97 $this->dk .= trim ( $v );
100 dbg_error_log( 'ischedule', 'getTxt '. $this->dk . ' XX');
102 else
104 dbg_error_log( 'ischedule', 'getTxt FAILED '. print_r ( $dkim ) );
105 $this->failed = true;
106 return false;
108 return true;
112 * strictly for testing purposes
114 function setTxt ( $dk )
116 $this->dk = $dk;
120 * parses DNS TXT record from domainkey lookup
122 function parseTxt ( )
124 if ( $this->failed == true )
125 return false;
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;
135 else
136 $this->parsed_ignored [ $key ] = $value;
138 return true;
142 * validates that domainkey is acceptable for the current request
144 function validateKey ( )
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' );
151 return false; // not a wildcard or calendar key
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' ] );
156 return false; // we only speak rsa for now
158 if ( isset ( $this->parsed [ 't' ] ) && ! preg_match ( '/^[y:s]+$/', $this->parsed [ 't' ] ) ) {
159 dbg_error_log( 'ischedule', 'validateKey ERROR: type mismatch' );
160 return false;
162 else
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' ];
171 else
172 $this->remote_user_rule = '*';
173 if ( isset ( $this->parsed [ 'p' ] ) )
175 if ( preg_match ( '/[^A-Za-z0-9_=+\/]/', $this->parsed [ 'p' ] ) )
176 return false;
177 $data = "-----BEGIN PUBLIC KEY-----\n" . implode ("\n",str_split ( $this->parsed [ 'p' ], 64 )) . "\n-----END PUBLIC KEY-----";
178 if ( $data === false )
179 return false;
180 $this->remote_public_key = $data;
182 else {
183 dbg_error_log( 'ischedule', 'validateKey ERROR: no key in dns record' . $this->parsed [ 'p' ] );
184 return false;
186 $this->failed = false;
187 return true;
191 * finds a remote calendar server via DNS SRV records
193 function getServer ( )
195 global $icfg;
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' ];
201 return true;
203 $this->remote_ssl = false;
204 $parts = explode ( '.', $this->domain );
205 $tld = $parts [ count ( $parts ) - 1 ];
206 $len = 2;
207 if ( strlen ( $tld ) == 2 && in_array ( $tld, Array ( 'uk', 'nz' ) ) )
208 $len = 3; // some country code tlds should have 3 components
209 if ( $this->domain == 'mycaldav' || $this->domain == 'altcaldav' )
210 $len = 1;
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;
219 break;
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' ];
228 break;
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 ) )
240 $remote_port = 80;
242 else {
243 dbg_error_log('ischedule', 'Domain %s did not have srv records for iSchedule', $this->domain );
244 return false;
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;
250 return true;
254 * get capabilities from remote server
256 function getCapabilities ( $domain = null )
258 if ( $domain != null && $this->domain != $domain )
259 $this->domain = $domain;
260 if ( ! isset ( $this->remote_server ) && isset ( $this->domain ) && ! $this->getServer ( ) )
261 return false;
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 )
266 return 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' );
277 return false;
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!") );
284 return false;
286 dbg_error_log('ischedule', $this->domain . ' got capabilites' );
287 $this->capabilities_xml = $xmltree;
288 return true;
292 * query capabilities retrieved from server
294 function queryCapabilities ( $capability, $domain = null )
296 if ( ! isset ( $this->capabilities_xml ) )
298 dbg_error_log('ischedule', $this->domain . ' capabilities not set, quering for capability:' . $capability );
299 if ( $domain == null )
300 return false;
301 if ( $this->domain != $domain )
302 $this->domain = $domain;
303 if ( ! $this->getCapabilities ( ) )
304 return false;
306 switch ( $capability )
308 case 'VEVENT':
309 case 'VFREEBUSY':
310 case 'VTODO':
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 )
315 return true;
317 return false;
318 case 'VFREEBUSY/REQUEST':
319 case 'VTODO/ADD':
320 case 'VTODO/REQUEST':
321 case 'VTODO/REPLY':
322 case 'VTODO/CANCEL':
323 case 'VEVENT/ADD':
324 case 'VEVENT/REQUEST':
325 case 'VEVENT/REPLY':
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 )
341 return true; // seems like we should accept everything if there are no children
342 foreach ( $methods as $m )
344 if ( $m->GetAttribute ( 'name' ) == $method )
345 return true;
349 return false;
350 default:
351 return false;
356 * signs a POST body and headers
358 * @param string $body the body of the POST
359 * @param array $headers the headers to sign as passed to header ();
361 function signDKIM ( $headers, $body )
363 if ( $this->scheduling_dkim_domain == null )
364 return false;
365 $b = '';
366 if ( is_array ( $headers ) !== true )
367 return false;
368 foreach ( $headers as $key => $value )
370 $b .= $key . ': ' . $value . "\r\n";
372 $dk['v'] = '1';
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'; // implied canonicalization of simple-http/simple from rfc4871 Section-3.5
377 if ( isset ( $_SERVER['SERVER_NAME'] ) && strstr ( $_SERVER['SERVER_NAME'], $this->domain ) !== false ) // don't use when testing
378 $dk['i'] = '@' . $_SERVER['SERVER_NAME']; //optional
379 $dk['q'] = 'dns/txt'; // optional, dns/txt is the default if missing
380 $dk['l'] = strlen ( $body ); //optional
381 $dk['t'] = time ( ); // timestamp of signature, optional
382 if ( isset ( $this->valid_time ) )
383 $dk['x'] = $this->valid_time; // unix timestamp expiriation of signature, optional
384 $dk['h'] = implode ( ':', array_keys ( $headers ) );
385 $dk['bh'] = base64_encode ( hash ( 'sha256', $body , true ) );
386 $value = '';
387 foreach ( $dk as $key => $val )
388 $value .= "$key=$val; ";
389 $value .= 'b=';
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 );
394 return $value;
398 * send request to remote server
399 * $address should be an email address or an array of email addresses all with the same domain
400 * $type should be in the format COMPONENT/METHOD eg (VFREEBUSY, VEVENT/REQUEST, VEVENT/REPLY, etc. )
401 * $data is the vcalendar data N.B. must already be rendered into text format
403 function sendRequest ( $address, $type, $data )
405 global $session;
406 if ( empty($this->scheduling_dkim_domain) )
407 return false;
408 if ( is_array ( $address ) )
409 list ( $user, $domain ) = explode ( '@', $address[0] );
410 else
411 list ( $user, $domain ) = explode ( '@', $address );
412 if ( ! $this->getCapabilities ( $domain ) )
414 dbg_error_log('ischedule', $domain . ' did not have iSchedule capabilities for ' . $type );
415 return false;
417 dbg_error_log('ischedule', $domain . ' trying with iSchedule capabilities for ' . $type );
418 if ( $this->queryCapabilities ( $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 );
427 else
428 $headers['Recipient'] = $address;
429 $headers['Content-Type'] = 'text/calendar; component=' . $component ;
430 if ( $method )
431 $headers['Content-Type'] .= '; method=' . $method;
432 $headers['DKIM-Signature'] = $this->signDKIM ( $headers, $body );
433 if ( $headers['DKIM-Signature'] == false )
434 return 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() ); // start with no headers set
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'] );
453 return false;
456 error_log ( 'remote response '. $xmlresponse . print_r ( $info, true ) );
457 $xml_parser = xml_parser_create_ns('UTF-8');
458 $xml_tags = array();
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) );
466 return false;
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!' );
472 return false;
474 $resp = $xmltree->GetPath ( '/*/urn:ietf:params:xml:ns:ischedule:response' );
475 $result = array();
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 )
482 continue; // this should be an error
483 if ( count ( $calendardata ) > 0 )
485 $result [ $recipient[0]->GetContent() ] = $calendardata[0]->GetContent();
487 else
489 $result [ $recipient[0]->GetContent() ] = $status[0]->GetContent();
492 if ( count ( $result ) < 1 )
493 return false;
494 else
495 return $result;
497 else
498 return false;
502 * parses and validates DK header
504 * @param string $sig the value of the DKIM-Signature header
506 function parseDKIM ( $sig )
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;
516 // the canonicalization method is currently undefined as of draft-01 of the iSchedule spec
517 // but it does define the value, it should be simple-http. RFC4871 also defines two methods
518 // simple and relaxed, simple is probably the same as simple http
519 // relaxed allows for header case folding and whitespace folding, see section 3.4.4 of RFC4871
520 if ( ! preg_match ( '{(simple|simple-http|relaxed)(/(simple|simple-http|relaxed))?}', $dkim['c'], $matches ) ) // canonicalization method
521 return 'bad canonicalization:' . $dkim['c'] ;
522 if ( count ( $matches ) > 2 )
523 $this->body_cannon = $matches[2];
524 else
525 $this->body_cannon = $matches[1];
526 $this->header_cannon = $matches[1];
527 // signing algorythm REQUIRED
528 if ( $dkim['a'] != 'rsa-sha1' && $dkim['a'] != 'rsa-sha256' ) // we only support the minimum required
529 return 'bad signing algorythm:' . $dkim['a'] ;
530 // query method to retrieve public key, could/should we add https to the spec? REQUIRED
531 if ( $dkim['q'] != 'dns/txt' )
532 return 'bad query method';
533 // domain of the signing entity REQUIRED
534 if ( ! isset ( $dkim['d'] ) )
535 return 'missing signing domain';
536 $this->remote_server = $dkim['d'];
537 // identity of signing AGENT, OPTIONAL
538 if ( isset ( $dkim['i'] ) )
540 // if present, domain of the signing agent must be a match or a subdomain of the signing domain
541 if ( ! stristr ( $dkim['i'], $dkim['d'] ) ) // RFC4871 does not specify a case match requirement
542 return 'signing domain mismatch';
543 // grab the local part of the signing agent if it's an email address
544 if ( strstr ( $dkim [ 'i' ], '@' ) )
545 $this->remote_user = substr ( $dkim [ 'i' ], 0, strpos ( $dkim [ 'i' ], '@' ) - 1 );
547 // selector used to retrieve public key REQUIRED
548 if ( ! isset ( $dkim['s'] ) )
549 return 'missing selector';
550 $this->remote_selector = $dkim['s'];
551 // signed header fields, colon seperated REQUIRED
552 if ( ! isset ( $dkim['h'] ) )
553 return 'missing list of signed headers';
554 $this->signed_headers = preg_split ( '/:/', $dkim['h'] );
556 $sh = Array ();
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";
566 // body hash REQUIRED
567 if ( ! isset ( $dkim['bh'] ) )
568 return 'missing body signature';
569 // signed header hash REQUIRED
570 if ( ! isset ( $dkim['b'] ) )
571 return 'missing signature in b field';
572 // length of body used for signing
573 if ( isset ( $dkim['l'] ) )
574 $this->signed_length = $dkim['l'];
575 $this->failed = false;
576 $this->DKSig = $dkim;
577 return true;
581 * split up a mailto uri into domain and user components
582 * TODO handle other uri types (eg http)
584 function parseURI ( $uri )
586 if ( preg_match ( '/^mailto:([^@]+)@([^\s\t\n]+)/', $uri, $matches ) )
588 $this->remote_user = $matches[1];
589 $this->domain = $matches[2];
591 else
592 return false;
596 * verifies parsed DKIM header is valid for current message with a signature from the public key in DNS
597 * TODO handle multiple headers of the same name
599 function verifySignature ( )
601 global $request,$c;
602 $this->failed = true;
603 $signed = '';
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";
607 else
608 $signed .= "$h: " . $_SERVER[ strtoupper ( strtr ( $h, '-', '_' ) ) ] . "\r\n";
609 if ( ! isset ( $_SERVER['HTTP_ORIGINATOR'] ) || stripos ( $signed, 'Originator' ) === false ) //required header, must be signed
610 return "missing Originator";
611 if ( ! isset ( $_SERVER['HTTP_RECIPIENT'] ) || stripos ( $signed, 'Recipient' ) === false ) //required header, must be signed
612 return "missing Recipient";
613 if ( ! isset ( $_SERVER['HTTP_ISCHEDULE_VERSION'] ) || $_SERVER['HTTP_ISCHEDULE_VERSION'] != '1' ) //required header and we only speak version 1 for now
614 return "missing or mismatch ischedule-version header";
615 $body = $request->raw_post;
616 if ( ! isset ( $this->signed_length ) ) // Should we use the Content-Length header if the signed length is missing?
617 $this->signed_length = strlen ( $body );
618 else
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 );
631 if ( $verify != 1 )
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;
639 return true;
643 * checks that current request has a valid DKIM signature signed by a currently valid key from DNS
645 function validateRequest ( )
647 global $request;
648 if ( isset ( $_SERVER['HTTP_DKIM_SIGNATURE'] ) )
649 $sig = $_SERVER['HTTP_DKIM_SIGNATURE'];
650 else
652 $request->DoResponse( 403, translate('DKIM signature missing') );
653 return false;
655 if ( isset ( $_SERVER['HTTP_ORGANIZER'] ) )
656 $request->DoResponse( 403, translate('Organizer Missing') );
658 dbg_error_log ('ischedule','beginning validation');
659 $err = $this->parseDKIM ( $sig );
660 if ( $err !== true || $this->failed )
661 $request->DoResponse( 412, 'DKIM signature invalid ' . "\n" . $err . "\n" );
662 if ( ! $this->getTxt () || $this->failed ) // this could also be a 424 failed dependency response
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)') );
666 if ( ! $this->validateKey () || $this->failed )
667 $request->DoResponse( 400, translate('DKIM signature validation failed(KEY Validation ERROR)') );
668 $err = $this->verifySignature ();
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');
672 return true;