sync(chunk 2b): CalDAV discovery and auth protocol
New :caldav module — MIT, plain JVM, api-depends on :dav. Separate from the vendored MPL tree so the licences stay unmixed, and so "no Android types" is a compile-time guarantee rather than a discipline. Chunk 2 split again: the Android account layer (Keystore, AccountManager, Custom Tabs, account-add UI) is 2c, with different verification and an on-device review. - ServiceDiscovery: the RFC 6764 ladder. SRV priority/weight, TXT path=, non-443 ports, "." targets, well-known then root. - CollectionClassifier: the two filters that are inversions of the obvious rule. An absent or empty supported-calendar-component-set means "supports everything", and classification is a positive test for CALDAV:calendar on an unordered set — excluding schedule-outbox would drop SOGo's main calendar. - CalDavDiscovery: OPTIONS gate, principal, every home set, Depth-1 by name. A failing home set does not fail the account, and every home set failing is reported as an error rather than as an account with no lists. - NextcloudLoginFlow: POST not GET, a User-Agent the user can recognise when revoking, 404-means-pending only, both URLs origin-checked, host mismatch carried rather than refused (reverse proxies are ordinary). - PreemptiveBasicInterceptor, ServerQuirks. dnsjava 3.6.3 (BSD-3) added: Android's DnsResolver is callback-only and cannot do the TXT path lookup, and JNDI's DNS provider does not exist on Android. Behind an interface, so every trap is tested with a fake and no network. :dav gains change 6 — <D:unauthenticated/> is parsed rather than inferred from a null href, which also fires on a merely non-conformant empty element. 52 tests here, 78 in :dav. SYNC.md's live-probed trap table is executable now.
This commit is contained in:
@@ -10,17 +10,29 @@ import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.Property
|
||||
import at.bitfire.dav4jvm.PropertyFactory
|
||||
import at.bitfire.dav4jvm.XmlUtils
|
||||
import at.bitfire.dav4jvm.XmlUtils.propertyName
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
|
||||
// see RFC 5397: WebDAV Current Principal Extension
|
||||
|
||||
data class CurrentUserPrincipal(
|
||||
val href: String?
|
||||
val href: String?,
|
||||
/**
|
||||
* RFC 5397 §3: the server answered 200 but the request was **not**
|
||||
* authenticated. Upstream parsed only the `<href>` child, so this arrived
|
||||
* indistinguishable from a conformant-but-empty element and from a server
|
||||
* that simply omits the property — and a rejected credential then looks
|
||||
* like a successful discovery that found nothing. Local addition; see
|
||||
* dav/PROVENANCE.md change 6.
|
||||
*/
|
||||
val unauthenticated: Boolean = false
|
||||
): Property {
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val NAME = Property.Name(XmlUtils.NS_WEBDAV, "current-user-principal")
|
||||
|
||||
val UNAUTHENTICATED = Property.Name(XmlUtils.NS_WEBDAV, "unauthenticated")
|
||||
}
|
||||
|
||||
|
||||
@@ -30,11 +42,23 @@ data class CurrentUserPrincipal(
|
||||
|
||||
override fun create(parser: XmlPullParser): CurrentUserPrincipal {
|
||||
// <!ELEMENT current-user-principal (unauthenticated | href)>
|
||||
// One pass over both children: XmlUtils.processTag consumes to the end
|
||||
// tag, so it cannot be called twice on the same element.
|
||||
var href: String? = null
|
||||
XmlUtils.processTag(parser, DavResource.HREF) {
|
||||
href = XmlUtils.readText(parser)
|
||||
var unauthenticated = false
|
||||
|
||||
val depth = parser.depth
|
||||
var eventType = parser.eventType
|
||||
while (!((eventType == XmlPullParser.END_TAG || eventType == XmlPullParser.END_DOCUMENT) &&
|
||||
parser.depth == depth)) {
|
||||
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1)
|
||||
when (parser.propertyName()) {
|
||||
DavResource.HREF -> href = XmlUtils.readText(parser)
|
||||
UNAUTHENTICATED -> unauthenticated = true
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
return CurrentUserPrincipal(href)
|
||||
return CurrentUserPrincipal(href, unauthenticated)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
|
||||
package at.bitfire.dav4jvm
|
||||
|
||||
import at.bitfire.dav4jvm.property.CurrentUserPrincipal
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
@@ -121,6 +123,50 @@ class LocalChangesTest {
|
||||
assertEquals(mockServer.url("/other/"), resource.location)
|
||||
}
|
||||
|
||||
// -- change 6: <D:unauthenticated/> is parsed, not inferred ---------------
|
||||
|
||||
private fun principalBody(inner: String) = MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setHeader("Content-Type", "application/xml; charset=utf-8")
|
||||
.setBody(
|
||||
"""
|
||||
<multistatus xmlns="DAV:"><response><href>/dav/</href>
|
||||
<propstat><prop><current-user-principal>$inner</current-user-principal></prop>
|
||||
<status>HTTP/1.1 200 OK</status></propstat>
|
||||
</response></multistatus>
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
private fun principalOf(inner: String): CurrentUserPrincipal? {
|
||||
mockServer.enqueue(principalBody(inner))
|
||||
var found: CurrentUserPrincipal? = null
|
||||
DavResource(httpClient, mockServer.url("/dav/"))
|
||||
.propfind(0, CurrentUserPrincipal.NAME) { response, _ ->
|
||||
found = response[CurrentUserPrincipal::class.java]
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unauthenticatedIsDistinguishableFromAMerelyEmptyElement() {
|
||||
// RFC 5397 §3: a 200 whose body says the request was not authenticated.
|
||||
// Upstream parsed only <href>, so this arrived identical to an empty
|
||||
// element and to a server that omits the property — and a rejected
|
||||
// credential then looks like a discovery that simply found nothing.
|
||||
assertTrue(principalOf("<unauthenticated/>")!!.unauthenticated)
|
||||
|
||||
val empty = principalOf("")
|
||||
assertNull(empty!!.href)
|
||||
assertFalse(empty.unauthenticated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anOrdinaryPrincipalHrefStillParses() {
|
||||
val principal = principalOf("<href>/principals/me/</href>")
|
||||
assertEquals("/principals/me/", principal!!.href)
|
||||
assertFalse(principal.unauthenticated)
|
||||
}
|
||||
|
||||
// -- change 1: commons-lang3 removed from date parsing --------------------
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user