Sunday, October 14, 2012

CalDAV handsake

When you add an CalDAV account in iCal (OS X 10.7, aka Lion, or earlier) or in Calendar (OS X 10.8, aka Mountain Lion), it does a lot of magic to configure the account. In this post, I will explain how the "handsake" is done, and what kind of data and queries is done between the client and the server. From now on, I will use iCal as the name of the client, but everything is valid for the Calendar app in 10.8 too.

When you set the account type to Automatic, as explained in my previous blog post, iCal will do a DNS query to find out what is the host name of the CalDAV server. If a SRV DNS entry have been found, iCal will get the hostname and TCP port from the SRV entry, and will do the following (here, I'm assuming that the CalDAV server found in the SRV entry is "server.macti.lan" and the TCP port is 8008):
  • Do a PROPFIND (WebDAV method) request on http://server.macti.lan:80/.well-known/caldav with the following body:
    <?xml version="1.0" encoding="UTF-8"?>
    <A:propfind xmlns:A="DAV:">
      <A:prop>
        <A:current-user-principal/>
        <A:principal-URL/>
        <A:resourcetype/>
      </A:prop>
    </A:propfind>
  • If iCal didn't get a valid response on port 80, it will try the same request, but on port 8080 instead.
  • If iCal didn't get a valid response on port 80 or 8080, it will try on the port it found in the SRV entry, in our case, port 8008.
The response from /.well-known/caldav might be a redirect (HTTP status code 301 or 302). In fact, that's what iCal Server 10.8 does, it redirect the request to http://server.macti.lan:8008/. So again, iCal will do the same request as before until it gets a 207 status code instead of a redirect. The response will look like this:

<?xml version='1.0' encoding='UTF-8'?>
<multistatus xmlns='DAV:'>
  <response>
    <href>/</href>
    <propstat>
      <prop>
        <current-user-principal>
          <href>/principals/__uids__/C0F07FAF-A20A-4A88-A518-D27E5D3074CA/</href>
        </current-user-principal>
        <resourcetype>
          <collection/>
        </resourcetype>
      </prop>
      <status>HTTP/1.1 200 OK</status>
    </propstat>
  </response>
</multistatus>
The value we need is the value of current-user-principal, which indicates the location of the user's "principals". With this value, iCal will make a OPTIONS request to /principals/__uids__/C0F07FAF-A20A-4A88-A518-D27E5D3074CA/. I don't know why iCal is making the request, but my guess it's doing it so that it can find which CalDAV features are supported by the CalDAV server. That list is available in the response's DAV header:
DAV: 1, access-control, calendar-access, calendar-schedule, calendar-auto-schedule, calendar-availability, inbox-availability, calendar-proxy, calendarserver-private-events, calendarserver-private-comments, calendarserver-sharing, calendarserver-sharing-no-scheduling, calendar-query-extended, calendar-default-alarms, addressbook, extended-mkcol, calendarserver-principal-property-search
This is a response from iCal Server 10.8, which supports all CalDAV features and extensions. Doing the same request to a Google Calendar server will return a lot less features/extensions.

Following the OPTIONS request, iCal will make a PROPFIND request on the same URL as the OPTIONS request, with the following body:
<?xml version="1.0" encoding="UTF-8"?>
<A:propfind xmlns:A="DAV:">
  <A:prop>
    <C:calendar-home-set xmlns:C="urn:ietf:params:xml:ns:caldav"/>
    <C:calendar-user-address-set xmlns:C="urn:ietf:params:xml:ns:caldav"/>

    <A:current-user-principal/>
    <A:displayname/>
    <B:dropbox-home-URL xmlns:B="http://calendarserver.org/ns/"/>
    <B:email-address-set xmlns:B="http://calendarserver.org/ns/"/>
    <B:notification-URL xmlns:B="http://calendarserver.org/ns/"/>
    <A:principal-collection-set/>
    <A:principal-URL/>
    <A:resource-id/>
    <C:schedule-inbox-URL xmlns:C="urn:ietf:params:xml:ns:caldav"/&gt
    <C:schedule-outbox-URL xmlns:C="urn:ietf:params:xml:ns:caldav"/>    <A:supported-report-set/>
  </A:prop>
</A:propfind>
The value that we need back from this request is the value of the calendar-home-set property. This property is the URL to calendar collections. So iCal can finally found where the calendars are, and it will do a PROPFIND request to the URL found with the value of calendar-home-set. The body of that PROPFIND request is quite large so I'm not going to past it here, but in short, that PROPFIND will find the request properties for each sub-collections in the main calendar collection. For example, if the main collection is located at:
/calendars/__uids__/0B9B4FA7-8ADC-4984-8258-D04A4939A574/
The "calendar" collection will be located at:
/calendars/__uids__/0B9B4FA7-8ADC-4984-8258-D04A4939A574/calendar/ 
And the "tasks" collection:
/calendars/__uids__/0B9B4FA7-8ADC-4984-8258-D04A4939A574/tasks
So, for each sub-collection, iCal will do the following:

  • A PROPFIND request to find the value of the checksum-versions property;
  • A PROPFIND request to find the value of the getctag and sync-token properties;
  • A PROPFIND request to find the value of the getcontenttype and getetag properties;
The third request, who fetches the content type and etag, is the one that will return a link to all iCalendar objects from the calendar collection. The response will return a <response> XML attribute for each iCalendar objects, with the following structure:
  <response>
    <href>/calendars/__uids__/C0F07FAF-A20A-4A88-A518-D27E5D3074CA/calendar/20111210T013923Z-uidGen%40mbp-pascal-robert-4.local.ics</href>
    <propstat>
      <prop>
        <getcontenttype>text/calendar;charset=utf-8</getcontenttype>
        <getetag>"9b6b2d11f86891f748ef09ab0993b75c"</getetag>
      </prop>
      <status>HTTP/1.1 200 OK</status>
    </propstat>
  </response>
From there, iCal will simply fetch all valid iCalendar objects and display them in the calendar window. This is done by doing a REPORT request on the calendar collection, with the calendar-multiget attribute, with the list of calendar objects that the previous PROPFIND response have returned.

<?xml version="1.0" encoding="UTF-8"?><B:calendar-multiget xmlns:B="urn:ietf:params:xml:ns:caldav">  <A:prop xmlns:A="DAV:">    <A:getetag/>    <B:calendar-data/>    <C:updated-by xmlns:C="http://calendarserver.org/ns/"/>    <C:created-by xmlns:C="http://calendarserver.org/ns/"/>  </A:prop>  <A:href xmlns:A="DAV:">/calendars/__uids__/C0F07FAF-A20A-4A88-A518-D27E5D3074CA/calendar/20111210T013923Z-uidGen%40mbp-pascal-robert-4.local.ics</A:href></B:calendar-multiget> 
This is the response:

<D:multistatus xmlns:D="DAV:">  <D:response>  <D:href>/calendars/__uids__/C0F07FAF-A20A-4A88-A518-D27E5D3074CA/calendar/20111210T013923Z-uidGen%40mbp-pascal-robert-4.local.ics</D:href>  <D:propstat>  <D:status>HTTP/1.1 200 OK</D:status>  <D:prop>  <D:getetag>"63459166486"</D:getetag>  <C:calendar-data xmlns:C="urn:ietf:params:xml:ns:caldav"> BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN X-WR-CALNAME:Pascal Robert X-WR-TIMEZONE:America/New_York  BEGIN:VEVENT DTSTART:20111209T140000Z  DTEND:20111209T180000Z DTSTAMP:20111210T021446Z  ORGANIZER;CN=probert@macti.lan:mailto:probert@macti.lan UID:20111210T021447Z-uidGen@mbp-pascal-robert-4.local  ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=Pascal Robert;X-NUM-GUESTS=0:mailto:pascal.probert@gmail.com  CLASS:CONFIDENTIAL CREATED:20111210T021446Z  DESCRIPTION:Un plus long texte LAST-MODIFIED:20111210T021446Z  LOCATION:Un endroit SEQUENCE:0  STATUS:CONFIRMED SUMMARY:Événement test  TRANSP:OPAQUE BEGIN:VALARM ACTION:DISPLAY DESCRIPTION:This is an event reminder  TRIGGER:-P0DT0H30M0S END:VALARM BEGIN:VALARM ACTION:AUDIO TRIGGER;VALUE=DATE-TIME:20111209T134500Z END:VALARM END:VEVENT END:VCALENDAR </C:calendar-data>  </D:prop>  </D:propstat>  <D:propstat>  <D:status>HTTP/1.1 404 Not Found</D:status>  <D:prop>  <C:updated-by xmlns:C="http://calendarserver.org/ns/" />  <C:created-by xmlns:C="http://calendarserver.org/ns/" />  </D:prop>  </D:propstat>  </D:response> </D:multistatus>


No comments:

Post a Comment