CalDAV and CardDAV handshake

Last modified by Pascal Robert on 2013/11/18 06:16

http://www.macti.ca/wiki/images/icons/emoticons/information.png

The RFC that explains the handshake is now available.

 

If you use iCal/Calendar on Mac OS X or iOS, you might have seen that you don't have to specify the type of server ("Automatic" is the default type). But to work, you need to setup DNS entries so that iCal/Calendar can find the server for the user, and your CalDAV server must support "well-known" URLs. "well-known" is supported by iCal Server (OS X Server 10.7 or 10.8), Calendar Server (the open source version of iCal Server) and Kerio Connect (starting with version 7.2).

For DNS, I found how to do it in this article. In short, you need at least one SRV entry that will tell where your server is located. If your domain is "macti.lan", you need to add the following entry:

 

_caldavs._tcp.macti.lan.            10800 IN SRV      10 1 8443 server.macti.lan.

 

Where 8443 is the port on which your CalDAV server is running (8443 is the HTTPS port for iCal Server/Calendar Server, for Kerio Connect by default it's on port 443). server.macti.lan is the DNS name of the host running the CalDAV service.

You will notice that the DNS entry is _caldavs_. If your CalDAV server is running on a non-protected port, or if it's available on both ports, you should add a DNS entry like this:

 

_caldav._tcp.macti.lan.            10800 IN SRV      10 1 8008 server.macti.lan.

 

Notice that the 's' is missing in the DNS entry. If you have both entries, iCal/Calendar will try the secure entry first, and it's not working, it will try the non-secure entry after.

If you have a iCloud account with a @me.com, a SRV entry do exist:

 

$ dig _caldavs._tcp.me.com srv
_caldavs._tcp.me.com. 3600 IN SRV 0 0 443 caldav.icloud.com.

 

Once the DNS entry is found, the CalDAV client will take the value off the SRV record and do the following:

  • Do a PROPFIND (WebDAV method) request on https://server.macti.lan:443/.well-known/caldav (or port 80 if the SRV record wasn't specifying https) 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 nothing was found on port 443 or port 80, it will do the same request on port 8443 or 8008.
  • If nothing was found on port 8443 or 8008, it will then use use the host and port from the SRV record.

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
>
</
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

lan:mailto:probert@macti.lan

ORGANIZER;CN=probert@macti.

mailto:pascal.probert@gmail.com

ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=Pascal Robert;X-NUM-GUESTS=0:     
CLASS:CONFIDENTIAL
CREATED:20111210T021446Z
DESCRIPTION:Un plus long texte
LAST-MODIFIED:20111210T021446Z
LOCATION:Un endroit
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Événement test
TRANSP:OPAQU
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
>