Loading build.gradle +0 −1 Original line number Diff line number Diff line Loading @@ -59,7 +59,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "com.squareup.okhttp3:okhttp:$okhttp_version" implementation "commons-io:commons-io:2.6" androidTestImplementation "com.squareup.okhttp3:mockwebserver:$okhttp_version" androidTestImplementation 'com.android.support.test:runner:1.0.2' Loading src/androidTest/java/at/bitfire/dav4android/DavCollectionTest.kt +2 −2 Original line number Diff line number Diff line Loading @@ -24,13 +24,13 @@ class DavCollectionTest { private val mockServer = MockWebServer() private fun sampleUrl() = mockServer.url("/dav/") @Before fun startServer() = mockServer.start() @After fun stopServer() = mockServer.shutdown() /** * Test sample response for an initial sync-collection report from RFC 6578 3.8. */ Loading Loading @@ -197,7 +197,7 @@ class DavCollectionTest { collection.reportChanges("http://example.com/ns/sync/1232", false, 100, GetETag.NAME).close() fail("Expected HttpException") } catch (e: HttpException) { assertEquals(507, e.status) assertEquals(507, e.code) } } Loading src/androidTest/java/at/bitfire/dav4android/DavExceptionTest.kt 0 → 100644 +113 −0 Original line number Diff line number Diff line /* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package at.bitfire.dav4android import at.bitfire.dav4android.exception.DavException import at.bitfire.dav4android.exception.HttpException import at.bitfire.dav4android.property.ResourceType import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Assert.* import org.junit.Before import org.junit.Test class DavExceptionTest { private val httpClient = OkHttpClient.Builder() .followRedirects(false) .build() private val mockServer = MockWebServer() private fun sampleUrl() = mockServer.url("/dav/") @Before fun startServer() = mockServer.start() @After fun stopServer() = mockServer.shutdown() /** * Test a large HTML response which has a multi-octet UTF-8 character * exactly at the cut-off position. */ @Test fun testLargeTextError() { val url = sampleUrl() val dav = DavResource(httpClient, url) val builder = StringBuilder() builder.append(CharArray(DavException.MAX_EXCERPT_SIZE-1, { '*' })) builder.append("\u03C0") // Pi val body = builder.toString() mockServer.enqueue(MockResponse() .setResponseCode(404) .setHeader("Content-Type", "text/html") .setBody(body)) try { dav.propfind(0, ResourceType.NAME).close() fail("Expected HttpException") } catch (e: HttpException) { assertEquals(e.code, 404) assertTrue(e.errors.isEmpty()) assertEquals( body.substring(0, DavException.MAX_EXCERPT_SIZE-1), e.responseBody!!.substring(0, DavException.MAX_EXCERPT_SIZE-1) ) } } @Test fun testNonTextError() { val url = sampleUrl() val dav = DavResource(httpClient, url) mockServer.enqueue(MockResponse() .setResponseCode(403) .setHeader("Content-Type", "application/octet-stream") .setBody("12345")) try { dav.propfind(0, ResourceType.NAME).close() fail("Expected HttpException") } catch (e: HttpException) { assertEquals(e.code, 403) assertTrue(e.errors.isEmpty()) assertNull(e.responseBody) } } /** * Test precondition XML element (sample from RFC 4918 16) */ @Test fun testXmlError() { val url = sampleUrl() val dav = DavResource(httpClient, url) val body = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" + "<D:error xmlns:D=\"DAV:\">\n" + " <D:lock-token-submitted>\n" + " <D:href>/workspace/webdav/</D:href>\n" + " </D:lock-token-submitted>\n" + "</D:error>\n" mockServer.enqueue(MockResponse() .setResponseCode(423) .setHeader("Content-Type", "application/xml; charset=\"utf-8\"") .setBody(body)) try { dav.propfind(0, ResourceType.NAME).close() fail("Expected HttpException") } catch (e: HttpException) { assertEquals(e.code, 423) assertTrue(e.errors.contains(Property.Name(XmlUtils.NS_WEBDAV, "lock-token-submitted"))) assertEquals(body, e.responseBody) } } } No newline at end of file src/main/java/at/bitfire/dav4android/DavResource.kt +2 −2 Original line number Diff line number Diff line Loading @@ -161,7 +161,7 @@ open class DavResource @JvmOverloads constructor( properties += GetETag(eTag) } val body = response.body() ?: throw HttpException("Received GET response without body", response) val body = response.body() ?: throw DavException("Received GET response without body", httpResponse = response) body.contentType()?.let { mimeType -> properties += GetContentType(mimeType) } Loading Loading @@ -393,7 +393,7 @@ open class DavResource @JvmOverloads constructor( log.fine("Redirected, new location = $target") location = target } else throw HttpException("Redirected without new Location") throw DavException("Redirected without new Location") } } finally { response.body()?.close() Loading src/main/java/at/bitfire/dav4android/exception/DavException.kt +146 −2 Original line number Diff line number Diff line Loading @@ -6,7 +6,151 @@ package at.bitfire.dav4android.exception import at.bitfire.dav4android.Constants import at.bitfire.dav4android.Property import at.bitfire.dav4android.XmlUtils import okhttp3.MediaType import okhttp3.Request import okhttp3.Response import okio.Buffer import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import java.io.ByteArrayOutputStream import java.io.IOException import java.io.Serializable import java.util.logging.Level /** * Signals that an error occurred during a WebDAV-related operation. * * This could be a logical error like when a required ETag has not been * received, but also an explicit HTTP error. */ open class DavException @JvmOverloads constructor( message: String, ex: Throwable? = null ): Exception(message, ex) No newline at end of file ex: Throwable? = null, /** * An associated HTTP [Response]. Will be closed after evaluation. */ httpResponse: Response? = null ): Exception(message, ex), Serializable { companion object { const val MAX_EXCERPT_SIZE = 10*1024 // don't dump more than 20 kB fun isPlainText(type: MediaType) = type.type() == "text" || (type.type() == "application" && type.subtype() in arrayOf("html", "xml")) } var request: Request? = null private set var requestBody: String? = null private set /** * Associated HTTP [Response]. Do not access [Response.body] because it will be closed. * Use [responseBody] instead. */ val response: Response? /** * Body excerpt of [response] (up to [MAX_EXCERPT_SIZE] characters). Only available * if the HTTP response body was textual content. */ var responseBody: String? = null private set /** * Precondition/postcondition XML elements which have been found in the XML response. */ var errors: Set<Property.Name> = setOf() private set init { if (httpResponse != null) { response = httpResponse try { request = httpResponse.request() request?.body()?.let { body -> body.contentType()?.let { if (isPlainText(it)) { val buffer = Buffer() body.writeTo(buffer) val baos = ByteArrayOutputStream() buffer.writeTo(baos) requestBody = baos.toString(it.charset(Charsets.UTF_8)!!.name()) } } } } catch (e: Exception) { Constants.log.log(Level.WARNING, "Couldn't read HTTP request", e) requestBody = "Couldn't read HTTP request: ${e.message}" } try { // save response body excerpt if (httpResponse.body()?.source() != null) { // response body has a source httpResponse.peekBody(MAX_EXCERPT_SIZE.toLong())?.use { body -> body.contentType()?.let { if (isPlainText(it)) responseBody = body.string() } } httpResponse.body()?.use { body -> body.contentType()?.let { if (it.type() in arrayOf("application", "text") && it.subtype() == "xml") { // look for precondition/postcondition XML elements try { val parser = XmlUtils.newPullParser() parser.setInput(body.charStream()) var eventType = parser.eventType while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG && parser.depth == 1) if (parser.namespace == XmlUtils.NS_WEBDAV && parser.name == "error") errors = parseXmlErrors(parser) eventType = parser.next() } } catch (e: XmlPullParserException) { Constants.log.log(Level.WARNING, "Couldn't parse XML response", e) } } } } } } catch (e: IOException) { Constants.log.log(Level.WARNING, "Couldn't read HTTP response", e) responseBody = "Couldn't read HTTP response: ${e.message}" } finally { httpResponse.body()?.close() } } else response = null } private fun parseXmlErrors(parser: XmlPullParser): Set<Property.Name> { val names = mutableSetOf<Property.Name>() val depth = parser.depth var eventType = parser.eventType while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) { if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1) names += Property.Name(parser.namespace, parser.name) eventType = parser.next() } return names } } No newline at end of file Loading
build.gradle +0 −1 Original line number Diff line number Diff line Loading @@ -59,7 +59,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "com.squareup.okhttp3:okhttp:$okhttp_version" implementation "commons-io:commons-io:2.6" androidTestImplementation "com.squareup.okhttp3:mockwebserver:$okhttp_version" androidTestImplementation 'com.android.support.test:runner:1.0.2' Loading
src/androidTest/java/at/bitfire/dav4android/DavCollectionTest.kt +2 −2 Original line number Diff line number Diff line Loading @@ -24,13 +24,13 @@ class DavCollectionTest { private val mockServer = MockWebServer() private fun sampleUrl() = mockServer.url("/dav/") @Before fun startServer() = mockServer.start() @After fun stopServer() = mockServer.shutdown() /** * Test sample response for an initial sync-collection report from RFC 6578 3.8. */ Loading Loading @@ -197,7 +197,7 @@ class DavCollectionTest { collection.reportChanges("http://example.com/ns/sync/1232", false, 100, GetETag.NAME).close() fail("Expected HttpException") } catch (e: HttpException) { assertEquals(507, e.status) assertEquals(507, e.code) } } Loading
src/androidTest/java/at/bitfire/dav4android/DavExceptionTest.kt 0 → 100644 +113 −0 Original line number Diff line number Diff line /* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package at.bitfire.dav4android import at.bitfire.dav4android.exception.DavException import at.bitfire.dav4android.exception.HttpException import at.bitfire.dav4android.property.ResourceType import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Assert.* import org.junit.Before import org.junit.Test class DavExceptionTest { private val httpClient = OkHttpClient.Builder() .followRedirects(false) .build() private val mockServer = MockWebServer() private fun sampleUrl() = mockServer.url("/dav/") @Before fun startServer() = mockServer.start() @After fun stopServer() = mockServer.shutdown() /** * Test a large HTML response which has a multi-octet UTF-8 character * exactly at the cut-off position. */ @Test fun testLargeTextError() { val url = sampleUrl() val dav = DavResource(httpClient, url) val builder = StringBuilder() builder.append(CharArray(DavException.MAX_EXCERPT_SIZE-1, { '*' })) builder.append("\u03C0") // Pi val body = builder.toString() mockServer.enqueue(MockResponse() .setResponseCode(404) .setHeader("Content-Type", "text/html") .setBody(body)) try { dav.propfind(0, ResourceType.NAME).close() fail("Expected HttpException") } catch (e: HttpException) { assertEquals(e.code, 404) assertTrue(e.errors.isEmpty()) assertEquals( body.substring(0, DavException.MAX_EXCERPT_SIZE-1), e.responseBody!!.substring(0, DavException.MAX_EXCERPT_SIZE-1) ) } } @Test fun testNonTextError() { val url = sampleUrl() val dav = DavResource(httpClient, url) mockServer.enqueue(MockResponse() .setResponseCode(403) .setHeader("Content-Type", "application/octet-stream") .setBody("12345")) try { dav.propfind(0, ResourceType.NAME).close() fail("Expected HttpException") } catch (e: HttpException) { assertEquals(e.code, 403) assertTrue(e.errors.isEmpty()) assertNull(e.responseBody) } } /** * Test precondition XML element (sample from RFC 4918 16) */ @Test fun testXmlError() { val url = sampleUrl() val dav = DavResource(httpClient, url) val body = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" + "<D:error xmlns:D=\"DAV:\">\n" + " <D:lock-token-submitted>\n" + " <D:href>/workspace/webdav/</D:href>\n" + " </D:lock-token-submitted>\n" + "</D:error>\n" mockServer.enqueue(MockResponse() .setResponseCode(423) .setHeader("Content-Type", "application/xml; charset=\"utf-8\"") .setBody(body)) try { dav.propfind(0, ResourceType.NAME).close() fail("Expected HttpException") } catch (e: HttpException) { assertEquals(e.code, 423) assertTrue(e.errors.contains(Property.Name(XmlUtils.NS_WEBDAV, "lock-token-submitted"))) assertEquals(body, e.responseBody) } } } No newline at end of file
src/main/java/at/bitfire/dav4android/DavResource.kt +2 −2 Original line number Diff line number Diff line Loading @@ -161,7 +161,7 @@ open class DavResource @JvmOverloads constructor( properties += GetETag(eTag) } val body = response.body() ?: throw HttpException("Received GET response without body", response) val body = response.body() ?: throw DavException("Received GET response without body", httpResponse = response) body.contentType()?.let { mimeType -> properties += GetContentType(mimeType) } Loading Loading @@ -393,7 +393,7 @@ open class DavResource @JvmOverloads constructor( log.fine("Redirected, new location = $target") location = target } else throw HttpException("Redirected without new Location") throw DavException("Redirected without new Location") } } finally { response.body()?.close() Loading
src/main/java/at/bitfire/dav4android/exception/DavException.kt +146 −2 Original line number Diff line number Diff line Loading @@ -6,7 +6,151 @@ package at.bitfire.dav4android.exception import at.bitfire.dav4android.Constants import at.bitfire.dav4android.Property import at.bitfire.dav4android.XmlUtils import okhttp3.MediaType import okhttp3.Request import okhttp3.Response import okio.Buffer import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import java.io.ByteArrayOutputStream import java.io.IOException import java.io.Serializable import java.util.logging.Level /** * Signals that an error occurred during a WebDAV-related operation. * * This could be a logical error like when a required ETag has not been * received, but also an explicit HTTP error. */ open class DavException @JvmOverloads constructor( message: String, ex: Throwable? = null ): Exception(message, ex) No newline at end of file ex: Throwable? = null, /** * An associated HTTP [Response]. Will be closed after evaluation. */ httpResponse: Response? = null ): Exception(message, ex), Serializable { companion object { const val MAX_EXCERPT_SIZE = 10*1024 // don't dump more than 20 kB fun isPlainText(type: MediaType) = type.type() == "text" || (type.type() == "application" && type.subtype() in arrayOf("html", "xml")) } var request: Request? = null private set var requestBody: String? = null private set /** * Associated HTTP [Response]. Do not access [Response.body] because it will be closed. * Use [responseBody] instead. */ val response: Response? /** * Body excerpt of [response] (up to [MAX_EXCERPT_SIZE] characters). Only available * if the HTTP response body was textual content. */ var responseBody: String? = null private set /** * Precondition/postcondition XML elements which have been found in the XML response. */ var errors: Set<Property.Name> = setOf() private set init { if (httpResponse != null) { response = httpResponse try { request = httpResponse.request() request?.body()?.let { body -> body.contentType()?.let { if (isPlainText(it)) { val buffer = Buffer() body.writeTo(buffer) val baos = ByteArrayOutputStream() buffer.writeTo(baos) requestBody = baos.toString(it.charset(Charsets.UTF_8)!!.name()) } } } } catch (e: Exception) { Constants.log.log(Level.WARNING, "Couldn't read HTTP request", e) requestBody = "Couldn't read HTTP request: ${e.message}" } try { // save response body excerpt if (httpResponse.body()?.source() != null) { // response body has a source httpResponse.peekBody(MAX_EXCERPT_SIZE.toLong())?.use { body -> body.contentType()?.let { if (isPlainText(it)) responseBody = body.string() } } httpResponse.body()?.use { body -> body.contentType()?.let { if (it.type() in arrayOf("application", "text") && it.subtype() == "xml") { // look for precondition/postcondition XML elements try { val parser = XmlUtils.newPullParser() parser.setInput(body.charStream()) var eventType = parser.eventType while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG && parser.depth == 1) if (parser.namespace == XmlUtils.NS_WEBDAV && parser.name == "error") errors = parseXmlErrors(parser) eventType = parser.next() } } catch (e: XmlPullParserException) { Constants.log.log(Level.WARNING, "Couldn't parse XML response", e) } } } } } } catch (e: IOException) { Constants.log.log(Level.WARNING, "Couldn't read HTTP response", e) responseBody = "Couldn't read HTTP response: ${e.message}" } finally { httpResponse.body()?.close() } } else response = null } private fun parseXmlErrors(parser: XmlPullParser): Set<Property.Name> { val names = mutableSetOf<Property.Name>() val depth = parser.depth var eventType = parser.eventType while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) { if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1) names += Property.Name(parser.namespace, parser.name) eventType = parser.next() } return names } } No newline at end of file