1
0
mirror of https://github.com/etesync/android synced 2025-02-23 21:02:26 +00:00

Rewrite initial configuration detection

* HttpClient: add Accept-Language header
* HttpClient: fix MemoryCookieStore NullPointerException
* DavResourceFinder: check for home sets, too
This commit is contained in:
Ricki Hirner 2016-01-17 00:34:26 +01:00
parent 89050d88c6
commit 85a6b68a56
4 changed files with 139 additions and 118 deletions

View File

@ -146,8 +146,10 @@ public class HttpClient {
static class UserAgentInterceptor implements Interceptor { static class UserAgentInterceptor implements Interceptor {
@Override @Override
public Response intercept(Chain chain) throws IOException { public Response intercept(Chain chain) throws IOException {
Locale locale = Locale.getDefault();
Request request = chain.request().newBuilder() Request request = chain.request().newBuilder()
.header("User-Agent", userAgent) .header("User-Agent", userAgent)
.header("Accept-Language", locale.getLanguage() + "-" + locale.getCountry() + ", " + locale.getLanguage() + ";q=0.7, *;q=0.5")
.build(); .build();
return chain.proceed(request); return chain.proceed(request);
} }

View File

@ -8,11 +8,14 @@
package at.bitfire.davdroid; package at.bitfire.davdroid;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import at.bitfire.davdroid.resource.DavResourceFinder;
import okhttp3.Cookie; import okhttp3.Cookie;
import okhttp3.CookieJar; import okhttp3.CookieJar;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
@ -35,7 +38,12 @@ public class MemoryCookieStore implements CookieJar {
@Override @Override
public List<Cookie> loadForRequest(HttpUrl url) { public List<Cookie> loadForRequest(HttpUrl url) {
return store.get(url); List<Cookie> cookies = store.get(url);
if (cookies == null)
cookies = Collections.emptyList();
return cookies;
} }
} }

View File

@ -10,8 +10,6 @@ package at.bitfire.davdroid.resource;
import android.content.Context; import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
import okhttp3.HttpUrl;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.xbill.DNS.Lookup; import org.xbill.DNS.Lookup;
import org.xbill.DNS.Record; import org.xbill.DNS.Record;
@ -22,9 +20,11 @@ import org.xbill.DNS.Type;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import at.bitfire.dav4android.DavResource; import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.UrlUtils; import at.bitfire.dav4android.UrlUtils;
@ -46,8 +46,11 @@ import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.log.StringLogger; import at.bitfire.davdroid.log.StringLogger;
import at.bitfire.davdroid.ui.setup.LoginCredentialsFragment; import at.bitfire.davdroid.ui.setup.LoginCredentialsFragment;
import lombok.Data; import lombok.Data;
import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString; import lombok.ToString;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
public class DavResourceFinder { public class DavResourceFinder {
@ -66,13 +69,7 @@ public class DavResourceFinder {
protected final Logger log = new StringLogger("DavResourceFinder", true); protected final Logger log = new StringLogger("DavResourceFinder", true);
protected OkHttpClient httpClient; protected OkHttpClient httpClient;
protected HttpUrl carddavPrincipal, caldavPrincipal; public DavResourceFinder(@NonNull Context context, @NonNull LoginCredentialsFragment.LoginCredentials credentials) {
protected Map<HttpUrl, ServerConfiguration.Collection>
addressBooks = new HashMap<>(),
calendars = new HashMap<>();
public DavResourceFinder(Context context, LoginCredentialsFragment.LoginCredentials credentials) {
this.context = context; this.context = context;
this.credentials = credentials; this.credentials = credentials;
@ -82,85 +79,38 @@ public class DavResourceFinder {
} }
public ServerConfiguration findInitialConfiguration() { public Configuration findInitialConfiguration() {
addressBooks.clear(); final Configuration.ServiceInfo
findInitialConfiguration(Service.CARDDAV); cardDavConfig = findInitialConfiguration(Service.CARDDAV),
calDavConfig = findInitialConfiguration(Service.CALDAV);
calendars.clear(); return new Configuration(cardDavConfig, calDavConfig, log.toString());
findInitialConfiguration(Service.CALDAV);
return new ServerConfiguration(
carddavPrincipal, addressBooks.values().toArray(new ServerConfiguration.Collection[0]),
caldavPrincipal, calendars.values().toArray(new ServerConfiguration.Collection[0]),
log.toString()
);
} }
protected void findInitialConfiguration(Service service) { protected Configuration.ServiceInfo findInitialConfiguration(@NonNull Service service) {
// user-given base URI (mailto or URL) // user-given base URI (either mailto: URI or http(s):// URL)
URI baseURI = credentials.getUri(); final URI baseURI = credentials.getUri();
// domain for service discovery // domain for service discovery
String domain = null; String discoveryFQDN = null;
HttpUrl principal = null; // put discovered information here
final Configuration.ServiceInfo config = new Configuration.ServiceInfo();
log.info("Finding initial {} service configuration", service.name);
// Step 1a (only when user-given URI is URL):
// * Check whether URL represents a calendar/address-book collection itself,
// * and/or whether it has a current-user-principal,
// * or whether it represents a principal itself.
if ("http".equalsIgnoreCase(baseURI.getScheme()) || "https".equalsIgnoreCase(baseURI.getScheme())) { if ("http".equalsIgnoreCase(baseURI.getScheme()) || "https".equalsIgnoreCase(baseURI.getScheme())) {
HttpUrl baseURL = HttpUrl.get(baseURI); final HttpUrl baseURL = HttpUrl.get(baseURI);
// remember domain for service discovery (if required) // remember domain for service discovery
// try service discovery only for https:// URLs because only secure service discovery is implemented // try service discovery only for https:// URLs because only secure service discovery is implemented
if ("https".equalsIgnoreCase(baseURL.scheme())) if ("https".equalsIgnoreCase(baseURL.scheme()))
domain = baseURI.getHost(); discoveryFQDN = baseURI.getHost();
log.info("Checking user-given URL: " + baseURL.toString()); checkUserGivenURL(baseURL, service, config);
try {
DavResource davBase = new DavResource(log, httpClient, baseURL);
if (service == Service.CARDDAV) { if (config.principal == null)
davBase.propfind(0,
AddressbookHomeSet.NAME,
ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME, CurrentUserPrivilegeSet.NAME,
CurrentUserPrincipal.NAME
);
addIfAddressBook(davBase);
} else 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);
}
// check for current-user-principal
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)davBase.properties.get(CurrentUserPrincipal.NAME);
if (currentUserPrincipal != null && currentUserPrincipal.href != null)
principal = davBase.location.resolve(currentUserPrincipal.href);
// check for resourcetype = principal
if (principal == null) {
ResourceType resourceType = (ResourceType)davBase.properties.get(ResourceType.NAME);
if (resourceType.types.contains(ResourceType.PRINCIPAL))
principal = davBase.location;
}
// If a principal has been detected successfully, ensure that it provides the required service.
if (principal != null && !providesService(principal, service))
principal = null;
} catch (IOException|HttpException|DavException e) {
log.debug("PROPFIND on user-given URL failed", e);
}
// Step 1b: Try well-known URL, too
if (principal == null)
try { try {
principal = getCurrentUserPrincipal(baseURL.resolve("/.well-known/" + service.name), service); config.principal = getCurrentUserPrincipal(baseURL.resolve("/.well-known/" + service.name), service);
} catch (IOException|HttpException|DavException e) { } catch (IOException|HttpException|DavException e) {
log.debug("Well-known URL detection failed", e); log.debug("Well-known URL detection failed", e);
} }
@ -170,36 +120,85 @@ public class DavResourceFinder {
int posAt = mailbox.lastIndexOf("@"); int posAt = mailbox.lastIndexOf("@");
if (posAt != -1) if (posAt != -1)
domain = mailbox.substring(posAt + 1); discoveryFQDN = mailbox.substring(posAt + 1);
} }
// Step 2: If user-given URL didn't reveal a principal, search for it: SERVICE DISCOVERY // Step 2: If user-given URL didn't reveal a principal, search for it: SERVICE DISCOVERY
if (principal == null && domain != null) { if (config.principal == null && discoveryFQDN != null) {
log.info("No principal found at user-given URL, trying to discover"); log.info("No principal found at user-given URL, trying to discover");
try { try {
principal = discoverPrincipalUrl(domain, service); config.principal = discoverPrincipalUrl(discoveryFQDN, service);
} catch (IOException|HttpException|DavException e) { } catch (IOException|HttpException|DavException e) {
log.debug(service.name + " service discovery failed", e); log.debug(service.name + " service discovery failed", e);
} }
} }
if (service == Service.CALDAV) return config;
caldavPrincipal = principal;
else if (service == Service.CARDDAV)
carddavPrincipal = principal;
} }
protected void addIfAddressBook(@NonNull DavResource dav) { protected void checkUserGivenURL(@NonNull HttpUrl baseURL, @NonNull Service service, @NonNull Configuration.ServiceInfo config) {
log.info("Checking user-given URL: " + baseURL.toString());
HttpUrl principal = null;
try {
DavResource davBase = new DavResource(log, httpClient, baseURL);
if (service == Service.CARDDAV) {
davBase.propfind(0,
ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME,
AddressbookHomeSet.NAME,
CurrentUserPrincipal.NAME
);
rememberIfAddressBookOrHomeset(davBase, config);
} else if (service == Service.CALDAV) {
davBase.propfind(0,
ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME, SupportedCalendarComponentSet.NAME,
CalendarHomeSet.NAME,
CurrentUserPrincipal.NAME
);
rememberIfCalendarOrHomeset(davBase, config);
}
// check for current-user-principal
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)davBase.properties.get(CurrentUserPrincipal.NAME);
if (currentUserPrincipal != null && currentUserPrincipal.href != null)
principal = davBase.location.resolve(currentUserPrincipal.href);
// check for resource type "principal"
if (principal == null) {
ResourceType resourceType = (ResourceType)davBase.properties.get(ResourceType.NAME);
if (resourceType.types.contains(ResourceType.PRINCIPAL))
principal = davBase.location;
}
// If a principal has been detected successfully, ensure that it provides the required service.
if (principal != null && providesService(principal, service))
config.principal = principal;
} catch (IOException|HttpException|DavException e) {
log.debug("PROPFIND/OPTIONS on user-given URL failed", e);
}
}
protected void rememberIfAddressBookOrHomeset(@NonNull DavResource dav, @NonNull Configuration.ServiceInfo config) {
// Is the collection an address book?
ResourceType resourceType = (ResourceType)dav.properties.get(ResourceType.NAME); ResourceType resourceType = (ResourceType)dav.properties.get(ResourceType.NAME);
if (resourceType != null && resourceType.types.contains(ResourceType.ADDRESSBOOK)) { if (resourceType != null && resourceType.types.contains(ResourceType.ADDRESSBOOK)) {
dav.location = UrlUtils.withTrailingSlash(dav.location); dav.location = UrlUtils.withTrailingSlash(dav.location);
log.info("Found address book at " + dav.location); log.info("Found address book at " + dav.location);
config.collections.put(dav.location, collectionInfo(dav, Configuration.Collection.Type.ADDRESS_BOOK));
addressBooks.put(dav.location, collectionInfo(dav, ServerConfiguration.Collection.Type.ADDRESS_BOOK));
} }
// Does the collection refer to address book homesets?
AddressbookHomeSet homeSets = (AddressbookHomeSet)dav.properties.get(AddressbookHomeSet.NAME);
if (homeSets != null)
for (String href : homeSets.hrefs)
config.homeSets.add(dav.location.resolve(href));
} }
protected void addIfCalendar(@NonNull DavResource dav) { protected void rememberIfCalendarOrHomeset(@NonNull DavResource dav, @NonNull Configuration.ServiceInfo config) {
// Is the collection a calendar collection?
ResourceType resourceType = (ResourceType)dav.properties.get(ResourceType.NAME); ResourceType resourceType = (ResourceType)dav.properties.get(ResourceType.NAME);
if (resourceType != null && resourceType.types.contains(ResourceType.CALENDAR)) { if (resourceType != null && resourceType.types.contains(ResourceType.CALENDAR)) {
dav.location = UrlUtils.withTrailingSlash(dav.location); dav.location = UrlUtils.withTrailingSlash(dav.location);
@ -212,16 +211,22 @@ public class DavResourceFinder {
supportsTasks = supportedCalendarComponentSet.supportsTasks; supportsTasks = supportedCalendarComponentSet.supportsTasks;
} }
if (supportsEvents || supportsTasks) { if (supportsEvents || supportsTasks) {
ServerConfiguration.Collection info = collectionInfo(dav, ServerConfiguration.Collection.Type.CALENDAR); Configuration.Collection info = collectionInfo(dav, Configuration.Collection.Type.CALENDAR);
info.supportsEvents = supportsEvents; info.supportsEvents = supportsEvents;
info.supportsTasks = supportsTasks; info.supportsTasks = supportsTasks;
calendars.put(dav.location, info); config.collections.put(dav.location, info);
} }
} }
// Does the collection refer to calendar homesets?
CalendarHomeSet homeSets = (CalendarHomeSet)dav.properties.get(CalendarHomeSet.NAME);
if (homeSets != null)
for (String href : homeSets.hrefs)
config.homeSets.add(dav.location.resolve(href));
} }
/** /**
* Builds a #{@link at.bitfire.davdroid.resource.ServerInfo.ResourceInfo} from a given * Builds a #{@link at.bitfire.davdroid.resource.DavResourceFinder.Configuration.Collection} from a given
* #{@link DavResource}. Uses these DAV properties: * #{@link DavResource}. Uses these DAV properties:
* <ul> * <ul>
* <li>calendars: current-user-properties, current-user-privilege-set, displayname, calendar-description, calendar-color</li> * <li>calendars: current-user-properties, current-user-privilege-set, displayname, calendar-description, calendar-color</li>
@ -231,7 +236,7 @@ public class DavResourceFinder {
* @param type must be ADDRESS_BOOK or CALENDAR * @param type must be ADDRESS_BOOK or CALENDAR
* @return ResourceInfo which represents the DavResource * @return ResourceInfo which represents the DavResource
*/ */
protected ServerConfiguration.Collection collectionInfo(DavResource dav, ServerConfiguration.Collection.Type type) { protected Configuration.Collection collectionInfo(DavResource dav, Configuration.Collection.Type type) {
boolean readOnly = false; boolean readOnly = false;
CurrentUserPrivilegeSet privilegeSet = (CurrentUserPrivilegeSet)dav.properties.get(CurrentUserPrivilegeSet.NAME); CurrentUserPrivilegeSet privilegeSet = (CurrentUserPrivilegeSet)dav.properties.get(CurrentUserPrivilegeSet.NAME);
if (privilegeSet != null) if (privilegeSet != null)
@ -246,11 +251,11 @@ public class DavResourceFinder {
String description = null; String description = null;
Integer color = null; Integer color = null;
if (type == ServerConfiguration.Collection.Type.ADDRESS_BOOK) { if (type == Configuration.Collection.Type.ADDRESS_BOOK) {
AddressbookDescription addressbookDescription = (AddressbookDescription)dav.properties.get(AddressbookDescription.NAME); AddressbookDescription addressbookDescription = (AddressbookDescription)dav.properties.get(AddressbookDescription.NAME);
if (addressbookDescription != null) if (addressbookDescription != null)
description = addressbookDescription.description; description = addressbookDescription.description;
} else if (type == ServerConfiguration.Collection.Type.CALENDAR) { } else if (type == Configuration.Collection.Type.CALENDAR) {
CalendarDescription calendarDescription = (CalendarDescription)dav.properties.get(CalendarDescription.NAME); CalendarDescription calendarDescription = (CalendarDescription)dav.properties.get(CalendarDescription.NAME);
if (calendarDescription != null) if (calendarDescription != null)
description = calendarDescription.description; description = calendarDescription.description;
@ -260,10 +265,9 @@ public class DavResourceFinder {
color = calendarColor.color; color = calendarColor.color;
} }
ServerConfiguration.Collection collection = new ServerConfiguration.Collection( Configuration.Collection collection = new Configuration.Collection(
type, type,
readOnly, readOnly,
UrlUtils.withTrailingSlash(dav.location).toString(),
title, title,
description, description,
color color
@ -273,7 +277,7 @@ public class DavResourceFinder {
} }
boolean providesService(HttpUrl url, Service service) { boolean providesService(HttpUrl url, Service service) throws IOException {
DavResource davPrincipal = new DavResource(log, httpClient, url); DavResource davPrincipal = new DavResource(log, httpClient, url);
try { try {
davPrincipal.options(); davPrincipal.options();
@ -282,7 +286,7 @@ public class DavResourceFinder {
(service == Service.CALDAV && davPrincipal.capabilities.contains("calendar-access"))) (service == Service.CALDAV && davPrincipal.capabilities.contains("calendar-access")))
return true; return true;
} catch (IOException|HttpException|DavException e) { } catch (HttpException|DavException e) {
log.error("Couldn't detect services on {}", url); log.error("Couldn't detect services on {}", url);
} }
return false; return false;
@ -399,19 +403,28 @@ public class DavResourceFinder {
// data classes // data classes
@Data @RequiredArgsConstructor
@ToString(exclude="logs") @ToString(exclude="logs")
public static class ServerConfiguration { public static class Configuration {
final public HttpUrl cardDavPrincipal;
final public Collection[] addressBooks;
final public HttpUrl calDavPrincipal; public final ServiceInfo cardDAV;
final public Collection[] calendars; public final ServiceInfo calDAV;
final String logs; public final String logs;
@ToString
public static class ServiceInfo {
@Getter
HttpUrl principal;
@Getter
Set<HttpUrl> homeSets = new HashSet<>();
@Getter
Map<HttpUrl, Collection> collections = new HashMap<>();
}
@Data @Data
@ToString
public static class Collection { public static class Collection {
public enum Type { public enum Type {
ADDRESS_BOOK, ADDRESS_BOOK,
@ -421,9 +434,7 @@ public class DavResourceFinder {
final Type type; final Type type;
final boolean readOnly; final boolean readOnly;
final String url, // absolute URL of resource final String title, description;
title,
description;
final Integer color; final Integer color;
/** /**

View File

@ -24,10 +24,10 @@ import java.io.StringReader;
import at.bitfire.davdroid.Constants; import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R; import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.DavResourceFinder; import at.bitfire.davdroid.resource.DavResourceFinder;
import at.bitfire.davdroid.resource.DavResourceFinder.ServerConfiguration; import at.bitfire.davdroid.resource.DavResourceFinder.Configuration;
import lombok.Cleanup; import lombok.Cleanup;
public class DetectConfigurationFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<ServerConfiguration> { public class DetectConfigurationFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<Configuration> {
static final String ARG_LOGIN_CREDENTIALS = "credentials"; static final String ARG_LOGIN_CREDENTIALS = "credentials";
@ -51,23 +51,23 @@ public class DetectConfigurationFragment extends DialogFragment implements Loade
} }
@Override @Override
public Loader<ServerConfiguration> onCreateLoader(int id, Bundle args) { public Loader<Configuration> onCreateLoader(int id, Bundle args) {
return new ServerConfigurationLoader(getContext(), args); return new ServerConfigurationLoader(getContext(), args);
} }
@Override @Override
public void onLoadFinished(Loader<ServerConfiguration> loader, ServerConfiguration data) { public void onLoadFinished(Loader<Configuration> loader, Configuration data) {
// show error / continue with next fragment // show error / continue with next fragment
Constants.log.info("detection results: {}", data); Constants.log.info("detection results: {}", data);
dismissAllowingStateLoss(); dismissAllowingStateLoss();
} }
@Override @Override
public void onLoaderReset(Loader<ServerConfiguration> loader) { public void onLoaderReset(Loader<Configuration> loader) {
} }
static class ServerConfigurationLoader extends AsyncTaskLoader<ServerConfiguration> { static class ServerConfigurationLoader extends AsyncTaskLoader<Configuration> {
final Context context; final Context context;
final LoginCredentialsFragment.LoginCredentials credentials; final LoginCredentialsFragment.LoginCredentials credentials;
@ -83,16 +83,16 @@ public class DetectConfigurationFragment extends DialogFragment implements Loade
} }
@Override @Override
public ServerConfiguration loadInBackground() { public Configuration loadInBackground() {
DavResourceFinder finder = new DavResourceFinder(context, credentials); DavResourceFinder finder = new DavResourceFinder(context, credentials);
ServerConfiguration configuration = finder.findInitialConfiguration(); Configuration configuration = finder.findInitialConfiguration();
try { try {
@Cleanup BufferedReader logStream = new BufferedReader(new StringReader(configuration.getLogs())); @Cleanup BufferedReader logStream = new BufferedReader(new StringReader(configuration.logs));
Constants.log.info("Successful resource detection:"); Constants.log.info("Resource detection finished:");
String line; String line;
while ((line = logStream.readLine()) != null) while ((line = logStream.readLine()) != null)
Constants.log.debug(line); Constants.log.info(line);
} catch (IOException e) { } catch (IOException e) {
Constants.log.error("Couldn't read resource detection logs", e); Constants.log.error("Couldn't read resource detection logs", e);
} }