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)
// Store the public key
storage.setOpenGroupPublicKey(server,publicKey)
// Get an auth token
OpenGroupApi.getAuthToken(room, server).get()
// Get capabilities
val capabilities = OpenGroupApi.getCapabilities(room, server).get()
// Get group info

View File

@ -1,12 +1,14 @@
package org.thoughtcrime.securesms.notifications
import android.content.Context
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.Version
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
@ -43,7 +45,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, OnionRequestAPI.Version.V2).map { json ->
getResponseBody(request.build()).map { json ->
val code = json["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, false)
@ -72,7 +74,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, OnionRequestAPI.Version.V2).map { json ->
getResponseBody(request.build()).map { json ->
val code = json["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, true)
@ -100,7 +102,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, OnionRequestAPI.Version.V2).map { json ->
getResponseBody(request.build()).map { json ->
val code = json["code"] as? Int
if (code == null || code == 0) {
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")
@ -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))
}
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)
}
} 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.snode.SnodeMessage
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.Version
import org.session.libsignal.utilities.Log
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 request = Request.Builder().url(url).post(body)
retryIfNeeded(4) {
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, OnionRequestAPI.Version.V2).map { json ->
val code = json["code"] as? Int
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, Version.V2).map { response ->
val code = response.info["code"] as? Int
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 ->
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.SodiumAndroid
import com.goterl.lazysodium.interfaces.GenericHash
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.flow.MutableSharedFlow
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import okhttp3.Headers
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.utilities.SodiumUtilities
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.libsignal.utilities.Base64.decode
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.Log
import org.session.libsignal.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.toHexString
import org.whispersystems.curve25519.Curve25519
import java.util.concurrent.TimeUnit
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
@ -109,7 +107,6 @@ object OpenGroupApi {
val queryParameters: Map<String, String> = mapOf(),
val parameters: Any? = null,
val headers: Map<String, String> = mapOf(),
val isAuthRequired: Boolean = true,
/**
* Always `true` under normal circumstances. You might want to disable
* this when running over Lokinet.
@ -124,7 +121,13 @@ object OpenGroupApi {
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 urlBuilder = HttpUrl.Builder()
.scheme(url.scheme())
@ -136,7 +139,7 @@ object OpenGroupApi {
urlBuilder.addQueryParameter(key, value)
}
}
fun execute(token: String?): Promise<Map<*, *>, Exception> {
fun execute(): Promise<OnionResponse, Exception> {
val publicKey =
MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
?: return Promise.ofFail(Error.NoPublicKey)
@ -191,15 +194,10 @@ object OpenGroupApi {
headers["X-SOGS-Timestamp"] = "$timestamp"
headers["X-SOGS-Pubkey"] = pubKey
headers["X-SOGS-Signature"] = encodeBytes(signature)
headers.forEach { entry -> Log.d("Loki", "${entry.key}: ${entry.value}") }
val requestBuilder = okhttp3.Request.Builder()
.url(urlRequest)
.headers(Headers.of(headers))
if (request.isAuthRequired) {
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request.")
requestBuilder.header("Authorization", token)
}
when (request.verb) {
GET -> requestBuilder.get()
PUT -> requestBuilder.put(createBody(request.parameters)!!)
@ -209,31 +207,13 @@ object OpenGroupApi {
if (!request.room.isNullOrEmpty()) {
requestBuilder.header("Room", request.room)
}
if (request.useOnionRouting) {
return OnionRequestAPI.sendOnionRequest(
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)
}
}
}
return if (request.useOnionRouting) {
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey)
} 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) {
getAuthToken(request.room!!, request.server).bind { execute(it) }
} else {
execute(null)
}
return execute()
}
fun downloadOpenGroupProfilePicture(
@ -244,78 +224,14 @@ object OpenGroupApi {
verb = GET,
room = roomID,
server = server,
endpoint = "rooms/$roomID/image",
isAuthRequired = false
endpoint = "rooms/$roomID/image"
)
return send(request).map { json ->
return getResponseBodyJson(request).map { json ->
val result = json["result"] as? String ?: throw Error.ParsingFailed
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
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
val base64EncodedFile = encodeBytes(file)
@ -327,14 +243,14 @@ object OpenGroupApi {
endpoint = "files",
parameters = parameters
)
return send(request).map { json ->
return getResponseBodyJson(request).map { json ->
(json["result"] as? Number)?.toLong() ?: throw Error.ParsingFailed
}
}
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
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
decode(base64EncodedFile) ?: throw Error.ParsingFailed
}
@ -356,7 +272,7 @@ object OpenGroupApi {
endpoint = "messages",
parameters = jsonMessage
)
return send(request).map { json ->
return getResponseBodyJson(request).map { json ->
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
?: throw Error.ParsingFailed
val result = OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.ParsingFailed
@ -381,7 +297,7 @@ object OpenGroupApi {
endpoint = "messages",
queryParameters = queryParameters
)
return send(request).map { json ->
return getResponseBodyJson(request).map { json ->
@Suppress("UNCHECKED_CAST") val rawMessages =
json["messages"] as? List<Map<String, Any>>
?: throw Error.ParsingFailed
@ -443,7 +359,8 @@ object OpenGroupApi {
endpoint = "deleted_messages",
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()
.constructCollectionType(List::class.java, MessageDeletion::class.java)
val idsAsString = JsonUtil.toJson(json["ids"])
@ -466,7 +383,7 @@ object OpenGroupApi {
fun getModerators(room: String, server: String): Promise<List<String>, Exception> {
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>
?: throw Error.ParsingFailed
val id = "$server.$room"
@ -519,11 +436,10 @@ object OpenGroupApi {
// region General
@Suppress("UNCHECKED_CAST")
fun batch(
fun poll(
rooms: List<String>,
server: String
): Promise<Map<String, BatchResult>, Exception> {
val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) }
val storage = MessagingModuleConfiguration.shared.storage
val context = MessagingModuleConfiguration.shared.context
val timeSinceLastOpen = this.timeSinceLastOpen
@ -535,15 +451,9 @@ object OpenGroupApi {
TextSecurePreferences.setLastOpenDate(context)
}
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(
roomID = room,
authToken = authToken,
authToken = "",
fromDeletionServerID = if (useMessageLimit) null else storage.getLastDeletionServerID(
room,
server
@ -559,22 +469,13 @@ object OpenGroupApi {
room = null,
server = server,
endpoint = "batch",
isAuthRequired = false,
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
results.mapNotNull { json ->
if (json !is Map<*, *>) 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
val moderators = json["moderators"] as? List<String> ?: return@mapNotNull null
handleModerators("$server.$roomID", moderators)
@ -632,10 +533,9 @@ object OpenGroupApi {
verb = GET,
room = null,
server = server,
endpoint = "rooms/$room",
isAuthRequired = false
endpoint = "rooms/$room"
)
return send(request).map { json ->
return getResponseBodyJson(request).map { json ->
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.ParsingFailed
val id = rawRoom["id"] as? String ?: throw Error.ParsingFailed
val name = rawRoom["name"] as? String ?: throw Error.ParsingFailed
@ -650,14 +550,13 @@ object OpenGroupApi {
room = null,
server = server,
endpoint = "rooms",
isAuthRequired = false,
isBlinded = true
)
return send(request).map { json ->
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.ParsingFailed
return send(request).map { response ->
val rawRooms = JsonUtil.fromJson(response.body, List::class.java) ?: throw Error.ParsingFailed
rawRooms.mapNotNull {
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 imageID = roomJson["image_id"] as? String
Info(id, name, imageID)
@ -667,7 +566,7 @@ object OpenGroupApi {
fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
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 storage = MessagingModuleConfiguration.shared.storage
storage.setUserCount(room, server, activeUserCount)
@ -677,7 +576,7 @@ object OpenGroupApi {
fun getCapabilities(room: String, server: String): Promise<List<String>, Exception> {
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
}
}

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.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.*
import org.session.libsession.messaging.open_groups.*
import org.session.libsession.messaging.messages.visible.LinkPreview
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.snode.RawResponsePromise
import org.session.libsession.snode.SnodeAPI

View File

@ -7,6 +7,7 @@ import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.Version
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.retryIfNeeded
import org.session.libsignal.utilities.JsonUtil
@ -38,12 +39,12 @@ object PushNotificationAPI {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, OnionRequestAPI.Version.V2).map { json ->
val code = json["code"] as? Int
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
val code = response.info["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, false)
} 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 ->
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 request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, OnionRequestAPI.Version.V2).map { json ->
val code = json["code"] as? Int
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
val code = response.info["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, true)
TextSecurePreferences.setFCMToken(context, token)
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
} 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 ->
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 request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, OnionRequestAPI.Version.V2).map { json ->
val code = json["code"] as? Int
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
val code = response.info["code"] as? Int
if (code == null || code == 0) {
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"] as? String ?: "null"}.")
}
}.fail { 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 rooms = storage.getAllV2OpenGroups().values.filter { it.server == server }.map { it.room }
rooms.forEach { downloadGroupAvatarIfNeeded(it) }
return OpenGroupApi.batch(rooms, server).successBackground { responses ->
return OpenGroupApi.poll(rooms, server).successBackground { responses ->
responses.forEach { (room, response) ->
val openGroupID = "$server.$room"
handleNewMessages(room, openGroupID, response.messages)

View File

@ -348,8 +348,12 @@ object OnionRequestAPI {
/**
* Sends an onion request to `destination`. Builds new paths as needed.
*/
private fun sendOnionRequest(destination: Destination, payload: ByteArray, version: Version): Promise<Map<*, *>, Exception> {
val deferred = deferred<Map<*, *>, Exception>()
private fun sendOnionRequest(
destination: Destination,
payload: ByteArray,
version: Version
): Promise<OnionResponse, Exception> {
val deferred = deferred<OnionResponse, Exception>()
lateinit var guardSnode: Snode
buildOnionForDestination(payload, destination, version).success { result ->
guardSnode = result.guardSnode
@ -439,7 +443,13 @@ object OnionRequestAPI {
/**
* 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(
"method" to method.rawValue,
"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.
*/
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 payload = generatePayload(request, server, version)
val destination = Destination.Server(url.host(), version.value, x25519PublicKey, url.scheme(), url.port())
@ -518,7 +533,7 @@ object OnionRequestAPI {
destinationSymmetricKey: ByteArray,
destination: Destination,
version: Version,
deferred: Deferred<Map<*, *>, Exception>
deferred: Deferred<OnionResponse, Exception>
) {
if (version == Version.V4) {
try {
@ -563,25 +578,24 @@ object OnionRequestAPI {
}
// 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))
if (plaintextString.length <= "l${infoLength}${info}e".length) {
return deferred.resolve(OnionResponse(responseInfo, null))
}
// 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 dataParts = data.split(":".toRegex(), 2)
val dataLength = dataParts.firstOrNull()?.toIntOrNull()
if (dataParts.size <= 1 || dataLength == null) return deferred.reject(Exception("Invalid response"))
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) {
deferred.reject(exception)
}
} else {
val bodyAsString = response.decodeToString()
val json = try {
JsonUtil.fromJson(bodyAsString, Map::class.java)
JsonUtil.fromJson(response, Map::class.java)
} 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 ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext)
@ -624,7 +638,7 @@ object OnionRequestAPI {
)
return deferred.reject(exception)
}
deferred.resolve(body)
deferred.resolve(OnionResponse(body, JsonUtil.toJson(body).toByteArray()))
}
else -> {
if (statusCode != 200) {
@ -635,7 +649,7 @@ object OnionRequestAPI {
)
return deferred.reject(exception)
}
deferred.resolve(json)
deferred.resolve(OnionResponse(json, JsonUtil.toJson(json).toByteArray()))
}
}
} catch (exception: Exception) {
@ -647,11 +661,15 @@ object OnionRequestAPI {
}
}
// 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(
payload: ByteArray,
destination: Destination,
version: OnionRequestAPI.Version
version: Version
): Promise<EncryptionResult, Exception> {
val deferred = deferred<EncryptionResult, Exception>()
ThreadUtils.queue {
try {
val plaintext = if (version == OnionRequestAPI.Version.V4) {
val plaintext = if (version == Version.V4) {
payload
} else {
// Wrapping isn't needed for file server or open group onion requests

View File

@ -79,13 +79,15 @@ object SnodeAPI {
snode: Snode,
parameters: Map<String, Any>,
publicKey: String? = null,
version: OnionRequestAPI.Version = OnionRequestAPI.Version.V3
version: Version = Version.V3
): RawResponsePromise {
val url = "${snode.address}:${snode.port}/storage_rpc/v1"
val deferred = deferred<Map<*, *>, Exception>()
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 {
val deferred = deferred<Map<*, *>, Exception>()
ThreadUtils.queue {
val payload = mapOf( "method" to method.rawValue, "params" to parameters )
try {
@ -102,8 +104,8 @@ object SnodeAPI {
deferred.reject(exception)
}
}
return deferred.promise
}
return deferred.promise
}
internal fun getRandomSnode(): Promise<Snode, Exception> {