Update onion response parsing

This commit is contained in:
ceokot 2022-04-04 09:12:07 +02:00
parent 95fd2baec0
commit 9581a75268
11 changed files with 117 additions and 184 deletions

View File

@ -66,8 +66,6 @@ object OpenGroupManager {
storage.removeLastMessageServerID(room, server) storage.removeLastMessageServerID(room, server)
// Store the public key // Store the public key
storage.setOpenGroupPublicKey(server,publicKey) storage.setOpenGroupPublicKey(server,publicKey)
// Get an auth token
OpenGroupApi.getAuthToken(room, server).get()
// Get capabilities // Get capabilities
val capabilities = OpenGroupApi.getCapabilities(room, server).get() val capabilities = OpenGroupApi.getCapabilities(room, server).get()
// Get group info // Get group info

View File

@ -1,12 +1,14 @@
package org.thoughtcrime.securesms.notifications package org.thoughtcrime.securesms.notifications
import android.content.Context import android.content.Context
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.Version
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -43,7 +45,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, OnionRequestAPI.Version.V2).map { json -> getResponseBody(request.build()).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 +74,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, OnionRequestAPI.Version.V2).map { json -> getResponseBody(request.build()).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 +102,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, OnionRequestAPI.Version.V2).map { json -> getResponseBody(request.build()).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"}.")
@ -110,4 +112,10 @@ object LokiPushNotificationManager {
} }
} }
} }
private fun getResponseBody(request: Request): Promise<Map<*, *>, Exception> {
return OnionRequestAPI.sendOnionRequest(request, server, pnServerPublicKey, Version.V2).map { response ->
JsonUtil.fromJson(response.body, Map::class.java)
}
}
} }

View File

@ -74,7 +74,9 @@ object FileServerApi {
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters)) HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
} }
return if (request.useOnionRouting) { return if (request.useOnionRouting) {
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).fail { e -> OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).map {
JsonUtil.fromJson(it.body, Map::class.java)
}.fail { e ->
Log.e("Loki", "File server request failed.", e) Log.e("Loki", "File server request failed.", e)
} }
} else { } else {

View File

@ -13,6 +13,7 @@ import org.session.libsession.messaging.sending_receiving.notifications.PushNoti
import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.SnodeMessage
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.Version
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
@ -38,10 +39,10 @@ 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, OnionRequestAPI.Version.V2).map { json -> OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, Version.V2).map { response ->
val code = json["code"] as? Int val code = response.info["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: ${response.info["message"] as? String ?: "null"}.")
} }
}.fail { exception -> }.fail { exception ->
Log.d("Loki", "Couldn't notify PN server due to error: $exception.") Log.d("Loki", "Couldn't notify PN server due to error: $exception.")

View File

@ -7,9 +7,9 @@ import com.fasterxml.jackson.databind.type.TypeFactory
import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.interfaces.GenericHash import com.goterl.lazysodium.interfaces.GenericHash
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl import okhttp3.HttpUrl
@ -19,7 +19,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller.Companion.maxInactivityPeriod import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller.Companion.maxInactivityPeriod
import org.session.libsession.messaging.utilities.SodiumUtilities 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.snode.OnionResponse
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
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
@ -32,9 +32,7 @@ import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.removing05PrefixIfNeeded import org.session.libsignal.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.toHexString
import org.whispersystems.curve25519.Curve25519 import org.whispersystems.curve25519.Curve25519
import java.util.concurrent.TimeUnit
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.set import kotlin.collections.set
@ -109,7 +107,6 @@ object OpenGroupApi {
val queryParameters: Map<String, String> = mapOf(), val queryParameters: Map<String, String> = mapOf(),
val parameters: Any? = null, val parameters: Any? = null,
val headers: Map<String, String> = mapOf(), val headers: Map<String, String> = mapOf(),
val isAuthRequired: Boolean = true,
/** /**
* Always `true` under normal circumstances. You might want to disable * Always `true` under normal circumstances. You might want to disable
* this when running over Lokinet. * this when running over Lokinet.
@ -124,7 +121,13 @@ object OpenGroupApi {
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON) return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
} }
private fun send(request: Request): Promise<Map<*, *>, Exception> { private fun getResponseBodyJson(request: Request): Promise<Map<*, *>, Exception> {
return send(request).map {
JsonUtil.fromJson(it.body, Map::class.java)
}
}
private fun send(request: Request): Promise<OnionResponse, Exception> {
val url = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL) val url = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL)
val urlBuilder = HttpUrl.Builder() val urlBuilder = HttpUrl.Builder()
.scheme(url.scheme()) .scheme(url.scheme())
@ -136,7 +139,7 @@ object OpenGroupApi {
urlBuilder.addQueryParameter(key, value) urlBuilder.addQueryParameter(key, value)
} }
} }
fun execute(token: String?): Promise<Map<*, *>, Exception> { fun execute(): Promise<OnionResponse, Exception> {
val publicKey = val publicKey =
MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server) MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
?: return Promise.ofFail(Error.NoPublicKey) ?: return Promise.ofFail(Error.NoPublicKey)
@ -191,15 +194,10 @@ object OpenGroupApi {
headers["X-SOGS-Timestamp"] = "$timestamp" headers["X-SOGS-Timestamp"] = "$timestamp"
headers["X-SOGS-Pubkey"] = pubKey headers["X-SOGS-Pubkey"] = pubKey
headers["X-SOGS-Signature"] = encodeBytes(signature) headers["X-SOGS-Signature"] = encodeBytes(signature)
headers.forEach { entry -> Log.d("Loki", "${entry.key}: ${entry.value}") }
val requestBuilder = okhttp3.Request.Builder() val requestBuilder = okhttp3.Request.Builder()
.url(urlRequest) .url(urlRequest)
.headers(Headers.of(headers)) .headers(Headers.of(headers))
if (request.isAuthRequired) {
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request.")
requestBuilder.header("Authorization", token)
}
when (request.verb) { when (request.verb) {
GET -> requestBuilder.get() GET -> requestBuilder.get()
PUT -> requestBuilder.put(createBody(request.parameters)!!) PUT -> requestBuilder.put(createBody(request.parameters)!!)
@ -209,31 +207,13 @@ object OpenGroupApi {
if (!request.room.isNullOrEmpty()) { if (!request.room.isNullOrEmpty()) {
requestBuilder.header("Room", request.room) requestBuilder.header("Room", request.room)
} }
if (request.useOnionRouting) { return if (request.useOnionRouting) {
return OnionRequestAPI.sendOnionRequest( OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey)
requestBuilder.build(),
request.server,
publicKey
).fail { e ->
// A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
val storage = MessagingModuleConfiguration.shared.storage
if (request.room != null) {
storage.removeAuthToken(request.room, request.server)
}
}
}
} 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."))
} }
} }
return if (request.isAuthRequired) { return execute()
getAuthToken(request.room!!, request.server).bind { execute(it) }
} else {
execute(null)
}
} }
fun downloadOpenGroupProfilePicture( fun downloadOpenGroupProfilePicture(
@ -244,78 +224,14 @@ object OpenGroupApi {
verb = GET, verb = GET,
room = roomID, room = roomID,
server = server, server = server,
endpoint = "rooms/$roomID/image", endpoint = "rooms/$roomID/image"
isAuthRequired = false
) )
return send(request).map { json -> return getResponseBodyJson(request).map { json ->
val result = json["result"] as? String ?: throw Error.ParsingFailed val result = json["result"] as? String ?: throw Error.ParsingFailed
decode(result) decode(result)
} }
} }
// region Authorization
fun getAuthToken(room: String, server: String): Promise<String, Exception> {
val storage = MessagingModuleConfiguration.shared.storage
return storage.getAuthToken(room, server)?.let {
Promise.of(it)
} ?: run {
requestNewAuthToken(room, server)
.bind { claimAuthToken(it, room, server) }
.success { authToken ->
storage.setAuthToken(room, server, authToken)
}
}
}
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair()
.let { it.publicKey.serialize() to it.privateKey.serialize() }
?: return Promise.ofFail(Error.Generic)
val queryParameters = mutableMapOf("public_key" to publicKey.toHexString())
val request = Request(
GET,
room,
server,
"auth_token_challenge",
queryParameters,
isAuthRequired = false,
parameters = null
)
return send(request).map { json ->
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.ParsingFailed
val base64EncodedCiphertext =
challenge["ciphertext"] as? String ?: throw Error.ParsingFailed
val base64EncodedEphemeralPublicKey =
challenge["ephemeral_public_key"] as? String ?: throw Error.ParsingFailed
val ciphertext = decode(base64EncodedCiphertext)
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
val tokenAsData = try {
AESGCM.decrypt(ciphertext, symmetricKey)
} catch (e: Exception) {
throw Error.DecryptionFailed
}
tokenAsData.toHexString()
}
}
fun claimAuthToken(
authToken: String,
room: String,
server: String
): Promise<String, Exception> {
val parameters =
mapOf("public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!)
val headers = mapOf("Authorization" to authToken)
val request = Request(
verb = POST, room = room, server = server, endpoint = "claim_auth_token",
parameters = parameters, headers = headers, isAuthRequired = false
)
return send(request).map { authToken }
}
// endregion
// region Upload/Download // region Upload/Download
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> { fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
val base64EncodedFile = encodeBytes(file) val base64EncodedFile = encodeBytes(file)
@ -327,14 +243,14 @@ object OpenGroupApi {
endpoint = "files", endpoint = "files",
parameters = parameters parameters = parameters
) )
return send(request).map { json -> return getResponseBodyJson(request).map { json ->
(json["result"] as? Number)?.toLong() ?: throw Error.ParsingFailed (json["result"] as? Number)?.toLong() ?: throw Error.ParsingFailed
} }
} }
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> { fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file") val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
return send(request).map { json -> return getResponseBodyJson(request).map { json ->
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
decode(base64EncodedFile) ?: throw Error.ParsingFailed decode(base64EncodedFile) ?: throw Error.ParsingFailed
} }
@ -356,7 +272,7 @@ object OpenGroupApi {
endpoint = "messages", endpoint = "messages",
parameters = jsonMessage parameters = jsonMessage
) )
return send(request).map { json -> return getResponseBodyJson(request).map { json ->
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any> @Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
?: throw Error.ParsingFailed ?: throw Error.ParsingFailed
val result = OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.ParsingFailed val result = OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.ParsingFailed
@ -381,7 +297,7 @@ object OpenGroupApi {
endpoint = "messages", endpoint = "messages",
queryParameters = queryParameters queryParameters = queryParameters
) )
return send(request).map { json -> return getResponseBodyJson(request).map { json ->
@Suppress("UNCHECKED_CAST") val rawMessages = @Suppress("UNCHECKED_CAST") val rawMessages =
json["messages"] as? List<Map<String, Any>> json["messages"] as? List<Map<String, Any>>
?: throw Error.ParsingFailed ?: throw Error.ParsingFailed
@ -443,7 +359,8 @@ object OpenGroupApi {
endpoint = "deleted_messages", endpoint = "deleted_messages",
queryParameters = queryParameters queryParameters = queryParameters
) )
return send(request).map { json -> return send(request).map { response ->
val json = JsonUtil.fromJson(response.body, Map::class.java)
val type = TypeFactory.defaultInstance() val type = TypeFactory.defaultInstance()
.constructCollectionType(List::class.java, MessageDeletion::class.java) .constructCollectionType(List::class.java, MessageDeletion::class.java)
val idsAsString = JsonUtil.toJson(json["ids"]) val idsAsString = JsonUtil.toJson(json["ids"])
@ -466,7 +383,7 @@ object OpenGroupApi {
fun getModerators(room: String, server: String): Promise<List<String>, Exception> { fun getModerators(room: String, server: String): Promise<List<String>, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators") val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
return send(request).map { json -> return getResponseBodyJson(request).map { json ->
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String> @Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
?: throw Error.ParsingFailed ?: throw Error.ParsingFailed
val id = "$server.$room" val id = "$server.$room"
@ -519,11 +436,10 @@ object OpenGroupApi {
// region General // region General
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun batch( fun poll(
rooms: List<String>, rooms: List<String>,
server: String server: String
): Promise<Map<String, BatchResult>, Exception> { ): Promise<Map<String, BatchResult>, Exception> {
val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) }
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val timeSinceLastOpen = this.timeSinceLastOpen val timeSinceLastOpen = this.timeSinceLastOpen
@ -535,15 +451,9 @@ object OpenGroupApi {
TextSecurePreferences.setLastOpenDate(context) TextSecurePreferences.setLastOpenDate(context)
} }
val requests = rooms.mapNotNull { room -> val requests = rooms.mapNotNull { room ->
val authToken = try {
authTokenRequests[room]?.get()
} catch (e: Exception) {
Log.e("Loki", "Failed to get auth token for $room.", e)
null
} ?: return@mapNotNull null
BatchRequest( BatchRequest(
roomID = room, roomID = room,
authToken = authToken, authToken = "",
fromDeletionServerID = if (useMessageLimit) null else storage.getLastDeletionServerID( fromDeletionServerID = if (useMessageLimit) null else storage.getLastDeletionServerID(
room, room,
server server
@ -559,22 +469,13 @@ object OpenGroupApi {
room = null, room = null,
server = server, server = server,
endpoint = "batch", endpoint = "batch",
isAuthRequired = false,
parameters = mapOf("requests" to requests) parameters = mapOf("requests" to requests)
) )
return send(request = request).map { json -> return getResponseBodyJson(request = request).map { json ->
val results = json["results"] as? List<*> ?: throw Error.ParsingFailed val results = json["results"] as? List<*> ?: throw Error.ParsingFailed
results.mapNotNull { json -> results.mapNotNull { json ->
if (json !is Map<*, *>) return@mapNotNull null if (json !is Map<*, *>) return@mapNotNull null
val roomID = json["room_id"] as? String ?: return@mapNotNull null val roomID = json["room_id"] as? String ?: return@mapNotNull null
// A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
val statusCode = json["status_code"] as? Int ?: return@mapNotNull null
if (statusCode == 401) {
// delete auth token and return null
storage.removeAuthToken(roomID, server)
}
// Moderators // Moderators
val moderators = json["moderators"] as? List<String> ?: return@mapNotNull null val moderators = json["moderators"] as? List<String> ?: return@mapNotNull null
handleModerators("$server.$roomID", moderators) handleModerators("$server.$roomID", moderators)
@ -632,10 +533,9 @@ object OpenGroupApi {
verb = GET, verb = GET,
room = null, room = null,
server = server, server = server,
endpoint = "rooms/$room", endpoint = "rooms/$room"
isAuthRequired = false
) )
return send(request).map { json -> return getResponseBodyJson(request).map { json ->
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.ParsingFailed val rawRoom = json["room"] as? Map<*, *> ?: throw Error.ParsingFailed
val id = rawRoom["id"] as? String ?: throw Error.ParsingFailed val id = rawRoom["id"] as? String ?: throw Error.ParsingFailed
val name = rawRoom["name"] as? String ?: throw Error.ParsingFailed val name = rawRoom["name"] as? String ?: throw Error.ParsingFailed
@ -650,14 +550,13 @@ object OpenGroupApi {
room = null, room = null,
server = server, server = server,
endpoint = "rooms", endpoint = "rooms",
isAuthRequired = false,
isBlinded = true isBlinded = true
) )
return send(request).map { json -> return send(request).map { response ->
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.ParsingFailed val rawRooms = JsonUtil.fromJson(response.body, List::class.java) ?: throw Error.ParsingFailed
rawRooms.mapNotNull { rawRooms.mapNotNull {
val roomJson = it as? Map<*, *> ?: return@mapNotNull null val roomJson = it as? Map<*, *> ?: return@mapNotNull null
val id = roomJson["id"] as? String ?: return@mapNotNull null val id = roomJson["token"] as? String ?: return@mapNotNull null
val name = roomJson["name"] as? String ?: return@mapNotNull null val name = roomJson["name"] as? String ?: return@mapNotNull null
val imageID = roomJson["image_id"] as? String val imageID = roomJson["image_id"] as? String
Info(id, name, imageID) Info(id, name, imageID)
@ -667,7 +566,7 @@ object OpenGroupApi {
fun getMemberCount(room: String, server: String): Promise<Int, Exception> { fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "active_users") val request = Request(verb = GET, room = room, server = server, endpoint = "active_users")
return send(request).map { json -> return getResponseBodyJson(request).map { json ->
val activeUserCount = json["active_users"] as? Int ?: throw Error.ParsingFailed val activeUserCount = json["active_users"] as? Int ?: throw Error.ParsingFailed
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
storage.setUserCount(room, server, activeUserCount) storage.setUserCount(room, server, activeUserCount)
@ -677,7 +576,7 @@ object OpenGroupApi {
fun getCapabilities(room: String, server: String): Promise<List<String>, Exception> { fun getCapabilities(room: String, server: String): Promise<List<String>, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "capabilities") val request = Request(verb = GET, room = room, server = server, endpoint = "capabilities")
return send(request).map { json -> return getResponseBodyJson(request).map { json ->
json["capabilities"] as? List<String> ?: throw Error.ParsingFailed json["capabilities"] as? List<String> ?: throw Error.ParsingFailed
} }
} }

View File

@ -12,8 +12,12 @@ import org.session.libsession.messaging.messages.control.ClosedGroupControlMessa
import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.* import org.session.libsession.messaging.messages.visible.LinkPreview
import org.session.libsession.messaging.open_groups.* import org.session.libsession.messaging.messages.visible.Profile
import org.session.libsession.messaging.messages.visible.Quote
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.open_groups.OpenGroupMessageV2
import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.snode.RawResponsePromise import org.session.libsession.snode.RawResponsePromise
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI

View File

@ -7,6 +7,7 @@ import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.Version
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.retryIfNeeded import org.session.libsignal.utilities.retryIfNeeded
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
@ -38,12 +39,12 @@ 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, OnionRequestAPI.Version.V2).map { json -> OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
val code = json["code"] as? Int val code = response.info["code"] as? Int
if (code != null && code != 0) { if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, false) TextSecurePreferences.setIsUsingFCM(context, false)
} else { } else {
Log.d("Loki", "Couldn't disable FCM due to error: ${json["message"] as? String ?: "null"}.") Log.d("Loki", "Couldn't disable FCM due to error: ${response.info["message"] as? String ?: "null"}.")
} }
}.fail { exception -> }.fail { exception ->
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.") Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
@ -66,14 +67,14 @@ 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, OnionRequestAPI.Version.V2).map { json -> OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
val code = json["code"] as? Int val code = response.info["code"] as? Int
if (code != null && code != 0) { if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, true) TextSecurePreferences.setIsUsingFCM(context, true)
TextSecurePreferences.setFCMToken(context, token) TextSecurePreferences.setFCMToken(context, token)
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
} else { } else {
Log.d("Loki", "Couldn't register for FCM due to error: ${json["message"] as? String ?: "null"}.") Log.d("Loki", "Couldn't register for FCM due to error: ${response.info["message"] as? String ?: "null"}.")
} }
}.fail { exception -> }.fail { exception ->
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.") Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
@ -93,10 +94,10 @@ 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, OnionRequestAPI.Version.V2).map { json -> OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
val code = json["code"] as? Int val code = response.info["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: ${response.info["message"] as? String ?: "null"}.")
} }
}.fail { exception -> }.fail { exception ->
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.") Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")

View File

@ -45,7 +45,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val rooms = storage.getAllV2OpenGroups().values.filter { it.server == server }.map { it.room } val rooms = storage.getAllV2OpenGroups().values.filter { it.server == server }.map { it.room }
rooms.forEach { downloadGroupAvatarIfNeeded(it) } rooms.forEach { downloadGroupAvatarIfNeeded(it) }
return OpenGroupApi.batch(rooms, server).successBackground { responses -> return OpenGroupApi.poll(rooms, server).successBackground { responses ->
responses.forEach { (room, response) -> responses.forEach { (room, response) ->
val openGroupID = "$server.$room" val openGroupID = "$server.$room"
handleNewMessages(room, openGroupID, response.messages) handleNewMessages(room, openGroupID, response.messages)

View File

@ -348,8 +348,12 @@ 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: ByteArray, version: Version): Promise<Map<*, *>, Exception> { private fun sendOnionRequest(
val deferred = deferred<Map<*, *>, Exception>() destination: Destination,
payload: ByteArray,
version: Version
): Promise<OnionResponse, Exception> {
val deferred = deferred<OnionResponse, Exception>()
lateinit var guardSnode: Snode lateinit var guardSnode: Snode
buildOnionForDestination(payload, destination, version).success { result -> buildOnionForDestination(payload, destination, version).success { result ->
guardSnode = result.guardSnode guardSnode = result.guardSnode
@ -439,7 +443,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, version: Version, publicKey: String? = null): Promise<Map<*, *>, Exception> { internal fun sendOnionRequest(
method: Snode.Method,
parameters: Map<*, *>,
snode: Snode,
version: Version,
publicKey: String? = null
): Promise<OnionResponse, Exception> {
val payload = mapOf( val payload = mapOf(
"method" to method.rawValue, "method" to method.rawValue,
"params" to parameters "params" to parameters
@ -461,7 +471,12 @@ 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, version: Version = Version.V4): Promise<Map<*, *>, Exception> { fun sendOnionRequest(
request: Request,
server: String,
x25519PublicKey: String,
version: Version = Version.V4
): Promise<OnionResponse, Exception> {
val url = request.url() val url = request.url()
val payload = generatePayload(request, server, version) val payload = generatePayload(request, server, version)
val destination = Destination.Server(url.host(), version.value, x25519PublicKey, url.scheme(), url.port()) val destination = Destination.Server(url.host(), version.value, x25519PublicKey, url.scheme(), url.port())
@ -518,7 +533,7 @@ object OnionRequestAPI {
destinationSymmetricKey: ByteArray, destinationSymmetricKey: ByteArray,
destination: Destination, destination: Destination,
version: Version, version: Version,
deferred: Deferred<Map<*, *>, Exception> deferred: Deferred<OnionResponse, Exception>
) { ) {
if (version == Version.V4) { if (version == Version.V4) {
try { try {
@ -563,25 +578,24 @@ object OnionRequestAPI {
} }
// If there is no data in the response then just return the ResponseInfo // If there is no data in the response then just return the ResponseInfo
if (info.length < "l${infoLength}${info}e".length) { if (plaintextString.length <= "l${infoLength}${info}e".length) {
return deferred.resolve(JsonUtil.fromJson(info, Map::class.java)) return deferred.resolve(OnionResponse(responseInfo, null))
} }
// Extract the response data as well // Extract the response data as well
val data = plaintextString.substring(infoEndIndex) val data = plaintextString.substring(infoEndIndex)
val dataParts = data.split(":") val dataParts = data.split(":".toRegex(), 2)
val dataLength = dataParts.firstOrNull()?.length val dataLength = dataParts.firstOrNull()?.toIntOrNull()
if (dataParts.size <= 1 || dataLength == null) return deferred.reject(Exception("Invalid JSON")) if (dataParts.size <= 1 || dataLength == null) return deferred.reject(Exception("Invalid response"))
val dataString = dataParts.last().dropLast(1) val dataString = dataParts.last().dropLast(1)
return deferred.resolve(JsonUtil.fromJson(dataString, Map::class.java)) return deferred.resolve(OnionResponse(responseInfo, dataString.encodeToByteArray()))
} catch (exception: Exception) { } catch (exception: Exception) {
deferred.reject(exception) deferred.reject(exception)
} }
} else { } else {
val bodyAsString = response.decodeToString()
val json = try { val json = try {
JsonUtil.fromJson(bodyAsString, Map::class.java) JsonUtil.fromJson(response, Map::class.java)
} catch (exception: Exception) { } catch (exception: Exception) {
mapOf( "result" to bodyAsString) mapOf( "result" to response.decodeToString())
} }
val base64EncodedIVAndCiphertext = json["result"] as? String ?: return deferred.reject(Exception("Invalid JSON")) val base64EncodedIVAndCiphertext = json["result"] as? String ?: return deferred.reject(Exception("Invalid JSON"))
val ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext) val ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext)
@ -624,7 +638,7 @@ object OnionRequestAPI {
) )
return deferred.reject(exception) return deferred.reject(exception)
} }
deferred.resolve(body) deferred.resolve(OnionResponse(body, JsonUtil.toJson(body).toByteArray()))
} }
else -> { else -> {
if (statusCode != 200) { if (statusCode != 200) {
@ -635,7 +649,7 @@ object OnionRequestAPI {
) )
return deferred.reject(exception) return deferred.reject(exception)
} }
deferred.resolve(json) deferred.resolve(OnionResponse(json, JsonUtil.toJson(json).toByteArray()))
} }
} }
} catch (exception: Exception) { } catch (exception: Exception) {
@ -647,11 +661,15 @@ object OnionRequestAPI {
} }
} }
// endregion // endregion
enum class Version(val value: String) {
V2("/loki/v2/lsrpc"),
V3("/loki/v3/lsrpc"),
V4("/oxen/v4/lsrpc");
}
} }
enum class Version(val value: String) {
V2("/loki/v2/lsrpc"),
V3("/loki/v3/lsrpc"),
V4("/oxen/v4/lsrpc");
}
data class OnionResponse(
val info: Map<*, *>,
val body: ByteArray? = null
)

View File

@ -35,12 +35,12 @@ object OnionRequestEncryption {
internal fun encryptPayloadForDestination( internal fun encryptPayloadForDestination(
payload: ByteArray, payload: ByteArray,
destination: Destination, destination: Destination,
version: OnionRequestAPI.Version version: Version
): Promise<EncryptionResult, Exception> { ): 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) { val plaintext = if (version == Version.V4) {
payload payload
} else { } 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

View File

@ -79,13 +79,15 @@ object SnodeAPI {
snode: Snode, snode: Snode,
parameters: Map<String, Any>, parameters: Map<String, Any>,
publicKey: String? = null, publicKey: String? = null,
version: OnionRequestAPI.Version = OnionRequestAPI.Version.V3 version: Version = Version.V3
): RawResponsePromise { ): RawResponsePromise {
val url = "${snode.address}:${snode.port}/storage_rpc/v1" val url = "${snode.address}:${snode.port}/storage_rpc/v1"
val deferred = deferred<Map<*, *>, Exception>()
if (useOnionRequests) { if (useOnionRequests) {
return OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey) OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey).map {
deferred.resolve(JsonUtil.fromJson(it.body, Map::class.java))
}.fail { deferred.reject(it) }
} else { } else {
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 {
@ -102,8 +104,8 @@ object SnodeAPI {
deferred.reject(exception) deferred.reject(exception)
} }
} }
return deferred.promise
} }
return deferred.promise
} }
internal fun getRandomSnode(): Promise<Snode, Exception> { internal fun getRandomSnode(): Promise<Snode, Exception> {