507 lines
20 KiB
Kotlin
507 lines
20 KiB
Kotlin
|
|
/*
|
|||
|
|
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
|||
|
|
*/
|
|||
|
|
package at.bitfire.davdroid.servicedetection
|
|||
|
|
|
|||
|
|
import android.app.ActivityManager
|
|||
|
|
import android.content.Context
|
|||
|
|
import androidx.core.content.getSystemService
|
|||
|
|
import at.bitfire.dav4jvm.DavResource
|
|||
|
|
import at.bitfire.dav4jvm.Property
|
|||
|
|
import at.bitfire.dav4jvm.Response
|
|||
|
|
import at.bitfire.dav4jvm.UrlUtils
|
|||
|
|
import at.bitfire.dav4jvm.exception.DavException
|
|||
|
|
import at.bitfire.dav4jvm.exception.HttpException
|
|||
|
|
import at.bitfire.dav4jvm.exception.UnauthorizedException
|
|||
|
|
import at.bitfire.dav4jvm.property.caldav.CalendarColor
|
|||
|
|
import at.bitfire.dav4jvm.property.caldav.CalendarDescription
|
|||
|
|
import at.bitfire.dav4jvm.property.caldav.CalendarHomeSet
|
|||
|
|
import at.bitfire.dav4jvm.property.caldav.CalendarTimezone
|
|||
|
|
import at.bitfire.dav4jvm.property.caldav.CalendarUserAddressSet
|
|||
|
|
import at.bitfire.dav4jvm.property.caldav.SupportedCalendarComponentSet
|
|||
|
|
import at.bitfire.dav4jvm.property.carddav.AddressbookDescription
|
|||
|
|
import at.bitfire.dav4jvm.property.carddav.AddressbookHomeSet
|
|||
|
|
import at.bitfire.dav4jvm.property.common.HrefListProperty
|
|||
|
|
import at.bitfire.dav4jvm.property.webdav.CurrentUserPrincipal
|
|||
|
|
import at.bitfire.dav4jvm.property.webdav.CurrentUserPrivilegeSet
|
|||
|
|
import at.bitfire.dav4jvm.property.webdav.DisplayName
|
|||
|
|
import at.bitfire.dav4jvm.property.webdav.ResourceType
|
|||
|
|
import at.bitfire.davdroid.db.Collection
|
|||
|
|
import at.bitfire.davdroid.log.StringHandler
|
|||
|
|
import at.bitfire.davdroid.network.DnsRecordResolver
|
|||
|
|
import at.bitfire.davdroid.network.HttpClient
|
|||
|
|
import at.bitfire.davdroid.settings.Credentials
|
|||
|
|
import dagger.assisted.Assisted
|
|||
|
|
import dagger.assisted.AssistedFactory
|
|||
|
|
import dagger.assisted.AssistedInject
|
|||
|
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
|
|
import okhttp3.HttpUrl
|
|||
|
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|||
|
|
import org.xbill.DNS.Type
|
|||
|
|
import java.io.InterruptedIOException
|
|||
|
|
import java.net.SocketTimeoutException
|
|||
|
|
import java.net.URI
|
|||
|
|
import java.net.URISyntaxException
|
|||
|
|
import java.util.LinkedList
|
|||
|
|
import java.util.logging.Level
|
|||
|
|
import java.util.logging.Logger
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Does initial resource detection when an account is added. It uses the (user given) base URL to find
|
|||
|
|
*
|
|||
|
|
* - services (CalDAV and/or CardDAV),
|
|||
|
|
* - principal,
|
|||
|
|
* - homeset/collections (multistatus responses are handled through dav4jvm).
|
|||
|
|
*
|
|||
|
|
* @param context to build the HTTP client
|
|||
|
|
* @param baseURI user-given base URI (either mailto: URI or http(s):// URL)
|
|||
|
|
* @param credentials optional login credentials (username/password, client certificate, OAuth state)
|
|||
|
|
*/
|
|||
|
|
class DavResourceFinder @AssistedInject constructor(
|
|||
|
|
@Assisted private val baseURI: URI,
|
|||
|
|
@Assisted private val credentials: Credentials? = null,
|
|||
|
|
@ApplicationContext val context: Context,
|
|||
|
|
private val dnsRecordResolver: DnsRecordResolver,
|
|||
|
|
httpClientBuilder: HttpClient.Builder
|
|||
|
|
): AutoCloseable {
|
|||
|
|
|
|||
|
|
@AssistedFactory
|
|||
|
|
interface Factory {
|
|||
|
|
fun create(baseURI: URI, credentials: Credentials?): DavResourceFinder
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
enum class Service(val wellKnownName: String) {
|
|||
|
|
CALDAV("caldav"),
|
|||
|
|
CARDDAV("carddav");
|
|||
|
|
|
|||
|
|
override fun toString() = wellKnownName
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
val log: Logger = Logger.getLogger(javaClass.name)
|
|||
|
|
private val logBuffer: StringHandler = initLogging()
|
|||
|
|
|
|||
|
|
private var encountered401 = false
|
|||
|
|
|
|||
|
|
private val httpClient = httpClientBuilder
|
|||
|
|
.setLogger(log)
|
|||
|
|
.apply {
|
|||
|
|
if (credentials != null)
|
|||
|
|
authenticate(
|
|||
|
|
host = null,
|
|||
|
|
getCredentials = { credentials }
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
.build()
|
|||
|
|
|
|||
|
|
override fun close() {
|
|||
|
|
httpClient.close()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private fun initLogging(): StringHandler {
|
|||
|
|
// don't use more than 1/4 of the available memory for a log string
|
|||
|
|
val activityManager = context.getSystemService<ActivityManager>()!!
|
|||
|
|
val maxLogSize = activityManager.memoryClass * (1024 * 1024 / 8)
|
|||
|
|
val handler = StringHandler(maxLogSize)
|
|||
|
|
|
|||
|
|
// add StringHandler to logger
|
|||
|
|
log.level = Level.ALL
|
|||
|
|
log.addHandler(handler)
|
|||
|
|
|
|||
|
|
return handler
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Finds the initial configuration (= runs the service detection process).
|
|||
|
|
*
|
|||
|
|
* In case of an error, it returns an empty [Configuration] with error logs
|
|||
|
|
* instead of throwing an [Exception].
|
|||
|
|
*
|
|||
|
|
* @return service information – if there's neither a CalDAV service nor a CardDAV service,
|
|||
|
|
* service detection was not successful
|
|||
|
|
*/
|
|||
|
|
fun findInitialConfiguration(): Configuration {
|
|||
|
|
var cardDavConfig: Configuration.ServiceInfo? = null
|
|||
|
|
var calDavConfig: Configuration.ServiceInfo? = null
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
try {
|
|||
|
|
cardDavConfig = findInitialConfiguration(Service.CARDDAV)
|
|||
|
|
} catch (e: Exception) {
|
|||
|
|
log.log(Level.INFO, "CardDAV service detection failed", e)
|
|||
|
|
processException(e)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
calDavConfig = findInitialConfiguration(Service.CALDAV)
|
|||
|
|
} catch (e: Exception) {
|
|||
|
|
log.log(Level.INFO, "CalDAV service detection failed", e)
|
|||
|
|
processException(e)
|
|||
|
|
}
|
|||
|
|
} catch(_: Exception) {
|
|||
|
|
// we have been interrupted; reset results so that an error message will be shown
|
|||
|
|
cardDavConfig = null
|
|||
|
|
calDavConfig = null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Configuration(
|
|||
|
|
cardDAV = cardDavConfig,
|
|||
|
|
calDAV = calDavConfig,
|
|||
|
|
encountered401 = encountered401,
|
|||
|
|
logs = logBuffer.toString()
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private fun findInitialConfiguration(service: Service): Configuration.ServiceInfo? {
|
|||
|
|
// domain for service discovery
|
|||
|
|
var discoveryFQDN: String? = null
|
|||
|
|
|
|||
|
|
// discovered information goes into this config
|
|||
|
|
val config = Configuration.ServiceInfo()
|
|||
|
|
|
|||
|
|
// Start discovering
|
|||
|
|
log.info("Finding initial ${service.wellKnownName} service configuration")
|
|||
|
|
when (baseURI.scheme.lowercase()) {
|
|||
|
|
"http", "https" ->
|
|||
|
|
baseURI.toHttpUrlOrNull()?.let { baseURL ->
|
|||
|
|
// remember domain for service discovery
|
|||
|
|
if (baseURL.scheme.equals("https", true))
|
|||
|
|
// service discovery will only be tried for https URLs, because only secure service discovery is implemented
|
|||
|
|
discoveryFQDN = baseURL.host
|
|||
|
|
|
|||
|
|
// Actual discovery process
|
|||
|
|
checkBaseURL(baseURL, service, config)
|
|||
|
|
|
|||
|
|
// If principal was not found already, try well known URI
|
|||
|
|
if (config.principal == null)
|
|||
|
|
try {
|
|||
|
|
config.principal = getCurrentUserPrincipal(baseURL.resolve("/.well-known/" + service.wellKnownName)!!, service)
|
|||
|
|
} catch(e: Exception) {
|
|||
|
|
log.log(Level.FINE, "Well-known URL detection failed", e)
|
|||
|
|
processException(e)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
"mailto" -> {
|
|||
|
|
val mailbox = baseURI.schemeSpecificPart
|
|||
|
|
val posAt = mailbox.lastIndexOf("@")
|
|||
|
|
if (posAt != -1)
|
|||
|
|
discoveryFQDN = mailbox.substring(posAt + 1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Second try: If user-given URL didn't reveal a principal, search for it (SERVICE DISCOVERY)
|
|||
|
|
if (config.principal == null)
|
|||
|
|
discoveryFQDN?.let { fqdn ->
|
|||
|
|
log.info("No principal found at user-given URL, trying to discover for domain $fqdn")
|
|||
|
|
try {
|
|||
|
|
config.principal = discoverPrincipalUrl(fqdn, service)
|
|||
|
|
} catch(e: Exception) {
|
|||
|
|
log.log(Level.FINE, "$service service discovery failed", e)
|
|||
|
|
processException(e)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// detect email address
|
|||
|
|
if (service == Service.CALDAV)
|
|||
|
|
config.principal?.let { principal ->
|
|||
|
|
config.emails.addAll(queryEmailAddress(principal))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// return config or null if config doesn't contain useful information
|
|||
|
|
val serviceAvailable = config.principal != null || config.homeSets.isNotEmpty() || config.collections.isNotEmpty()
|
|||
|
|
return if (serviceAvailable)
|
|||
|
|
config
|
|||
|
|
else
|
|||
|
|
null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Entry point of the actual discovery process.
|
|||
|
|
*
|
|||
|
|
* Queries the user-given URL (= base URL) to detect whether it contains a current-user-principal
|
|||
|
|
* or whether it is a homeset or collection.
|
|||
|
|
*
|
|||
|
|
* @param baseURL base URL provided by the user
|
|||
|
|
* @param service service to detect configuration for
|
|||
|
|
* @param config found configuration will be written to this object
|
|||
|
|
*/
|
|||
|
|
private fun checkBaseURL(baseURL: HttpUrl, service: Service, config: Configuration.ServiceInfo) {
|
|||
|
|
log.info("Checking user-given URL: $baseURL")
|
|||
|
|
|
|||
|
|
val davBaseURL = DavResource(httpClient.okHttpClient, baseURL, log)
|
|||
|
|
try {
|
|||
|
|
when (service) {
|
|||
|
|
Service.CARDDAV -> {
|
|||
|
|
davBaseURL.propfind(
|
|||
|
|
0,
|
|||
|
|
ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME,
|
|||
|
|
AddressbookHomeSet.NAME,
|
|||
|
|
CurrentUserPrincipal.NAME
|
|||
|
|
) { response, _ ->
|
|||
|
|
scanResponse(ResourceType.ADDRESSBOOK, response, config)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Service.CALDAV -> {
|
|||
|
|
davBaseURL.propfind(
|
|||
|
|
0,
|
|||
|
|
ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME, SupportedCalendarComponentSet.NAME,
|
|||
|
|
CalendarHomeSet.NAME,
|
|||
|
|
CurrentUserPrincipal.NAME
|
|||
|
|
) { response, _ ->
|
|||
|
|
scanResponse(ResourceType.CALENDAR, response, config)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch(e: Exception) {
|
|||
|
|
log.log(Level.FINE, "PROPFIND/OPTIONS on user-given URL failed", e)
|
|||
|
|
processException(e)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Queries a user's email address using CalDAV scheduling: calendar-user-address-set.
|
|||
|
|
* @param principal principal URL of the user
|
|||
|
|
* @return list of found email addresses (empty if none)
|
|||
|
|
*/
|
|||
|
|
fun queryEmailAddress(principal: HttpUrl): List<String> {
|
|||
|
|
val mailboxes = LinkedList<String>()
|
|||
|
|
try {
|
|||
|
|
DavResource(httpClient.okHttpClient, principal, log).propfind(0, CalendarUserAddressSet.NAME) { response, _ ->
|
|||
|
|
response[CalendarUserAddressSet::class.java]?.let { addressSet ->
|
|||
|
|
for (href in addressSet.hrefs)
|
|||
|
|
try {
|
|||
|
|
val uri = URI(href)
|
|||
|
|
if (uri.scheme.equals("mailto", true))
|
|||
|
|
mailboxes.add(uri.schemeSpecificPart)
|
|||
|
|
} catch(e: URISyntaxException) {
|
|||
|
|
log.log(Level.WARNING, "Couldn't parse user address", e)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch(e: Exception) {
|
|||
|
|
log.log(Level.WARNING, "Couldn't query user email address", e)
|
|||
|
|
processException(e)
|
|||
|
|
}
|
|||
|
|
return mailboxes
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Depending on [resourceType] (CalDAV or CardDAV), this method checks whether [davResponse] references
|
|||
|
|
* - an address book or calendar (actual resource), and/or
|
|||
|
|
* - an "address book home set" or a "calendar home set", and/or
|
|||
|
|
* - whether it's a principal.
|
|||
|
|
*
|
|||
|
|
* Respectively, this method will add the response to [config.collections], [config.homesets] and/or [config.principal].
|
|||
|
|
* Collection URLs will be stored with trailing "/".
|
|||
|
|
*
|
|||
|
|
* @param resourceType type of service to search for in the response
|
|||
|
|
* @param davResponse response whose properties are evaluated
|
|||
|
|
* @param config structure storing the references
|
|||
|
|
*/
|
|||
|
|
fun scanResponse(resourceType: Property.Name, davResponse: Response, config: Configuration.ServiceInfo) {
|
|||
|
|
var principal: HttpUrl? = null
|
|||
|
|
|
|||
|
|
// Type mapping
|
|||
|
|
val homeSetClass: Class<out HrefListProperty>
|
|||
|
|
val serviceType: Service
|
|||
|
|
when (resourceType) {
|
|||
|
|
ResourceType.ADDRESSBOOK -> {
|
|||
|
|
homeSetClass = AddressbookHomeSet::class.java
|
|||
|
|
serviceType = Service.CARDDAV
|
|||
|
|
}
|
|||
|
|
ResourceType.CALENDAR -> {
|
|||
|
|
homeSetClass = CalendarHomeSet::class.java
|
|||
|
|
serviceType = Service.CALDAV
|
|||
|
|
}
|
|||
|
|
else -> throw IllegalArgumentException()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// check for current-user-principal
|
|||
|
|
davResponse[CurrentUserPrincipal::class.java]?.href?.let { currentUserPrincipal ->
|
|||
|
|
principal = davResponse.requestedUrl.resolve(currentUserPrincipal)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
davResponse[ResourceType::class.java]?.let {
|
|||
|
|
// Is it a calendar or an address book, ...
|
|||
|
|
if (it.types.contains(resourceType))
|
|||
|
|
Collection.fromDavResponse(davResponse)?.let { info ->
|
|||
|
|
log.info("Found resource of type $resourceType at ${info.url}")
|
|||
|
|
config.collections[info.url] = info
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ... and/or a principal?
|
|||
|
|
if (it.types.contains(ResourceType.PRINCIPAL))
|
|||
|
|
principal = davResponse.href
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Is it an addressbook-home-set or calendar-home-set?
|
|||
|
|
davResponse[homeSetClass]?.let { homeSet ->
|
|||
|
|
for (href in homeSet.hrefs) {
|
|||
|
|
davResponse.requestedUrl.resolve(href)?.let {
|
|||
|
|
val location = UrlUtils.withTrailingSlash(it)
|
|||
|
|
log.info("Found home-set of type $resourceType at $location")
|
|||
|
|
config.homeSets += location
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Is there a principal too?
|
|||
|
|
principal?.let {
|
|||
|
|
if (providesService(it, serviceType))
|
|||
|
|
config.principal = principal
|
|||
|
|
else
|
|||
|
|
log.warning("Principal $principal doesn't provide $serviceType service")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Sends an OPTIONS request to determine whether a URL provides a given service.
|
|||
|
|
*
|
|||
|
|
* @param url URL to check; often a principal URL
|
|||
|
|
* @param service service to check for
|
|||
|
|
*
|
|||
|
|
* @return whether the URL provides the given service
|
|||
|
|
*/
|
|||
|
|
fun providesService(url: HttpUrl, service: Service): Boolean {
|
|||
|
|
var provided = false
|
|||
|
|
try {
|
|||
|
|
DavResource(httpClient.okHttpClient, url, log).options { capabilities, _ ->
|
|||
|
|
if ((service == Service.CARDDAV && capabilities.contains("addressbook")) ||
|
|||
|
|
(service == Service.CALDAV && capabilities.contains("calendar-access")))
|
|||
|
|
provided = true
|
|||
|
|
}
|
|||
|
|
} catch(e: Exception) {
|
|||
|
|
log.log(Level.SEVERE, "Couldn't detect services on $url", e)
|
|||
|
|
if (e !is HttpException && e !is DavException)
|
|||
|
|
throw e
|
|||
|
|
}
|
|||
|
|
return provided
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Try to find the principal URL by performing service discovery on a given domain name.
|
|||
|
|
* Only secure services (caldavs, carddavs) will be discovered!
|
|||
|
|
*
|
|||
|
|
* @param domain domain name, e.g. "icloud.com"
|
|||
|
|
* @param service service to discover (CALDAV or CARDDAV)
|
|||
|
|
* @return principal URL, or null if none found
|
|||
|
|
*/
|
|||
|
|
fun discoverPrincipalUrl(domain: String, service: Service): HttpUrl? {
|
|||
|
|
val scheme: String
|
|||
|
|
val fqdn: String
|
|||
|
|
var port = 443
|
|||
|
|
val paths = LinkedList<String>() // there may be multiple paths to try
|
|||
|
|
|
|||
|
|
val query = "_${service.wellKnownName}s._tcp.$domain"
|
|||
|
|
log.fine("Looking up SRV records for $query")
|
|||
|
|
|
|||
|
|
val srvRecords = dnsRecordResolver.resolve(query, Type.SRV)
|
|||
|
|
val srv = dnsRecordResolver.bestSRVRecord(srvRecords)
|
|||
|
|
|
|||
|
|
if (srv != null) {
|
|||
|
|
// choose SRV record to use (query may return multiple SRV records)
|
|||
|
|
scheme = "https"
|
|||
|
|
fqdn = srv.target.toString(true)
|
|||
|
|
port = srv.port
|
|||
|
|
log.info("Found $service service at https://$fqdn:$port")
|
|||
|
|
} else {
|
|||
|
|
// no SRV records, try domain name as FQDN
|
|||
|
|
log.info("Didn't find $service service, trying at https://$domain:$port")
|
|||
|
|
|
|||
|
|
scheme = "https"
|
|||
|
|
fqdn = domain
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// look for TXT record too (for initial context path)
|
|||
|
|
val txtRecords = dnsRecordResolver.resolve(query, Type.TXT)
|
|||
|
|
paths.addAll(dnsRecordResolver.pathsFromTXTRecords(txtRecords))
|
|||
|
|
|
|||
|
|
// in case there's a TXT record, but it's wrong, try well-known
|
|||
|
|
paths.add("/.well-known/" + service.wellKnownName)
|
|||
|
|
// if this fails too, try "/"
|
|||
|
|
paths.add("/")
|
|||
|
|
|
|||
|
|
for (path in paths)
|
|||
|
|
try {
|
|||
|
|
val initialContextPath = HttpUrl.Builder()
|
|||
|
|
.scheme(scheme)
|
|||
|
|
.host(fqdn).port(port)
|
|||
|
|
.encodedPath(path)
|
|||
|
|
.build()
|
|||
|
|
|
|||
|
|
log.info("Trying to determine principal from initial context path=$initialContextPath")
|
|||
|
|
val principal = getCurrentUserPrincipal(initialContextPath, service)
|
|||
|
|
|
|||
|
|
principal?.let { return it }
|
|||
|
|
} catch(e: Exception) {
|
|||
|
|
log.log(Level.WARNING, "No resource found", e)
|
|||
|
|
processException(e)
|
|||
|
|
}
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Queries a given URL for current-user-principal
|
|||
|
|
*
|
|||
|
|
* @param url URL to query with PROPFIND (Depth: 0)
|
|||
|
|
* @param service required service (may be null, in which case no service check is done)
|
|||
|
|
* @return current-user-principal URL that provides required service, or null if none
|
|||
|
|
*/
|
|||
|
|
fun getCurrentUserPrincipal(url: HttpUrl, service: Service?): HttpUrl? {
|
|||
|
|
var principal: HttpUrl? = null
|
|||
|
|
DavResource(httpClient.okHttpClient, url, log).propfind(0, CurrentUserPrincipal.NAME) { response, _ ->
|
|||
|
|
response[CurrentUserPrincipal::class.java]?.href?.let { href ->
|
|||
|
|
response.requestedUrl.resolve(href)?.let {
|
|||
|
|
log.info("Found current-user-principal: $it")
|
|||
|
|
|
|||
|
|
// service check
|
|||
|
|
if (service != null && !providesService(it, service))
|
|||
|
|
log.warning("Principal $it doesn't provide $service service")
|
|||
|
|
else
|
|||
|
|
principal = it
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return principal
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Processes a thrown exception like this:
|
|||
|
|
*
|
|||
|
|
* - If the Exception is an [UnauthorizedException] (HTTP 401), [encountered401] is set to *true*.
|
|||
|
|
* - Re-throws the exception if it signals that the current thread was interrupted to stop the current operation.
|
|||
|
|
*/
|
|||
|
|
private fun processException(e: Exception) {
|
|||
|
|
if (e is UnauthorizedException)
|
|||
|
|
encountered401 = true
|
|||
|
|
else if ((e is InterruptedIOException && e !is SocketTimeoutException) || e is InterruptedException)
|
|||
|
|
throw e
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// data classes
|
|||
|
|
|
|||
|
|
class Configuration(
|
|||
|
|
val cardDAV: ServiceInfo?,
|
|||
|
|
val calDAV: ServiceInfo?,
|
|||
|
|
|
|||
|
|
val encountered401: Boolean,
|
|||
|
|
val logs: String
|
|||
|
|
) {
|
|||
|
|
|
|||
|
|
data class ServiceInfo(
|
|||
|
|
var principal: HttpUrl? = null,
|
|||
|
|
val homeSets: MutableSet<HttpUrl> = HashSet(),
|
|||
|
|
val collections: MutableMap<HttpUrl, Collection> = HashMap(),
|
|||
|
|
|
|||
|
|
val emails: MutableList<String> = LinkedList()
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
override fun toString() =
|
|||
|
|
"DavResourceFinder.Configuration(cardDAV=$cardDAV, calDAV=$calDAV, encountered401=$encountered401, logs=(${logs.length} chars))"
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
}
|