From 03525e5c26caa9ac6c00b1d339612041fb41a4c3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 14 Nov 2016 11:32:20 +0100 Subject: [PATCH 1/5] Fix IndexHTTPTest --- .../qabel/core/index/server/IndexHTTPTest.kt | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt b/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt index b94bfdf8..6da384f8 100644 --- a/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt +++ b/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt @@ -9,11 +9,10 @@ import de.qabel.core.index.* import org.apache.http.StatusLine import org.apache.http.client.methods.HttpUriRequest import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Test import java.util.* -@Ignore + class IndexHTTPTest { private val server = IndexHTTPLocation(TestServer.INDEX) private val index = IndexHTTP(server) @@ -70,16 +69,14 @@ class IndexHTTPTest { } private class UpdateTestParts(private val index: IndexHTTP) { - private val mail1 = getRandomMail() - private val mail2 = getRandomMail() + val mail = getRandomMail() val identity = UpdateIdentity( keyPair = QblECKeyPair(), dropURL = DropURL("http://example.net/somewhere/abcdefghijklmnopqrstuvwxyzabcdefghijklmnopo"), alias = "Major Anya, Unicode: 裸共產主義", fields = listOf( - UpdateField(UpdateAction.CREATE, FieldType.EMAIL, mail1), - UpdateField(UpdateAction.CREATE, FieldType.EMAIL, mail2) + UpdateField(UpdateAction.CREATE, FieldType.EMAIL, mail) ) ) @@ -90,22 +87,20 @@ class IndexHTTPTest { */ assertEquals(UpdateResult.ACCEPTED_IMMEDIATE, updateResult) - /* Will be found with both mails */ - searchForMailAndAssertOurs(mail1) - searchForMailAndAssertOurs(mail2) + /* Will be found */ + searchForMailAndAssertOurs(mail) } fun unpublishTest() { val identity = identity.copy( fields = listOf( - UpdateField(UpdateAction.DELETE, FieldType.EMAIL, mail2) + UpdateField(UpdateAction.DELETE, FieldType.EMAIL, mail) ) ) assertEquals(UpdateResult.ACCEPTED_IMMEDIATE, index.updateIdentity(identity)) - searchForMailAndAssertOurs(mail1) // still found - assertEquals(0, index.search(mapOf(Pair(FieldType.EMAIL, mail2))).size) // gone + assertEquals(0, index.search(mapOf(Pair(FieldType.EMAIL, mail))).size) // gone } fun assertIdentityEquals(identity: UpdateIdentity, indexContact: IndexContact) { From d9fbd1bd08903ffcf0729881273f93c02f281601 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 14 Nov 2016 11:32:21 +0100 Subject: [PATCH 2/5] Add IndexHTTPTest showing how to update drop URL / alias --- .../de/qabel/core/index/server/IndexServer.kt | 3 ++ .../qabel/core/index/server/IndexHTTPTest.kt | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/core/src/main/java/de/qabel/core/index/server/IndexServer.kt b/core/src/main/java/de/qabel/core/index/server/IndexServer.kt index 8ef88206..a6160d74 100644 --- a/core/src/main/java/de/qabel/core/index/server/IndexServer.kt +++ b/core/src/main/java/de/qabel/core/index/server/IndexServer.kt @@ -49,6 +49,9 @@ interface IndexServer { * 2. [UpdateField] with the new email address and [UpdateAction.CREATE] * * As soon as the user confirms the new email address the old email address will be replaced seamlessly. + * + * The drop URL and alias on the server are updated with the values of [identity] (to only update these, + * leave [identity.fields] empty). */ @Throws(IOException::class) fun updateIdentity(identity: UpdateIdentity): UpdateResult diff --git a/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt b/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt index 6da384f8..301e964a 100644 --- a/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt +++ b/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt @@ -68,6 +68,38 @@ class IndexHTTPTest { assertEquals(400, exception.code) } + @Test + fun testUpdateDropURL() { + val testParts = UpdateTestParts(index) + testParts.publishTest() + + val updatedIdentity = testParts.identity.copy(dropURL = DropURL("http://example.net/somewhere/abcdefghijklmnopqrstuvwxyzabcdefghijklmonop")) + val updateResult = index.updateIdentity(updatedIdentity) + assertEquals(UpdateResult.ACCEPTED_IMMEDIATE, updateResult) + + val search1Result = index.search(mapOf(Pair(FieldType.EMAIL, testParts.mail))) + assertEquals(1, search1Result.size) + val foundPublicIdentity = search1Result[0] + assertEquals(updatedIdentity.dropURL, foundPublicIdentity.dropUrl) + assertEquals(updatedIdentity.alias, foundPublicIdentity.alias) + } + + @Test + fun testUpdateAlias() { + val testParts = UpdateTestParts(index) + testParts.publishTest() + + val updatedIdentity = testParts.identity.copy(alias = "1234") + val updateResult = index.updateIdentity(updatedIdentity) + assertEquals(UpdateResult.ACCEPTED_IMMEDIATE, updateResult) + + val search1Result = index.search(mapOf(Pair(FieldType.EMAIL, testParts.mail))) + assertEquals(1, search1Result.size) + val foundPublicIdentity = search1Result[0] + assertEquals(updatedIdentity.dropURL, foundPublicIdentity.dropUrl) + assertEquals(updatedIdentity.alias, foundPublicIdentity.alias) + } + private class UpdateTestParts(private val index: IndexHTTP) { val mail = getRandomMail() From 684395c5347a7aec7eea348710119478a9783922 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 14 Nov 2016 11:32:22 +0100 Subject: [PATCH 3/5] search(many), identityStatus, deleteIdentity --- .../main/java/de/qabel/core/index/models.kt | 73 +++++++++++++++++-- .../index/server/DeleteIdentityEndpoint.kt | 9 +++ .../server/DeleteIdentityEndpointImpl.kt | 33 +++++++++ .../index/server/IdentityStatusEndpoint.kt | 10 +++ .../server/IdentityStatusEndpointImpl.kt | 44 +++++++++++ .../de/qabel/core/index/server/IndexHTTP.kt | 60 ++++++++++++++- .../de/qabel/core/index/server/IndexServer.kt | 21 ++++++ .../qabel/core/index/server/SearchEndpoint.kt | 4 +- .../core/index/server/SearchEndpointImpl.kt | 38 ++++++---- .../core/index/server/UpdateEndpointImpl.kt | 10 +-- .../java/de/qabel/core/index/server/utils.kt | 30 ++++++++ .../qabel/core/index/server/IndexHTTPTest.kt | 22 +++++- .../core/index/server/SearchEndpointTest.kt | 23 +++--- 13 files changed, 332 insertions(+), 45 deletions(-) create mode 100644 core/src/main/java/de/qabel/core/index/server/DeleteIdentityEndpoint.kt create mode 100644 core/src/main/java/de/qabel/core/index/server/DeleteIdentityEndpointImpl.kt create mode 100644 core/src/main/java/de/qabel/core/index/server/IdentityStatusEndpoint.kt create mode 100644 core/src/main/java/de/qabel/core/index/server/IdentityStatusEndpointImpl.kt create mode 100644 core/src/main/java/de/qabel/core/index/server/utils.kt diff --git a/core/src/main/java/de/qabel/core/index/models.kt b/core/src/main/java/de/qabel/core/index/models.kt index 660356c2..81b779ab 100644 --- a/core/src/main/java/de/qabel/core/index/models.kt +++ b/core/src/main/java/de/qabel/core/index/models.kt @@ -19,7 +19,8 @@ import java.util.* data class IndexContact( val publicKey: QblECPublicKey, val dropUrl: DropURL, - val alias: String + val alias: String, + val matches: List = listOf() ) { fun toContact(): Contact { return Contact(alias, listOf(dropUrl), publicKey) @@ -40,7 +41,7 @@ data class UpdateIdentity( val keyPair: QblECKeyPair, val dropURL: DropURL, val alias: String, - val fields: List + val fields: List = listOf() ) { constructor(identity: Identity, fields: List) : @@ -92,12 +93,59 @@ enum class FieldType { PHONE, } + +data class Field( + val field: FieldType, + val value: String +) + + data class UpdateField( val action: UpdateAction, val field: FieldType, val value: String ) +enum class EntryStatusEnum { + /** + * This field entry was confirmed and is live; publicly visible. + */ + @SerializedName("confirmed") + CONFIRMED, + + /** + * Confirmation (by user) pending, not publicly visible. + */ + @SerializedName("unconfirmed") + UNCONFIRMED, + + /** + * Confirmation (by user) to delete publicly visible entry. Currently this does normally not happen as there + * is no way at the moment to craft such a request through any of the clients, which authorize + * deletions through encrypted requests instead. + */ + @SerializedName("deletion-pending") + DELETION_PENDING +} + +data class EntryStatus( + val status: EntryStatusEnum, + val field: FieldType, + val value: String +) + +data class IdentityStatus( + /** + * Current identity data as returned by server (drop URL, alias, public key). + */ + val identity: IndexContact, + /** + * A list of field statuses for every confirmed or unconfirmed (pending) entry associated with the + * identity. + */ + val fieldStatus: List +) + /** * Result of an update request issued by [IndexServer.publishIdentity] or [IndexServer.unpublishIdentity]. */ @@ -115,18 +163,28 @@ private val IndexContactDeserializer = jsonDeserializer { */ val obj = it.json.obj if (!obj.contains("public_key") || !obj.contains("alias") || !obj.contains("drop_url")) { - throw IllegalArgumentException("missing key in identity") + throw IllegalArgumentException("missing key in contact") + } + val matches = if (obj.contains("matches") && obj["matches"].isJsonArray()) { + it.context.deserialize>(obj["matches"], Array::class.java).asList() + } else { + listOf() } - /* If a custom TypeAdapter is around, at least this level has to be spelled out, since we don't have access to - * the generic type adapter here. - */ IndexContact( publicKey = it.context.deserialize(obj["public_key"], QblECPublicKey::class.java), dropUrl = it.context.deserialize(obj["drop_url"], DropURL::class.java), - alias = obj["alias"].string + alias = obj["alias"].string, + matches = matches ) } +private val IndexContactSerializer = jsonSerializer { + it.context.serialize(mapOf( + Pair("public_key", it.src.publicKey), + Pair("drop_url", it.src.dropUrl), + Pair("alias", it.src.alias))) +} + /** * Return Gson instance with necessary TypeAdapters to serdes JSON according to the spec * http://qabel.github.io/docs/Qabel-Index/ @@ -135,6 +193,7 @@ internal fun createGson(): Gson { return GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeAdapter(IndexContactDeserializer) + .registerTypeAdapter(IndexContactSerializer) .create() } diff --git a/core/src/main/java/de/qabel/core/index/server/DeleteIdentityEndpoint.kt b/core/src/main/java/de/qabel/core/index/server/DeleteIdentityEndpoint.kt new file mode 100644 index 00000000..c026034f --- /dev/null +++ b/core/src/main/java/de/qabel/core/index/server/DeleteIdentityEndpoint.kt @@ -0,0 +1,9 @@ +package de.qabel.core.index.server + +import de.qabel.core.crypto.QblECPublicKey +import de.qabel.core.index.UpdateIdentity +import org.apache.http.client.methods.HttpUriRequest + +internal interface DeleteIdentityEndpoint : EndpointBase { + fun buildRequest(identity: UpdateIdentity, serverPublicKey: QblECPublicKey): HttpUriRequest +} diff --git a/core/src/main/java/de/qabel/core/index/server/DeleteIdentityEndpointImpl.kt b/core/src/main/java/de/qabel/core/index/server/DeleteIdentityEndpointImpl.kt new file mode 100644 index 00000000..bd7370a6 --- /dev/null +++ b/core/src/main/java/de/qabel/core/index/server/DeleteIdentityEndpointImpl.kt @@ -0,0 +1,33 @@ +package de.qabel.core.index.server + +import com.google.gson.Gson +import de.qabel.core.crypto.QblECPublicKey +import de.qabel.core.index.* +import de.qabel.core.logging.QabelLog +import org.apache.http.StatusLine +import org.apache.http.client.methods.HttpPost +import org.apache.http.client.methods.HttpUriRequest + + +internal class DeleteIdentityEndpointImpl( + val location: IndexHTTPLocation, + val gson: Gson = createGson() +) : DeleteIdentityEndpoint, QabelLog { + fun buildJsonRequest(identity: UpdateIdentity): String { + return gson.toJson(EncryptedApiRequest( + api = "delete-identity", + timestamp = System.currentTimeMillis() / 1000 + )) + } + + override fun buildRequest(identity: UpdateIdentity, serverPublicKey: QblECPublicKey): HttpUriRequest { + val json = buildJsonRequest(identity) + val uri = location.getUriBuilderForEndpoint("delete-identity").build() + val request = HttpPost(uri) + encryptJsonIntoRequest(json, identity.keyPair, serverPublicKey, request) + return request + } + + override fun parseResponse(jsonString: String, statusLine: StatusLine) { + } +} diff --git a/core/src/main/java/de/qabel/core/index/server/IdentityStatusEndpoint.kt b/core/src/main/java/de/qabel/core/index/server/IdentityStatusEndpoint.kt new file mode 100644 index 00000000..4b3ae20e --- /dev/null +++ b/core/src/main/java/de/qabel/core/index/server/IdentityStatusEndpoint.kt @@ -0,0 +1,10 @@ +package de.qabel.core.index.server + +import de.qabel.core.crypto.QblECPublicKey +import de.qabel.core.index.IdentityStatus +import de.qabel.core.index.UpdateIdentity +import org.apache.http.client.methods.HttpUriRequest + +internal interface IdentityStatusEndpoint : EndpointBase { + fun buildRequest(identity: UpdateIdentity, serverPublicKey: QblECPublicKey): HttpUriRequest +} diff --git a/core/src/main/java/de/qabel/core/index/server/IdentityStatusEndpointImpl.kt b/core/src/main/java/de/qabel/core/index/server/IdentityStatusEndpointImpl.kt new file mode 100644 index 00000000..bd3f236e --- /dev/null +++ b/core/src/main/java/de/qabel/core/index/server/IdentityStatusEndpointImpl.kt @@ -0,0 +1,44 @@ +package de.qabel.core.index.server + +import com.google.gson.Gson +import de.qabel.core.crypto.QblECPublicKey +import de.qabel.core.index.* +import de.qabel.core.logging.QabelLog +import org.apache.http.StatusLine +import org.apache.http.client.methods.HttpPost +import org.apache.http.client.methods.HttpUriRequest + + +internal class IdentityStatusEndpointImpl( + val location: IndexHTTPLocation, + val gson: Gson = createGson() +) : IdentityStatusEndpoint, QabelLog { + fun buildJsonRequest(identity: UpdateIdentity): String { + return gson.toJson(EncryptedApiRequest( + api = "status", + timestamp = System.currentTimeMillis() / 1000 + )) + } + + override fun buildRequest(identity: UpdateIdentity, serverPublicKey: QblECPublicKey): HttpUriRequest { + val json = buildJsonRequest(identity) + val uri = location.getUriBuilderForEndpoint("status").build() + val request = HttpPost(uri) + encryptJsonIntoRequest(json, identity.keyPair, serverPublicKey, request) + return request + } + + private data class IdentityStatusResponse( + val identity: IndexContact, + val entries: List + ) + + override fun parseResponse(jsonString: String, statusLine: StatusLine): IdentityStatus { + debug("Received identity status response: ${jsonString}") + val response = gson.fromJson(jsonString, IdentityStatusResponse::class.java) + return IdentityStatus( + identity = response.identity, + fieldStatus = response.entries + ) + } +} diff --git a/core/src/main/java/de/qabel/core/index/server/IndexHTTP.kt b/core/src/main/java/de/qabel/core/index/server/IndexHTTP.kt index f90c3cad..93b3c168 100644 --- a/core/src/main/java/de/qabel/core/index/server/IndexHTTP.kt +++ b/core/src/main/java/de/qabel/core/index/server/IndexHTTP.kt @@ -13,17 +13,75 @@ internal constructor ( private val key: ServerPublicKeyEndpoint = ServerPublicKeyEndpointImpl(location), private val search: SearchEndpoint = SearchEndpointImpl(location), private val update: UpdateEndpoint = UpdateEndpointImpl(location), + private val status: IdentityStatusEndpoint = IdentityStatusEndpointImpl(location), + private val deleteIdentity: DeleteIdentityEndpoint = DeleteIdentityEndpointImpl(location), private val verificationCode: VerificationCodeEndpoint = VerificationCodeEndpointImpl(location) ): IndexServer { constructor (location: IndexHTTPLocation, httpClient: CloseableHttpClient) : this(location, httpClient, ServerPublicKeyEndpointImpl(location)) + override fun search(manyAttributes: List): List { + val request = search.buildRequest(manyAttributes) + val response = httpClient.execute(request) + return search.handleResponse(response) + } + override fun search(attributes: Map): List { - val request = search.buildRequest(attributes) + val request = search.buildRequest(attributes.map { + Field(it.key, it.value) + }) val response = httpClient.execute(request) return search.handleResponse(response) } + override fun identityStatus(identity: UpdateIdentity): IdentityStatus { + return identityStatusWithRetries(identity) + } + + fun identityStatus(identity: UpdateIdentity, serverPublicKey: QblECPublicKey): IdentityStatus { + val request = status.buildRequest(identity, serverPublicKey) + val response = httpClient.execute(request) + return status.handleResponse(response) + } + + fun identityStatusWithRetries(identity: UpdateIdentity, retries: Int = 0): IdentityStatus { + val serverPublicKey = retrieveServerPublicKey() + try { + return identityStatus(identity, serverPublicKey) + } catch (e: APIError) { + e.retries = retries + if (e.code == HttpStatus.SC_BAD_REQUEST && retries < 2) { + // a bad request / 400 may be caused by an outdated server public key, retry two times (total tries = 3) + return identityStatusWithRetries(identity, retries + 1) + } + throw e + } + } + + override fun deleteIdentity(identity: UpdateIdentity) { + return deleteIdentityWithRetries(identity) + } + + fun deleteIdentity(identity: UpdateIdentity, serverPublicKey: QblECPublicKey) { + val request = deleteIdentity.buildRequest(identity, serverPublicKey) + val response = httpClient.execute(request) + return deleteIdentity.handleResponse(response) + } + + fun deleteIdentityWithRetries(identity: UpdateIdentity, retries: Int = 0) { + val serverPublicKey = retrieveServerPublicKey() + try { + return deleteIdentity(identity, serverPublicKey) + } catch (e: APIError) { + e.retries = retries + if (e.code == HttpStatus.SC_BAD_REQUEST && retries < 2) { + // a bad request / 400 may be caused by an outdated server public key, retry two times (total tries = 3) + return deleteIdentityWithRetries(identity, retries + 1) + } + throw e + } + } + override fun updateIdentity(identity: UpdateIdentity): UpdateResult { return updateIdentityWithRetries(identity) } diff --git a/core/src/main/java/de/qabel/core/index/server/IndexServer.kt b/core/src/main/java/de/qabel/core/index/server/IndexServer.kt index a6160d74..a0d87043 100644 --- a/core/src/main/java/de/qabel/core/index/server/IndexServer.kt +++ b/core/src/main/java/de/qabel/core/index/server/IndexServer.kt @@ -11,6 +11,15 @@ import java.io.IOException * [APIError] and [MalformedResponseException]. */ interface IndexServer { + /** + * Search for many attributes in one request. + * + * Returns a list of [IndexContact] instances where each [IndexContact.matches] list is a list of attributes + * from [manyAttributes] that matched it. If nothing is found, returns an empty list. + */ + @Throws(IOException::class) + fun search(manyAttributes: List): List + /** * SearchEndpoint for identities on the index server. * @@ -37,6 +46,18 @@ interface IndexServer { return search(mapOf(Pair(FieldType.PHONE, phone))) } + /** + * Fetch the [IdentityStatus] of [identity]. [identity.fields] are ignored. + */ + @Throws(IOException::class) + fun identityStatus(identity: UpdateIdentity): IdentityStatus + + /** + * Delete all data related to [identity] from the index. [identity.fields] are ignored. + */ + @Throws(IOException::class) + fun deleteIdentity(identity: UpdateIdentity) + /** * UpdateEndpoint published data of [identity] on the index. * diff --git a/core/src/main/java/de/qabel/core/index/server/SearchEndpoint.kt b/core/src/main/java/de/qabel/core/index/server/SearchEndpoint.kt index 2369a3d9..49156c7c 100644 --- a/core/src/main/java/de/qabel/core/index/server/SearchEndpoint.kt +++ b/core/src/main/java/de/qabel/core/index/server/SearchEndpoint.kt @@ -1,9 +1,9 @@ package de.qabel.core.index.server -import de.qabel.core.index.FieldType +import de.qabel.core.index.Field import de.qabel.core.index.IndexContact import org.apache.http.client.methods.HttpUriRequest internal interface SearchEndpoint : EndpointBase> { - fun buildRequest(attributes: Map): HttpUriRequest + fun buildRequest(manyAttributes: List): HttpUriRequest } diff --git a/core/src/main/java/de/qabel/core/index/server/SearchEndpointImpl.kt b/core/src/main/java/de/qabel/core/index/server/SearchEndpointImpl.kt index 526982eb..5e359928 100644 --- a/core/src/main/java/de/qabel/core/index/server/SearchEndpointImpl.kt +++ b/core/src/main/java/de/qabel/core/index/server/SearchEndpointImpl.kt @@ -3,13 +3,14 @@ package de.qabel.core.index.server import com.github.salomonbrys.kotson.* import com.google.gson.* import de.qabel.core.exceptions.QblDropInvalidURL -import de.qabel.core.index.FieldType -import de.qabel.core.index.IndexContact -import de.qabel.core.index.MalformedResponseException -import de.qabel.core.index.createGson +import de.qabel.core.index.* +import de.qabel.core.logging.QabelLog import org.apache.http.StatusLine import org.apache.http.client.methods.HttpGet +import org.apache.http.client.methods.HttpPost import org.apache.http.client.methods.HttpUriRequest +import org.apache.http.entity.ByteArrayEntity +import org.apache.http.entity.StringEntity import org.slf4j.Logger import org.slf4j.LoggerFactory import java.net.URISyntaxException @@ -19,21 +20,25 @@ import java.util.* internal class SearchEndpointImpl( private val location: IndexHTTPLocation, private val gson: Gson = createGson() -): SearchEndpoint { +): SearchEndpoint, QabelLog { private val logger: Logger by lazy { LoggerFactory.getLogger(SearchEndpointImpl::class.java) } - override fun buildRequest(attributes: Map): HttpUriRequest { - if (attributes.size == 0) { + private data class SearchRequest( + val query: List + ) + + override fun buildRequest(manyAttributes: List): HttpUriRequest { + if (manyAttributes.size == 0) { throw IllegalArgumentException("Need at least one attribute to search for.") } - val uriBuilder = location.getUriBuilderForEndpoint("search") - // query parameters are part of the URI(Builder) - for ((type, value) in attributes) { - uriBuilder.addParameter(type.name.toLowerCase(), value) - } - return HttpGet(uriBuilder.build()) + val uri = location.getUriBuilderForEndpoint("search").build() + val json = gson.toJson(SearchRequest(manyAttributes)) + val request = HttpPost(uri) + request.addHeader("Content-Type", "application/json") + request.entity = StringEntity(json) + return request } override fun parseResponse(jsonString: String, statusLine: StatusLine): List { @@ -48,10 +53,10 @@ internal class SearchEndpointImpl( else -> throw e } } - val identities = ArrayList() + val contacts = ArrayList() for (serializedIdentity in root) { try { - identities += gson.fromJson(serializedIdentity) + contacts += gson.fromJson(serializedIdentity) } catch (e: Throwable) { when (e) { is JsonSyntaxException, @@ -62,6 +67,7 @@ internal class SearchEndpointImpl( } } } - return identities + logger.debug("parsed response, returning ${contacts.size} out of ${root.size()} contacts") + return contacts } } diff --git a/core/src/main/java/de/qabel/core/index/server/UpdateEndpointImpl.kt b/core/src/main/java/de/qabel/core/index/server/UpdateEndpointImpl.kt index 3a1881a7..70edcd6e 100644 --- a/core/src/main/java/de/qabel/core/index/server/UpdateEndpointImpl.kt +++ b/core/src/main/java/de/qabel/core/index/server/UpdateEndpointImpl.kt @@ -29,19 +29,11 @@ internal class UpdateEndpointImpl( return gson.toJson(updateRequest) } - fun encryptJson(json: String, senderKeyPair: QblECKeyPair, serverPublicKey: QblECPublicKey): ByteArray { - val box = CryptoUtils().createBox(senderKeyPair, serverPublicKey, json.toByteArray(), 0) - return box - } - override fun buildRequest(identity: UpdateIdentity, serverPublicKey: QblECPublicKey): HttpUriRequest { val json = buildJsonUpdateRequest(identity) - val encryptedJson = encryptJson(json, identity.keyPair, serverPublicKey) - val uri = location.getUriBuilderForEndpoint("update").build() val request = HttpPut(uri) - request.addHeader("Content-Type", "application/vnd.qabel.noisebox+json") - request.entity = ByteArrayEntity(encryptedJson) + encryptJsonIntoRequest(json, identity.keyPair, serverPublicKey, request) return request } diff --git a/core/src/main/java/de/qabel/core/index/server/utils.kt b/core/src/main/java/de/qabel/core/index/server/utils.kt new file mode 100644 index 00000000..264d6f47 --- /dev/null +++ b/core/src/main/java/de/qabel/core/index/server/utils.kt @@ -0,0 +1,30 @@ +package de.qabel.core.index.server + +import de.qabel.core.crypto.CryptoUtils +import de.qabel.core.crypto.QblECKeyPair +import de.qabel.core.crypto.QblECPublicKey +import de.qabel.core.index.IndexContact +import de.qabel.core.index.UpdateField +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase +import org.apache.http.entity.ByteArrayEntity + +internal fun encryptJson(json: String, senderKeyPair: QblECKeyPair, serverPublicKey: QblECPublicKey): ByteArray { + val box = CryptoUtils().createBox(senderKeyPair, serverPublicKey, json.toByteArray(), 0) + return box +} + + +internal fun encryptJsonIntoRequest(json: String, + senderKeyPair: QblECKeyPair, + serverPublicKey: QblECPublicKey, + request: HttpEntityEnclosingRequestBase) { + val encryptedJson = encryptJson(json, senderKeyPair, serverPublicKey) + request.addHeader("Content-Type", "application/vnd.qabel.noisebox+json") + request.entity = ByteArrayEntity(encryptedJson) +} + + +internal data class EncryptedApiRequest( + val api: String, + val timestamp: Long +) diff --git a/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt b/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt index 301e964a..bc407a80 100644 --- a/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt +++ b/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt @@ -50,7 +50,7 @@ class IndexHTTPTest { val index = IndexHTTP(server, key = outdatedKey) val testParts = UpdateTestParts(index) testParts.publishTest() - assertEquals(2, outdatedKey.numCalls) + assertEquals(3, outdatedKey.numCalls) outdatedKey.numCalls = 0 testParts.unpublishTest() assertEquals(2, outdatedKey.numCalls) @@ -100,6 +100,18 @@ class IndexHTTPTest { assertEquals(updatedIdentity.alias, foundPublicIdentity.alias) } + @Test + fun testDeleteIdentity() { + val testParts = UpdateTestParts(index) + testParts.publishTest() + + val changedIdentity = testParts.identity.copy(alias = "1234", fields = listOf()) + index.deleteIdentity(changedIdentity) // only the key matters + + val search1Result = index.search(mapOf(Pair(FieldType.EMAIL, testParts.mail))) + assertEquals(0, search1Result.size) + } + private class UpdateTestParts(private val index: IndexHTTP) { val mail = getRandomMail() @@ -121,6 +133,14 @@ class IndexHTTPTest { /* Will be found */ searchForMailAndAssertOurs(mail) + + val status = index.identityStatus(identity) + assertIdentityEquals(identity, status.identity) + assertEquals(1, status.fieldStatus.size) + val field = status.fieldStatus[0] + assertEquals(EntryStatusEnum.CONFIRMED, field.status) + assertEquals(FieldType.EMAIL, field.field) + assertEquals(mail, field.value) } fun unpublishTest() { diff --git a/core/src/test/java/de/qabel/core/index/server/SearchEndpointTest.kt b/core/src/test/java/de/qabel/core/index/server/SearchEndpointTest.kt index 43b1d9cc..fafa6b81 100644 --- a/core/src/test/java/de/qabel/core/index/server/SearchEndpointTest.kt +++ b/core/src/test/java/de/qabel/core/index/server/SearchEndpointTest.kt @@ -2,10 +2,8 @@ package de.qabel.core.index.server import de.qabel.core.TestServer import de.qabel.core.extensions.assertThrows -import de.qabel.core.index.FieldType -import de.qabel.core.index.MalformedResponseException -import de.qabel.core.index.createGson -import de.qabel.core.index.dummyStatusLine +import de.qabel.core.index.* +import org.apache.http.client.methods.HttpPost import org.junit.Assert.assertEquals import org.junit.Test @@ -15,14 +13,19 @@ class SearchEndpointTest { @Test(expected = IllegalArgumentException::class) fun testBuildSearchRequestEmpty() { - search.buildRequest(mapOf()) + search.buildRequest(listOf()) } @Test fun testBuildSearchRequestSingle() { - val attributes = mapOf(Pair(FieldType.EMAIL, "test@example.net")) + val attributes = listOf(Field(FieldType.EMAIL, "test@example.net")) val request = search.buildRequest(attributes) - assertEquals("email=test@example.net", request.uri.query) + assertEquals(null, request.uri.query) + assertEquals("POST", request.method) + assertEquals("application/json", request.getFirstHeader("Content-Type").value) + val postRequest = request as HttpPost + val json = postRequest.entity.content.reader(Charsets.UTF_8).readText() + assertEquals("""{"query":[{"field":"email","value":"test@example.net"}]}""", json) } @Test @@ -101,7 +104,8 @@ class SearchEndpointTest { {"identities": [{ "public_key": "0f82b0018d1140a37b9cf3a4570bbdd415c8bbbcfc7efe7ef8aa912ce3760520", "drop_url": "http://example.net/somewhere/abcdefghijklmnopqrstuvwxyzabcdefghijklmnopo", - "alias": "1234" + "alias": "1234", + "matches": [] }]} """ val result = search.parseResponse(json, dummyStatusLine()) @@ -120,7 +124,8 @@ class SearchEndpointTest { "public_key": "0f82b0018d1140a37b9cf3a4570bbdd415c8bbbcfc7efe7ef8aa912ce3760520", "drop_url": "http://example.net/somewhere/abcdefghijklmnopqrstuvwxyzabcdefghijklmnopo", "alias": "1234", - "field_from_the_future": "I'm back" + "field_from_the_future": "I'm back", + "matches": [] }]} """ val result = search.parseResponse(json, dummyStatusLine()) From b2217bfcbcb3ce9e72d9cc522d90081785acaae8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 21 Nov 2016 12:27:17 +0100 Subject: [PATCH 4/5] index: IdentityStatus officially nullable --- core/src/main/java/de/qabel/core/index/models.kt | 2 +- .../test/java/de/qabel/core/index/server/IndexHTTPTest.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/de/qabel/core/index/models.kt b/core/src/main/java/de/qabel/core/index/models.kt index 81b779ab..a7053ea1 100644 --- a/core/src/main/java/de/qabel/core/index/models.kt +++ b/core/src/main/java/de/qabel/core/index/models.kt @@ -138,7 +138,7 @@ data class IdentityStatus( /** * Current identity data as returned by server (drop URL, alias, public key). */ - val identity: IndexContact, + val identity: IndexContact?, /** * A list of field statuses for every confirmed or unconfirmed (pending) entry associated with the * identity. diff --git a/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt b/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt index bc407a80..73f6e376 100644 --- a/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt +++ b/core/src/test/java/de/qabel/core/index/server/IndexHTTPTest.kt @@ -110,6 +110,9 @@ class IndexHTTPTest { val search1Result = index.search(mapOf(Pair(FieldType.EMAIL, testParts.mail))) assertEquals(0, search1Result.size) + + val status = index.identityStatus(testParts.identity) + assertEquals(null, status.identity) } private class UpdateTestParts(private val index: IndexHTTP) { @@ -135,7 +138,7 @@ class IndexHTTPTest { searchForMailAndAssertOurs(mail) val status = index.identityStatus(identity) - assertIdentityEquals(identity, status.identity) + assertIdentityEquals(identity, status.identity!!) assertEquals(1, status.fieldStatus.size) val field = status.fieldStatus[0] assertEquals(EntryStatusEnum.CONFIRMED, field.status) From fec53e7d9d8ba0c89c720af23bf7ed784936dea4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 21 Nov 2016 13:50:26 +0100 Subject: [PATCH 5/5] index http: deduplicat with-retries logic --- .../de/qabel/core/index/server/IndexHTTP.kt | 90 ++++++------------- 1 file changed, 28 insertions(+), 62 deletions(-) diff --git a/core/src/main/java/de/qabel/core/index/server/IndexHTTP.kt b/core/src/main/java/de/qabel/core/index/server/IndexHTTP.kt index 93b3c168..89dedb71 100644 --- a/core/src/main/java/de/qabel/core/index/server/IndexHTTP.kt +++ b/core/src/main/java/de/qabel/core/index/server/IndexHTTP.kt @@ -35,94 +35,60 @@ internal constructor ( } override fun identityStatus(identity: UpdateIdentity): IdentityStatus { - return identityStatusWithRetries(identity) - } - - fun identityStatus(identity: UpdateIdentity, serverPublicKey: QblECPublicKey): IdentityStatus { - val request = status.buildRequest(identity, serverPublicKey) - val response = httpClient.execute(request) - return status.handleResponse(response) + return requestWithRetries({ serverPublicKey -> + val request = status.buildRequest(identity, serverPublicKey) + val response = httpClient.execute(request) + status.handleResponse(response) + }) } - fun identityStatusWithRetries(identity: UpdateIdentity, retries: Int = 0): IdentityStatus { - val serverPublicKey = retrieveServerPublicKey() - try { - return identityStatus(identity, serverPublicKey) - } catch (e: APIError) { - e.retries = retries - if (e.code == HttpStatus.SC_BAD_REQUEST && retries < 2) { - // a bad request / 400 may be caused by an outdated server public key, retry two times (total tries = 3) - return identityStatusWithRetries(identity, retries + 1) - } - throw e - } + override fun deleteIdentity(identity: UpdateIdentity) { + return requestWithRetries({ serverPublicKey -> + val request = deleteIdentity.buildRequest(identity, serverPublicKey) + val response = httpClient.execute(request) + deleteIdentity.handleResponse(response) + }) } - override fun deleteIdentity(identity: UpdateIdentity) { - return deleteIdentityWithRetries(identity) + override fun updateIdentity(identity: UpdateIdentity): UpdateResult { + return requestWithRetries({ serverPublicKey -> + val request = update.buildRequest(identity, serverPublicKey) + val response = httpClient.execute(request) + update.handleResponse(response) + }) } - fun deleteIdentity(identity: UpdateIdentity, serverPublicKey: QblECPublicKey) { - val request = deleteIdentity.buildRequest(identity, serverPublicKey) - val response = httpClient.execute(request) - return deleteIdentity.handleResponse(response) + override fun confirmVerificationCode(code: String) { + doVerificationCodeRequest(code, confirm=true) } - fun deleteIdentityWithRetries(identity: UpdateIdentity, retries: Int = 0) { - val serverPublicKey = retrieveServerPublicKey() - try { - return deleteIdentity(identity, serverPublicKey) - } catch (e: APIError) { - e.retries = retries - if (e.code == HttpStatus.SC_BAD_REQUEST && retries < 2) { - // a bad request / 400 may be caused by an outdated server public key, retry two times (total tries = 3) - return deleteIdentityWithRetries(identity, retries + 1) - } - throw e - } + override fun denyVerificationCode(code: String) { + doVerificationCodeRequest(code, confirm=false) } - override fun updateIdentity(identity: UpdateIdentity): UpdateResult { - return updateIdentityWithRetries(identity) + internal fun doVerificationCodeRequest(code: String, confirm: Boolean) { + val request = verificationCode.buildRequest(code, confirm) + val response = httpClient.execute(request) + verificationCode.handleResponse(response) } - fun updateIdentityWithRetries(identity: UpdateIdentity, retries: Int = 0): UpdateResult { + fun requestWithRetries(body: (serverPublicKey: QblECPublicKey) -> T, retries: Int = 0): T { val serverPublicKey = retrieveServerPublicKey() try { - return updateIdentity(identity, serverPublicKey) + return body(serverPublicKey) } catch (e: APIError) { e.retries = retries if (e.code == HttpStatus.SC_BAD_REQUEST && retries < 2) { // a bad request / 400 may be caused by an outdated server public key, retry two times (total tries = 3) - return updateIdentityWithRetries(identity, retries + 1) + return requestWithRetries(body, retries + 1) } throw e } } - internal fun updateIdentity(identity: UpdateIdentity, serverPublicKey: QblECPublicKey): UpdateResult { - val request = update.buildRequest(identity, serverPublicKey) - val response = httpClient.execute(request) - return update.handleResponse(response) - } - internal fun retrieveServerPublicKey(): QblECPublicKey { val request = key.buildRequest() val response = httpClient.execute(request) return key.handleResponse(response) } - - override fun confirmVerificationCode(code: String) { - doVerificationCodeRequest(code, confirm=true) - } - - override fun denyVerificationCode(code: String) { - doVerificationCodeRequest(code, confirm=false) - } - - internal fun doVerificationCodeRequest(code: String, confirm: Boolean) { - val request = verificationCode.buildRequest(code, confirm) - val response = httpClient.execute(request) - verificationCode.handleResponse(response) - } }