diff --git a/app/src/androidTest/java/network/loki/messenger/SodiumUtilitiesTest.kt b/app/src/androidTest/java/network/loki/messenger/SodiumUtilitiesTest.kt index f9f6c5e089..3479176d9d 100644 --- a/app/src/androidTest/java/network/loki/messenger/SodiumUtilitiesTest.kt +++ b/app/src/androidTest/java/network/loki/messenger/SodiumUtilitiesTest.kt @@ -8,6 +8,7 @@ import org.hamcrest.MatcherAssert.assertThat import org.junit.Test import org.junit.runner.RunWith import org.session.libsession.messaging.utilities.SodiumUtilities +import org.session.libsignal.utilities.toHexString @RunWith(AndroidJUnit4::class) class SodiumUtilitiesTest { @@ -25,4 +26,27 @@ class SodiumUtilitiesTest { assertThat(keyPair.publicKey.asHexString.lowercase(), equalTo(blindedKey)) } + @Test + fun sharedBlindedEncryptionKey() { + val key = ByteArray(0) + val encryptionKey = SodiumUtilities.sharedBlindedEncryptionKey(key, key, key, key) + } + + @Test + fun sogsSignature() { +// val expectedSignature = "K1N3A+H4dxV/wiN6Mr9cEj9TWUUqxESDoGW1cmoqDp7zMzCuCraTQKPX1tIiPuOBmFvB8VSUuYsHZrfGis1hDA==" +// val expectedSignature = "xxLpXHbomAJMB9AtGMyqvBsXrdd2040y+Ol/IKzElWfKJa3EYZRv1GLO6CTLhrDFUwVQe8PPltyGs54Kd7O5Cg==" + val expectedSignature = "gYqpWZX6fnF4Gb2xQM3xaXs0WIYEI49+B8q4mUUEg8Rw0ObaHUWfoWjMHMArAtP9QlORfiydsKWz1o6zdPVeCQ==" + val keyPair = SodiumUtilities.blindedKeyPair(serverPublicKey, KeyPair(pubKey, secKey))!! + + val signature = SodiumUtilities.sogsSignature( + ByteArray(0), + secKey.asBytes, + keyPair.secretKey.asBytes, + keyPair.publicKey.asBytes + )!! + + assertThat(signature.toHexString(), equalTo(expectedSignature)) + } + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java index f71050aba9..179c28bc3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; -import org.session.libsession.messaging.file_server.FileServerAPIV2; +import org.session.libsession.messaging.file_server.FileServerApi; public class PushMediaConstraints extends MediaConstraints { @@ -21,26 +21,26 @@ public class PushMediaConstraints extends MediaConstraints { @Override public int getImageMaxSize(Context context) { - return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); + return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); } @Override public int getGifMaxSize(Context context) { - return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); + return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); } @Override public int getVideoMaxSize(Context context) { - return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); + return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); } @Override public int getAudioMaxSize(Context context) { - return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); + return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); } @Override public int getDocumentMaxSize(Context context) { - return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); + return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt index 48a649dee3..6d135c6b1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt @@ -43,7 +43,7 @@ object LokiPushNotificationManager { val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body) retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json -> + OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, OnionRequestAPI.Version.V2).map { json -> val code = json["code"] as? Int if (code != null && code != 0) { TextSecurePreferences.setIsUsingFCM(context, false) @@ -72,7 +72,7 @@ object LokiPushNotificationManager { val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body) retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json -> + OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, OnionRequestAPI.Version.V2).map { json -> val code = json["code"] as? Int if (code != null && code != 0) { TextSecurePreferences.setIsUsingFCM(context, true) @@ -100,7 +100,7 @@ object LokiPushNotificationManager { val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body) retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json -> + OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, OnionRequestAPI.Version.V2).map { json -> val code = json["code"] as? Int if (code == null || code == 0) { Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.") diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt similarity index 93% rename from libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt rename to libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt index e2b14b5398..97794f9f68 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt @@ -13,7 +13,7 @@ import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log -object FileServerAPIV2 { +object FileServerApi { private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" const val server = "http://filev2.getsession.org" @@ -73,12 +73,12 @@ object FileServerAPIV2 { HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!) HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters)) } - if (request.useOnionRouting) { - return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).fail { e -> + return if (request.useOnionRouting) { + OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).fail { e -> Log.e("Loki", "File server request failed.", e) } } else { - return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests.")) + Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests.")) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index 431c9ec935..b39886cdb9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -6,9 +6,8 @@ import com.esotericsoftware.kryo.io.Output import nl.komponents.kovenant.Promise import okio.Buffer import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.file_server.FileServerAPIV2 +import org.session.libsession.messaging.file_server.FileServerApi import org.session.libsession.messaging.messages.Message -import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupApiV4 import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.Data @@ -58,8 +57,8 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess } handleSuccess(attachment, keyAndResult.first, keyAndResult.second) } else { - val keyAndResult = upload(attachment, FileServerAPIV2.server, true) { - FileServerAPIV2.upload(it) + val keyAndResult = upload(attachment, FileServerApi.server, true) { + FileServerApi.upload(it) } handleSuccess(attachment, keyAndResult.first, keyAndResult.second) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index 90262fc358..2b001cd7f4 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -38,7 +38,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body) retryIfNeeded(4) { - OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, "/loki/v2/lsrpc").map { json -> + OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, OnionRequestAPI.Version.V2).map { json -> val code = json["code"] as? Int if (code == null || code == 0) { Log.d("Loki", "Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.") diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApiV4.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApiV4.kt index 5a21cb44a8..d200837bd9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApiV4.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApiV4.kt @@ -20,7 +20,6 @@ import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.utilities.AESGCM import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64.decode import org.session.libsignal.utilities.Base64.encodeBytes import org.session.libsignal.utilities.HTTP diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index 64949e06e8..fa667382e4 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -38,7 +38,7 @@ object PushNotificationAPI { val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body) retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json -> + OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, OnionRequestAPI.Version.V2).map { json -> val code = json["code"] as? Int if (code != null && code != 0) { TextSecurePreferences.setIsUsingFCM(context, false) @@ -66,7 +66,7 @@ object PushNotificationAPI { val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body) retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json -> + OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, OnionRequestAPI.Version.V2).map { json -> val code = json["code"] as? Int if (code != null && code != 0) { TextSecurePreferences.setIsUsingFCM(context, true) @@ -93,7 +93,7 @@ object PushNotificationAPI { val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body) retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json -> + OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, OnionRequestAPI.Version.V2).map { json -> val code = json["code"] as? Int if (code == null || code == 0) { Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.") diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt index 6b41773bfb..e613602387 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt @@ -47,8 +47,8 @@ object SodiumUtilities { /* Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` */ fun blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair): KeyPair? { -// if (edKeyPair.publicKey.asBytes.size != Sign.PUBLICKEYBYTES || -// edKeyPair.secretKey.asBytes.size != Sign.SECRETKEYBYTES) return null + if (edKeyPair.publicKey.asBytes.size != Sign.PUBLICKEYBYTES || + edKeyPair.secretKey.asBytes.size != Sign.SECRETKEYBYTES) return null val kBytes = generateBlindingFactor(serverPublicKey) val aBytes = generatePrivateKeyScalar(edKeyPair.secretKey.asBytes) // Generate the blinded key pair `ka`, `kA` @@ -183,17 +183,17 @@ object SodiumUtilities { } val hexString - get() = prefix?.prefix + publicKey + get() = prefix?.value + publicKey } - enum class IdPrefix(val prefix: String) { + enum class IdPrefix(val value: String) { STANDARD("05"), BLINDED("15"), UN_BLINDED("00"); companion object { - fun fromValue(prefix: String): IdPrefix? = when(prefix) { - STANDARD.prefix -> STANDARD - BLINDED.prefix -> BLINDED - UN_BLINDED.prefix -> UN_BLINDED + fun fromValue(rawValue: String): IdPrefix? = when(rawValue) { + STANDARD.value -> STANDARD + BLINDED.value -> BLINDED + UN_BLINDED.value -> UN_BLINDED else -> null } } diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index 9dccbe4cd5..1c62743e22 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -1,27 +1,58 @@ package org.session.libsession.snode +import nl.komponents.kovenant.Deferred import nl.komponents.kovenant.Promise import nl.komponents.kovenant.all import nl.komponents.kovenant.deferred import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map import okhttp3.Request -import org.session.libsession.messaging.file_server.FileServerAPIV2 +import org.session.libsession.messaging.file_server.FileServerApi import org.session.libsession.utilities.AESGCM -import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.utilities.* -import org.session.libsignal.utilities.Snode import org.session.libsession.utilities.AESGCM.EncryptionResult import org.session.libsession.utilities.getBodyForOnionRequest import org.session.libsession.utilities.getHeadersForOnionRequest import org.session.libsignal.crypto.getRandomElement import org.session.libsignal.crypto.getRandomElementOrNull +import org.session.libsignal.database.LokiAPIDatabaseProtocol +import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Broadcaster import org.session.libsignal.utilities.HTTP -import org.session.libsignal.database.LokiAPIDatabaseProtocol -import java.util.* -import kotlin.math.abs +import org.session.libsignal.utilities.JsonUtil +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.Snode +import org.session.libsignal.utilities.ThreadUtils +import org.session.libsignal.utilities.recover +import org.session.libsignal.utilities.toHexString +import java.util.Date +import kotlin.collections.List +import kotlin.collections.Map +import kotlin.collections.Set +import kotlin.collections.any +import kotlin.collections.contains +import kotlin.collections.count +import kotlin.collections.dropLast +import kotlin.collections.filter +import kotlin.collections.first +import kotlin.collections.firstOrNull +import kotlin.collections.flatten +import kotlin.collections.forEach +import kotlin.collections.get +import kotlin.collections.indexOfFirst +import kotlin.collections.isNotEmpty +import kotlin.collections.last +import kotlin.collections.listOf +import kotlin.collections.map +import kotlin.collections.mapOf +import kotlin.collections.minus +import kotlin.collections.mutableMapOf +import kotlin.collections.mutableSetOf +import kotlin.collections.plus +import kotlin.collections.set +import kotlin.collections.setOf +import kotlin.collections.toMutableList +import kotlin.collections.toSet +import kotlin.collections.toString private typealias Path = List @@ -96,7 +127,8 @@ object OnionRequestAPI { ThreadUtils.queue { // No need to block the shared context for this val url = "${snode.address}:${snode.port}/get_stats/v1" try { - val json = HTTP.execute(HTTP.Verb.GET, url, 3) + val response = HTTP.execute(HTTP.Verb.GET, url, 3).decodeToString() + val json = JsonUtil.fromJson(response, Map::class.java) val version = json["version"] as? String if (version == null) { deferred.reject(Exception("Missing snode version.")); return@queue } if (version >= "2.0.7") { @@ -209,26 +241,30 @@ object OnionRequestAPI { } OnionRequestAPI.guardSnodes = guardSnodes fun getPath(paths: List): Path { - if (snodeToExclude != null) { - return paths.filter { !it.contains(snodeToExclude) }.getRandomElement() + return if (snodeToExclude != null) { + paths.filter { !it.contains(snodeToExclude) }.getRandomElement() } else { - return paths.getRandomElement() + paths.getRandomElement() } } - if (paths.count() >= targetPathCount) { - return Promise.of(getPath(paths)) - } else if (paths.isNotEmpty()) { - if (paths.any { !it.contains(snodeToExclude) }) { - buildPaths(paths) // Re-build paths in the background + when { + paths.count() >= targetPathCount -> { return Promise.of(getPath(paths)) - } else { - return buildPaths(paths).map { newPaths -> - getPath(newPaths) + } + paths.isNotEmpty() -> { + return if (paths.any { !it.contains(snodeToExclude) }) { + buildPaths(paths) // Re-build paths in the background + Promise.of(getPath(paths)) + } else { + buildPaths(paths).map { newPaths -> + getPath(newPaths) + } } } - } else { - return buildPaths(listOf()).map { newPaths -> - getPath(newPaths) + else -> { + return buildPaths(listOf()).map { newPaths -> + getPath(newPaths) + } } } } @@ -270,7 +306,11 @@ object OnionRequestAPI { /** * Builds an onion around `payload` and returns the result. */ - private fun buildOnionForDestination(payload: Map<*, *>, destination: Destination): Promise { + private fun buildOnionForDestination( + payload: ByteArray, + destination: Destination, + version: Version + ): Promise { lateinit var guardSnode: Snode lateinit var destinationSymmetricKey: ByteArray // Needed by LokiAPI to decrypt the response sent back by the destination lateinit var encryptionResult: EncryptionResult @@ -281,19 +321,19 @@ object OnionRequestAPI { return getPath(snodeToExclude).bind { path -> guardSnode = path.first() // Encrypt in reverse order, i.e. the destination first - OnionRequestEncryption.encryptPayloadForDestination(payload, destination).bind { r -> + OnionRequestEncryption.encryptPayloadForDestination(payload, destination, version).bind { r -> destinationSymmetricKey = r.symmetricKey // Recursively encrypt the layers of the onion (again in reverse order) encryptionResult = r @Suppress("NAME_SHADOWING") var path = path var rhs = destination fun addLayer(): Promise { - if (path.isEmpty()) { - return Promise.of(encryptionResult) + return if (path.isEmpty()) { + Promise.of(encryptionResult) } else { val lhs = Destination.Snode(path.last()) path = path.dropLast(1) - return OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind { r -> + OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind { r -> encryptionResult = r rhs = lhs addLayer() @@ -308,15 +348,15 @@ object OnionRequestAPI { /** * Sends an onion request to `destination`. Builds new paths as needed. */ - private fun sendOnionRequest(destination: Destination, payload: Map<*, *>): Promise, Exception> { + private fun sendOnionRequest(destination: Destination, payload: ByteArray, version: Version): Promise, Exception> { val deferred = deferred, Exception>() lateinit var guardSnode: Snode - buildOnionForDestination(payload, destination).success { result -> + buildOnionForDestination(payload, destination, version).success { result -> guardSnode = result.guardSnode val url = "${guardSnode.address}:${guardSnode.port}/onion_req/v2" val finalEncryptionResult = result.finalEncryptionResult val onion = finalEncryptionResult.ciphertext - if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerAPIV2.maxFileSize.toDouble()) { + if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerApi.maxFileSize.toDouble()) { Log.d("Loki", "Approaching request size limit: ~${onion.count()} bytes.") } @Suppress("NAME_SHADOWING") val parameters = mapOf( @@ -331,49 +371,8 @@ object OnionRequestAPI { val destinationSymmetricKey = result.destinationSymmetricKey ThreadUtils.queue { try { - val json = HTTP.execute(HTTP.Verb.POST, url, body) - val base64EncodedIVAndCiphertext = json["result"] as? String ?: return@queue deferred.reject(Exception("Invalid JSON")) - val ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext) - try { - val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey) - try { - @Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java) - val statusCode = json["status_code"] as? Int ?: json["status"] as Int - if (statusCode == 406) { - @Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." ) - val exception = HTTPRequestFailedAtDestinationException(statusCode, body, destination.description) - return@queue deferred.reject(exception) - } else if (json["body"] != null) { - @Suppress("NAME_SHADOWING") val body: Map<*, *> - if (json["body"] is Map<*, *>) { - body = json["body"] as Map<*, *> - } else { - val bodyAsString = json["body"] as String - body = JsonUtil.fromJson(bodyAsString, Map::class.java) - } - if (body["t"] != null) { - val timestamp = body["t"] as Long - val offset = timestamp - Date().time - SnodeAPI.clockOffset = offset - } - if (statusCode != 200) { - val exception = HTTPRequestFailedAtDestinationException(statusCode, body, destination.description) - return@queue deferred.reject(exception) - } - deferred.resolve(body) - } else { - if (statusCode != 200) { - val exception = HTTPRequestFailedAtDestinationException(statusCode, json, destination.description) - return@queue deferred.reject(exception) - } - deferred.resolve(json) - } - } catch (exception: Exception) { - deferred.reject(Exception("Invalid JSON: ${plaintext.toString(Charsets.UTF_8)}.")) - } - } catch (exception: Exception) { - deferred.reject(exception) - } + val response = HTTP.execute(HTTP.Verb.POST, url, body) + handleResponse(response, destinationSymmetricKey, destination, version, deferred) } catch (exception: Exception) { deferred.reject(exception) } @@ -440,12 +439,13 @@ object OnionRequestAPI { /** * Sends an onion request to `snode`. Builds new paths as needed. */ - internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, publicKey: String? = null): Promise, Exception> { + internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, version: Version, publicKey: String? = null): Promise, Exception> { val payload = mapOf( "method" to method.rawValue, "params" to parameters ) - return sendOnionRequest(Destination.Snode(snode), payload).recover { exception -> + val payloadData = JsonUtil.toJson(payload).toByteArray() + return sendOnionRequest(Destination.Snode(snode), payloadData, version).recover { exception -> val error = when (exception) { is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey) is HTTPRequestFailedAtDestinationException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey) @@ -461,27 +461,198 @@ object OnionRequestAPI { * * `publicKey` is the hex encoded public key of the user the call is associated with. This is needed for swarm cache maintenance. */ - fun sendOnionRequest(request: Request, server: String, x25519PublicKey: String, target: String = "/loki/v3/lsrpc"): Promise, Exception> { - val headers = request.getHeadersForOnionRequest() + fun sendOnionRequest(request: Request, server: String, x25519PublicKey: String, version: Version = Version.V4): Promise, Exception> { + val url = request.url() + val payload = generatePayload(request, server, version) + val destination = Destination.Server(url.host(), version.value, x25519PublicKey, url.scheme(), url.port()) + return sendOnionRequest(destination, payload, version).recover { exception -> + Log.d("Loki", "Couldn't reach server: $url due to error: $exception.") + throw exception + } + } + + private fun generatePayload(request: Request, server: String, version: Version): ByteArray { + val headers = request.getHeadersForOnionRequest().toMutableMap() val url = request.url() val urlAsString = url.toString() - val host = url.host() + val body = request.getBodyForOnionRequest() ?: "null" val endpoint = when { server.count() < urlAsString.count() -> urlAsString.substringAfter(server).removePrefix("/") else -> "" } - val body = request.getBodyForOnionRequest() ?: "null" - val payload = mapOf( - "body" to body, - "endpoint" to endpoint, - "method" to request.method(), - "headers" to headers - ) - val destination = Destination.Server(host, target, x25519PublicKey, url.scheme(), url.port()) - return sendOnionRequest(destination, payload).recover { exception -> - Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.") - throw exception + return if (version == Version.V4) { + if (request.body() != null && !headers.containsKey("Content-Type")) { + headers["Content-Type"] = "application/json" + } + val requestPayload = mapOf( + "endpoint" to endpoint, + "method" to request.method(), + "headers" to headers + ) + val requestData = JsonUtil.toJson(requestPayload).toByteArray() + val prefixData = "l${requestData.size}".toByteArray() + val suffixData = "e".toByteArray() + if (request.body() != null) { + val bodyPayload = mapOf( + "body" to body + ) + val bodyData = JsonUtil.toJson(bodyPayload).toByteArray() + val bodyLengthData = "${bodyData.size}".toByteArray() + prefixData + requestData + bodyLengthData + bodyData + suffixData + } else { + prefixData + requestData + suffixData + } + } else { + val payload = mapOf( + "body" to body, + "endpoint" to endpoint, + "method" to request.method(), + "headers" to headers + ) + JsonUtil.toJson(payload).toByteArray() + } + } + + private fun handleResponse( + response: ByteArray, + destinationSymmetricKey: ByteArray, + destination: Destination, + version: Version, + deferred: Deferred, Exception> + ) { + if (version == Version.V4) { + try { + if (response.size <= AESGCM.ivSize) return deferred.reject(Exception("Invalid response")) + // The data will be in the form of `l123:jsone` or `l123:json456:bodye` so we need to break the data into + // parts to properly process it + val plaintext = AESGCM.decrypt(response, destinationSymmetricKey) + val plaintextString = plaintext.decodeToString() + if (!plaintextString.startsWith("l")) return deferred.reject(Exception("Invalid response")) + val infoParts = plaintextString.split(":") + val infoLength = infoParts.firstOrNull()?.drop(1)?.toIntOrNull() + if (infoParts.size <= 1 || infoLength == null) return deferred.reject(Exception("Invalid response")) + val infoStartIndex = "l$infoLength".length + 1 + val infoEndIndex = infoStartIndex + infoLength + val info = plaintextString.substring(infoStartIndex, infoEndIndex) + val responseInfo = JsonUtil.fromJson(info, Map::class.java) + when (val statusCode = responseInfo["code"].toString().toInt()) { + // Custom handle a clock out of sync error (v4 returns '425' but included the '406' just in case) + 406, 425 -> { + @Suppress("NAME_SHADOWING") + val body = + mapOf("result" to "Your clock is out of sync with the service node network.") + val exception = HTTPRequestFailedAtDestinationException( + statusCode, + body, + destination.description + ) + return deferred.reject(exception) + } + // Handle error status codes + !in 200..299 -> { + val exception = HTTPRequestFailedAtDestinationException( + statusCode, + responseInfo, + destination.description + ) + return deferred.reject(exception) + } + } + + // If there is no data in the response then just return the ResponseInfo + if (info.length < "l${infoLength}${info}e".length) { + return deferred.resolve(JsonUtil.fromJson(info, Map::class.java)) + } + // Extract the response data as well + val data = plaintextString.substring(infoEndIndex) + val dataParts = data.split(":") + val dataLength = dataParts.firstOrNull()?.length + if (dataParts.size <= 1 || dataLength == null) return deferred.reject(Exception("Invalid JSON")) + val dataString = dataParts.last().dropLast(1) + return deferred.resolve(JsonUtil.fromJson(dataString, Map::class.java)) + } catch (exception: Exception) { + deferred.reject(exception) + } + } else { + val bodyAsString = response.decodeToString() + val json = try { + JsonUtil.fromJson(bodyAsString, Map::class.java) + } catch (exception: Exception) { + mapOf( "result" to bodyAsString) + } + val base64EncodedIVAndCiphertext = json["result"] as? String ?: return deferred.reject(Exception("Invalid JSON")) + val ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext) + try { + val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey) + try { + @Suppress("NAME_SHADOWING") val json = + JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java) + val statusCode = json["status_code"] as? Int ?: json["status"] as Int + when { + statusCode == 406 -> { + @Suppress("NAME_SHADOWING") + val body = + mapOf("result" to "Your clock is out of sync with the service node network.") + val exception = HTTPRequestFailedAtDestinationException( + statusCode, + body, + destination.description + ) + return deferred.reject(exception) + } + json["body"] != null -> { + @Suppress("NAME_SHADOWING") + val body = if (json["body"] is Map<*, *>) { + json["body"] as Map<*, *> + } else { + val bodyAsString = json["body"] as String + JsonUtil.fromJson(bodyAsString, Map::class.java) + } + if (body["t"] != null) { + val timestamp = body["t"] as Long + val offset = timestamp - Date().time + SnodeAPI.clockOffset = offset + } + if (statusCode != 200) { + val exception = HTTPRequestFailedAtDestinationException( + statusCode, + body, + destination.description + ) + return deferred.reject(exception) + } + deferred.resolve(body) + } + else -> { + if (statusCode != 200) { + val exception = HTTPRequestFailedAtDestinationException( + statusCode, + json, + destination.description + ) + return deferred.reject(exception) + } + deferred.resolve(json) + } + } + } catch (exception: Exception) { + deferred.reject(Exception("Invalid JSON: ${plaintext.toString(Charsets.UTF_8)}.")) + } + } catch (exception: Exception) { + deferred.reject(exception) + } } } // endregion + + enum class Version(val value: String) { + V2("/loki/v2/lsrpc"), + V3("/loki/v3/lsrpc"), + V4("/oxen/v4/lsrpc"); + } + + data class ResponseInfo( + val code: String, + val headers: Map + ) } diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestEncryption.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestEncryption.kt index cd9ac4d1d8..109371f31c 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestEncryption.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestEncryption.kt @@ -2,6 +2,7 @@ package org.session.libsession.snode import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred +import org.session.libsession.snode.OnionRequestAPI.Destination import org.session.libsession.utilities.AESGCM import org.session.libsession.utilities.AESGCM.EncryptionResult import org.session.libsignal.utilities.toHexString @@ -31,25 +32,29 @@ object OnionRequestEncryption { /** * Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request. */ - internal fun encryptPayloadForDestination(payload: Map<*, *>, destination: OnionRequestAPI.Destination): Promise { + internal fun encryptPayloadForDestination( + payload: ByteArray, + destination: Destination, + version: OnionRequestAPI.Version + ): Promise { val deferred = deferred() ThreadUtils.queue { try { - // Wrapping isn't needed for file server or open group onion requests - when (destination) { - is OnionRequestAPI.Destination.Snode -> { - val snodeX25519PublicKey = destination.snode.publicKeySet!!.x25519Key - val payloadAsData = JsonUtil.toJson(payload).toByteArray() - val plaintext = encode(payloadAsData, mapOf( "headers" to "" )) - val result = AESGCM.encrypt(plaintext, snodeX25519PublicKey) - deferred.resolve(result) - } - is OnionRequestAPI.Destination.Server -> { - val plaintext = JsonUtil.toJson(payload).toByteArray() - val result = AESGCM.encrypt(plaintext, destination.x25519PublicKey) - deferred.resolve(result) + val plaintext = if (version == OnionRequestAPI.Version.V4) { + payload + } else { + // Wrapping isn't needed for file server or open group onion requests + when (destination) { + is Destination.Snode -> encode(payload, mapOf("headers" to "")) + is Destination.Server -> payload } } + val x25519PublicKey = when (destination) { + is Destination.Snode -> destination.snode.publicKeySet!!.x25519Key + is Destination.Server -> destination.x25519PublicKey + } + val result = AESGCM.encrypt(plaintext, x25519PublicKey) + deferred.resolve(result) } catch (exception: Exception) { deferred.reject(exception) } @@ -60,17 +65,16 @@ object OnionRequestEncryption { /** * Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request. */ - internal fun encryptHop(lhs: OnionRequestAPI.Destination, rhs: OnionRequestAPI.Destination, previousEncryptionResult: EncryptionResult): Promise { + internal fun encryptHop(lhs: Destination, rhs: Destination, previousEncryptionResult: EncryptionResult): Promise { val deferred = deferred() ThreadUtils.queue { try { - val payload: MutableMap - when (rhs) { - is OnionRequestAPI.Destination.Snode -> { - payload = mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key ) + val payload: MutableMap = when (rhs) { + is Destination.Snode -> { + mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key ) } - is OnionRequestAPI.Destination.Server -> { - payload = mutableMapOf( + is Destination.Server -> { + mutableMapOf( "host" to rhs.host, "target" to rhs.target, "method" to "POST", @@ -80,13 +84,12 @@ object OnionRequestEncryption { } } payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() - val x25519PublicKey: String - when (lhs) { - is OnionRequestAPI.Destination.Snode -> { - x25519PublicKey = lhs.snode.publicKeySet!!.x25519Key + val x25519PublicKey = when (lhs) { + is Destination.Snode -> { + lhs.snode.publicKeySet!!.x25519Key } - is OnionRequestAPI.Destination.Server -> { - x25519PublicKey = lhs.x25519PublicKey + is Destination.Server -> { + lhs.x25519PublicKey } } val plaintext = encode(previousEncryptionResult.ciphertext, payload) diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index 5c12be21ae..8496c2aecb 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -74,16 +74,23 @@ object SnodeAPI { } // Internal API - internal fun invoke(method: Snode.Method, snode: Snode, publicKey: String? = null, parameters: Map): RawResponsePromise { + internal fun invoke( + method: Snode.Method, + snode: Snode, + parameters: Map, + publicKey: String? = null, + version: OnionRequestAPI.Version = OnionRequestAPI.Version.V3 + ): RawResponsePromise { val url = "${snode.address}:${snode.port}/storage_rpc/v1" if (useOnionRequests) { - return OnionRequestAPI.sendOnionRequest(method, parameters, snode, publicKey) + return OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey) } else { val deferred = deferred, Exception>() ThreadUtils.queue { val payload = mapOf( "method" to method.rawValue, "params" to parameters ) try { - val json = HTTP.execute(HTTP.Verb.POST, url, payload) + val response = HTTP.execute(HTTP.Verb.POST, url, payload).toString() + val json = JsonUtil.fromJson(response, Map::class.java) deferred.resolve(json) } catch (exception: Exception) { val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException @@ -117,7 +124,12 @@ object SnodeAPI { deferred() ThreadUtils.queue { try { - val json = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true) + val response = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true).toString() + val json = try { + JsonUtil.fromJson(response, Map::class.java) + } catch (exception: Exception) { + mapOf( "result" to response) + } val intermediate = json["result"] as? Map<*, *> val rawSnodes = intermediate?.get("service_node_states") as? List<*> if (rawSnodes != null) { @@ -192,7 +204,7 @@ object SnodeAPI { val promises = (1..validationCount).map { getRandomSnode().bind { snode -> retryIfNeeded(maxRetryCount) { - invoke(Snode.Method.OxenDaemonRPCCall, snode, null, parameters) + invoke(Snode.Method.OxenDaemonRPCCall, snode, parameters) } } } @@ -268,7 +280,7 @@ object SnodeAPI { } else { val parameters = mapOf( "pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey ) return getRandomSnode().bind { - invoke(Snode.Method.GetSwarm, it, publicKey, parameters) + invoke(Snode.Method.GetSwarm, it, parameters, publicKey) }.map { parseSnodes(it).toSet() }.success { @@ -299,7 +311,7 @@ object SnodeAPI { // "pubkey_ed25519" to ed25519PublicKey, // "signature" to Base64.encodeBytes(signature) ) - return invoke(Snode.Method.GetMessages, snode, publicKey, parameters) + return invoke(Snode.Method.GetMessages, snode, parameters, publicKey) } fun getMessages(publicKey: String): MessageListPromise { @@ -311,7 +323,7 @@ object SnodeAPI { } private fun getNetworkTime(snode: Snode): Promise, Exception> { - return invoke(Snode.Method.Info, snode, null, emptyMap()).map { rawResponse -> + return invoke(Snode.Method.Info, snode, emptyMap()).map { rawResponse -> val timestamp = rawResponse["timestamp"] as? Long ?: -1 snode to timestamp } @@ -323,7 +335,7 @@ object SnodeAPI { getTargetSnodes(destination).map { swarm -> swarm.map { snode -> val parameters = message.toJSON() - invoke(Snode.Method.SendMessage, snode, destination, parameters) + invoke(Snode.Method.SendMessage, snode, parameters, destination) }.toSet() } } @@ -345,7 +357,7 @@ object SnodeAPI { "messages" to serverHashes, "signature" to Base64.encodeBytes(signature) ) - invoke(Snode.Method.DeleteMessage, snode, publicKey, deleteMessageParams).map { rawResponse -> + invoke(Snode.Method.DeleteMessage, snode, deleteMessageParams, publicKey).map { rawResponse -> val swarms = rawResponse["swarm"] as? Map ?: return@map mapOf() val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) -> val json = rawJSON as? Map ?: return@mapNotNull null @@ -415,7 +427,7 @@ object SnodeAPI { "timestamp" to timestamp, "signature" to Base64.encodeBytes(signature) ) - invoke(Snode.Method.DeleteAll, snode, userPublicKey, deleteMessageParams).map { + invoke(Snode.Method.DeleteAll, snode, deleteMessageParams, userPublicKey).map { rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse) }.fail { e -> Log.e("Loki", "Failed to clear data", e) @@ -530,7 +542,7 @@ object SnodeAPI { 400, 500, 502, 503 -> { // Usually indicates that the snode isn't up to date handleBadSnode() } - 425 -> { + 406 -> { Log.d("Loki", "The user's clock is out of sync with the service node network.") broadcaster.broadcast("clockOutOfSync") return Error.ClockOutOfSync diff --git a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt index 8ccb659dbb..68fb53d381 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt @@ -1,9 +1,8 @@ package org.session.libsession.utilities import okhttp3.HttpUrl -import org.session.libsession.messaging.file_server.FileServerAPIV2 +import org.session.libsession.messaging.file_server.FileServerApi import org.session.libsignal.utilities.Log -import org.session.libsignal.messages.SignalServiceAttachment import java.io.* object DownloadUtilities { @@ -37,7 +36,7 @@ object DownloadUtilities { val url = HttpUrl.parse(urlAsString)!! val fileID = url.pathSegments().last() try { - FileServerAPIV2.download(fileID.toLong()).get().let { + FileServerApi.download(fileID.toLong()).get().let { outputStream.write(it) } } catch (e: Exception) { diff --git a/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt index 47223c8096..fb5af4b12c 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt @@ -4,7 +4,7 @@ import android.content.Context import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred import okio.Buffer -import org.session.libsession.messaging.file_server.FileServerAPIV2 +import org.session.libsession.messaging.file_server.FileServerApi import org.session.libsignal.streams.ProfileCipherOutputStream import org.session.libsignal.utilities.ProfileAvatarData import org.session.libsignal.streams.DigestingRequestBody @@ -30,13 +30,13 @@ object ProfilePictureUtilities { var id: Long = 0 try { id = retryIfNeeded(4) { - FileServerAPIV2.upload(data) + FileServerApi.upload(data) }.get() } catch (e: Exception) { deferred.reject(e) } TextSecurePreferences.setLastProfilePictureUpload(context, Date().time) - val url = "${FileServerAPIV2.server}/files/$id" + val url = "${FileServerApi.server}/files/$id" TextSecurePreferences.setProfilePictureURL(context, url) deferred.resolve(Unit) } diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt b/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt index 8937bee708..a282f8a092 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt @@ -1,7 +1,10 @@ package org.session.libsignal.utilities -import okhttp3.* -import java.lang.IllegalStateException +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response import java.security.SecureRandom import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit @@ -68,26 +71,26 @@ object HTTP { /** * Sync. Don't call from the main thread. */ - fun execute(verb: Verb, url: String, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> { + fun execute(verb: Verb, url: String, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray { return execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection) } /** * Sync. Don't call from the main thread. */ - fun execute(verb: Verb, url: String, parameters: Map?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> { - if (parameters != null) { + fun execute(verb: Verb, url: String, parameters: Map?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray { + return if (parameters != null) { val body = JsonUtil.toJson(parameters).toByteArray() - return execute(verb = verb, url = url, body = body, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection) + execute(verb = verb, url = url, body = body, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection) } else { - return execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection) + execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection) } } /** * Sync. Don't call from the main thread. */ - fun execute(verb: Verb, url: String, body: ByteArray?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> { + fun execute(verb: Verb, url: String, body: ByteArray?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray { val request = Request.Builder().url(url) .removeHeader("User-Agent").addHeader("User-Agent", "WhatsApp") // Set a fake value .removeHeader("Accept-Language").addHeader("Accept-Language", "en-us") // Set a fake value @@ -103,14 +106,13 @@ object HTTP { } lateinit var response: Response try { - val connection: OkHttpClient - if (timeout != HTTP.timeout) { // Custom timeout + val connection = if (timeout != HTTP.timeout) { // Custom timeout if (useSeedNodeConnection) { throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.") } - connection = getDefaultConnection(timeout) + getDefaultConnection(timeout) } else { - connection = if (useSeedNodeConnection) seedNodeConnection else defaultConnection + if (useSeedNodeConnection) seedNodeConnection else defaultConnection } response = connection.newCall(request.build()).execute() } catch (exception: Exception) { @@ -118,14 +120,9 @@ object HTTP { // Override the actual error so that we can correctly catch failed requests in OnionRequestAPI throw HTTPRequestFailedException(0, null) } - when (val statusCode = response.code()) { + return when (val statusCode = response.code()) { 200 -> { - val bodyAsString = response.body()?.string() ?: throw Exception("An error occurred.") - try { - return JsonUtil.fromJson(bodyAsString, Map::class.java) - } catch (exception: Exception) { - return mapOf( "result" to bodyAsString) - } + response.body()?.bytes() ?: throw Exception("An error occurred.") } else -> { Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $statusCode.")