diff --git a/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java b/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java index e5f50906..98d058a5 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java @@ -9,7 +9,6 @@ package at.bitfire.davdroid.resource; import android.content.Context; import android.text.TextUtils; -import android.util.Log; import com.squareup.okhttp.HttpUrl; @@ -17,7 +16,6 @@ import org.xbill.DNS.Lookup; import org.xbill.DNS.Record; import org.xbill.DNS.SRVRecord; import org.xbill.DNS.TXTRecord; -import org.xbill.DNS.TextParseException; import org.xbill.DNS.Type; import java.io.IOException; @@ -33,6 +31,8 @@ import at.bitfire.dav4android.UrlUtils; import at.bitfire.dav4android.exception.DavException; import at.bitfire.dav4android.exception.HttpException; import at.bitfire.dav4android.exception.NotFoundException; +import at.bitfire.dav4android.property.AddressbookDescription; +import at.bitfire.dav4android.property.AddressbookHomeSet; import at.bitfire.dav4android.property.CalendarColor; import at.bitfire.dav4android.property.CalendarDescription; import at.bitfire.dav4android.property.CalendarHomeSet; @@ -48,12 +48,22 @@ import lombok.NonNull; public class DavResourceFinder { private final static String TAG = "davdroid.ResourceFinder"; - + + protected enum Service { + CALDAV("caldav"), + CARDDAV("carddav"); + + final String name; + Service(String name) { this.name = name;} + @Override public String toString() { return name; } + }; + protected Context context; protected final HttpClient httpClient; protected final ServerInfo serverInfo; protected List + addressbooks = new LinkedList<>(), calendars = new LinkedList<>(), taskLists = new LinkedList<>(); @@ -66,12 +76,25 @@ public class DavResourceFinder { } public void findResources() throws URISyntaxException, IOException, HttpException, DavException { + findResources(Service.CARDDAV); + findResources(Service.CALDAV); + } + + public void findResources(Service service) throws URISyntaxException, IOException, HttpException, DavException { { URI baseURI = serverInfo.getBaseURI(); String domain = null; HttpUrl principalUrl = null; - Set calendarHomeSets = new HashSet<>(); + Set calendarHomeSets = new HashSet<>(), + addressbookHomeSets = new HashSet<>(); + if (service == Service.CALDAV) { + calendars.clear(); + taskLists.clear(); + } else if (service == Service.CARDDAV) + addressbooks.clear(); + + Constants.log.info("STARTING COLLECTION DISCOVERY FOR SERVICE " + service); if ("http".equals(baseURI.getScheme()) || "https".equals(baseURI.getScheme())) { HttpUrl userURL = HttpUrl.get(baseURI); @@ -82,23 +105,44 @@ public class DavResourceFinder { Constants.log.info("Check whether user-given URL is a calendar collection and/or contains and/or has "); DavResource davBase = new DavResource(httpClient, userURL); try { - davBase.propfind(0, - CalendarHomeSet.NAME, SupportedCalendarComponentSet.NAME, - ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME, - CurrentUserPrincipal.NAME - ); - addIfCalendar(davBase); - } catch (IOException | HttpException | DavException e) { + if (service == Service.CALDAV) { + davBase.propfind(0, + CalendarHomeSet.NAME, SupportedCalendarComponentSet.NAME, + ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME, + CurrentUserPrincipal.NAME + ); + addIfCalendar(davBase); + } else if (service == Service.CARDDAV) { + davBase.propfind(0, + AddressbookHomeSet.NAME, + ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME, CurrentUserPrivilegeSet.NAME, + CurrentUserPrincipal.NAME + ); + addIfAddressBook(davBase); + } + } catch (IOException|HttpException|DavException e) { Constants.log.debug("PROPFIND on user-given URL failed", e); } - CalendarHomeSet calendarHomeSet = (CalendarHomeSet) davBase.properties.get(CalendarHomeSet.NAME); - if (calendarHomeSet != null) { - Constants.log.info("Found at user-given URL"); - for (String href : calendarHomeSet.hrefs) { - HttpUrl url = userURL.resolve(href); - if (url != null) - calendarHomeSets.add(url); + if (service == Service.CALDAV) { + CalendarHomeSet calendarHomeSet = (CalendarHomeSet)davBase.properties.get(CalendarHomeSet.NAME); + if (calendarHomeSet != null) { + Constants.log.info("Found at user-given URL"); + for (String href : calendarHomeSet.hrefs) { + HttpUrl url = userURL.resolve(href); + if (url != null) + calendarHomeSets.add(url); + } + } + } else if (service == Service.CARDDAV) { + AddressbookHomeSet addressbookHomeSet = (AddressbookHomeSet)davBase.properties.get(AddressbookHomeSet.NAME); + if (addressbookHomeSet != null) { + Constants.log.info("Found at user-given URL"); + for (String href : addressbookHomeSet.hrefs) { + HttpUrl url = userURL.resolve(href); + if (url != null) + addressbookHomeSets.add(url); + } } } @@ -112,12 +156,8 @@ public class DavResourceFinder { principalUrl = davBase.location.resolve(currentUserPrincipal.href); if (principalUrl == null) { - Constants.log.info("User-given URL doesn't contain , trying /.well-known/caldav"); - try { - principalUrl = getCurrentUserPrincipal(userURL.resolve("/.well-known/caldav")); - } catch (IOException|HttpException|DavException e) { - Constants.log.debug("PROPFIND on /.well-known/caldav failed", e); - } + Constants.log.info("User-given URL doesn't contain , trying /.well-known/" + service.name); + principalUrl = getCurrentUserPrincipal(userURL.resolve("/.well-known/" + service.name)); } } @@ -136,53 +176,99 @@ public class DavResourceFinder { if (principalUrl == null && domain != null) { Constants.log.info("No principal URL yet, trying SRV/TXT records with domain " + domain); - principalUrl = discoverPrincipalUrl(domain, "caldavs"); + principalUrl = discoverPrincipalUrl(domain, service); } - // principal URL has been found, get calendar-home-set + // principal URL has been found, get addressbook-home-set/calendar-home-set if (principalUrl != null) { Constants.log.info("Principal URL=" + principalUrl + ", getting "); try { DavResource principal = new DavResource(httpClient, principalUrl); - principal.propfind(0, CalendarHomeSet.NAME); - CalendarHomeSet calendarHomeSet = (CalendarHomeSet)principal.properties.get(CalendarHomeSet.NAME); - if (calendarHomeSet != null) - Constants.log.info("Found at principal URL"); - for (String href : calendarHomeSet.hrefs) { - HttpUrl url = principal.location.resolve(href); - if (url != null) - calendarHomeSets.add(url); + + if (service == Service.CALDAV) { + principal.propfind(0, CalendarHomeSet.NAME); + CalendarHomeSet calendarHomeSet = (CalendarHomeSet)principal.properties.get(CalendarHomeSet.NAME); + if (calendarHomeSet != null) { + Constants.log.info("Found at principal URL"); + for (String href : calendarHomeSet.hrefs) { + HttpUrl url = principal.location.resolve(href); + if (url != null) + calendarHomeSets.add(url); + } } + } else if (service == Service.CARDDAV) { + principal.propfind(0, AddressbookHomeSet.NAME); + AddressbookHomeSet addressbookHomeSet = (AddressbookHomeSet)principal.properties.get(AddressbookHomeSet.NAME); + if (addressbookHomeSet != null) { + Constants.log.info("Found at principal URL"); + for (String href : addressbookHomeSet.hrefs) { + HttpUrl url = principal.location.resolve(href); + if (url != null) + addressbookHomeSets.add(url); + } + } + } + } catch (IOException|HttpException|DavException e) { Constants.log.debug("PROPFIND on " + principalUrl + " failed", e); } } // now query all home sets - for (HttpUrl url : calendarHomeSets) - try { - Constants.log.info("Listing collections in home set " + url); - DavResource homeSet = new DavResource(httpClient, url); - homeSet.propfind(1, SupportedCalendarComponentSet.NAME, ResourceType.NAME, DisplayName.NAME, CurrentUserPrivilegeSet.NAME, - CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME); + if (service == Service.CALDAV) + for (HttpUrl url : calendarHomeSets) + try { + Constants.log.info("Listing calendar collections in home set " + url); + DavResource homeSet = new DavResource(httpClient, url); + homeSet.propfind(1, SupportedCalendarComponentSet.NAME, ResourceType.NAME, DisplayName.NAME, CurrentUserPrivilegeSet.NAME, + CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME); - // home set should not be a calendar, but some servers have only one calendar and it's the home set - addIfCalendar(homeSet); + // home set should not be a calendar, but some servers have only one calendar and it's the home set + addIfCalendar(homeSet); - // members of the home set can be calendars, too - for (DavResource member : homeSet.members) - addIfCalendar(member); - } catch (IOException|HttpException|DavException e) { - Constants.log.debug("PROPFIND on " + url + " failed", e); - } + // members of the home set can be calendars, too + for (DavResource member : homeSet.members) + addIfCalendar(member); + } catch (IOException|HttpException|DavException e) { + Constants.log.debug("PROPFIND on " + url + " failed", e); + } + else if (service == Service.CARDDAV) + for (HttpUrl url : addressbookHomeSets) + try { + Constants.log.info("Listing address books in home set " + url); + DavResource homeSet = new DavResource(httpClient, url); + homeSet.propfind(1, ResourceType.NAME, DisplayName.NAME, CurrentUserPrivilegeSet.NAME, AddressbookDescription.NAME); - // TODO CardDAV + // home set should not be an address book, but some servers have only one address book and it's the home set + addIfAddressBook(homeSet); + + // members of the home set can be calendars, too + for (DavResource member : homeSet.members) + addIfAddressBook(member); + } catch (IOException|HttpException|DavException e) { + Constants.log.debug("PROPFIND on " + url + " failed", e); + } // TODO remove duplicates // TODO notify user on errors? - serverInfo.setCalendars(calendars); - serverInfo.setTaskLists(taskLists); + if (service == Service.CALDAV) { + serverInfo.setCalendars(calendars); + serverInfo.setTaskLists(taskLists); + } else if (service == Service.CARDDAV) + serverInfo.setAddressBooks(addressbooks); + }} + + /** + * If the given DavResource is a #{@link ResourceType#ADDRESSBOOK}, add it to #{@link #addressbooks}. + * @param dav DavResource to check + */ + protected void addIfAddressBook(@NonNull DavResource dav) { + ResourceType resourceType = (ResourceType)dav.properties.get(ResourceType.NAME); + if (resourceType != null && resourceType.types.contains(ResourceType.ADDRESSBOOK)) { + Constants.log.info("Found address book at " + dav.location); + addressbooks.add(resourceInfo(dav, ServerInfo.ResourceInfo.Type.ADDRESS_BOOK)); + } } /** @@ -204,21 +290,24 @@ public class DavResourceFinder { supportsTasks = supportedCalendarComponentSet.supportsTasks; } if (supportsEvents) - calendars.add(resourceInfo(dav)); + calendars.add(resourceInfo(dav, ServerInfo.ResourceInfo.Type.CALENDAR)); if (supportsTasks) - taskLists.add(resourceInfo(dav)); + taskLists.add(resourceInfo(dav, ServerInfo.ResourceInfo.Type.CALENDAR)); } } /** * Builds a #{@link at.bitfire.davdroid.resource.ServerInfo.ResourceInfo} from a given - * #{@link DavResource}. Uses the DAV properties current-user-properties, current-user-privilege-set, - * displayname, calendar-description and calendar-color. Make sure you have queried these - * properties from the DavResource. + * #{@link DavResource}. Uses these DAV properties: + *
    + *
  • calendars: current-user-properties, current-user-privilege-set, displayname, calendar-description, calendar-color
  • + *
  • address books: current-user-properties, current-user-privilege-set, displayname, addressbook-description
  • + *
. Make sure you have queried these properties from the DavResource. * @param dav DavResource to take the resource info from + * @param type must be ADDRESS_BOOK or CALENDAR * @return ResourceInfo which represents the DavResource */ - protected ServerInfo.ResourceInfo resourceInfo(DavResource dav) { + protected ServerInfo.ResourceInfo resourceInfo(DavResource dav, ServerInfo.ResourceInfo.Type type) { boolean readOnly = false; CurrentUserPrivilegeSet privilegeSet = (CurrentUserPrivilegeSet)dav.properties.get(CurrentUserPrivilegeSet.NAME); if (privilegeSet != null) @@ -232,17 +321,23 @@ public class DavResourceFinder { title = UrlUtils.lastSegment(dav.location); String description = null; - CalendarDescription calendarDescription = (CalendarDescription)dav.properties.get(CalendarDescription.NAME); - if (calendarDescription != null) - description = calendarDescription.description; - Integer color = null; - CalendarColor calendarColor = (CalendarColor)dav.properties.get(CalendarColor.NAME); - if (calendarColor != null) + if (type == ServerInfo.ResourceInfo.Type.ADDRESS_BOOK) { + AddressbookDescription addressbookDescription = (AddressbookDescription)dav.properties.get(AddressbookDescription.NAME); + if (addressbookDescription != null) + description = addressbookDescription.description; + } else if (type == ServerInfo.ResourceInfo.Type.CALENDAR) { + CalendarDescription calendarDescription = (CalendarDescription)dav.properties.get(CalendarDescription.NAME); + if (calendarDescription != null) + description = calendarDescription.description; + + CalendarColor calendarColor = (CalendarColor)dav.properties.get(CalendarColor.NAME); + if (calendarColor != null) color = calendarColor.color; + } return new ServerInfo.ResourceInfo( - ServerInfo.ResourceInfo.Type.CALENDAR, + type, readOnly, dav.location.toString(), title, @@ -254,17 +349,17 @@ public class DavResourceFinder { /** * Try to find the principal URL by performing service discovery on a given domain name. * @param domain domain name, e.g. "icloud.com" - * @param serviceName service name: "caldavs" or "carddavs" + * @param service service to discover (CALDAV or CARDDAV) * @return principal URL, or null if none found */ - protected HttpUrl discoverPrincipalUrl(String domain, String serviceName) { + protected HttpUrl discoverPrincipalUrl(String domain, Service service) { String scheme = null; String fqdn = null; Integer port = null; - String path = null; + List paths = new LinkedList<>(); // there may be multiple paths to try try { - final String query = "_" + serviceName + "._tcp." + domain; + final String query = "_" + service.name + "s._tcp." + domain; Constants.log.debug("Looking up SRV records for " + query); Record[] records = new Lookup(query, Type.SRV).run(); if (records != null && records.length >= 1) { @@ -274,7 +369,7 @@ public class DavResourceFinder { scheme = "https"; fqdn = srv.getTarget().toString(true); port = srv.getPort(); - Constants.log.info("Found " + serviceName + " service: fqdn=" + fqdn + ", port=" + port); + Constants.log.info("Found " + service + " service: fqdn=" + fqdn + ", port=" + port); // look for TXT record too (for initial context path) records = new Lookup(domain, Type.TXT).run(); @@ -282,33 +377,35 @@ public class DavResourceFinder { TXTRecord txt = (TXTRecord)records[0]; for (String segment : (String[])txt.getStrings().toArray(new String[0])) if (segment.startsWith("path=")) { - path = segment.substring(5); - Constants.log.info("Found TXT record; initial context path=" + path); + paths.add(segment.substring(5)); + Constants.log.info("Found TXT record; initial context path=" + paths); break; } } - if (path == null) // no path from TXT records, use .well-known - path = "/.well-known/caldav"; + // if there's TXT record if it it's wrong, try well-known + paths.add("/.well-known/" + service.name); + // if this fails, too, try "/" + paths.add("/"); } + } catch (IOException e) { + Constants.log.debug("SRV/TXT record discovery failed", e); + return null; + } - if (!TextUtils.isEmpty(scheme) && !TextUtils.isEmpty(fqdn) && port != null && path != null) { + for (String path : paths) { + if (!TextUtils.isEmpty(scheme) && !TextUtils.isEmpty(fqdn) && port != null && paths != null) { HttpUrl initialContextPath = new HttpUrl.Builder() .scheme(scheme) .host(fqdn).port(port) .encodedPath(path) .build(); - HttpUrl principal = null; - try { - principal = getCurrentUserPrincipal(initialContextPath); - } catch(NotFoundException e) { - principal = getCurrentUserPrincipal(initialContextPath.resolve("/")); - } - return principal; + Constants.log.info("Trying to determine principal from initial context path=" + initialContextPath); + HttpUrl principal = getCurrentUserPrincipal(initialContextPath); + if (principal != null) + return principal; } - } catch (IOException|HttpException|DavException e) { - Constants.log.debug("Service discovery failed", e); } return null; } @@ -318,89 +415,24 @@ public class DavResourceFinder { * @param url URL to query with PROPFIND (Depth: 0) * @return current-user-principal URL, or null if none */ - protected HttpUrl getCurrentUserPrincipal(HttpUrl url) throws IOException, HttpException, DavException { - DavResource dav = new DavResource(httpClient, url); - dav.propfind(0, CurrentUserPrincipal.NAME); - CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)dav.properties.get(CurrentUserPrincipal.NAME); - if (currentUserPrincipal != null && currentUserPrincipal.href != null) - return url.resolve(currentUserPrincipal.href); + protected HttpUrl getCurrentUserPrincipal(HttpUrl url) { + try { + DavResource dav = new DavResource(httpClient, url); + dav.propfind(0, CurrentUserPrincipal.NAME); + CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal) dav.properties.get(CurrentUserPrincipal.NAME); + if (currentUserPrincipal != null && currentUserPrincipal.href != null) { + HttpUrl principal = url.resolve(currentUserPrincipal.href); + if (principal != null) { + Constants.log.info("Found current-user-principal: " + principal); + return principal; + } + } + } catch(IOException|HttpException|DavException e) { + Constants.log.debug("PROPFIND for current-user-principal on " + url + " failed", e); + } return null; } - /** - * Finds the initial service URL from a given base URI (HTTP[S] or mailto URI, user name, password) - * @param serverInfo User-given service information (including base URI, i.e. HTTP[S] URL+user name+password or mailto URI and password) - * @param serviceName Service name ("carddav" or "caldav") - * @return Initial service URL (HTTP/HTTPS), without user credentials - * @throws URISyntaxException when the user-given URI is invalid - */ - public HttpUrl getInitialContextURL(ServerInfo serverInfo, String serviceName) throws URISyntaxException { - String scheme, - domain; - int port = -1; - String path = "/"; - - URI baseURI = serverInfo.getBaseURI(); - if ("mailto".equalsIgnoreCase(baseURI.getScheme())) { - // mailto URIs - String mailbox = serverInfo.getBaseURI().getSchemeSpecificPart(); - - // determine service FQDN - int pos = mailbox.lastIndexOf("@"); - if (pos == -1) - throw new URISyntaxException(mailbox, "Missing @ sign"); - - scheme = "https"; - domain = mailbox.substring(pos + 1); - if (domain.isEmpty()) - throw new URISyntaxException(mailbox, "Missing domain name"); - } else { - // HTTP(S) URLs - scheme = baseURI.getScheme(); - domain = baseURI.getHost(); - port = baseURI.getPort(); - path = baseURI.getPath(); - } - - // try to determine FQDN and port number using SRV records - try { - String name = "_" + serviceName + "._tcp." + domain; - Constants.log.debug("Looking up SRV records for " + name); - Record[] records = new Lookup(name, Type.SRV).run(); - if (records != null && records.length >= 1) { - SRVRecord srv = selectSRVRecord(records); - - scheme = "https"; - domain = srv.getTarget().toString(true); - port = srv.getPort(); - Log.d(TAG, "Found " + serviceName + " service for " + domain + " -> " + domain + ":" + port); - - // SRV record found, look for TXT record too (for initial context path) - records = new Lookup(name, Type.TXT).run(); - if (records != null && records.length >= 1) { - TXTRecord txt = (TXTRecord)records[0]; - for (Object o : txt.getStrings().toArray()) { - String segment = (String)o; - if (segment.startsWith("path=")) { - path = segment.substring(5); - Constants.log.debug("Found initial context path for " + serviceName + " at " + domain + " -> " + path); - break; - } - } - } - } - } catch (TextParseException e) { - throw new URISyntaxException(domain, "Invalid domain name"); - } - - HttpUrl.Builder builder = new HttpUrl.Builder().scheme(scheme).host(domain); - if (port != -1) - builder.port(port); - if (TextUtils.isEmpty(path)) - path = "/"; - return builder.encodedPath(path).build(); - } - // helpers