Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 66 additions & 7 deletions core/src/main/java/de/qabel/core/index/models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Field> = listOf()
) {
fun toContact(): Contact {
return Contact(alias, listOf(dropUrl), publicKey)
Expand All @@ -40,7 +41,7 @@ data class UpdateIdentity(
val keyPair: QblECKeyPair,
val dropURL: DropURL,
val alias: String,
val fields: List<UpdateField>
val fields: List<UpdateField> = listOf()
) {

constructor(identity: Identity, fields: List<UpdateField>) :
Expand Down Expand Up @@ -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<EntryStatus>
)

/**
* Result of an update request issued by [IndexServer.publishIdentity] or [IndexServer.unpublishIdentity].
*/
Expand All @@ -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<Array<Field>>(obj["matches"], Array<Field>::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<IndexContact> {
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/
Expand All @@ -135,6 +193,7 @@ internal fun createGson(): Gson {
return GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter<IndexContact>(IndexContactDeserializer)
.registerTypeAdapter<IndexContact>(IndexContactSerializer)
.create()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Unit> {
fun buildRequest(identity: UpdateIdentity, serverPublicKey: QblECPublicKey): HttpUriRequest
}
Original file line number Diff line number Diff line change
@@ -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) {
}
}
Original file line number Diff line number Diff line change
@@ -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<IdentityStatus> {
fun buildRequest(identity: UpdateIdentity, serverPublicKey: QblECPublicKey): HttpUriRequest
}
Original file line number Diff line number Diff line change
@@ -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<EntryStatus>
)

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
)
}
}
72 changes: 48 additions & 24 deletions core/src/main/java/de/qabel/core/index/server/IndexHTTP.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,49 @@ 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(attributes: Map<FieldType, String>): List<IndexContact> {
val request = search.buildRequest(attributes)
override fun search(manyAttributes: List<Field>): List<IndexContact> {
val request = search.buildRequest(manyAttributes)
val response = httpClient.execute(request)
return search.handleResponse(response)
}

override fun updateIdentity(identity: UpdateIdentity): UpdateResult {
return updateIdentityWithRetries(identity)
override fun search(attributes: Map<FieldType, String>): List<IndexContact> {
val request = search.buildRequest(attributes.map {
Field(it.key, it.value)
})
val response = httpClient.execute(request)
return search.handleResponse(response)
}

fun updateIdentityWithRetries(identity: UpdateIdentity, retries: Int = 0): UpdateResult {
val serverPublicKey = retrieveServerPublicKey()
try {
return updateIdentity(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 updateIdentityWithRetries(identity, retries + 1)
}
throw e
}
override fun identityStatus(identity: UpdateIdentity): IdentityStatus {
return requestWithRetries({ serverPublicKey ->
val request = status.buildRequest(identity, serverPublicKey)
val response = httpClient.execute(request)
status.handleResponse(response)
})
}

internal fun updateIdentity(identity: UpdateIdentity, serverPublicKey: QblECPublicKey): UpdateResult {
val request = update.buildRequest(identity, serverPublicKey)
val response = httpClient.execute(request)
return update.handleResponse(response)
override fun deleteIdentity(identity: UpdateIdentity) {
return requestWithRetries({ serverPublicKey ->
val request = deleteIdentity.buildRequest(identity, serverPublicKey)
val response = httpClient.execute(request)
deleteIdentity.handleResponse(response)
})
}

internal fun retrieveServerPublicKey(): QblECPublicKey {
val request = key.buildRequest()
val response = httpClient.execute(request)
return key.handleResponse(response)
override fun updateIdentity(identity: UpdateIdentity): UpdateResult {
return requestWithRetries({ serverPublicKey ->
val request = update.buildRequest(identity, serverPublicKey)
val response = httpClient.execute(request)
update.handleResponse(response)
})
}

override fun confirmVerificationCode(code: String) {
Expand All @@ -67,4 +71,24 @@ internal constructor (
val response = httpClient.execute(request)
verificationCode.handleResponse(response)
}

fun <T> requestWithRetries(body: (serverPublicKey: QblECPublicKey) -> T, retries: Int = 0): T {
val serverPublicKey = retrieveServerPublicKey()
try {
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 requestWithRetries(body, retries + 1)
}
throw e
}
}

internal fun retrieveServerPublicKey(): QblECPublicKey {
val request = key.buildRequest()
val response = httpClient.execute(request)
return key.handleResponse(response)
}
}
24 changes: 24 additions & 0 deletions core/src/main/java/de/qabel/core/index/server/IndexServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Field>): List<IndexContact>

/**
* SearchEndpoint for identities on the index server.
*
Expand All @@ -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.
*
Expand All @@ -49,6 +70,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<List<IndexContact>> {
fun buildRequest(attributes: Map<FieldType, String>): HttpUriRequest
fun buildRequest(manyAttributes: List<Field>): HttpUriRequest
}
Loading