From 73a7b681f6719af84f7a0c9f6817ef4c49b65ce2 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 15 Jul 2025 14:34:19 +0200 Subject: [PATCH 1/4] Implement WebDAV detection via OPTIONS request with redirect support --- .../kotlin/at/bitfire/dav4jvm/DavResource.kt | 35 +++++++++++++++ .../at/bitfire/dav4jvm/DavResourceTest.kt | 45 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt b/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt index b0c5204..d6d3296 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt @@ -68,6 +68,8 @@ open class DavResource @JvmOverloads constructor( companion object { const val MAX_REDIRECTS = 5 + const val WEBDAV_VERSIONS = "1, 2, 3" // valid WebDAV versions + const val HTTP_MULTISTATUS = 207 val MIME_XML = "application/xml; charset=utf-8".toMediaType() @@ -175,6 +177,39 @@ open class DavResource @JvmOverloads constructor( } } + /** + * Finds WebDAV capable location by sending OPTIONS request to this resource without HTTP + * compression (because some servers have broken compression for OPTIONS). Follows up + * to [MAX_REDIRECTS] redirects. + * + * @return the location of the WebDAV resource if it supports WebDAV, `null` otherwise + * + * @throws IOException on I/O error + * @throws HttpException on HTTP error + * @throws DavException on HTTPS -> HTTP redirect + */ + @Throws(IOException::class, HttpException::class, DavException::class) + fun detectWebDav(): HttpUrl? = followRedirects { + httpClient.newCall( + Request.Builder() + .method("OPTIONS", null) + .header("Content-Length", "0") + .url(location) + .header("Accept-Encoding", "identity") // disable compression + .build() + ).execute() + }.use { response -> + checkStatus(response) + + // check whether WebDAV is supported + val davCapabilities = HttpUtils.listHeader(response, "DAV").map { it.trim() }.toSet() + val supportsWebDav = davCapabilities.any { it in WEBDAV_VERSIONS } + return if (supportsWebDav) + location + else + null + } + /** * Sends a MOVE request to this resource. Follows up to [MAX_REDIRECTS] redirects. * Updates [location] on success. diff --git a/src/test/kotlin/at/bitfire/dav4jvm/DavResourceTest.kt b/src/test/kotlin/at/bitfire/dav4jvm/DavResourceTest.kt index 5673ac8..089daf4 100644 --- a/src/test/kotlin/at/bitfire/dav4jvm/DavResourceTest.kt +++ b/src/test/kotlin/at/bitfire/dav4jvm/DavResourceTest.kt @@ -516,6 +516,51 @@ class DavResourceTest { assertTrue(called) } + @Test + fun testDetectWebDav() { + val url = sampleUrl() + val dav = DavResource(httpClient, url) + + // Without WebDAV + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("DAV", " 0 ,, 4,5,6, hyperactive-access") + ) + assertNull(dav.detectWebDav()) + + // With WebDAV + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("DAV", " 1 ") // Class 1 + ) + assertEquals(url.encodedPath, dav.detectWebDav()?.encodedPath) + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("DAV", "1, 2") // Class 2 + ) + assertEquals(url.encodedPath, dav.detectWebDav()?.encodedPath) + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("DAV", " 1, 3 ") // Class 3 + ) + assertEquals(url.encodedPath, dav.detectWebDav()?.encodedPath) + + // Follows redirects + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .setHeader("Location", "/moved") + ) + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .setHeader("Location", "/redirected") + ) + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("DAV", "2") + ) + assertEquals("/redirected", dav.detectWebDav()?.encodedPath) + } + @Test fun testPropfindAndMultiStatus() { val url = sampleUrl() From 494b979cee4a90262c5aa7e1490945f74a86fe0f Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 16 Jul 2025 11:34:15 +0200 Subject: [PATCH 2/4] Revert "Implement WebDAV detection via OPTIONS request with redirect support" This reverts commit 76d46a88bc358d480a7b4e1f46a07b44cb1eb478. --- .../kotlin/at/bitfire/dav4jvm/DavResource.kt | 35 --------------- .../at/bitfire/dav4jvm/DavResourceTest.kt | 45 ------------------- 2 files changed, 80 deletions(-) diff --git a/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt b/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt index d6d3296..b0c5204 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt @@ -68,8 +68,6 @@ open class DavResource @JvmOverloads constructor( companion object { const val MAX_REDIRECTS = 5 - const val WEBDAV_VERSIONS = "1, 2, 3" // valid WebDAV versions - const val HTTP_MULTISTATUS = 207 val MIME_XML = "application/xml; charset=utf-8".toMediaType() @@ -177,39 +175,6 @@ open class DavResource @JvmOverloads constructor( } } - /** - * Finds WebDAV capable location by sending OPTIONS request to this resource without HTTP - * compression (because some servers have broken compression for OPTIONS). Follows up - * to [MAX_REDIRECTS] redirects. - * - * @return the location of the WebDAV resource if it supports WebDAV, `null` otherwise - * - * @throws IOException on I/O error - * @throws HttpException on HTTP error - * @throws DavException on HTTPS -> HTTP redirect - */ - @Throws(IOException::class, HttpException::class, DavException::class) - fun detectWebDav(): HttpUrl? = followRedirects { - httpClient.newCall( - Request.Builder() - .method("OPTIONS", null) - .header("Content-Length", "0") - .url(location) - .header("Accept-Encoding", "identity") // disable compression - .build() - ).execute() - }.use { response -> - checkStatus(response) - - // check whether WebDAV is supported - val davCapabilities = HttpUtils.listHeader(response, "DAV").map { it.trim() }.toSet() - val supportsWebDav = davCapabilities.any { it in WEBDAV_VERSIONS } - return if (supportsWebDav) - location - else - null - } - /** * Sends a MOVE request to this resource. Follows up to [MAX_REDIRECTS] redirects. * Updates [location] on success. diff --git a/src/test/kotlin/at/bitfire/dav4jvm/DavResourceTest.kt b/src/test/kotlin/at/bitfire/dav4jvm/DavResourceTest.kt index 089daf4..5673ac8 100644 --- a/src/test/kotlin/at/bitfire/dav4jvm/DavResourceTest.kt +++ b/src/test/kotlin/at/bitfire/dav4jvm/DavResourceTest.kt @@ -516,51 +516,6 @@ class DavResourceTest { assertTrue(called) } - @Test - fun testDetectWebDav() { - val url = sampleUrl() - val dav = DavResource(httpClient, url) - - // Without WebDAV - mockServer.enqueue(MockResponse() - .setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("DAV", " 0 ,, 4,5,6, hyperactive-access") - ) - assertNull(dav.detectWebDav()) - - // With WebDAV - mockServer.enqueue(MockResponse() - .setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("DAV", " 1 ") // Class 1 - ) - assertEquals(url.encodedPath, dav.detectWebDav()?.encodedPath) - mockServer.enqueue(MockResponse() - .setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("DAV", "1, 2") // Class 2 - ) - assertEquals(url.encodedPath, dav.detectWebDav()?.encodedPath) - mockServer.enqueue(MockResponse() - .setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("DAV", " 1, 3 ") // Class 3 - ) - assertEquals(url.encodedPath, dav.detectWebDav()?.encodedPath) - - // Follows redirects - mockServer.enqueue(MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .setHeader("Location", "/moved") - ) - mockServer.enqueue(MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .setHeader("Location", "/redirected") - ) - mockServer.enqueue(MockResponse() - .setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("DAV", "2") - ) - assertEquals("/redirected", dav.detectWebDav()?.encodedPath) - } - @Test fun testPropfindAndMultiStatus() { val url = sampleUrl() From 6bd22ce7f1fe3086acd7f2aab5ec861e12fe0b77 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 16 Jul 2025 11:35:59 +0200 Subject: [PATCH 3/4] Add support for following redirects in OPTIONS request --- .../kotlin/at/bitfire/dav4jvm/DavResource.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt b/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt index b0c5204..1b5dd55 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt @@ -151,8 +151,9 @@ open class DavResource @JvmOverloads constructor( /** * Sends an OPTIONS request to this resource without HTTP compression (because some servers have - * broken compression for OPTIONS). Doesn't follow redirects. + * broken compression for OPTIONS). Follows up to [MAX_REDIRECTS] redirects when set. * + * @param followRedirects whether redirects should be followed (default: *false*) * @param callback called with server response unless an exception is thrown * * @throws IOException on I/O error @@ -160,13 +161,20 @@ open class DavResource @JvmOverloads constructor( * @throws DavException on HTTPS -> HTTP redirect */ @Throws(IOException::class, HttpException::class) - fun options(callback: CapabilitiesCallback) { - httpClient.newCall(Request.Builder() + fun options(followRedirects: Boolean = false, callback: CapabilitiesCallback) { + val callBlock = { + httpClient.newCall(Request.Builder() .method("OPTIONS", null) .header("Content-Length", "0") .url(location) .header("Accept-Encoding", "identity") // disable compression - .build()).execute().use { response -> + .build()).execute() + } + val response = if (followRedirects) + followRedirects(callBlock) + else + callBlock() + response.use { checkStatus(response) callback.onCapabilities( HttpUtils.listHeader(response, "DAV").map { it.trim() }.toSet(), From 1a386682e566164f0a85cfb52fe45b661a857171 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Thu, 24 Jul 2025 11:01:53 +0200 Subject: [PATCH 4/4] Rename call block --- src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt b/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt index 1b5dd55..25a94bb 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt @@ -162,7 +162,7 @@ open class DavResource @JvmOverloads constructor( */ @Throws(IOException::class, HttpException::class) fun options(followRedirects: Boolean = false, callback: CapabilitiesCallback) { - val callBlock = { + val requestOptions = { httpClient.newCall(Request.Builder() .method("OPTIONS", null) .header("Content-Length", "0") @@ -171,9 +171,9 @@ open class DavResource @JvmOverloads constructor( .build()).execute() } val response = if (followRedirects) - followRedirects(callBlock) + followRedirects(requestOptions) else - callBlock() + requestOptions() response.use { checkStatus(response) callback.onCapabilities(