From 4be031e2742adea86eb5f76ed6dc6c0cff3a70f7 Mon Sep 17 00:00:00 2001 From: rfc2822 Date: Sat, 19 Jul 2014 00:39:07 +0200 Subject: [PATCH] Add support for well-known URIs (RFC 5785) - closes #148 --- .../QueryServerDialogFragment.java | 137 ++++++++++++------ .../davdroid/syncadapter/ServerInfo.java | 2 +- .../davdroid/webdav/WebDavResource.java | 4 - .../webdav/test/WebDavResourceTest.java | 8 +- 4 files changed, 96 insertions(+), 55 deletions(-) diff --git a/src/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java b/src/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java index 878e0207..bc0d6236 100644 --- a/src/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java +++ b/src/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java @@ -115,51 +115,22 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC CloseableHttpClient httpClient = DavHttpClient.create(true, true); try { - // (1/5) detect capabilities WebDavResource base = new WebDavResource(httpClient, new URI(serverInfo.getProvidedURL()), serverInfo.getUserName(), serverInfo.getPassword(), serverInfo.isAuthPreemptive()); - base.options(); + + // CardDAV + WebDavResource principal = getCurrentUserPrincipal(base, "carddav", "addressbook"); + if (principal != null) { + serverInfo.setCardDAV(true); - serverInfo.setCardDAV(base.supportsDAV("addressbook")); - serverInfo.setCalDAV(base.supportsDAV("calendar-access")); - if (!base.supportsMethod("PROPFIND") || !base.supportsMethod("REPORT") || - (!serverInfo.isCalDAV() && !serverInfo.isCardDAV())) - throw new DavIncapableException(getContext().getString(R.string.neither_caldav_nor_carddav)); - - // (2/5) get principal URL - base.propfind(Mode.CURRENT_USER_PRINCIPAL); - - String principalPath = base.getCurrentUserPrincipal(); - if (principalPath != null) - Log.i(TAG, "Found principal path: " + principalPath); - else - throw new DavIncapableException(getContext().getString(R.string.error_principal_path)); - - // (3/5) get home sets - WebDavResource principal = new WebDavResource(base, principalPath); - principal.propfind(Mode.HOME_SETS); - - String pathAddressBooks = null; - if (serverInfo.isCardDAV()) { - pathAddressBooks = principal.getAddressbookHomeSet(); + principal.propfind(Mode.HOME_SETS); + String pathAddressBooks = principal.getAddressbookHomeSet(); if (pathAddressBooks != null) Log.i(TAG, "Found address book home set: " + pathAddressBooks); else throw new DavIncapableException(getContext().getString(R.string.error_home_set_address_books)); - } - - String pathCalendars = null; - if (serverInfo.isCalDAV()) { - pathCalendars = principal.getCalendarHomeSet(); - if (pathCalendars != null) - Log.i(TAG, "Found calendar home set: " + pathCalendars); - else - throw new DavIncapableException(getContext().getString(R.string.error_home_set_calendars)); - } - - // (4/5) get address books - if (serverInfo.isCardDAV()) { - WebDavResource homeSetAddressBooks = new WebDavResource(principal, pathAddressBooks, true); + + WebDavResource homeSetAddressBooks = new WebDavResource(principal, pathAddressBooks); homeSetAddressBooks.propfind(Mode.MEMBERS_COLLECTIONS); List addressBooks = new LinkedList(); @@ -176,13 +147,22 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC ); addressBooks.add(info); } - serverInfo.setAddressBooks(addressBooks); } + + // CalDAV + principal = getCurrentUserPrincipal(base, "caldav", "calendar-access"); + if (principal != null) { + serverInfo.setCalDAV(true); - // (5/5) get calendars - if (serverInfo.isCalDAV()) { - WebDavResource homeSetCalendars = new WebDavResource(principal, pathCalendars, true); + principal.propfind(Mode.HOME_SETS); + String pathCalendars = principal.getCalendarHomeSet(); + if (pathCalendars != null) + Log.i(TAG, "Found calendar home set: " + pathCalendars); + else + throw new DavIncapableException(getContext().getString(R.string.error_home_set_calendars)); + + WebDavResource homeSetCalendars = new WebDavResource(principal, pathCalendars); homeSetCalendars.propfind(Mode.MEMBERS_COLLECTIONS); List calendars = new LinkedList(); @@ -209,23 +189,88 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC info.setTimezone(resource.getTimezone()); calendars.add(info); } - serverInfo.setCalendars(calendars); } + + if (!serverInfo.isCalDAV() && !serverInfo.isCardDAV()) + throw new DavIncapableException(getContext().getString(R.string.neither_caldav_nor_carddav)); } catch (URISyntaxException e) { serverInfo.setErrorMessage(getContext().getString(R.string.exception_uri_syntax, e.getMessage())); } catch (IOException e) { serverInfo.setErrorMessage(getContext().getString(R.string.exception_io, e.getLocalizedMessage())); - } catch (DavException e) { - Log.e(TAG, "DAV error while querying server info", e); - serverInfo.setErrorMessage(getContext().getString(R.string.exception_incapable_resource, e.getLocalizedMessage())); } catch (HttpException e) { Log.e(TAG, "HTTP error while querying server info", e); serverInfo.setErrorMessage(getContext().getString(R.string.exception_http, e.getLocalizedMessage())); + } catch (DavException e) { + Log.e(TAG, "DAV error while querying server info", e); + serverInfo.setErrorMessage(getContext().getString(R.string.exception_incapable_resource, e.getLocalizedMessage())); } return serverInfo; } + + /** + * Detects the current-user-principal for a given WebDavResource. At first, /.well-known/ is tried. Only + * if no current-user-principal can be detected for the .well-known location, the given location of the resource + * is tried. + * When a current-user-principal is found, it is queried for davCapability via OPTIONS. + * @param resource Location that will be queried + * @param serviceName Well-known service name ("carddav", "caldav") + * @param davCapability DAV capability to check for ("addressbook", "calendar-access") + * @return WebDavResource of current-user-principal for the given service, or null if it can't be found or it is incapable + */ + private WebDavResource getCurrentUserPrincipal(WebDavResource resource, String serviceName, String davCapability) throws IOException { + // look for well-known service (RFC 5785) + try { + WebDavResource wellKnown = new WebDavResource(resource, "/.well-known/" + serviceName); + wellKnown.propfind(Mode.CURRENT_USER_PRINCIPAL); + if (wellKnown.getCurrentUserPrincipal() != null) { + WebDavResource principal = new WebDavResource(wellKnown, wellKnown.getCurrentUserPrincipal()); + if (checkCapabilities(principal, davCapability)) + return principal; + else + Log.w(TAG, "Current-user-principal " + resource.getLocation() + " found via well-known service, but it doesn't support required DAV facilities"); + } + } catch (HttpException e) { + Log.d(TAG, "Well-known service detection failed with HTTP error", e); + } catch (DavException e) { + Log.d(TAG, "Well-known service detection failed at DAV level", e); + } + + try { + // fall back to user-given initial context path + resource.propfind(Mode.CURRENT_USER_PRINCIPAL); + if (resource.getCurrentUserPrincipal() != null) { + WebDavResource principal = new WebDavResource(resource, resource.getCurrentUserPrincipal()); + if (checkCapabilities(principal, davCapability)) + return principal; + else + Log.w(TAG, "Current-user-principal " + resource.getLocation() + " found at user-given location, but it doesn't support required DAV facilities"); + } + } catch (HttpException e) { + Log.d(TAG, "Service detection failed with HTTP error", e); + } catch (DavException e) { + Log.d(TAG, "Service detection failed at DAV level", e); + } + return null; + } + + private boolean checkCapabilities(WebDavResource resource, String davCapability) throws IOException { + // check for necessary capabilities + try { + resource.options(); + if (resource.supportsDAV(davCapability) && + resource.supportsMethod("PROPFIND") && + resource.supportsMethod("GET") && + resource.supportsMethod("REPORT") && + resource.supportsMethod("PUT") && + resource.supportsMethod("DELETE")) + return true; + } catch(HttpException e) { + // for instance, 405 Method not allowed + } + return false; + } } } diff --git a/src/at/bitfire/davdroid/syncadapter/ServerInfo.java b/src/at/bitfire/davdroid/syncadapter/ServerInfo.java index 50c66c93..839d54d3 100644 --- a/src/at/bitfire/davdroid/syncadapter/ServerInfo.java +++ b/src/at/bitfire/davdroid/syncadapter/ServerInfo.java @@ -25,7 +25,7 @@ public class ServerInfo implements Serializable { private String errorMessage; - private boolean calDAV, cardDAV; + private boolean calDAV = false, cardDAV = false; private List addressBooks = new LinkedList(), calendars = new LinkedList(); diff --git a/src/at/bitfire/davdroid/webdav/WebDavResource.java b/src/at/bitfire/davdroid/webdav/WebDavResource.java index 870e90f8..120528f3 100644 --- a/src/at/bitfire/davdroid/webdav/WebDavResource.java +++ b/src/at/bitfire/davdroid/webdav/WebDavResource.java @@ -134,10 +134,6 @@ public class WebDavResource { location = parent.location.resolve(URIUtils.sanitize(member)); } - public WebDavResource(WebDavResource parent, String member, boolean trailingSlash) { - this(parent, (trailingSlash && !member.endsWith("/")) ? (member + "/") : member); - } - public WebDavResource(WebDavResource parent, String member, String ETag) { this(parent, member); properties.put(Property.ETAG, ETag); diff --git a/test/src/at/bitfire/davdroid/webdav/test/WebDavResourceTest.java b/test/src/at/bitfire/davdroid/webdav/test/WebDavResourceTest.java index aff0d5f2..e1fc9954 100644 --- a/test/src/at/bitfire/davdroid/webdav/test/WebDavResourceTest.java +++ b/test/src/at/bitfire/davdroid/webdav/test/WebDavResourceTest.java @@ -49,7 +49,7 @@ public class WebDavResourceTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - httpClient = DavHttpClient.create(true, true); + httpClient = DavHttpClient.create(true, true); assetMgr = getInstrumentation().getContext().getResources().getAssets(); @@ -127,7 +127,7 @@ public class WebDavResourceTest extends InstrumentationTestCase { } public void testPropfindAddressBooks() throws IOException, HttpException, DavException { - WebDavResource dav = new WebDavResource(davCollection, "addressbooks/test", true); + WebDavResource dav = new WebDavResource(davCollection, "addressbooks/test"); dav.propfind(HttpPropfind.Mode.MEMBERS_COLLECTIONS); assertEquals(2, dav.getMembers().size()); for (WebDavResource member : dav.getMembers()) { @@ -140,7 +140,7 @@ public class WebDavResourceTest extends InstrumentationTestCase { } public void testPropfindCalendars() throws IOException, HttpException, DavException { - WebDavResource dav = new WebDavResource(davCollection, "calendars/test", true); + WebDavResource dav = new WebDavResource(davCollection, "calendars/test"); dav.propfind(HttpPropfind.Mode.MEMBERS_COLLECTIONS); assertEquals(3, dav.getMembers().size()); assertEquals("0xFF00FF", dav.getMembers().get(2).getColor()); @@ -188,7 +188,7 @@ public class WebDavResourceTest extends InstrumentationTestCase { } public void testMultiGet() throws DavException, IOException, HttpException { - WebDavResource davAddressBook = new WebDavResource(davCollection, "addressbooks/default.vcf", true); + WebDavResource davAddressBook = new WebDavResource(davCollection, "addressbooks/default.vcf"); davAddressBook.multiGet(DavMultiget.Type.ADDRESS_BOOK, new String[] { "1.vcf", "2.vcf" }); assertEquals(2, davAddressBook.getMembers().size()); for (WebDavResource member : davAddressBook.getMembers()) {