CalDAV and CardDAV handshake
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:
<?xmlversion="1.0"encoding="UTF-8"?><A:propfindxmlns: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 > < A:current-user-principal /> < A:displayname /> < A:principal-collection-set /> < A:principal-URL /> < A:resource-id /> </ 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" ?> < A:prop
xmlns:A = "DAV:" > < A:getetag /> < B:calendar-data /> </ A:prop > < A:href
xmlns:A = "DAV:" /calendars/__uids__/C0F07FAF-A20A-4A88-A518-D27E5D3074CA/calendar/2011Z@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 > 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. 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 > </ D:prop > </ D:propstat > </ D:response > </ D:multistatus > |