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

Classes

  • AtomEntry
  • AtomFeed
  • AtomXHTMLContent
  • CalDAVClient
  • CalDAVRequest
  • CalendarInfo
  • CheckResult
  • DAViCalSession
  • DAVPrincipal
  • DAVResource
  • DAVTicket
  • FakeSession
  • HTTPAuthSession
  • imapPamDriver
  • iSchedule
  • ldapDriver
  • Principal
  • PublicSession
  • pwauthPamDriver
  • RepeatRule
  • RepeatRuleDateRange
  • RepeatRuleDateTime
  • RepeatRuleTimeZone
  • Rfc5545Duration
  • rimapPamDriver
  • setupFakeSession
  • squidPamDriver
  • Tools
  • VCard
  • VTimezone
  • WritableCollection

Functions

  • access_ticket_browser
  • add_failure
  • add_proxy_response
  • array_values_mapping
  • auth_functions_deprecated
  • AuthExternalAWL
  • binding_row_editor
  • bindings_to_other_browser
  • bindings_to_us_browser
  • bits_to_privilege
  • build_dependencies_table
  • build_privileges_html
  • build_site_statistics
  • BuildSqlFilter
  • caldav_get_feed
  • calquery_apply_filter
  • cardquery_apply_filter
  • catch_setup_errors
  • check_awl_version
  • check_calendar
  • check_curl
  • check_database_connection
  • check_datetime
  • check_davical_version
  • check_for_expansion
  • check_gettext
  • check_iconv
  • check_ldap
  • check_magic_quotes_gpc
  • check_magic_quotes_runtime
  • check_pdo
  • check_pdo_pgsql
  • check_pgsql
  • check_real_php
  • check_schema_version
  • check_string
  • check_suhosin_server_strip
  • check_xml
  • checkiSchedule
  • collection_privilege_format_function
  • component_to_xml
  • confirm_delete_bind_in
  • confirm_delete_binding
  • confirm_delete_collection
  • confirm_delete_principal
  • confirm_delete_ticket
  • ConstructURL
  • controlRequestContainer
  • create_external
  • CreateDefaultRelationships
  • CreateHomeCalendar
  • CreateHomeCollections
  • DateToISODate
  • DeconstructURL
  • delete_collection
  • deliverItipCancel
  • display_status
  • do_error
  • do_scheduling_for_delete
  • do_scheduling_reply
  • do_scheduling_requests
  • doImipMessage
  • doItipAttendeeReply
  • doItipOrganizerCancel
  • early_catch_fatal_error
  • early_exception_handler
  • edit_binding_row
  • edit_grant_row_collection
  • edit_grant_row_principal
  • edit_group_row
  • edit_ticket_row
  • errorResponse
  • expand_event_instances
  • expand_properties
  • expand_timezone_onsets
  • export_iCalendar
  • fetch_external
  • fix_unique_member
  • generateKeys
  • get_address_properties
  • get_collection_contents
  • get_freebusy
  • get_href_containers
  • get_phpinfo
  • getComponentRange
  • GetItip
  • getPrincipalByID
  • getStaticLdap
  • getStatusMessage
  • GetTZID
  • getUserByEMail
  • getUserByID
  • getUserByName
  • getVCalendarRange
  • grant_row_editor
  • group_members_browser
  • group_memberships_browser
  • group_row_editor
  • handle_cancel_request
  • handle_freebusy_request
  • handle_schedule_reply
  • handle_schedule_request
  • handle_subaction
  • hyperlink
  • i18n
  • IMAP_PAM_check
  • import_addressbook_collection
  • import_calendar_collection
  • import_collection
  • ischedule_cancel
  • ischedule_freebusy_request
  • ischedule_get
  • ischedule_request
  • ISODateToHTTPDate
  • late_catch_fatal_error
  • LDAP_check
  • local_session_sql
  • log_caldav_action
  • log_setup_error
  • logRequestHeaders
  • make_help_link
  • obfuscated_event
  • olson_from_vtimezone
  • principal_collection_browser
  • principal_editor
  • principal_grants_browser
  • principal_privilege_format_function
  • print_metric
  • privilege_to_bits
  • privileges_to_XML
  • process_ace
  • processItipCancel
  • property_response
  • public_events_only
  • PWAUTH_PAM_check
  • rdate_expand
  • RIMAP_check
  • rollback
  • rollback_on_error
  • rrule_expand
  • send_dav_header
  • send_page_header
  • simple_write_resource
  • SqlFilterCardDAV
  • SqlFilterFragment
  • SQUID_PAM_check
  • SRVFormat
  • SRVOk
  • sync_LDAP
  • sync_LDAP_groups
  • sync_user_from_LDAP
  • ticket_row_editor
  • unicodeToUtf8
  • update_external
  • UpdateCollectionTimezones
  • UpdateUserFromExternal
  • utf8ToUnicode
  • write_alarms
  • write_attendees
  • write_resource
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  • Todo
  1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563: 564: 565: 566: 567: 568: 569: 570: 571: 572: 573: 574: 575: 576: 577: 578: 579: 580: 581: 582: 583: 584: 585: 586: 587: 588: 589: 590: 591: 592: 593: 594: 595: 596: 597: 598: 599: 600: 601: 602: 603: 604: 605: 606: 607: 608: 609: 610: 611: 612: 613: 614: 615: 616: 617: 618: 619: 620: 621: 622: 623: 624: 625: 626: 627: 628: 629: 630: 631: 632: 633: 634: 635: 636: 637: 638: 639: 640: 641: 642: 643: 644: 645: 646: 647: 648: 649: 650: 651: 652: 653: 654: 655: 656: 657: 658: 659: 660: 661: 662: 663: 664: 665: 666: 667: 668: 669: 670: 671: 672: 673: 674: 675: 676: 
<?php
/**
* Functions that are needed for iScheduling requests
*
*  - verifying Domain Key signatures
*  - delivering remote scheduling requests to local users inboxes
*  - Utility functions which we can use to decide whether this
*    is a permitted activity for this user.
*
* @package   davical
* @subpackage   iSchedule
* @author    Rob Ostensen <rob@boxacle.net>
* @copyright Rob Ostensen
* @license   http://gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

require_once("XMLDocument.php");

/**
* A class for handling iScheduling requests.
*
* @package   davical
* @subpackage   iSchedule
*/
class iSchedule
{
  public $parsed;
  public $selector;
  public $domain;
  private $dk;
  private $DKSig;
  private $try_anyway = false;
  private $failed = false;
  private $failOnError = true;
  private $subdomainsOK = true;
  private $remote_public_key ;
  private $required_headers = Array ( 'host',  // draft 01 section 7.1 required headers
                                      'originator',
                                      'recipient',
                                      'content-type' );
  private $disallowed_headers = Array ( 'connection',  // draft 01 section 7.1 disallowed headers
                                        'keep-alive',
                                        'dkim-signature',
                                        'proxy-authenticate',
                                        'proxy-authorization',
                                        'te',
                                        'trailers',
                                        'transfer-encoding',
                                        'upgrade' );

  function __construct ( )
  {
    global $c;
    $this->selector = 'cal';
    if ( is_object ( $c ) && isset ( $c->scheduling_dkim_selector ) )
    {
      $this->scheduling_dkim_domain = $c->scheduling_dkim_domain ;
      $this->scheduling_dkim_selector = $c->scheduling_dkim_selector ;
      $this->schedule_private_key = $c->schedule_private_key ;
      if ( ! preg_match ( '/BEGIN RSA PRIVATE KEY/', $this->schedule_private_key ) )
      {
        $key = file_get_contents ( $this->schedule_private_key );
        if ( $key !== false )
          $this->schedule_private_key = $key;
      }
      if ( isset ( $c->scheduling_dkim_algo ) )
        $this->scheduling_dkim_algo = $c->scheduling_dkim_algo;
      else
        $this->scheduling_dkim_algo = 'sha256';
      if ( isset ( $c->scheduling_dkim_valid_time ) )
        $this->valid_time = $c->scheduling_dkim_valid_time;
    }
  }

  /**
  * gets the domainkey TXT record from DNS
  */
  function getTxt ()
  {
    global $icfg;
    // TODO handle parents of subdomains and procuration records
    if ( $icfg [ $this->remote_selector . '._domainkey.' . $this->remote_server ] )
    {
      $this->dk = $icfg [ $this->remote_selector . '._domainkey.' . $this->remote_server ];
      return true;
    }

    $dkim = dns_get_record ( $this->remote_selector . '._domainkey.' . $this->remote_server , DNS_TXT );
    if ( count ( $dkim ) > 0 )
    {
      $this->dk = $dkim [ 0 ] [ 'txt' ];
      if ( $dkim [ 0 ] [ 'entries' ] )
      {
        $this->dk = '';
        foreach ( $dkim [ 0 ] [ 'entries' ] as $v )
        {
          $this->dk .= trim ( $v );
        }
      }
      dbg_error_log( 'ischedule', 'getTxt '. $this->dk . ' XX');
    }
    else
    {
      dbg_error_log( 'ischedule', 'getTxt FAILED '. print_r ( $dkim ) );
      $this->failed = true;
      return false;
    }
    return true;
  }

  /**
  * strictly for testing purposes
  */
  function setTxt ( $dk )
  {
    $this->dk = $dk;
  }

  /**
  * parses DNS TXT record from domainkey lookup
  */
  function parseTxt ( )
  {
    if ( $this->failed == true )
      return false;
    $clean = preg_replace ( '/\s?([;=])\s?/', '$1', $this->dk );
    $pairs = preg_split ( '/;/', $clean );
    $this->parsed = array();
    foreach ( $pairs as $v )
    {
      list($key,$value) = preg_split ( '/=/', $v, 2 );
      $value = trim ( $value, '\\' );
      if ( preg_match ( '/(g|k|n|p|s|t|v)/', $key ) )
        $this->parsed [ $key ] = $value;
      else
        $this->parsed_ignored [ $key ] = $value;
    }
    return true;
  }

  /**
  * validates that domainkey is acceptable for the current request
  */
  function validateKey ( )
  {
    $this->failed = true;
    if ( isset ( $this->parsed [ 's' ] ) )
    {
      if ( ! preg_match ( '/(\*|calendar)/', $this->parsed [ 's' ] ) ) {
        dbg_error_log( 'ischedule', 'validateKey ERROR: bad selector' );
        return false; // not a wildcard or calendar key
      }
    }
    if ( isset ( $this->parsed [ 'k' ] ) && $this->parsed [ 'k' ] != 'rsa' ) {
      dbg_error_log( 'ischedule', 'validateKey ERROR: bad key algorythm, algo was:' . $this->parsed [ 'k' ] );
      return false; // we only speak rsa for now
    }
    if ( isset ( $this->parsed [ 't' ] ) && ! preg_match ( '/^[y:s]+$/', $this->parsed [ 't' ] ) ) {
      dbg_error_log( 'ischedule', 'validateKey ERROR: type mismatch' );
      return false;
    }
    else
    {
      if ( preg_match ( '/y/', $this->parsed [ 't' ] ) )
        $this->failOnError = false;
      if ( preg_match ( '/s/', $this->parsed [ 't' ] ) )
        $this->subdomainsOK = false;
    }
    if ( isset ( $this->parsed [ 'g' ] ) )
      $this->remote_user_rule = $this->parsed [ 'g' ];
    else
      $this->remote_user_rule = '*';
    if ( isset ( $this->parsed [ 'p' ] ) )
    {
      if ( preg_match ( '/[^A-Za-z0-9_=+\/]/', $this->parsed [ 'p' ] ) )
        return false;
      $data = "-----BEGIN PUBLIC KEY-----\n" . implode ("\n",str_split ( $this->parsed [ 'p' ], 64 )) . "\n-----END PUBLIC KEY-----";
      if ( $data === false )
        return false;
      $this->remote_public_key = $data;
    }
    else {
      dbg_error_log( 'ischedule', 'validateKey ERROR: no key in dns record' . $this->parsed [ 'p' ] );
      return false;
    }
    $this->failed = false;
    return true;
  }

  /**
  * finds a remote calendar server via DNS SRV records
  */
  function getServer ( )
  {
    global $icfg;
    if ( $icfg [ $this->domain ] )
    {
      $this->remote_server = $icfg [ $this->domain ] [ 'server' ];
      $this->remote_port = $icfg [ $this->domain ] [ 'port' ];
      $this->remote_ssl = $icfg [ $this->domain ] [ 'ssl' ];
      return true;
    }
    $this->remote_ssl = false;
    $parts = explode ( '.', $this->domain );
    $tld = $parts [ count ( $parts ) - 1 ];
    $len = 2;
    if ( strlen ( $tld ) == 2 && in_array ( $tld, Array ( 'uk', 'nz' ) ) )
      $len = 3; // some country code tlds should have 3 components
    if ( $this->domain == 'mycaldav' || $this->domain == 'altcaldav' )
      $len = 1;
    while ( count ( $parts ) >= $len )
    {
      $r = dns_get_record ( '_ischedules._tcp.' . implode ( '.', $parts ) , DNS_SRV );
      if ( 0 < count ( $r ) )
      {
        $remote_server            = $r [ 0 ] [ 'target' ];
        $remote_port              = $r [ 0 ] [ 'port' ];
        $this->remote_ssl = true;
        break;
      }
      if ( ! isset ( $remote_server ) )
      {
        $r = dns_get_record ( '_ischedule._tcp.' . implode ( '.', $parts ) , DNS_SRV );
        if ( 0 < count ( $r ) )
        {
          $remote_server            = $r [ 0 ] [ 'target' ];
          $remote_port              = $r [ 0 ] [ 'port' ];
          break;
        }
      }
      array_shift ( $parts );
    }
    if ( ! isset ( $remote_server ) )
    {
      if ( $this->try_anyway == true )
      {
        if ( ! isset ( $remote_server ) )
          $remote_server = $this->domain;
        if ( ! isset ( $remote_port ) )
          $remote_port = 80;
      }
      else {
        dbg_error_log('ischedule', 'Domain %s did not have srv records for iSchedule', $this->domain );
        return false;
      }
    }
    dbg_error_log('ischedule', $this->domain . ' found srv records for ' . $remote_server . ':' . $remote_port );
    $this->remote_server = $remote_server;
    $this->remote_port = $remote_port;
    return true;
  }

  /**
  * get capabilities from remote server
  */
  function getCapabilities ( $domain = null )
  {
    if ( $domain != null && $this->domain != $domain )
      $this->domain = $domain;
    if ( ! isset ( $this->remote_server ) && isset ( $this->domain ) && ! $this->getServer ( ) )
      return false;
    $this->remote_url = 'http'. ( $this->remote_ssl ? 's' : '' ) . '://' .
      $this->remote_server . ':' . $this->remote_port . '/.well-known/ischedule';
    $remote_capabilities = file_get_contents ( $this->remote_url . '?query=capabilities' );
    if ( $remote_capabilities === false )
      return false;
    $xml_parser = xml_parser_create_ns('UTF-8');
    $this->xml_tags = array();
    xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
    xml_parser_set_option ( $xml_parser, XML_OPTION_CASE_FOLDING, 0 );
    $rc = xml_parse_into_struct( $xml_parser, $remote_capabilities, $this->xml_tags );
    if ( $rc == false ) {
      dbg_error_log( 'ERROR', 'XML parsing error: %s at line %d, column %d',
                  xml_error_string(xml_get_error_code($xml_parser)),
                  xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser) );
      dbg_error_log('ischedule', $this->domain . ' iSchedule error parsing remote xml' );
      return false;
    }
    xml_parser_free($xml_parser);
    $xmltree = BuildXMLTree( $this->xml_tags );
    if ( !is_object($xmltree) ) {
      dbg_error_log('ischedule', $this->domain . ' iSchedule error in remote xml' );
      $request->DoResponse( 406, translate("REPORT body is not valid XML data!") );
      return false;
    }
    dbg_error_log('ischedule', $this->domain . ' got capabilites' );
    $this->capabilities_xml = $xmltree;
    return true;
  }

  /**
  *  query capabilities retrieved from server
  */
  function queryCapabilities ( $capability, $domain = null )
  {
    if ( ! isset ( $this->capabilities_xml ) )
    {
      dbg_error_log('ischedule', $this->domain . ' capabilities not set, quering for capability:' . $capability );
      if ( $domain == null )
        return false;
      if ( $this->domain != $domain )
        $this->domain = $domain;
      if ( ! $this->getCapabilities ( ) )
        return false;
    }
    switch ( $capability )
    {
      case 'VEVENT':
      case 'VFREEBUSY':
      case 'VTODO':
        $comp = $this->capabilities_xml->GetPath ( 'urn:ietf:params:xml:ns:ischedule:supported-scheduling-message-set/urn:ietf:params:xml:ns:ischedule:comp' );
        foreach ( $comp as $c )
        {
          if ( $c->GetAttribute ( 'name' ) == $capability )
            return true;
        }
        return false;
      case 'VFREEBUSY/REQUEST':
      case 'VTODO/ADD':
      case 'VTODO/REQUEST':
      case 'VTODO/REPLY':
      case 'VTODO/CANCEL':
      case 'VEVENT/ADD':
      case 'VEVENT/REQUEST':
      case 'VEVENT/REPLY':
      case 'VEVENT/CANCEL':
      case 'VEVENT/PUBLISH':
      case 'VEVENT/COUNTER':
      case 'VEVENT/DECLINECOUNTER':
        dbg_error_log('ischedule', $this->domain . ' xml query' );
        $comp = $this->capabilities_xml->GetPath ( 'urn:ietf:params:xml:ns:ischedule:supported-scheduling-message-set/urn:ietf:params:xml:ns:ischedule:comp' );
        list ( $component, $method ) = explode ( '/', $capability );
        dbg_error_log('ischedule', $this->domain . ' quering for capability:' . count ( $comp ) . ' ' . $component );
        foreach ( $comp as $c )
        {
          dbg_error_log('ischedule', $this->domain . ' quering for capability:' . $c->GetAttribute ( 'name' ) . ' == ' . $component );
          if ( $c->GetAttribute ( 'name' ) == $component )
          {
            $methods = $c->GetElements ( 'urn:ietf:params:xml:ns:ischedule:method' );
            if ( count ( $methods ) == 0 )
              return true; // seems like we should accept everything if there are no children
            foreach ( $methods as $m )
            {
              if ( $m->GetAttribute ( 'name' ) == $method )
                return true;
            }
          }
        }
        return false;
      default:
        return false;
    }
  }

  /**
  * signs a POST body and headers
  *
  * @param string $body the body of the POST
  * @param array  $headers the headers to sign as passed to header ();
  */
  function signDKIM ( $headers, $body )
  {
    if ( $this->scheduling_dkim_domain == null )
      return false;
    $b = '';
    if ( is_array ( $headers ) !== true )
      return false;
    foreach ( $headers as $key => $value )
    {
      $b .= $key . ': ' . $value . "\r\n";
    }
    $dk['v'] = '1';
    $dk['a'] = 'rsa-' . $this->scheduling_dkim_algo;
    $dk['s'] = $this->selector;
    $dk['d'] = $this->scheduling_dkim_domain;
    $dk['c'] = 'simple-http'; // implied canonicalization of simple-http/simple from rfc4871 Section-3.5
    if ( isset ( $_SERVER['SERVER_NAME'] ) && strstr ( $_SERVER['SERVER_NAME'], $this->domain ) !== false ) // don't use when testing
      $dk['i'] = '@' . $_SERVER['SERVER_NAME']; //optional
    $dk['q'] = 'dns/txt'; // optional, dns/txt is the default if missing
    $dk['l'] = strlen ( $body ); //optional
    $dk['t'] = time ( ); // timestamp of signature, optional
    if ( isset ( $this->valid_time ) )
      $dk['x'] = $this->valid_time; // unix timestamp expiriation of signature, optional
    $dk['h'] = implode ( ':', array_keys ( $headers ) );
    $dk['bh'] = base64_encode ( hash ( 'sha256', $body , true ) );
    $value = '';
    foreach ( $dk as $key => $val )
      $value .= "$key=$val; ";
    $value .= 'b=';
    $tosign = $b . 'DKIM-Signature: ' . $value;
    openssl_sign ( $tosign, $sig, $this->schedule_private_key, $this->scheduling_dkim_algo );
    $this->tosign = $tosign;
    $value .= base64_encode ( $sig );
    return $value;
  }

  /**
  * send request to remote server
  * $address should be an email address or an array of email addresses all with the same domain
  * $type should be in the format COMPONENT/METHOD eg (VFREEBUSY, VEVENT/REQUEST, VEVENT/REPLY, etc. )
  * $data is the vcalendar data N.B. must already be rendered into text format
  */
  function sendRequest ( $address, $type, $data )
  {
    global $session;
    if ( empty($this->scheduling_dkim_domain) )
      return false;
    if ( is_array ( $address ) )
      list ( $user, $domain ) = explode ( '@', $address[0] );
    else
      list ( $user, $domain ) = explode ( '@', $address );
    if ( ! $this->getCapabilities ( $domain ) )
    {
      dbg_error_log('ischedule', $domain . ' did not have iSchedule capabilities for ' . $type );
      return false;
    }
    dbg_error_log('ischedule', $domain . ' trying with iSchedule capabilities for ' . $type );
    if ( $this->queryCapabilities ( $type ) )
    {
      dbg_error_log('ischedule', $domain . ' trying with iSchedule capabilities for ' . $type . ' OK');
      list ( $component, $method ) = explode ( '/', $type );
      $headers = array ( );
      $headers['iSchedule-Version'] = '1.0';
      $headers['Originator'] = 'mailto:' . $session->email;
      if ( is_array ( $address ) )
        $headers['Recipient'] = implode ( ', ' , $address );
      else
        $headers['Recipient'] = $address;
      $headers['Content-Type'] = 'text/calendar; component=' . $component ;
      if ( $method )
        $headers['Content-Type'] .= '; method=' . $method;
      $headers['DKIM-Signature'] = $this->signDKIM ( $headers, $body );
      if ( $headers['DKIM-Signature'] == false )
        return false;
      $request_headers = array ( );
      foreach ( $headers as $k => $v )
        $request_headers[] = $k . ': ' . $v;
      $curl = curl_init ( $this->remote_url );
      curl_setopt ( $curl, CURLOPT_RETURNTRANSFER, true );
      curl_setopt ( $curl, CURLOPT_HTTPHEADER, array() ); // start with no headers set
      curl_setopt ( $curl, CURLOPT_HTTPHEADER, $request_headers );
      curl_setopt ( $curl, CURLOPT_SSL_VERIFYPEER, false);
      curl_setopt ( $curl, CURLOPT_SSL_VERIFYHOST, false);
      curl_setopt ( $curl, CURLOPT_POST, 1);
      curl_setopt ( $curl, CURLOPT_POSTFIELDS, $data);
      curl_setopt ( $curl, CURLOPT_CUSTOMREQUEST, 'POST' );
      $xmlresponse = curl_exec ( $curl );
      $info = curl_getinfo ( $curl );
      curl_close ( $curl );
      if ( $info['http_code'] >= 400 )
      {
        dbg_error_log ( 'ischedule', 'remote server returned error (%s)', $info['http_code'] );
        return false;
      }

      error_log ( 'remote response '. $xmlresponse . print_r ( $info, true ) );
      $xml_parser = xml_parser_create_ns('UTF-8');
      $xml_tags = array();
      xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
      xml_parser_set_option ( $xml_parser, XML_OPTION_CASE_FOLDING, 0 );
      $rc = xml_parse_into_struct( $xml_parser, $xmlresponse, $xml_tags );
      if ( $rc == false ) {
        dbg_error_log( 'ERROR', 'XML parsing error: %s at line %d, column %d',
                    xml_error_string(xml_get_error_code($xml_parser)),
                    xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser) );
        return false;
      }
      $xmltree = BuildXMLTree( $xml_tags );
      xml_parser_free($xml_parser);
      if ( !is_object($xmltree) ) {
        dbg_error_log( 'ERROR', 'iSchedule RESPONSE body is not valid XML data!' );
        return false;
      }
      $resp = $xmltree->GetPath ( '/*/urn:ietf:params:xml:ns:ischedule:response' );
      $result = array();
      foreach ( $resp as $r )
      {
        $recipient     = $r->GetElements ( 'urn:ietf:params:xml:ns:ischedule:recipient' );
        $status        = $r->GetElements ( 'urn:ietf:params:xml:ns:ischedule:request-status' );
        $calendardata  = $r->GetElements ( 'urn:ietf:params:xml:ns:ischedule:calendar-data' );
        if ( count ( $recipient ) < 1 )
          continue; // this should be an error
        if ( count ( $calendardata ) > 0 )
        {
          $result [ $recipient[0]->GetContent() ] = $calendardata[0]->GetContent();
        }
        else
        {
          $result [ $recipient[0]->GetContent() ] = $status[0]->GetContent();
        }
      }
      if ( count ( $result ) < 1 )
        return false;
      else
        return $result;
    }
    else
      return false;
  }

  /**
  * parses and validates DK header
  *
  * @param string $sig the value of the DKIM-Signature header
  */
  function parseDKIM ( $sig )
  {

    $this->failed = true;
    $tags = preg_split ( '/;[\s\t]/', $sig );
    foreach ( $tags as $v )
    {
      list($key,$value) = preg_split ( '/=/', $v, 2 );
      $dkim[$key] = $value;
    }
    // the canonicalization method is currently undefined as of draft-01 of the iSchedule spec
    // but it does define the value, it should be simple-http.  RFC4871 also defines two methods
    // simple and relaxed, simple is probably the same as simple http
    // relaxed allows for header case folding and whitespace folding, see section 3.4.4 of RFC4871
    if ( ! preg_match ( '{(simple|simple-http|relaxed)(/(simple|simple-http|relaxed))?}', $dkim['c'], $matches ) ) // canonicalization method
      return 'bad canonicalization:' . $dkim['c'] ;
    if ( count ( $matches ) > 2 )
      $this->body_cannon = $matches[2];
    else
      $this->body_cannon = $matches[1];
    $this->header_cannon = $matches[1];
    // signing algorythm REQUIRED
    if ( $dkim['a'] != 'rsa-sha1' && $dkim['a'] != 'rsa-sha256' ) // we only support the minimum required
      return 'bad signing algorythm:' . $dkim['a'] ;
    // query method to retrieve public key, could/should we add https to the spec?  REQUIRED
    if ( $dkim['q'] != 'dns/txt' )
      return 'bad query method';
    // domain of the signing entity REQUIRED
    if ( ! isset ( $dkim['d'] ) )
      return 'missing signing domain';
    $this->remote_server = $dkim['d'];
    // identity of signing AGENT, OPTIONAL
    if ( isset ( $dkim['i'] ) )
    {
      // if present, domain of the signing agent must be a match or a subdomain of the signing domain
      if ( ! stristr ( $dkim['i'], $dkim['d'] ) ) // RFC4871 does not specify a case match requirement
        return 'signing domain mismatch';
      // grab the local part of the signing agent if it's an email address
      if ( strstr ( $dkim [ 'i' ], '@' ) )
        $this->remote_user = substr ( $dkim [ 'i' ], 0, strpos ( $dkim [ 'i' ], '@' ) - 1 );
    }
    // selector used to retrieve public key REQUIRED
    if ( ! isset ( $dkim['s'] ) )
      return 'missing selector';
    $this->remote_selector = $dkim['s'];
    // signed header fields, colon seperated  REQUIRED
    if ( ! isset ( $dkim['h'] ) )
      return 'missing list of signed headers';
    $this->signed_headers = preg_split ( '/:/', $dkim['h'] );

    $sh = Array ();
    foreach ( $this->signed_headers as $h )
    {
      $sh[] = strtolower ( $h );
      if ( in_array ( strtolower ( $h ), $this->disallowed_headers ) )
        return "$h is NOT allowed in signed header fields per RFC4871 or iSchedule";
    }
    foreach ( $this->required_headers as $h )
      if ( ! in_array ( strtolower ( $h ), $sh ) )
        return "$h is REQUIRED but missing in signed header fields per iSchedule";
    // body hash REQUIRED
    if ( ! isset ( $dkim['bh'] ) )
      return 'missing body signature';
    // signed header hash REQUIRED
    if ( ! isset ( $dkim['b'] ) )
      return 'missing signature in b field';
    // length of body used for signing
    if ( isset ( $dkim['l'] ) )
      $this->signed_length = $dkim['l'];
    $this->failed = false;
    $this->DKSig = $dkim;
    return true;
  }

  /**
  * split up a mailto uri into domain and user components
  * TODO handle other uri types (eg http)
  */
  function parseURI ( $uri )
  {
    if ( preg_match ( '/^mailto:([^@]+)@([^\s\t\n]+)/', $uri, $matches ) )
    {
      $this->remote_user = $matches[1];
      $this->domain = $matches[2];
    }
    else
      return false;
  }

  /**
  * verifies parsed DKIM header is valid for current message with a signature from the public key in DNS
  * TODO handle multiple headers of the same name
  */
  function verifySignature ( )
  {
    global $request,$c;
    $this->failed = true;
    $signed = '';
    foreach ( $this->signed_headers as $h )
      if ( isset ( $_SERVER['HTTP_' . strtoupper ( strtr ( $h, '-', '_' ) ) ] ) )
        $signed .= "$h: " . $_SERVER['HTTP_' . strtoupper ( strtr ( $h, '-', '_' ) ) ] . "\r\n";
      else
        $signed .= "$h: " . $_SERVER[ strtoupper ( strtr ( $h, '-', '_' ) ) ] . "\r\n";
    if ( ! isset ( $_SERVER['HTTP_ORIGINATOR'] ) || stripos ( $signed, 'Originator' ) === false ) //required header, must be signed
      return "missing Originator";
    if ( ! isset ( $_SERVER['HTTP_RECIPIENT'] ) || stripos ( $signed, 'Recipient' ) === false ) //required header, must be signed
      return "missing Recipient";
    if ( ! isset ( $_SERVER['HTTP_ISCHEDULE_VERSION'] ) || $_SERVER['HTTP_ISCHEDULE_VERSION'] != '1' ) //required header and we only speak version 1 for now
      return "missing or mismatch ischedule-version header";
    $body = $request->raw_post;
    if ( ! isset ( $this->signed_length ) ) // Should we use the Content-Length header if the signed length is missing?
      $this->signed_length = strlen ( $body );
    else
      $body = substr ( $body, 0, $this->signed_length );
    if ( isset ( $this->remote_user_rule ) )
      if ( $this->remote_user_rule != '*' && ! stristr ( $this->remote_user, $this->remote_user_rule ) )
        return "remote user rule failure";
    $hash_algo = preg_replace ( '/^.*(sha1|sha256).*/','$1', $this->DKSig['a'] );
    $body_hash = base64_encode ( hash ( $hash_algo, $body , true ) );
    if ( $this->DKSig['bh'] != $body_hash )
      return "body hash mismatch";
    $sig = $_SERVER['HTTP_DKIM_SIGNATURE'];
    $sig = preg_replace ( '/ b=[^;\s\r\n\t]+/', ' b=', $sig );
    $signed .= 'DKIM-Signature: ' . $sig;
    $verify = openssl_verify ( $signed, base64_decode ( $this->DKSig['b'] ), $this->remote_public_key, $hash_algo );
    if (  $verify != 1 )
    {
      openssl_sign ( $signed, $sigb, $this->schedule_private_key, $hash_algo );
      $sigc = base64_encode ( $sigb );
      $verify1 = openssl_verify ( $signed, $sigc, $this->remote_public_key, $hash_algo );
      return "signature verification failed " . $this->remote_public_key . " \n\n". $sig . " \n" . $hash_algo . "\n". print_r ($verify,1) . " XX " . $verify1 . "\n";
    }
    $this->failed = false;
    return true;
  }

  /**
  * checks that current request has a valid DKIM signature signed by a currently valid key from DNS
  */
  function validateRequest ( )
  {
    global $request;
    if ( isset ( $_SERVER['HTTP_DKIM_SIGNATURE'] ) )
      $sig = $_SERVER['HTTP_DKIM_SIGNATURE'];
    else
    {
      $request->DoResponse( 403, translate('DKIM signature missing') );
      return false;
    }
    if ( isset ( $_SERVER['HTTP_ORGANIZER'] ) )
      $request->DoResponse( 403, translate('Organizer Missing') );

    dbg_error_log ('ischedule','beginning validation');
    $err = $this->parseDKIM ( $sig );
    if ( $err !== true || $this->failed )
      $request->DoResponse( 412, 'DKIM signature invalid ' . "\n" . $err . "\n" );
    if ( ! $this->getTxt () || $this->failed ) // this could also be a 424 failed dependency response
      $request->DoResponse( 400, translate('DKIM signature validation failed(DNS ERROR)') );
    if ( ! $this->parseTxt () || $this->failed )
      $request->DoResponse( 400, translate('DKIM signature validation failed(KEY Parse ERROR)') );
    if ( ! $this->validateKey () || $this->failed )
      $request->DoResponse( 400, translate('DKIM signature validation failed(KEY Validation ERROR)') );
    $err = $this->verifySignature ();
    if ( $err !== true || $this->failed )
      $request->DoResponse( 412, translate('DKIM signature validation failed(Signature verification ERROR)') . '\n' . $err );
    dbg_error_log ('ischedule','signature ok');
    return true;
  }
}

DAViCal API documentation generated by ApiGen