Loading app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java +198 −166 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -49,11 +49,21 @@ 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<ServerInfo.ResourceInfo> addressbooks = new LinkedList<>(), calendars = new LinkedList<>(), taskLists = new LinkedList<>(); Loading @@ -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<HttpUrl> calendarHomeSets = new HashSet<>(); Set<HttpUrl> 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); Loading @@ -82,16 +105,26 @@ public class DavResourceFinder { Constants.log.info("Check whether user-given URL is a calendar collection and/or contains <calendar-home-set> and/or has <current-user-principal>"); DavResource davBase = new DavResource(httpClient, userURL); try { 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); } if (service == Service.CALDAV) { CalendarHomeSet calendarHomeSet = (CalendarHomeSet)davBase.properties.get(CalendarHomeSet.NAME); if (calendarHomeSet != null) { Constants.log.info("Found <calendar-home-set> at user-given URL"); Loading @@ -101,6 +134,17 @@ public class DavResourceFinder { calendarHomeSets.add(url); } } } else if (service == Service.CARDDAV) { AddressbookHomeSet addressbookHomeSet = (AddressbookHomeSet)davBase.properties.get(AddressbookHomeSet.NAME); if (addressbookHomeSet != null) { Constants.log.info("Found <addressbook-home-set> at user-given URL"); for (String href : addressbookHomeSet.hrefs) { HttpUrl url = userURL.resolve(href); if (url != null) addressbookHomeSets.add(url); } } } /* When home sets haven already been found, skip further searching. * Otherwise (no home sets found), treat the user-given URL as "initial context path" for service discovery. */ Loading @@ -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 <current-user-principal>, 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 <current-user-principal>, trying /.well-known/" + service.name); principalUrl = getCurrentUserPrincipal(userURL.resolve("/.well-known/" + service.name)); } } Loading @@ -136,32 +176,49 @@ 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 <calendar-home-set>"); try { DavResource principal = new DavResource(httpClient, principalUrl); if (service == Service.CALDAV) { principal.propfind(0, CalendarHomeSet.NAME); CalendarHomeSet calendarHomeSet = (CalendarHomeSet)principal.properties.get(CalendarHomeSet.NAME); if (calendarHomeSet != null) if (calendarHomeSet != null) { Constants.log.info("Found <calendar-home-set> 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 <addressbook-home-set> 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 if (service == Service.CALDAV) for (HttpUrl url : calendarHomeSets) try { Constants.log.info("Listing collections in home set " + url); 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); Loading @@ -175,14 +232,43 @@ public class DavResourceFinder { } 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? 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)); } } /** Loading @@ -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: * <ul> * <li>calendars: current-user-properties, current-user-privilege-set, displayname, calendar-description, calendar-color</li> * <li>address books: current-user-properties, current-user-privilege-set, displayname, addressbook-description</li> * </ul>. 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) Loading @@ -232,17 +321,23 @@ public class DavResourceFinder { title = UrlUtils.lastSegment(dav.location); String description = null; Integer color = 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; Integer color = null; 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, Loading @@ -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<String> 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) { Loading @@ -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(); Loading @@ -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("/")); } 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; } Loading @@ -318,87 +415,22 @@ 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 { 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) return url.resolve(currentUserPrincipal.href); 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; } } 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 (TextParseException e) { throw new URISyntaxException(domain, "Invalid domain name"); } catch(IOException|HttpException|DavException e) { Constants.log.debug("PROPFIND for current-user-principal on " + url + " failed", e); } 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(); return null; } Loading Loading
app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java +198 −166 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -49,11 +49,21 @@ 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<ServerInfo.ResourceInfo> addressbooks = new LinkedList<>(), calendars = new LinkedList<>(), taskLists = new LinkedList<>(); Loading @@ -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<HttpUrl> calendarHomeSets = new HashSet<>(); Set<HttpUrl> 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); Loading @@ -82,16 +105,26 @@ public class DavResourceFinder { Constants.log.info("Check whether user-given URL is a calendar collection and/or contains <calendar-home-set> and/or has <current-user-principal>"); DavResource davBase = new DavResource(httpClient, userURL); try { 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); } if (service == Service.CALDAV) { CalendarHomeSet calendarHomeSet = (CalendarHomeSet)davBase.properties.get(CalendarHomeSet.NAME); if (calendarHomeSet != null) { Constants.log.info("Found <calendar-home-set> at user-given URL"); Loading @@ -101,6 +134,17 @@ public class DavResourceFinder { calendarHomeSets.add(url); } } } else if (service == Service.CARDDAV) { AddressbookHomeSet addressbookHomeSet = (AddressbookHomeSet)davBase.properties.get(AddressbookHomeSet.NAME); if (addressbookHomeSet != null) { Constants.log.info("Found <addressbook-home-set> at user-given URL"); for (String href : addressbookHomeSet.hrefs) { HttpUrl url = userURL.resolve(href); if (url != null) addressbookHomeSets.add(url); } } } /* When home sets haven already been found, skip further searching. * Otherwise (no home sets found), treat the user-given URL as "initial context path" for service discovery. */ Loading @@ -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 <current-user-principal>, 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 <current-user-principal>, trying /.well-known/" + service.name); principalUrl = getCurrentUserPrincipal(userURL.resolve("/.well-known/" + service.name)); } } Loading @@ -136,32 +176,49 @@ 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 <calendar-home-set>"); try { DavResource principal = new DavResource(httpClient, principalUrl); if (service == Service.CALDAV) { principal.propfind(0, CalendarHomeSet.NAME); CalendarHomeSet calendarHomeSet = (CalendarHomeSet)principal.properties.get(CalendarHomeSet.NAME); if (calendarHomeSet != null) if (calendarHomeSet != null) { Constants.log.info("Found <calendar-home-set> 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 <addressbook-home-set> 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 if (service == Service.CALDAV) for (HttpUrl url : calendarHomeSets) try { Constants.log.info("Listing collections in home set " + url); 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); Loading @@ -175,14 +232,43 @@ public class DavResourceFinder { } 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? 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)); } } /** Loading @@ -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: * <ul> * <li>calendars: current-user-properties, current-user-privilege-set, displayname, calendar-description, calendar-color</li> * <li>address books: current-user-properties, current-user-privilege-set, displayname, addressbook-description</li> * </ul>. 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) Loading @@ -232,17 +321,23 @@ public class DavResourceFinder { title = UrlUtils.lastSegment(dav.location); String description = null; Integer color = 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; Integer color = null; 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, Loading @@ -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<String> 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) { Loading @@ -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(); Loading @@ -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("/")); } 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; } Loading @@ -318,87 +415,22 @@ 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 { 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) return url.resolve(currentUserPrincipal.href); 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; } } 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 (TextParseException e) { throw new URISyntaxException(domain, "Invalid domain name"); } catch(IOException|HttpException|DavException e) { Constants.log.debug("PROPFIND for current-user-principal on " + url + " failed", e); } 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(); return null; } Loading