Overview

Packages

  • awl
    • caldav-client-v2
    • RRule
  • davical
    • authentication
      • drivers
    • caldav
    • DAViCalSession
    • DAVTicket
    • external-bind
    • feed
    • HTTPAuthSession
    • iSchedule
    • iSchedule-POST
    • logging
    • metrics
    • Principal
    • propfind
    • PublicSession
    • Request
    • Resource
    • tzservice
  • None
  • PHP

Classes

  • iSchedule

Functions

  • checkiSchedule
  • generateKeys
  • SRVFormat
  • SRVOk
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  • Todo
  1: <?php
  2: /**
  3: * Functions that are needed for iScheduling requests
  4: *
  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.
  9: *
 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
 15: */
 16: 
 17: require_once("XMLDocument.php");
 18: 
 19: /**
 20: * A class for handling iScheduling requests.
 21: *
 22: * @package   davical
 23: * @subpackage   iSchedule
 24: */
 25: class iSchedule
 26: {
 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' );
 50: 
 51:   function __construct ( )
 52:   {
 53:     global $c;
 54:     $this->selector = 'cal';
 55:     if ( is_object ( $c ) && isset ( $c->scheduling_dkim_selector ) )
 56:     {
 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 ) )
 61:       {
 62:         $key = file_get_contents ( $this->schedule_private_key );
 63:         if ( $key !== false )
 64:           $this->schedule_private_key = $key;
 65:       }
 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;
 72:     }
 73:   }
 74: 
 75:   /**
 76:   * gets the domainkey TXT record from DNS
 77:   */
 78:   function getTxt ()
 79:   {
 80:     global $icfg;
 81:     // TODO handle parents of subdomains and procuration records
 82:     if ( $icfg [ $this->remote_selector . '._domainkey.' . $this->remote_server ] )
 83:     {
 84:       $this->dk = $icfg [ $this->remote_selector . '._domainkey.' . $this->remote_server ];
 85:       return true;
 86:     }
 87: 
 88:     $dkim = dns_get_record ( $this->remote_selector . '._domainkey.' . $this->remote_server , DNS_TXT );
 89:     if ( count ( $dkim ) > 0 )
 90:     {
 91:       $this->dk = $dkim [ 0 ] [ 'txt' ];
 92:       if ( $dkim [ 0 ] [ 'entries' ] )
 93:       {
 94:         $this->dk = '';
 95:         foreach ( $dkim [ 0 ] [ 'entries' ] as $v )
 96:         {
 97:           $this->dk .= trim ( $v );
 98:         }
 99:       }
100:       dbg_error_log( 'ischedule', 'getTxt '. $this->dk . ' XX');
101:     }
102:     else
103:     {
104:       dbg_error_log( 'ischedule', 'getTxt FAILED '. print_r ( $dkim ) );
105:       $this->failed = true;
106:       return false;
107:     }
108:     return true;
109:   }
110: 
111:   /**
112:   * strictly for testing purposes
113:   */
114:   function setTxt ( $dk )
115:   {
116:     $this->dk = $dk;
117:   }
118: 
119:   /**
120:   * parses DNS TXT record from domainkey lookup
121:   */
122:   function parseTxt ( )
123:   {
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 )
130:     {
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;
137:     }
138:     return true;
139:   }
140: 
141:   /**
142:   * validates that domainkey is acceptable for the current request
143:   */
144:   function validateKey ( )
145:   {
146:     $this->failed = true;
147:     if ( isset ( $this->parsed [ 's' ] ) )
148:     {
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
152:       }
153:     }
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
157:     }
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;
161:     }
162:     else
163:     {
164:       if ( preg_match ( '/y/', $this->parsed [ 't' ] ) )
165:         $this->failOnError = false;
166:       if ( preg_match ( '/s/', $this->parsed [ 't' ] ) )
167:         $this->subdomainsOK = false;
168:     }
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' ] ) )
174:     {
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;
181:     }
182:     else {
183:       dbg_error_log( 'ischedule', 'validateKey ERROR: no key in dns record' . $this->parsed [ 'p' ] );
184:       return false;
185:     }
186:     $this->failed = false;
187:     return true;
188:   }
189: 
190:   /**
191:   * finds a remote calendar server via DNS SRV records
192:   */
193:   function getServer ( )
194:   {
195:     global $icfg;
196:     if ( $icfg [ $this->domain ] )
197:     {
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;
202:     }
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 )
212:     {
213:       $r = dns_get_record ( '_ischedules._tcp.' . implode ( '.', $parts ) , DNS_SRV );
214:       if ( 0 < count ( $r ) )
215:       {
216:         $remote_server            = $r [ 0 ] [ 'target' ];
217:         $remote_port              = $r [ 0 ] [ 'port' ];
218:         $this->remote_ssl = true;
219:         break;
220:       }
221:       if ( ! isset ( $remote_server ) )
222:       {
223:         $r = dns_get_record ( '_ischedule._tcp.' . implode ( '.', $parts ) , DNS_SRV );
224:         if ( 0 < count ( $r ) )
225:         {
226:           $remote_server            = $r [ 0 ] [ 'target' ];
227:           $remote_port              = $r [ 0 ] [ 'port' ];
228:           break;
229:         }
230:       }
231:       array_shift ( $parts );
232:     }
233:     if ( ! isset ( $remote_server ) )
234:     {
235:       if ( $this->try_anyway == true )
236:       {
237:         if ( ! isset ( $remote_server ) )
238:           $remote_server = $this->domain;
239:         if ( ! isset ( $remote_port ) )
240:           $remote_port = 80;
241:       }
242:       else {
243:         dbg_error_log('ischedule', 'Domain %s did not have srv records for iSchedule', $this->domain );
244:         return false;
245:       }
246:     }
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;
251:   }
252: 
253:   /**
254:   * get capabilities from remote server
255:   */
256:   function getCapabilities ( $domain = null )
257:   {
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;
278:     }
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;
285:     }
286:     dbg_error_log('ischedule', $this->domain . ' got capabilites' );
287:     $this->capabilities_xml = $xmltree;
288:     return true;
289:   }
290: 
291:   /**
292:   *  query capabilities retrieved from server
293:   */
294:   function queryCapabilities ( $capability, $domain = null )
295:   {
296:     if ( ! isset ( $this->capabilities_xml ) )
297:     {
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;
305:     }
306:     switch ( $capability )
307:     {
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 )
313:         {
314:           if ( $c->GetAttribute ( 'name' ) == $capability )
315:             return true;
316:         }
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 )
335:         {
336:           dbg_error_log('ischedule', $this->domain . ' quering for capability:' . $c->GetAttribute ( 'name' ) . ' == ' . $component );
337:           if ( $c->GetAttribute ( 'name' ) == $component )
338:           {
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 )
343:             {
344:               if ( $m->GetAttribute ( 'name' ) == $method )
345:                 return true;
346:             }
347:           }
348:         }
349:         return false;
350:       default:
351:         return false;
352:     }
353:   }
354: 
355:   /**
356:   * signs a POST body and headers
357:   *
358:   * @param string $body the body of the POST
359:   * @param array  $headers the headers to sign as passed to header ();
360:   */
361:   function signDKIM ( $headers, $body )
362:   {
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 )
369:     {
370:       $b .= $key . ': ' . $value . "\r\n";
371:     }
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;
395:   }
396: 
397:   /**
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
402:   */
403:   function sendRequest ( $address, $type, $data )
404:   {
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 ) )
413:     {
414:       dbg_error_log('ischedule', $domain . ' did not have iSchedule capabilities for ' . $type );
415:       return false;
416:     }
417:     dbg_error_log('ischedule', $domain . ' trying with iSchedule capabilities for ' . $type );
418:     if ( $this->queryCapabilities ( $type ) )
419:     {
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 )
451:       {
452:         dbg_error_log ( 'ischedule', 'remote server returned error (%s)', $info['http_code'] );
453:         return false;
454:       }
455: 
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;
467:       }
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;
473:       }
474:       $resp = $xmltree->GetPath ( '/*/urn:ietf:params:xml:ns:ischedule:response' );
475:       $result = array();
476:       foreach ( $resp as $r )
477:       {
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 )
484:         {
485:           $result [ $recipient[0]->GetContent() ] = $calendardata[0]->GetContent();
486:         }
487:         else
488:         {
489:           $result [ $recipient[0]->GetContent() ] = $status[0]->GetContent();
490:         }
491:       }
492:       if ( count ( $result ) < 1 )
493:         return false;
494:       else
495:         return $result;
496:     }
497:     else
498:       return false;
499:   }
500: 
501:   /**
502:   * parses and validates DK header
503:   *
504:   * @param string $sig the value of the DKIM-Signature header
505:   */
506:   function parseDKIM ( $sig )
507:   {
508: 
509:     $this->failed = true;
510:     $tags = preg_split ( '/;[\s\t]/', $sig );
511:     foreach ( $tags as $v )
512:     {
513:       list($key,$value) = preg_split ( '/=/', $v, 2 );
514:       $dkim[$key] = $value;
515:     }
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'] ) )
539:     {
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 );
546:     }
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'] );
555: 
556:     $sh = Array ();
557:     foreach ( $this->signed_headers as $h )
558:     {
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";
562:     }
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;
578:   }
579: 
580:   /**
581:   * split up a mailto uri into domain and user components
582:   * TODO handle other uri types (eg http)
583:   */
584:   function parseURI ( $uri )
585:   {
586:     if ( preg_match ( '/^mailto:([^@]+)@([^\s\t\n]+)/', $uri, $matches ) )
587:     {
588:       $this->remote_user = $matches[1];
589:       $this->domain = $matches[2];
590:     }
591:     else
592:       return false;
593:   }
594: 
595:   /**
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
598:   */
599:   function verifySignature ( )
600:   {
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 )
632:     {
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";
637:     }
638:     $this->failed = false;
639:     return true;
640:   }
641: 
642:   /**
643:   * checks that current request has a valid DKIM signature signed by a currently valid key from DNS
644:   */
645:   function validateRequest ( )
646:   {
647:     global $request;
648:     if ( isset ( $_SERVER['HTTP_DKIM_SIGNATURE'] ) )
649:       $sig = $_SERVER['HTTP_DKIM_SIGNATURE'];
650:     else
651:     {
652:       $request->DoResponse( 403, translate('DKIM signature missing') );
653:       return false;
654:     }
655:     if ( isset ( $_SERVER['HTTP_ORGANIZER'] ) )
656:       $request->DoResponse( 403, translate('Organizer Missing') );
657: 
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;
673:   }
674: }
675: 
676: 
DAViCal API documentation generated by ApiGen 2.8.0