Add v4 onion request handling

This commit is contained in:
ceokot 2022-03-28 08:08:08 +02:00
parent 85456b5ea2
commit b51013f050
15 changed files with 388 additions and 184 deletions

View File

@ -8,6 +8,7 @@ import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsignal.utilities.toHexString
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SodiumUtilitiesTest { class SodiumUtilitiesTest {
@ -25,4 +26,27 @@ class SodiumUtilitiesTest {
assertThat(keyPair.publicKey.asHexString.lowercase(), equalTo(blindedKey)) 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))
}
} }

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context; 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 { public class PushMediaConstraints extends MediaConstraints {
@ -21,26 +21,26 @@ public class PushMediaConstraints extends MediaConstraints {
@Override @Override
public int getImageMaxSize(Context context) { public int getImageMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
} }
@Override @Override
public int getGifMaxSize(Context context) { public int getGifMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
} }
@Override @Override
public int getVideoMaxSize(Context context) { public int getVideoMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
} }
@Override @Override
public int getAudioMaxSize(Context context) { public int getAudioMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
} }
@Override @Override
public int getDocumentMaxSize(Context context) { public int getDocumentMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
} }
} }

View File

@ -43,7 +43,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body) val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) { 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 val code = json["code"] as? Int
if (code != null && code != 0) { if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, false) TextSecurePreferences.setIsUsingFCM(context, false)
@ -72,7 +72,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body) val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) { 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 val code = json["code"] as? Int
if (code != null && code != 0) { if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, true) TextSecurePreferences.setIsUsingFCM(context, true)
@ -100,7 +100,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body) val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) { 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 val code = json["code"] as? Int
if (code == null || code == 0) { if (code == null || code == 0) {
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.") Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")

View File

@ -13,7 +13,7 @@ import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
object FileServerAPIV2 { object FileServerApi {
private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
const val server = "http://filev2.getsession.org" const val server = "http://filev2.getsession.org"
@ -73,12 +73,12 @@ object FileServerAPIV2 {
HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!) HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!)
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters)) HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
} }
if (request.useOnionRouting) { return if (request.useOnionRouting) {
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).fail { e -> OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).fail { e ->
Log.e("Loki", "File server request failed.", e) Log.e("Loki", "File server request failed.", e)
} }
} else { } 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."))
} }
} }

View File

@ -6,9 +6,8 @@ import com.esotericsoftware.kryo.io.Output
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import okio.Buffer import okio.Buffer
import org.session.libsession.messaging.MessagingModuleConfiguration 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.messages.Message
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupApiV4 import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data 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) handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
} else { } else {
val keyAndResult = upload(attachment, FileServerAPIV2.server, true) { val keyAndResult = upload(attachment, FileServerApi.server, true) {
FileServerAPIV2.upload(it) FileServerApi.upload(it)
} }
handleSuccess(attachment, keyAndResult.first, keyAndResult.second) handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
} }

View File

@ -38,7 +38,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body) val request = Request.Builder().url(url).post(body)
retryIfNeeded(4) { 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 val code = json["code"] as? Int
if (code == null || code == 0) { if (code == null || code == 0) {
Log.d("Loki", "Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.") Log.d("Loki", "Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.")

View File

@ -20,7 +20,6 @@ import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.AESGCM import org.session.libsession.utilities.AESGCM
import org.session.libsession.utilities.TextSecurePreferences 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.decode
import org.session.libsignal.utilities.Base64.encodeBytes import org.session.libsignal.utilities.Base64.encodeBytes
import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.HTTP

View File

@ -38,7 +38,7 @@ object PushNotificationAPI {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body) val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) { 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 val code = json["code"] as? Int
if (code != null && code != 0) { if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, false) TextSecurePreferences.setIsUsingFCM(context, false)
@ -66,7 +66,7 @@ object PushNotificationAPI {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body) val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) { 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 val code = json["code"] as? Int
if (code != null && code != 0) { if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, true) TextSecurePreferences.setIsUsingFCM(context, true)
@ -93,7 +93,7 @@ object PushNotificationAPI {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body) val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) { 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 val code = json["code"] as? Int
if (code == null || code == 0) { if (code == null || code == 0) {
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.") Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")

View File

@ -47,8 +47,8 @@ object SodiumUtilities {
/* Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` */ /* 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? { fun blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair): KeyPair? {
// if (edKeyPair.publicKey.asBytes.size != Sign.PUBLICKEYBYTES || if (edKeyPair.publicKey.asBytes.size != Sign.PUBLICKEYBYTES ||
// edKeyPair.secretKey.asBytes.size != Sign.SECRETKEYBYTES) return null edKeyPair.secretKey.asBytes.size != Sign.SECRETKEYBYTES) return null
val kBytes = generateBlindingFactor(serverPublicKey) val kBytes = generateBlindingFactor(serverPublicKey)
val aBytes = generatePrivateKeyScalar(edKeyPair.secretKey.asBytes) val aBytes = generatePrivateKeyScalar(edKeyPair.secretKey.asBytes)
// Generate the blinded key pair `ka`, `kA` // Generate the blinded key pair `ka`, `kA`
@ -183,17 +183,17 @@ object SodiumUtilities {
} }
val hexString 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"); STANDARD("05"), BLINDED("15"), UN_BLINDED("00");
companion object { companion object {
fun fromValue(prefix: String): IdPrefix? = when(prefix) { fun fromValue(rawValue: String): IdPrefix? = when(rawValue) {
STANDARD.prefix -> STANDARD STANDARD.value -> STANDARD
BLINDED.prefix -> BLINDED BLINDED.value -> BLINDED
UN_BLINDED.prefix -> UN_BLINDED UN_BLINDED.value -> UN_BLINDED
else -> null else -> null
} }
} }

View File

@ -1,27 +1,58 @@
package org.session.libsession.snode package org.session.libsession.snode
import nl.komponents.kovenant.Deferred
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all import nl.komponents.kovenant.all
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.Request 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.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.AESGCM.EncryptionResult
import org.session.libsession.utilities.getBodyForOnionRequest import org.session.libsession.utilities.getBodyForOnionRequest
import org.session.libsession.utilities.getHeadersForOnionRequest import org.session.libsession.utilities.getHeadersForOnionRequest
import org.session.libsignal.crypto.getRandomElement import org.session.libsignal.crypto.getRandomElement
import org.session.libsignal.crypto.getRandomElementOrNull 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.Broadcaster
import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.HTTP
import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.JsonUtil
import java.util.* import org.session.libsignal.utilities.Log
import kotlin.math.abs 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<Snode> private typealias Path = List<Snode>
@ -96,7 +127,8 @@ object OnionRequestAPI {
ThreadUtils.queue { // No need to block the shared context for this ThreadUtils.queue { // No need to block the shared context for this
val url = "${snode.address}:${snode.port}/get_stats/v1" val url = "${snode.address}:${snode.port}/get_stats/v1"
try { 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 val version = json["version"] as? String
if (version == null) { deferred.reject(Exception("Missing snode version.")); return@queue } if (version == null) { deferred.reject(Exception("Missing snode version.")); return@queue }
if (version >= "2.0.7") { if (version >= "2.0.7") {
@ -209,29 +241,33 @@ object OnionRequestAPI {
} }
OnionRequestAPI.guardSnodes = guardSnodes OnionRequestAPI.guardSnodes = guardSnodes
fun getPath(paths: List<Path>): Path { fun getPath(paths: List<Path>): Path {
if (snodeToExclude != null) { return if (snodeToExclude != null) {
return paths.filter { !it.contains(snodeToExclude) }.getRandomElement() paths.filter { !it.contains(snodeToExclude) }.getRandomElement()
} else { } else {
return paths.getRandomElement() paths.getRandomElement()
} }
} }
if (paths.count() >= targetPathCount) { when {
paths.count() >= targetPathCount -> {
return Promise.of(getPath(paths)) return Promise.of(getPath(paths))
} else if (paths.isNotEmpty()) { }
if (paths.any { !it.contains(snodeToExclude) }) { paths.isNotEmpty() -> {
return if (paths.any { !it.contains(snodeToExclude) }) {
buildPaths(paths) // Re-build paths in the background buildPaths(paths) // Re-build paths in the background
return Promise.of(getPath(paths)) Promise.of(getPath(paths))
} else { } else {
return buildPaths(paths).map { newPaths -> buildPaths(paths).map { newPaths ->
getPath(newPaths) getPath(newPaths)
} }
} }
} else { }
else -> {
return buildPaths(listOf()).map { newPaths -> return buildPaths(listOf()).map { newPaths ->
getPath(newPaths) getPath(newPaths)
} }
} }
} }
}
private fun dropGuardSnode(snode: Snode) { private fun dropGuardSnode(snode: Snode) {
guardSnodes = guardSnodes.filter { it != snode }.toSet() guardSnodes = guardSnodes.filter { it != snode }.toSet()
@ -270,7 +306,11 @@ object OnionRequestAPI {
/** /**
* Builds an onion around `payload` and returns the result. * Builds an onion around `payload` and returns the result.
*/ */
private fun buildOnionForDestination(payload: Map<*, *>, destination: Destination): Promise<OnionBuildingResult, Exception> { private fun buildOnionForDestination(
payload: ByteArray,
destination: Destination,
version: Version
): Promise<OnionBuildingResult, Exception> {
lateinit var guardSnode: Snode lateinit var guardSnode: Snode
lateinit var destinationSymmetricKey: ByteArray // Needed by LokiAPI to decrypt the response sent back by the destination lateinit var destinationSymmetricKey: ByteArray // Needed by LokiAPI to decrypt the response sent back by the destination
lateinit var encryptionResult: EncryptionResult lateinit var encryptionResult: EncryptionResult
@ -281,19 +321,19 @@ object OnionRequestAPI {
return getPath(snodeToExclude).bind { path -> return getPath(snodeToExclude).bind { path ->
guardSnode = path.first() guardSnode = path.first()
// Encrypt in reverse order, i.e. the destination 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 destinationSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order) // Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r encryptionResult = r
@Suppress("NAME_SHADOWING") var path = path @Suppress("NAME_SHADOWING") var path = path
var rhs = destination var rhs = destination
fun addLayer(): Promise<EncryptionResult, Exception> { fun addLayer(): Promise<EncryptionResult, Exception> {
if (path.isEmpty()) { return if (path.isEmpty()) {
return Promise.of(encryptionResult) Promise.of(encryptionResult)
} else { } else {
val lhs = Destination.Snode(path.last()) val lhs = Destination.Snode(path.last())
path = path.dropLast(1) path = path.dropLast(1)
return OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind { r -> OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind { r ->
encryptionResult = r encryptionResult = r
rhs = lhs rhs = lhs
addLayer() addLayer()
@ -308,15 +348,15 @@ object OnionRequestAPI {
/** /**
* Sends an onion request to `destination`. Builds new paths as needed. * Sends an onion request to `destination`. Builds new paths as needed.
*/ */
private fun sendOnionRequest(destination: Destination, payload: Map<*, *>): Promise<Map<*, *>, Exception> { private fun sendOnionRequest(destination: Destination, payload: ByteArray, version: Version): Promise<Map<*, *>, Exception> {
val deferred = deferred<Map<*, *>, Exception>() val deferred = deferred<Map<*, *>, Exception>()
lateinit var guardSnode: Snode lateinit var guardSnode: Snode
buildOnionForDestination(payload, destination).success { result -> buildOnionForDestination(payload, destination, version).success { result ->
guardSnode = result.guardSnode guardSnode = result.guardSnode
val url = "${guardSnode.address}:${guardSnode.port}/onion_req/v2" val url = "${guardSnode.address}:${guardSnode.port}/onion_req/v2"
val finalEncryptionResult = result.finalEncryptionResult val finalEncryptionResult = result.finalEncryptionResult
val onion = finalEncryptionResult.ciphertext 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.") Log.d("Loki", "Approaching request size limit: ~${onion.count()} bytes.")
} }
@Suppress("NAME_SHADOWING") val parameters = mapOf( @Suppress("NAME_SHADOWING") val parameters = mapOf(
@ -331,49 +371,8 @@ object OnionRequestAPI {
val destinationSymmetricKey = result.destinationSymmetricKey val destinationSymmetricKey = result.destinationSymmetricKey
ThreadUtils.queue { ThreadUtils.queue {
try { try {
val json = HTTP.execute(HTTP.Verb.POST, url, body) val response = HTTP.execute(HTTP.Verb.POST, url, body)
val base64EncodedIVAndCiphertext = json["result"] as? String ?: return@queue deferred.reject(Exception("Invalid JSON")) handleResponse(response, destinationSymmetricKey, destination, version, deferred)
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)
}
} catch (exception: Exception) { } catch (exception: Exception) {
deferred.reject(exception) deferred.reject(exception)
} }
@ -440,12 +439,13 @@ object OnionRequestAPI {
/** /**
* Sends an onion request to `snode`. Builds new paths as needed. * 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<Map<*, *>, Exception> { internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, version: Version, publicKey: String? = null): Promise<Map<*, *>, Exception> {
val payload = mapOf( val payload = mapOf(
"method" to method.rawValue, "method" to method.rawValue,
"params" to parameters "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) { val error = when (exception) {
is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey) is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
is HTTPRequestFailedAtDestinationException -> 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. * `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<Map<*, *>, Exception> { fun sendOnionRequest(request: Request, server: String, x25519PublicKey: String, version: Version = Version.V4): Promise<Map<*, *>, Exception> {
val headers = request.getHeadersForOnionRequest() 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 url = request.url()
val urlAsString = url.toString() val urlAsString = url.toString()
val host = url.host() val body = request.getBodyForOnionRequest() ?: "null"
val endpoint = when { val endpoint = when {
server.count() < urlAsString.count() -> urlAsString.substringAfter(server).removePrefix("/") server.count() < urlAsString.count() -> urlAsString.substringAfter(server).removePrefix("/")
else -> "" else -> ""
} }
val body = request.getBodyForOnionRequest() ?: "null" 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( val payload = mapOf(
"body" to body, "body" to body,
"endpoint" to endpoint, "endpoint" to endpoint,
"method" to request.method(), "method" to request.method(),
"headers" to headers "headers" to headers
) )
val destination = Destination.Server(host, target, x25519PublicKey, url.scheme(), url.port()) JsonUtil.toJson(payload).toByteArray()
return sendOnionRequest(destination, payload).recover { exception -> }
Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.") }
throw exception
private fun handleResponse(
response: ByteArray,
destinationSymmetricKey: ByteArray,
destination: Destination,
version: Version,
deferred: Deferred<Map<*, *>, 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 // 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<String, String>
)
} }

View File

@ -2,6 +2,7 @@ package org.session.libsession.snode
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import org.session.libsession.snode.OnionRequestAPI.Destination
import org.session.libsession.utilities.AESGCM import org.session.libsession.utilities.AESGCM
import org.session.libsession.utilities.AESGCM.EncryptionResult import org.session.libsession.utilities.AESGCM.EncryptionResult
import org.session.libsignal.utilities.toHexString 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. * 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<EncryptionResult, Exception> { internal fun encryptPayloadForDestination(
payload: ByteArray,
destination: Destination,
version: OnionRequestAPI.Version
): Promise<EncryptionResult, Exception> {
val deferred = deferred<EncryptionResult, Exception>() val deferred = deferred<EncryptionResult, Exception>()
ThreadUtils.queue { ThreadUtils.queue {
try { try {
val plaintext = if (version == OnionRequestAPI.Version.V4) {
payload
} else {
// Wrapping isn't needed for file server or open group onion requests // Wrapping isn't needed for file server or open group onion requests
when (destination) { when (destination) {
is OnionRequestAPI.Destination.Snode -> { is Destination.Snode -> encode(payload, mapOf("headers" to ""))
val snodeX25519PublicKey = destination.snode.publicKeySet!!.x25519Key is Destination.Server -> payload
val payloadAsData = JsonUtil.toJson(payload).toByteArray() }
val plaintext = encode(payloadAsData, mapOf( "headers" to "" )) }
val result = AESGCM.encrypt(plaintext, snodeX25519PublicKey) 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) deferred.resolve(result)
}
is OnionRequestAPI.Destination.Server -> {
val plaintext = JsonUtil.toJson(payload).toByteArray()
val result = AESGCM.encrypt(plaintext, destination.x25519PublicKey)
deferred.resolve(result)
}
}
} catch (exception: Exception) { } catch (exception: Exception) {
deferred.reject(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. * 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<EncryptionResult, Exception> { internal fun encryptHop(lhs: Destination, rhs: Destination, previousEncryptionResult: EncryptionResult): Promise<EncryptionResult, Exception> {
val deferred = deferred<EncryptionResult, Exception>() val deferred = deferred<EncryptionResult, Exception>()
ThreadUtils.queue { ThreadUtils.queue {
try { try {
val payload: MutableMap<String, Any> val payload: MutableMap<String, Any> = when (rhs) {
when (rhs) { is Destination.Snode -> {
is OnionRequestAPI.Destination.Snode -> { mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key )
payload = mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key )
} }
is OnionRequestAPI.Destination.Server -> { is Destination.Server -> {
payload = mutableMapOf( mutableMapOf(
"host" to rhs.host, "host" to rhs.host,
"target" to rhs.target, "target" to rhs.target,
"method" to "POST", "method" to "POST",
@ -80,13 +84,12 @@ object OnionRequestEncryption {
} }
} }
payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
val x25519PublicKey: String val x25519PublicKey = when (lhs) {
when (lhs) { is Destination.Snode -> {
is OnionRequestAPI.Destination.Snode -> { lhs.snode.publicKeySet!!.x25519Key
x25519PublicKey = lhs.snode.publicKeySet!!.x25519Key
} }
is OnionRequestAPI.Destination.Server -> { is Destination.Server -> {
x25519PublicKey = lhs.x25519PublicKey lhs.x25519PublicKey
} }
} }
val plaintext = encode(previousEncryptionResult.ciphertext, payload) val plaintext = encode(previousEncryptionResult.ciphertext, payload)

View File

@ -74,16 +74,23 @@ object SnodeAPI {
} }
// Internal API // Internal API
internal fun invoke(method: Snode.Method, snode: Snode, publicKey: String? = null, parameters: Map<String, Any>): RawResponsePromise { internal fun invoke(
method: Snode.Method,
snode: Snode,
parameters: Map<String, Any>,
publicKey: String? = null,
version: OnionRequestAPI.Version = OnionRequestAPI.Version.V3
): RawResponsePromise {
val url = "${snode.address}:${snode.port}/storage_rpc/v1" val url = "${snode.address}:${snode.port}/storage_rpc/v1"
if (useOnionRequests) { if (useOnionRequests) {
return OnionRequestAPI.sendOnionRequest(method, parameters, snode, publicKey) return OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey)
} else { } else {
val deferred = deferred<Map<*, *>, Exception>() val deferred = deferred<Map<*, *>, Exception>()
ThreadUtils.queue { ThreadUtils.queue {
val payload = mapOf( "method" to method.rawValue, "params" to parameters ) val payload = mapOf( "method" to method.rawValue, "params" to parameters )
try { 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) deferred.resolve(json)
} catch (exception: Exception) { } catch (exception: Exception) {
val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException
@ -117,7 +124,12 @@ object SnodeAPI {
deferred<Snode, Exception>() deferred<Snode, Exception>()
ThreadUtils.queue { ThreadUtils.queue {
try { 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 intermediate = json["result"] as? Map<*, *>
val rawSnodes = intermediate?.get("service_node_states") as? List<*> val rawSnodes = intermediate?.get("service_node_states") as? List<*>
if (rawSnodes != null) { if (rawSnodes != null) {
@ -192,7 +204,7 @@ object SnodeAPI {
val promises = (1..validationCount).map { val promises = (1..validationCount).map {
getRandomSnode().bind { snode -> getRandomSnode().bind { snode ->
retryIfNeeded(maxRetryCount) { retryIfNeeded(maxRetryCount) {
invoke(Snode.Method.OxenDaemonRPCCall, snode, null, parameters) invoke(Snode.Method.OxenDaemonRPCCall, snode, parameters)
} }
} }
} }
@ -268,7 +280,7 @@ object SnodeAPI {
} else { } else {
val parameters = mapOf( "pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey ) val parameters = mapOf( "pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey )
return getRandomSnode().bind { return getRandomSnode().bind {
invoke(Snode.Method.GetSwarm, it, publicKey, parameters) invoke(Snode.Method.GetSwarm, it, parameters, publicKey)
}.map { }.map {
parseSnodes(it).toSet() parseSnodes(it).toSet()
}.success { }.success {
@ -299,7 +311,7 @@ object SnodeAPI {
// "pubkey_ed25519" to ed25519PublicKey, // "pubkey_ed25519" to ed25519PublicKey,
// "signature" to Base64.encodeBytes(signature) // "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 { fun getMessages(publicKey: String): MessageListPromise {
@ -311,7 +323,7 @@ object SnodeAPI {
} }
private fun getNetworkTime(snode: Snode): Promise<Pair<Snode,Long>, Exception> { private fun getNetworkTime(snode: Snode): Promise<Pair<Snode,Long>, 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 val timestamp = rawResponse["timestamp"] as? Long ?: -1
snode to timestamp snode to timestamp
} }
@ -323,7 +335,7 @@ object SnodeAPI {
getTargetSnodes(destination).map { swarm -> getTargetSnodes(destination).map { swarm ->
swarm.map { snode -> swarm.map { snode ->
val parameters = message.toJSON() val parameters = message.toJSON()
invoke(Snode.Method.SendMessage, snode, destination, parameters) invoke(Snode.Method.SendMessage, snode, parameters, destination)
}.toSet() }.toSet()
} }
} }
@ -345,7 +357,7 @@ object SnodeAPI {
"messages" to serverHashes, "messages" to serverHashes,
"signature" to Base64.encodeBytes(signature) "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<String, Any> ?: return@map mapOf() val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@map mapOf()
val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) -> val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) ->
val json = rawJSON as? Map<String, Any> ?: return@mapNotNull null val json = rawJSON as? Map<String, Any> ?: return@mapNotNull null
@ -415,7 +427,7 @@ object SnodeAPI {
"timestamp" to timestamp, "timestamp" to timestamp,
"signature" to Base64.encodeBytes(signature) "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) rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse)
}.fail { e -> }.fail { e ->
Log.e("Loki", "Failed to clear data", 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 400, 500, 502, 503 -> { // Usually indicates that the snode isn't up to date
handleBadSnode() handleBadSnode()
} }
425 -> { 406 -> {
Log.d("Loki", "The user's clock is out of sync with the service node network.") Log.d("Loki", "The user's clock is out of sync with the service node network.")
broadcaster.broadcast("clockOutOfSync") broadcaster.broadcast("clockOutOfSync")
return Error.ClockOutOfSync return Error.ClockOutOfSync

View File

@ -1,9 +1,8 @@
package org.session.libsession.utilities package org.session.libsession.utilities
import okhttp3.HttpUrl 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.utilities.Log
import org.session.libsignal.messages.SignalServiceAttachment
import java.io.* import java.io.*
object DownloadUtilities { object DownloadUtilities {
@ -37,7 +36,7 @@ object DownloadUtilities {
val url = HttpUrl.parse(urlAsString)!! val url = HttpUrl.parse(urlAsString)!!
val fileID = url.pathSegments().last() val fileID = url.pathSegments().last()
try { try {
FileServerAPIV2.download(fileID.toLong()).get().let { FileServerApi.download(fileID.toLong()).get().let {
outputStream.write(it) outputStream.write(it)
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -4,7 +4,7 @@ import android.content.Context
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import okio.Buffer 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.streams.ProfileCipherOutputStream
import org.session.libsignal.utilities.ProfileAvatarData import org.session.libsignal.utilities.ProfileAvatarData
import org.session.libsignal.streams.DigestingRequestBody import org.session.libsignal.streams.DigestingRequestBody
@ -30,13 +30,13 @@ object ProfilePictureUtilities {
var id: Long = 0 var id: Long = 0
try { try {
id = retryIfNeeded(4) { id = retryIfNeeded(4) {
FileServerAPIV2.upload(data) FileServerApi.upload(data)
}.get() }.get()
} catch (e: Exception) { } catch (e: Exception) {
deferred.reject(e) deferred.reject(e)
} }
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time) TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
val url = "${FileServerAPIV2.server}/files/$id" val url = "${FileServerApi.server}/files/$id"
TextSecurePreferences.setProfilePictureURL(context, url) TextSecurePreferences.setProfilePictureURL(context, url)
deferred.resolve(Unit) deferred.resolve(Unit)
} }

View File

@ -1,7 +1,10 @@
package org.session.libsignal.utilities package org.session.libsignal.utilities
import okhttp3.* import okhttp3.MediaType
import java.lang.IllegalStateException import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import java.security.SecureRandom import java.security.SecureRandom
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -68,26 +71,26 @@ object HTTP {
/** /**
* Sync. Don't call from the main thread. * 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) return execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
} }
/** /**
* Sync. Don't call from the main thread. * Sync. Don't call from the main thread.
*/ */
fun execute(verb: Verb, url: String, parameters: Map<String, Any>?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> { fun execute(verb: Verb, url: String, parameters: Map<String, Any>?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray {
if (parameters != null) { return if (parameters != null) {
val body = JsonUtil.toJson(parameters).toByteArray() 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 { } 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. * 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) val request = Request.Builder().url(url)
.removeHeader("User-Agent").addHeader("User-Agent", "WhatsApp") // Set a fake value .removeHeader("User-Agent").addHeader("User-Agent", "WhatsApp") // Set a fake value
.removeHeader("Accept-Language").addHeader("Accept-Language", "en-us") // 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 lateinit var response: Response
try { try {
val connection: OkHttpClient val connection = if (timeout != HTTP.timeout) { // Custom timeout
if (timeout != HTTP.timeout) { // Custom timeout
if (useSeedNodeConnection) { if (useSeedNodeConnection) {
throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.") throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.")
} }
connection = getDefaultConnection(timeout) getDefaultConnection(timeout)
} else { } else {
connection = if (useSeedNodeConnection) seedNodeConnection else defaultConnection if (useSeedNodeConnection) seedNodeConnection else defaultConnection
} }
response = connection.newCall(request.build()).execute() response = connection.newCall(request.build()).execute()
} catch (exception: Exception) { } catch (exception: Exception) {
@ -118,14 +120,9 @@ object HTTP {
// Override the actual error so that we can correctly catch failed requests in OnionRequestAPI // Override the actual error so that we can correctly catch failed requests in OnionRequestAPI
throw HTTPRequestFailedException(0, null) throw HTTPRequestFailedException(0, null)
} }
when (val statusCode = response.code()) { return when (val statusCode = response.code()) {
200 -> { 200 -> {
val bodyAsString = response.body()?.string() ?: throw Exception("An error occurred.") response.body()?.bytes() ?: throw Exception("An error occurred.")
try {
return JsonUtil.fromJson(bodyAsString, Map::class.java)
} catch (exception: Exception) {
return mapOf( "result" to bodyAsString)
}
} }
else -> { else -> {
Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $statusCode.") Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $statusCode.")