You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
etesync-android/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java

299 lines
13 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.resource;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import com.squareup.okhttp.HttpUrl;
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;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedList;
import java.util.List;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.HttpClient;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
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;
import at.bitfire.dav4android.property.CurrentUserPrincipal;
import at.bitfire.dav4android.property.DisplayName;
import at.bitfire.dav4android.property.ResourceType;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.DAVUtils;
public class DavResourceFinder {
private final static String TAG = "davdroid.ResourceFinder";
final protected Context context;
public DavResourceFinder(Context context) {
this.context = context;
}
public void findResources(final ServerInfo serverInfo) throws URISyntaxException, IOException, HttpException, DavException {
final HttpClient httpClient = new HttpClient();
/*httpClient.setAuthenticator(new Authenticator() {
@Override
public Request authenticate(Proxy proxy, Response response) throws IOException {
String credential = Credentials.basic(serverInfo.getUserName(), serverInfo.getPassword());
return response.request().newBuilder()
.header("Authorization", credential)
.build();
}
@Override
public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
return null;
}
});*/
// CardDAV
Constants.log.info("*** CardDAV resource detection ***");
HttpUrl principalUrl = getCurrentUserPrincipal(httpClient, serverInfo, "carddav");
DavResource principal = new DavResource(httpClient, principalUrl);
principal.propfind(0, AddressbookHomeSet.NAME);
AddressbookHomeSet addrHomeSet = (AddressbookHomeSet)principal.properties.get(AddressbookHomeSet.NAME);
if (addrHomeSet != null && !addrHomeSet.hrefs.isEmpty()) {
Constants.log.info("Found addressbook home set(s): " + addrHomeSet);
serverInfo.setCardDAV(true);
// enumerate address books
List<ServerInfo.ResourceInfo> addressBooks = new LinkedList<>();
for (String href : addrHomeSet.hrefs) {
DavResource homeSet = new DavResource(httpClient, principalUrl.resolve(href));
homeSet.propfind(1, ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME);
for (DavResource member : homeSet.members) {
ResourceType type = (ResourceType)member.properties.get(ResourceType.NAME);
if (type != null && type.types.contains(ResourceType.ADDRESSBOOK)) {
Constants.log.info("Found address book: " + member.location);
DisplayName displayName = (DisplayName)member.properties.get(DisplayName.NAME);
AddressbookDescription description = (AddressbookDescription)member.properties.get(AddressbookDescription.NAME);
// TODO read-only
addressBooks.add(new ServerInfo.ResourceInfo(
ServerInfo.ResourceInfo.Type.ADDRESS_BOOK,
false,
member.location.toString(),
displayName != null ? displayName.displayName : null,
description != null ? description.description : null,
null
));
}
}
}
serverInfo.setAddressBooks(addressBooks);
}
// CalDAV
Constants.log.info("*** CalDAV resource detection ***");
principalUrl = getCurrentUserPrincipal(httpClient, serverInfo, "caldav");
principal = new DavResource(httpClient, principalUrl);
principal.propfind(0, CalendarHomeSet.NAME);
CalendarHomeSet calHomeSet = (CalendarHomeSet)principal.properties.get(CalendarHomeSet.NAME);
if (calHomeSet != null && !calHomeSet.hrefs.isEmpty()) {
Constants.log.info("Found calendar home set(s): " + calHomeSet);
serverInfo.setCalDAV(true);
// enumerate address books
List<ServerInfo.ResourceInfo> calendars = new LinkedList<>();
for (String href : calHomeSet.hrefs) {
DavResource homeSet = new DavResource(httpClient, principalUrl.resolve(href));
homeSet.propfind(1, ResourceType.NAME, DisplayName.NAME, CalendarDescription.NAME, CalendarColor.NAME);
for (DavResource member : homeSet.members) {
ResourceType type = (ResourceType)member.properties.get(ResourceType.NAME);
if (type != null && type.types.contains(ResourceType.CALENDAR)) {
Constants.log.info("Found calendar: " + member.location);
DisplayName displayName = (DisplayName)member.properties.get(DisplayName.NAME);
CalendarDescription description = (CalendarDescription)member.properties.get(CalendarDescription.NAME);
CalendarColor color = (CalendarColor)member.properties.get(CalendarColor.NAME);
// TODO read-only, time-zone, supported components
calendars.add(new ServerInfo.ResourceInfo(
ServerInfo.ResourceInfo.Type.ADDRESS_BOOK,
false,
member.location.toString(),
displayName != null ? displayName.displayName : null,
description != null ? description.description : null,
color != null ? DAVUtils.CalDAVtoARGBColor(color.color) : null
));
}
}
}
serverInfo.setCalendars(calendars);
}
/*if (!serverInfo.isCalDAV() && !serverInfo.isCardDAV())
throw new DavIncapableException(context.getString(R.string.setup_neither_caldav_nor_carddav));*/
}
/**
* 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 + "s._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 + "s 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();
}
/**
* 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.
* @param serverInfo Location that will be queried
* @param serviceName Well-known service name ("carddav", "caldav")
* @return WebDavResource of current-user-principal for the given service, or initial context URL if it can't be found
*
* TODO: If a TXT record is given, always use it instead of trying .well-known first
*/
HttpUrl getCurrentUserPrincipal(HttpClient httpClient, ServerInfo serverInfo, String serviceName) throws URISyntaxException {
HttpUrl initialURL = getInitialContextURL(serverInfo, serviceName);
if (initialURL != null) {
Constants.log.info("Looking up principal URL for service " + serviceName + "; initial context: " + initialURL);
// look for well-known service (RFC 5785)
try {
DavResource wellKnown = new DavResource(httpClient, initialURL.resolve("/.well-known/" + serviceName));
wellKnown.propfind(0, CurrentUserPrincipal.NAME);
CurrentUserPrincipal principal = (CurrentUserPrincipal)wellKnown.properties.get(CurrentUserPrincipal.NAME);
if (principal != null) {
HttpUrl url = wellKnown.location.resolve(principal.href);
Constants.log.info("Found principal URL from well-known URL: " + url);
return url;
}
} catch (IOException e) {
Constants.log.warn("Well-known " + serviceName + " service detection failed with I/O error", e);
} catch (HttpException e) {
Constants.log.warn("Well-known " + serviceName + " service detection failed with HTTP error", e);
} catch (DavException e) {
Constants.log.warn("Well-known " + serviceName + " service detection failed with DAV error", e);
}
}
// fall back to user-given initial context path
Log.d(TAG, "Well-known service detection failed, trying initial context path " + initialURL);
try {
DavResource base = new DavResource(httpClient, initialURL);
base.propfind(0, CurrentUserPrincipal.NAME);
CurrentUserPrincipal principal = (CurrentUserPrincipal)base.properties.get(CurrentUserPrincipal.NAME);
if (principal != null) {
HttpUrl url = base.location.resolve(principal.href);
Constants.log.info("Found principal URL from initial context URL: " + url);
return url;
}
} catch (IOException e) {
Constants.log.warn("Well-known " + serviceName + " service detection failed with I/O error", e);
} catch (HttpException e) {
Log.e(TAG, "HTTP error when querying principal", e);
} catch (DavException e) {
Log.e(TAG, "DAV error when querying principal", e);
}
Log.i(TAG, "Couldn't find current-user-principal for service " + serviceName + ". Assuming principal path is initial context path!");
return initialURL;
}
SRVRecord selectSRVRecord(Record[] records) {
if (records.length > 1)
Log.w(TAG, "Multiple SRV records not supported yet; using first one");
return (SRVRecord)records[0];
}
}