mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Fix duplicated API
This commit is contained in:
parent
9f26436041
commit
979c21ccbf
@ -32,12 +32,15 @@ import androidx.multidex.MultiDexApplication;
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.session.libsession.messaging.MessagingConfiguration;
|
||||
import org.session.libsession.messaging.avatars.AvatarHelper;
|
||||
import org.session.libsession.messaging.fileserver.FileServerAPI;
|
||||
import org.session.libsession.messaging.jobs.JobQueue;
|
||||
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI;
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller;
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.snode.SnodeAPI;
|
||||
import org.session.libsession.snode.SnodeConfiguration;
|
||||
import org.session.libsession.utilities.IdentityKeyUtil;
|
||||
import org.session.libsession.utilities.SSKEnvironment;
|
||||
@ -47,10 +50,6 @@ import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWr
|
||||
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil;
|
||||
import org.session.libsignal.service.api.util.StreamDetails;
|
||||
import org.session.libsignal.service.loki.api.PushNotificationAPI;
|
||||
import org.session.libsignal.service.loki.api.SnodeAPI;
|
||||
import org.session.libsignal.service.loki.api.SwarmAPI;
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
@ -96,6 +95,7 @@ import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.util.Date;
|
||||
@ -179,11 +179,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
new SessionProtocolImpl(this));
|
||||
SnodeConfiguration.Companion.configure(apiDB, broadcaster);
|
||||
if (userPublicKey != null) {
|
||||
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
||||
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
|
||||
MentionsManager.Companion.configureIfNeeded(userPublicKey, threadDB, userDB);
|
||||
}
|
||||
PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG);
|
||||
setUpStorageAPIIfNeeded();
|
||||
resubmitProfilePictureIfNeeded();
|
||||
publicChatManager = new PublicChatManager(this);
|
||||
@ -427,7 +424,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
}
|
||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
||||
LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
||||
org.session.libsession.messaging.fileserver.FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
||||
return true;
|
||||
}
|
||||
@ -458,13 +454,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null) return;
|
||||
if (poller != null) {
|
||||
SnodeAPI.shared.setUserPublicKey(userPublicKey);
|
||||
poller.setUserPublicKey(userPublicKey);
|
||||
return;
|
||||
}
|
||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
||||
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
|
||||
poller = new Poller();
|
||||
closedGroupPoller = new ClosedGroupPoller();
|
||||
}
|
||||
@ -503,12 +496,13 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
try {
|
||||
File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey));
|
||||
StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length());
|
||||
FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> {
|
||||
TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime());
|
||||
TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt());
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
throw new IOException();
|
||||
// FileServerAPI.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> {
|
||||
// TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime());
|
||||
// TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt());
|
||||
// ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey);
|
||||
// return Unit.INSTANCE;
|
||||
// });
|
||||
} catch (Exception exception) {
|
||||
// Do nothing
|
||||
}
|
||||
|
@ -19,12 +19,12 @@ import android.widget.Toast
|
||||
import androidx.annotation.ColorRes
|
||||
import kotlinx.android.synthetic.main.activity_path.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.loki.utilities.*
|
||||
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.loki.views.PathDotView
|
||||
import org.session.libsignal.service.loki.api.Snode
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
|
||||
class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
|
||||
|
@ -28,13 +28,13 @@ import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.ui.alwaysUi
|
||||
import org.session.libsession.messaging.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||
import org.session.libsignal.service.api.util.StreamDetails
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||
|
@ -10,11 +10,12 @@ import nl.komponents.kovenant.functional.map
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.opengroups.OpenGroup
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.service.loki.api.SnodeAPI
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
@ -70,10 +71,11 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
||||
|
||||
// Private chats
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val privateChatsPromise = SnodeAPI.shared.getMessages(userPublicKey).map { envelopes ->
|
||||
envelopes.map { envelope ->
|
||||
MessageReceiveJob(envelope.toByteArray(), false).executeAsync()
|
||||
}
|
||||
val privateChatsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes ->
|
||||
throw IOException()
|
||||
// envelopes.map { envelope ->
|
||||
// MessageReceiveJob(envelope.toByteArray(), false).executeAsync()
|
||||
// }
|
||||
}
|
||||
promises.addAll(privateChatsPromise.get())
|
||||
|
||||
|
@ -3,12 +3,12 @@ package org.thoughtcrime.securesms.loki.api
|
||||
import android.content.Context
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.*
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.api.PushNotificationAPI
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
|
||||
object LokiPushNotificationManager {
|
||||
@ -16,10 +16,10 @@ object LokiPushNotificationManager {
|
||||
private val tokenExpirationInterval = 12 * 60 * 60 * 1000
|
||||
|
||||
private val server by lazy {
|
||||
PushNotificationAPI.shared.server
|
||||
PushNotificationAPI.server
|
||||
}
|
||||
private val pnServerPublicKey by lazy {
|
||||
PushNotificationAPI.pnServerPublicKey
|
||||
PushNotificationAPI.serverPublicKey
|
||||
}
|
||||
|
||||
enum class ClosedGroupOperation {
|
||||
|
@ -7,7 +7,7 @@ import android.content.IntentFilter
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import com.opencsv.CSVReader
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
@ -11,9 +11,9 @@ import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorInt
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
|
||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
|
||||
class PathStatusView : View {
|
||||
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
|
||||
|
@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
|
||||
import org.session.libsession.messaging.fileserver.FileServerAPI;
|
||||
|
||||
public class PushMediaConstraints extends MediaConstraints {
|
||||
|
||||
|
@ -4,10 +4,10 @@ import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Request
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.*
|
||||
import java.net.URL
|
||||
|
@ -8,12 +8,12 @@ import nl.komponents.kovenant.then
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsignal.service.loki.utilities.DownloadUtilities
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@ -316,7 +316,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
Log.d("Loki", "Downloading open group profile picture from \"$url\".")
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
try {
|
||||
DownloadUtilities.downloadFile(outputStream, url, FileServerAPI.maxFileSize, null)
|
||||
throw IOException();
|
||||
Log.d("Loki", "Open group profile picture was successfully loaded from \"$url\"")
|
||||
return outputStream.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
|
@ -23,7 +23,6 @@ import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.api.crypto.ProofOfWork
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
@ -151,11 +150,9 @@ object MessageSender {
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeConfiguration.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!)
|
||||
}
|
||||
val recipient = message.recipient!!
|
||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||
val nonce = ProofOfWork.calculate(base64EncodedData, recipient, message.sentTimestamp!!, message.ttl.toInt()) ?: throw Error.ProofOfWorkCalculationFailed
|
||||
// Send the result
|
||||
val snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, message.sentTimestamp!!, nonce)
|
||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||
val snodeMessage = SnodeMessage(message.recipient!!, base64EncodedData, message.ttl, message.sentTimestamp!!)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
@ -6,12 +6,12 @@ import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Request
|
||||
import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||
import org.session.libsession.utilities.AESGCM
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.service.loki.api.Snode
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
||||
import org.session.libsignal.service.loki.api.utilities.*
|
||||
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
|
@ -8,9 +8,7 @@ data class SnodeMessage(
|
||||
// The time to live for the message in milliseconds.
|
||||
val ttl: Long,
|
||||
// When the proof of work was calculated.
|
||||
val timestamp: Long,
|
||||
// The base 64 encoded proof of work.
|
||||
val nonce: String
|
||||
val timestamp: Long
|
||||
) {
|
||||
internal fun toJSON(): Map<String, String> {
|
||||
return mutableMapOf(
|
||||
@ -18,6 +16,6 @@ data class SnodeMessage(
|
||||
"data" to data,
|
||||
"ttl" to ttl.toString(),
|
||||
"timestamp" to timestamp.toString(),
|
||||
"nonce" to nonce)
|
||||
"nonce" to "")
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import org.session.libsignal.service.api.crypto.ProfileCipherInputStream;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment.ProgressListener;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.loki.utilities.DownloadUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@ -46,8 +45,7 @@ public class SignalServiceMessageReceiver {
|
||||
public InputStream retrieveProfileAvatar(String path, File destination, byte[] profileKey, int maxSizeBytes)
|
||||
throws IOException
|
||||
{
|
||||
DownloadUtilities.downloadFile(destination, path, maxSizeBytes, null);
|
||||
return new ProfileCipherInputStream(new FileInputStream(destination), profileKey);
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,13 +63,6 @@ public class SignalServiceMessageReceiver {
|
||||
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes, ProgressListener listener)
|
||||
throws IOException, InvalidMessageException
|
||||
{
|
||||
// Loki - Fetch attachment
|
||||
if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL.");
|
||||
DownloadUtilities.downloadFile(destination, pointer.getUrl(), maxSizeBytes, listener);
|
||||
|
||||
// Loki - Assume we're retrieving an attachment for an open group server if the digest is not set
|
||||
if (!pointer.getDigest().isPresent()) { return new FileInputStream(destination); }
|
||||
|
||||
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
@ -1,252 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.then
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.DiffieHellman
|
||||
import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream
|
||||
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException
|
||||
import org.session.libsignal.service.api.util.StreamDetails
|
||||
import org.session.libsignal.service.internal.push.ProfileAvatarData
|
||||
import org.session.libsignal.service.internal.push.PushAttachmentData
|
||||
import org.session.libsignal.service.internal.push.http.DigestingRequestBody
|
||||
import org.session.libsignal.service.internal.push.http.ProfileCipherOutputStreamFactory
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import org.session.libsignal.service.loki.api.utilities.HTTP
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.recover
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Base class that provides utilities for .NET based APIs.
|
||||
*/
|
||||
open class LokiDotNetAPI(internal val userPublicKey: String, private val userPrivateKey: ByteArray, private val apiDatabase: LokiAPIDatabaseProtocol) {
|
||||
|
||||
internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH }
|
||||
|
||||
companion object {
|
||||
private val authTokenRequestCache = hashMapOf<String, Promise<String, Exception>>()
|
||||
}
|
||||
|
||||
public data class UploadResult(val id: Long, val url: String, val digest: ByteArray?)
|
||||
|
||||
public fun getAuthToken(server: String): Promise<String, Exception> {
|
||||
val token = apiDatabase.getAuthToken(server)
|
||||
if (token != null) { return Promise.of(token) }
|
||||
// Avoid multiple token requests to the server by caching
|
||||
var promise = authTokenRequestCache[server]
|
||||
if (promise == null) {
|
||||
promise = requestNewAuthToken(server).bind { submitAuthToken(it, server) }.then { newToken ->
|
||||
apiDatabase.setAuthToken(server, newToken)
|
||||
newToken
|
||||
}.always {
|
||||
authTokenRequestCache.remove(server)
|
||||
}
|
||||
authTokenRequestCache[server] = promise
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
private fun requestNewAuthToken(server: String): Promise<String, Exception> {
|
||||
Log.d("Loki", "Requesting auth token for server: $server.")
|
||||
val parameters: Map<String, Any> = mapOf( "pubKey" to userPublicKey )
|
||||
return execute(HTTPVerb.GET, server, "loki/v1/get_challenge", false, parameters).map(SnodeAPI.sharedContext) { json ->
|
||||
try {
|
||||
val base64EncodedChallenge = json["cipherText64"] as String
|
||||
val challenge = Base64.decode(base64EncodedChallenge)
|
||||
val base64EncodedServerPublicKey = json["serverPubKey64"] as String
|
||||
var serverPublicKey = Base64.decode(base64EncodedServerPublicKey)
|
||||
// Discard the "05" prefix if needed
|
||||
if (serverPublicKey.count() == 33) {
|
||||
val hexEncodedServerPublicKey = Hex.toStringCondensed(serverPublicKey)
|
||||
serverPublicKey = Hex.fromStringCondensed(hexEncodedServerPublicKey.removing05PrefixIfNeeded())
|
||||
}
|
||||
// The challenge is prefixed by the 16 bit IV
|
||||
val tokenAsData = DiffieHellman.decrypt(challenge, serverPublicKey, userPrivateKey)
|
||||
val token = tokenAsData.toString(Charsets.UTF_8)
|
||||
token
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse auth token for server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitAuthToken(token: String, server: String): Promise<String, Exception> {
|
||||
Log.d("Loki", "Submitting auth token for server: $server.")
|
||||
val parameters = mapOf( "pubKey" to userPublicKey, "token" to token )
|
||||
return execute(HTTPVerb.POST, server, "loki/v1/submit_challenge", false, parameters, isJSONRequired = false).map { token }
|
||||
}
|
||||
|
||||
internal fun execute(verb: HTTPVerb, server: String, endpoint: String, isAuthRequired: Boolean = true, parameters: Map<String, Any> = mapOf(), isJSONRequired: Boolean = true): Promise<Map<*, *>, Exception> {
|
||||
fun execute(token: String?): Promise<Map<*, *>, Exception> {
|
||||
val sanitizedEndpoint = endpoint.removePrefix("/")
|
||||
var url = "$server/$sanitizedEndpoint"
|
||||
if (verb == HTTPVerb.GET || verb == HTTPVerb.DELETE) {
|
||||
val queryParameters = parameters.map { "${it.key}=${it.value}" }.joinToString("&")
|
||||
if (queryParameters.isNotEmpty()) { url += "?$queryParameters" }
|
||||
}
|
||||
var request = Request.Builder().url(url)
|
||||
if (isAuthRequired) {
|
||||
if (token == null) { throw IllegalStateException() }
|
||||
request = request.header("Authorization", "Bearer $token")
|
||||
}
|
||||
when (verb) {
|
||||
HTTPVerb.GET -> request = request.get()
|
||||
HTTPVerb.DELETE -> request = request.delete()
|
||||
else -> {
|
||||
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||
val body = RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||
when (verb) {
|
||||
HTTPVerb.PUT -> request = request.put(body)
|
||||
HTTPVerb.POST -> request = request.post(body)
|
||||
HTTPVerb.PATCH -> request = request.patch(body)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
val serverPublicKeyPromise = if (server == FileServerAPI.shared.server) Promise.of(FileServerAPI.fileServerPublicKey)
|
||||
else FileServerAPI.shared.getPublicKeyForOpenGroupServer(server)
|
||||
return serverPublicKeyPromise.bind { serverPublicKey ->
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, isJSONRequired = isJSONRequired).recover { exception ->
|
||||
if (exception is HTTP.HTTPRequestFailedException) {
|
||||
val statusCode = exception.statusCode
|
||||
if (statusCode == 401 || statusCode == 403) {
|
||||
apiDatabase.setAuthToken(server, null)
|
||||
throw SnodeAPI.Error.TokenExpired
|
||||
}
|
||||
}
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (isAuthRequired) {
|
||||
getAuthToken(server).bind { execute(it) }
|
||||
} else {
|
||||
execute(null)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getUserProfiles(publicKeys: Set<String>, server: String, includeAnnotations: Boolean): Promise<List<Map<*, *>>, Exception> {
|
||||
val parameters = mapOf( "include_user_annotations" to includeAnnotations.toInt(), "ids" to publicKeys.joinToString { "@$it" } )
|
||||
return execute(HTTPVerb.GET, server, "users", parameters = parameters).map { json ->
|
||||
val data = json["data"] as? List<Map<*, *>>
|
||||
if (data == null) {
|
||||
Log.d("Loki", "Couldn't parse user profiles for: $publicKeys from: $json.")
|
||||
throw SnodeAPI.Error.ParsingFailed
|
||||
}
|
||||
data!! // For some reason the compiler can't infer that this can't be null at this point
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setSelfAnnotation(server: String, type: String, newValue: Any?): Promise<Map<*, *>, Exception> {
|
||||
val annotation = mutableMapOf<String, Any>( "type" to type )
|
||||
if (newValue != null) { annotation["value"] = newValue }
|
||||
val parameters = mapOf( "annotations" to listOf( annotation ) )
|
||||
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters)
|
||||
}
|
||||
|
||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
||||
fun uploadAttachment(server: String, attachment: PushAttachmentData): UploadResult {
|
||||
// This function mimics what Signal does in PushServiceSocket
|
||||
val contentType = "application/octet-stream"
|
||||
val file = DigestingRequestBody(attachment.data, attachment.outputStreamFactory, contentType, attachment.dataSize, attachment.listener)
|
||||
Log.d("Loki", "File size: ${attachment.dataSize} bytes.")
|
||||
val body = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("type", "network.loki")
|
||||
.addFormDataPart("Content-Type", contentType)
|
||||
.addFormDataPart("content", UUID.randomUUID().toString(), file)
|
||||
.build()
|
||||
val request = Request.Builder().url("$server/files").post(body)
|
||||
return upload(server, request) { json -> // Retrying is handled by AttachmentUploadJob
|
||||
val data = json["data"] as? Map<*, *>
|
||||
if (data == null) {
|
||||
Log.d("Loki", "Couldn't parse attachment from: $json.")
|
||||
throw SnodeAPI.Error.ParsingFailed
|
||||
}
|
||||
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
||||
val url = data["url"] as? String
|
||||
if (id == null || url == null || url.isEmpty()) {
|
||||
Log.d("Loki", "Couldn't parse upload from: $json.")
|
||||
throw SnodeAPI.Error.ParsingFailed
|
||||
}
|
||||
UploadResult(id, url, file.transmittedDigest)
|
||||
}.get()
|
||||
}
|
||||
|
||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
||||
fun uploadProfilePicture(server: String, key: ByteArray, profilePicture: StreamDetails, setLastProfilePictureUpload: () -> Unit): UploadResult {
|
||||
val profilePictureUploadData = ProfileAvatarData(profilePicture.stream, ProfileCipherOutputStream.getCiphertextLength(profilePicture.length), profilePicture.contentType, ProfileCipherOutputStreamFactory(key))
|
||||
val file = DigestingRequestBody(profilePictureUploadData.data, profilePictureUploadData.outputStreamFactory,
|
||||
profilePictureUploadData.contentType, profilePictureUploadData.dataLength, null)
|
||||
val body = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("type", "network.loki")
|
||||
.addFormDataPart("Content-Type", "application/octet-stream")
|
||||
.addFormDataPart("content", UUID.randomUUID().toString(), file)
|
||||
.build()
|
||||
val request = Request.Builder().url("$server/files").post(body)
|
||||
return retryIfNeeded(4) {
|
||||
upload(server, request) { json ->
|
||||
val data = json["data"] as? Map<*, *>
|
||||
if (data == null) {
|
||||
Log.d("Loki", "Couldn't parse profile picture from: $json.")
|
||||
throw SnodeAPI.Error.ParsingFailed
|
||||
}
|
||||
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
||||
val url = data["url"] as? String
|
||||
if (id == null || url == null || url.isEmpty()) {
|
||||
Log.d("Loki", "Couldn't parse profile picture from: $json.")
|
||||
throw SnodeAPI.Error.ParsingFailed
|
||||
}
|
||||
setLastProfilePictureUpload()
|
||||
UploadResult(id, url, file.transmittedDigest)
|
||||
}
|
||||
}.get()
|
||||
}
|
||||
|
||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
||||
private fun upload(server: String, request: Request.Builder, parse: (Map<*, *>) -> UploadResult): Promise<UploadResult, Exception> {
|
||||
val promise: Promise<Map<*, *>, Exception>
|
||||
if (server == FileServerAPI.shared.server) {
|
||||
request.addHeader("Authorization", "Bearer loki")
|
||||
// Uploads to the Loki File Server shouldn't include any personally identifiable information, so use a dummy auth token
|
||||
promise = OnionRequestAPI.sendOnionRequest(request.build(), FileServerAPI.shared.server, FileServerAPI.fileServerPublicKey)
|
||||
} else {
|
||||
promise = FileServerAPI.shared.getPublicKeyForOpenGroupServer(server).bind { openGroupServerPublicKey ->
|
||||
getAuthToken(server).bind { token ->
|
||||
request.addHeader("Authorization", "Bearer $token")
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, openGroupServerPublicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
return promise.map { json ->
|
||||
parse(json)
|
||||
}.recover { exception ->
|
||||
if (exception is HTTP.HTTPRequestFailedException) {
|
||||
val statusCode = exception.statusCode
|
||||
if (statusCode == 401 || statusCode == 403) {
|
||||
apiDatabase.setAuthToken(server, null)
|
||||
}
|
||||
throw NonSuccessfulResponseCodeException("Request returned with status code ${exception.statusCode}.")
|
||||
}
|
||||
throw PushNetworkException(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Boolean.toInt(): Int { return if (this) 1 else 0 }
|
@ -1,86 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.service.loki.api.crypto.ProofOfWork
|
||||
import org.session.libsignal.service.loki.utilities.TTLUtilities
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.service.loki.utilities.prettifiedDescription
|
||||
|
||||
internal data class LokiMessage(
|
||||
/**
|
||||
* The hex encoded public key of the receiver.
|
||||
*/
|
||||
internal val recipientPublicKey: String,
|
||||
/**
|
||||
* The content of the message.
|
||||
*/
|
||||
internal val data: String,
|
||||
/**
|
||||
* The time to live for the message in milliseconds.
|
||||
*/
|
||||
internal val ttl: Int,
|
||||
/**
|
||||
* Whether this message is a ping.
|
||||
*/
|
||||
internal val isPing: Boolean,
|
||||
/**
|
||||
* When the proof of work was calculated, if applicable (P2P messages don't require proof of work).
|
||||
*
|
||||
* - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
|
||||
*/
|
||||
internal var timestamp: Long? = null,
|
||||
/**
|
||||
* The base 64 encoded proof of work, if applicable (P2P messages don't require proof of work).
|
||||
*/
|
||||
internal var nonce: String? = null
|
||||
) {
|
||||
|
||||
internal companion object {
|
||||
|
||||
internal fun from(message: SignalMessageInfo): LokiMessage? {
|
||||
try {
|
||||
val wrappedMessage = MessageWrapper.wrap(message)
|
||||
val data = Base64.encodeBytes(wrappedMessage)
|
||||
val destination = message.recipientPublicKey
|
||||
var ttl = TTLUtilities.fallbackMessageTTL
|
||||
val messageTTL = message.ttl
|
||||
if (messageTTL != null && messageTTL != 0) { ttl = messageTTL }
|
||||
val isPing = message.isPing
|
||||
return LokiMessage(destination, data, ttl, isPing)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to convert Signal message to Loki message: ${message.prettifiedDescription()}.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@kotlin.ExperimentalUnsignedTypes
|
||||
internal fun calculatePoW(): Promise<LokiMessage, Exception> {
|
||||
val deferred = deferred<LokiMessage, Exception>()
|
||||
// Run PoW in a background thread
|
||||
ThreadUtils.queue {
|
||||
val now = System.currentTimeMillis()
|
||||
val nonce = ProofOfWork.calculate(data, recipientPublicKey, now, ttl)
|
||||
if (nonce != null ) {
|
||||
deferred.resolve(copy(nonce = nonce, timestamp = now))
|
||||
} else {
|
||||
deferred.reject(SnodeAPI.Error.ProofOfWorkCalculationFailed)
|
||||
}
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
internal fun toJSON(): Map<String, String> {
|
||||
val result = mutableMapOf( "pubKey" to recipientPublicKey, "data" to data, "ttl" to ttl.toString() )
|
||||
val timestamp = timestamp
|
||||
val nonce = nonce
|
||||
if (timestamp != null && nonce != null) {
|
||||
result["timestamp"] = timestamp.toString()
|
||||
result["nonce"] = nonce
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api
|
||||
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.*
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
|
||||
public class PushNotificationAPI private constructor(public val server: String) {
|
||||
|
||||
companion object {
|
||||
private val maxRetryCount = 4
|
||||
public val pnServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||
|
||||
lateinit var shared: PushNotificationAPI
|
||||
|
||||
public fun configureIfNeeded(isDebugMode: Boolean) {
|
||||
if (::shared.isInitialized) { return; }
|
||||
val server = if (isDebugMode) "https://live.apns.getsession.org" else "https://live.apns.getsession.org"
|
||||
shared = PushNotificationAPI(server)
|
||||
}
|
||||
}
|
||||
|
||||
public fun notify(messageInfo: SignalMessageInfo) {
|
||||
val message = LokiMessage.from(messageInfo) ?: return
|
||||
val parameters = mapOf( "data" to message.data, "send_to" to message.recipientPublicKey )
|
||||
val url = "${server}/notify"
|
||||
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, PushNotificationAPI.pnServerPublicKey, "/loki/v2/lsrpc").map { json ->
|
||||
val code = json["code"] as? Int
|
||||
if (code == null || code == 0) {
|
||||
Log.d("Loki", "[Loki] Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "[Loki] Couldn't notify PN server due to error: $exception.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,280 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api
|
||||
|
||||
import nl.komponents.kovenant.Kovenant
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.task
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import org.session.libsignal.service.loki.api.utilities.HTTP
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.*
|
||||
import org.session.libsignal.utilities.*
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
class SnodeAPI private constructor(public var userPublicKey: String, public val database: LokiAPIDatabaseProtocol, public val broadcaster: Broadcaster) {
|
||||
|
||||
companion object {
|
||||
val messageSendingContext = Kovenant.createContext()
|
||||
val messagePollingContext = Kovenant.createContext()
|
||||
/**
|
||||
* For operations that are shared between message sending and message polling.
|
||||
*/
|
||||
val sharedContext = Kovenant.createContext()
|
||||
|
||||
// region Initialization
|
||||
lateinit var shared: SnodeAPI
|
||||
|
||||
fun configureIfNeeded(userHexEncodedPublicKey: String, database: LokiAPIDatabaseProtocol, broadcaster: Broadcaster) {
|
||||
if (::shared.isInitialized) { return; }
|
||||
shared = SnodeAPI(userHexEncodedPublicKey, database, broadcaster)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Settings
|
||||
private val maxRetryCount = 6
|
||||
private val useOnionRequests = true
|
||||
|
||||
internal var powDifficulty = 1
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Error
|
||||
sealed class Error(val description: String) : Exception() {
|
||||
class HTTPRequestFailed(val code: Int) : Error("HTTP request failed with error code: $code.")
|
||||
object Generic : Error("An error occurred.")
|
||||
object ResponseBodyMissing: Error("Response body missing.")
|
||||
object MessageSigningFailed: Error("Failed to sign message.")
|
||||
/**
|
||||
* Only applicable to snode targets as proof of work isn't required for P2P messaging.
|
||||
*/
|
||||
object ProofOfWorkCalculationFailed : Error("Failed to calculate proof of work.")
|
||||
object MessageConversionFailed : Error("Failed to convert Signal message to Loki message.")
|
||||
object ClockOutOfSync : Error("The user's clock is out of sync with the service node network.")
|
||||
object SnodeMigrated : Error("The snode previously associated with the given public key has migrated to a different swarm.")
|
||||
object InsufficientProofOfWork : Error("The proof of work is insufficient.")
|
||||
object TokenExpired : Error("The auth token being used has expired.")
|
||||
object ParsingFailed : Error("Couldn't parse JSON.")
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Internal API
|
||||
/**
|
||||
* `publicKey` is the hex encoded public key of the user the call is associated with. This is needed for swarm cache maintenance.
|
||||
*/
|
||||
internal fun invoke(method: Snode.Method, snode: Snode, publicKey: String, parameters: Map<String, String>): RawResponsePromise {
|
||||
val url = "${snode.address}:${snode.port}/storage_rpc/v1"
|
||||
if (useOnionRequests) {
|
||||
return OnionRequestAPI.sendOnionRequest(method, parameters, snode, publicKey)
|
||||
} else {
|
||||
val deferred = deferred<Map<*, *>, Exception>()
|
||||
ThreadUtils.queue {
|
||||
val payload = mapOf( "method" to method.rawValue, "params" to parameters )
|
||||
try {
|
||||
val json = HTTP.execute(HTTP.Verb.POST, url, payload)
|
||||
deferred.resolve(json)
|
||||
} catch (exception: Exception) {
|
||||
if (exception is ConnectException || exception is SocketTimeoutException) {
|
||||
dropSnodeIfNeeded(snode, publicKey)
|
||||
} else {
|
||||
val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException
|
||||
if (httpRequestFailedException != null) {
|
||||
@Suppress("NAME_SHADOWING") val exception = handleSnodeError(httpRequestFailedException.statusCode, httpRequestFailedException.json, snode, publicKey)
|
||||
return@queue deferred.reject(exception)
|
||||
}
|
||||
Log.d("Loki", "Unhandled exception: $exception.")
|
||||
}
|
||||
deferred.reject(exception)
|
||||
}
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
}
|
||||
|
||||
public fun getRawMessages(snode: Snode, publicKey: String): RawResponsePromise {
|
||||
val lastHashValue = database.getLastMessageHashValue(snode, publicKey) ?: ""
|
||||
val parameters = mapOf( "pubKey" to publicKey, "lastHash" to lastHashValue )
|
||||
return invoke(Snode.Method.GetMessages, snode, publicKey, parameters)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Public API
|
||||
fun getMessages(publicKey: String): MessageListPromise {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
SwarmAPI.shared.getSingleTargetSnode(publicKey).bind(messagePollingContext) { snode ->
|
||||
getRawMessages(snode, publicKey).map(messagePollingContext) { parseRawMessagesResponse(it, snode, publicKey) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@kotlin.ExperimentalUnsignedTypes
|
||||
fun sendSignalMessage(message: SignalMessageInfo): Promise<Set<RawResponsePromise>, Exception> {
|
||||
val lokiMessage = LokiMessage.from(message) ?: return task { throw Error.MessageConversionFailed }
|
||||
val destination = lokiMessage.recipientPublicKey
|
||||
fun broadcast(event: String) {
|
||||
val dayInMs = 86400000
|
||||
if (message.ttl != dayInMs && message.ttl != 4 * dayInMs) { return }
|
||||
broadcaster.broadcast(event, message.timestamp)
|
||||
}
|
||||
broadcast("calculatingPoW")
|
||||
return lokiMessage.calculatePoW().bind { lokiMessageWithPoW ->
|
||||
broadcast("contactingNetwork")
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
SwarmAPI.shared.getTargetSnodes(destination).map { swarm ->
|
||||
swarm.map { snode ->
|
||||
broadcast("sendingMessage")
|
||||
val parameters = lokiMessageWithPoW.toJSON()
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
invoke(Snode.Method.SendMessage, snode, destination, parameters).map { rawResponse ->
|
||||
val json = rawResponse as? Map<*, *>
|
||||
val powDifficulty = json?.get("difficulty") as? Int
|
||||
if (powDifficulty != null) {
|
||||
if (powDifficulty != SnodeAPI.powDifficulty && powDifficulty < 100) {
|
||||
Log.d("Loki", "Setting proof of work difficulty to $powDifficulty (snode: $snode).")
|
||||
SnodeAPI.powDifficulty = powDifficulty
|
||||
}
|
||||
} else {
|
||||
Log.d("Loki", "Failed to update proof of work difficulty from: ${rawResponse.prettifiedDescription()}.")
|
||||
}
|
||||
rawResponse
|
||||
}
|
||||
}
|
||||
}.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Parsing
|
||||
|
||||
// The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions.
|
||||
|
||||
public fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List<Envelope> {
|
||||
val messages = rawResponse["messages"] as? List<*>
|
||||
if (messages != null) {
|
||||
updateLastMessageHashValueIfPossible(snode, publicKey, messages)
|
||||
val newRawMessages = removeDuplicates(publicKey, messages)
|
||||
return parseEnvelopes(newRawMessages)
|
||||
} else {
|
||||
return listOf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLastMessageHashValueIfPossible(snode: Snode, publicKey: String, rawMessages: List<*>) {
|
||||
val lastMessageAsJSON = rawMessages.lastOrNull() as? Map<*, *>
|
||||
val hashValue = lastMessageAsJSON?.get("hash") as? String
|
||||
val expiration = lastMessageAsJSON?.get("expiration") as? Int
|
||||
if (hashValue != null) {
|
||||
database.setLastMessageHashValue(snode, publicKey, hashValue)
|
||||
} else if (rawMessages.isNotEmpty()) {
|
||||
Log.d("Loki", "Failed to update last message hash value from: ${rawMessages.prettifiedDescription()}.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeDuplicates(publicKey: String, rawMessages: List<*>): List<*> {
|
||||
val receivedMessageHashValues = database.getReceivedMessageHashValues(publicKey)?.toMutableSet() ?: mutableSetOf()
|
||||
return rawMessages.filter { rawMessage ->
|
||||
val rawMessageAsJSON = rawMessage as? Map<*, *>
|
||||
val hashValue = rawMessageAsJSON?.get("hash") as? String
|
||||
if (hashValue != null) {
|
||||
val isDuplicate = receivedMessageHashValues.contains(hashValue)
|
||||
receivedMessageHashValues.add(hashValue)
|
||||
database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues)
|
||||
!isDuplicate
|
||||
} else {
|
||||
Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseEnvelopes(rawMessages: List<*>): List<Envelope> {
|
||||
return rawMessages.mapNotNull { rawMessage ->
|
||||
val rawMessageAsJSON = rawMessage as? Map<*, *>
|
||||
val base64EncodedData = rawMessageAsJSON?.get("data") as? String
|
||||
val data = base64EncodedData?.let { Base64.decode(it) }
|
||||
if (data != null) {
|
||||
try {
|
||||
MessageWrapper.unwrap(data)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
Log.d("Loki", "Failed to decode data for message: ${rawMessage?.prettifiedDescription()}.")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Error Handling
|
||||
private fun dropSnodeIfNeeded(snode: Snode, publicKey: String? = null) {
|
||||
val oldFailureCount = SwarmAPI.shared.snodeFailureCount[snode] ?: 0
|
||||
val newFailureCount = oldFailureCount + 1
|
||||
SwarmAPI.shared.snodeFailureCount[snode] = newFailureCount
|
||||
Log.d("Loki", "Couldn't reach snode at $snode; setting failure count to $newFailureCount.")
|
||||
if (newFailureCount >= SwarmAPI.snodeFailureThreshold) {
|
||||
Log.d("Loki", "Failure threshold reached for: $snode; dropping it.")
|
||||
if (publicKey != null) {
|
||||
SwarmAPI.shared.dropSnodeFromSwarmIfNeeded(snode, publicKey)
|
||||
}
|
||||
SwarmAPI.shared.snodePool = SwarmAPI.shared.snodePool.toMutableSet().minus(snode).toSet()
|
||||
Log.d("Loki", "Snode pool count: ${SwarmAPI.shared.snodePool.count()}.")
|
||||
SwarmAPI.shared.snodeFailureCount[snode] = 0
|
||||
}
|
||||
}
|
||||
|
||||
internal fun handleSnodeError(statusCode: Int, json: Map<*, *>?, snode: Snode, publicKey: String? = null): Exception {
|
||||
when (statusCode) {
|
||||
400, 500, 503 -> { // Usually indicates that the snode isn't up to date
|
||||
dropSnodeIfNeeded(snode, publicKey)
|
||||
return Error.HTTPRequestFailed(statusCode)
|
||||
}
|
||||
406 -> {
|
||||
Log.d("Loki", "The user's clock is out of sync with the service node network.")
|
||||
broadcaster.broadcast("clockOutOfSync")
|
||||
return Error.ClockOutOfSync
|
||||
}
|
||||
421 -> {
|
||||
// The snode isn't associated with the given public key anymore
|
||||
if (publicKey != null) {
|
||||
Log.d("Loki", "Invalidating swarm for: $publicKey.")
|
||||
SwarmAPI.shared.dropSnodeFromSwarmIfNeeded(snode, publicKey)
|
||||
} else {
|
||||
Log.d("Loki", "Got a 421 without an associated public key.")
|
||||
}
|
||||
return Error.SnodeMigrated
|
||||
}
|
||||
432 -> {
|
||||
// The PoW difficulty is too low
|
||||
val powDifficulty = json?.get("difficulty") as? Int
|
||||
if (powDifficulty != null && powDifficulty < 100) {
|
||||
Log.d("Loki", "Setting proof of work difficulty to $powDifficulty (snode: $snode).")
|
||||
SnodeAPI.powDifficulty = powDifficulty
|
||||
} else {
|
||||
Log.d("Loki", "Failed to update proof of work difficulty.")
|
||||
}
|
||||
return Error.InsufficientProofOfWork
|
||||
}
|
||||
else -> {
|
||||
dropSnodeIfNeeded(snode, publicKey)
|
||||
Log.d("Loki", "Unhandled response code: ${statusCode}.")
|
||||
return Error.Generic
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
typealias RawResponse = Map<*, *>
|
||||
typealias MessageListPromise = Promise<List<Envelope>, Exception>
|
||||
typealias RawResponsePromise = Promise<RawResponse, Exception>
|
||||
// endregion
|
@ -1,185 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api
|
||||
|
||||
import android.os.Build
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.task
|
||||
import org.session.libsignal.service.loki.api.utilities.HTTP
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.getRandomElement
|
||||
import org.session.libsignal.service.loki.utilities.prettifiedDescription
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
|
||||
class SwarmAPI private constructor(private val database: LokiAPIDatabaseProtocol) {
|
||||
internal var snodeFailureCount: MutableMap<Snode, Int> = mutableMapOf()
|
||||
|
||||
internal var snodePool: Set<Snode>
|
||||
get() = database.getSnodePool()
|
||||
set(newValue) { database.setSnodePool(newValue) }
|
||||
|
||||
companion object {
|
||||
|
||||
// use port 4433 if API level can handle network security config and enforce pinned certificates
|
||||
private val seedPort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433
|
||||
private val seedNodePool: Set<String> = setOf(
|
||||
"https://storage.seed1.loki.network:$seedPort",
|
||||
"https://storage.seed3.loki.network:$seedPort",
|
||||
"https://public.loki.foundation:$seedPort"
|
||||
)
|
||||
|
||||
// region Settings
|
||||
private val minimumSnodePoolCount = 64
|
||||
private val minimumSwarmSnodeCount = 2
|
||||
private val targetSwarmSnodeCount = 2
|
||||
private val maxRetryCount = 6
|
||||
|
||||
/**
|
||||
* A snode is kicked out of a swarm and/or the snode pool if it fails this many times.
|
||||
*/
|
||||
internal val snodeFailureThreshold = 2
|
||||
// endregion
|
||||
|
||||
// region Initialization
|
||||
lateinit var shared: SwarmAPI
|
||||
|
||||
fun configureIfNeeded(database: LokiAPIDatabaseProtocol) {
|
||||
if (::shared.isInitialized) { return; }
|
||||
shared = SwarmAPI(database)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Swarm API
|
||||
internal fun getRandomSnode(): Promise<Snode, Exception> {
|
||||
val snodePool = this.snodePool
|
||||
val lastRefreshDate = database.getLastSnodePoolRefreshDate()
|
||||
val now = Date()
|
||||
val needsRefresh = (snodePool.count() < minimumSnodePoolCount) || lastRefreshDate == null || (now.time - lastRefreshDate.time) > 24 * 60 * 60 * 1000
|
||||
if (needsRefresh) {
|
||||
database.setLastSnodePoolRefreshDate(now)
|
||||
|
||||
val target = seedNodePool.random()
|
||||
val url = "$target/json_rpc"
|
||||
Log.d("Loki", "Populating snode pool using: $target.")
|
||||
val parameters = mapOf(
|
||||
"method" to "get_n_service_nodes",
|
||||
"params" to mapOf(
|
||||
"active_only" to true,
|
||||
"fields" to mapOf( "public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true )
|
||||
)
|
||||
)
|
||||
val deferred = deferred<Snode, Exception>()
|
||||
deferred<Snode, Exception>(SnodeAPI.sharedContext)
|
||||
ThreadUtils.queue {
|
||||
try {
|
||||
val json = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true)
|
||||
val intermediate = json["result"] as? Map<*, *>
|
||||
val rawSnodes = intermediate?.get("service_node_states") as? List<*>
|
||||
if (rawSnodes != null) {
|
||||
@Suppress("NAME_SHADOWING") val snodePool = rawSnodes.mapNotNull { rawSnode ->
|
||||
val rawSnodeAsJSON = rawSnode as? Map<*, *>
|
||||
val address = rawSnodeAsJSON?.get("public_ip") as? String
|
||||
val port = rawSnodeAsJSON?.get("storage_port") as? Int
|
||||
val ed25519Key = rawSnodeAsJSON?.get("pubkey_ed25519") as? String
|
||||
val x25519Key = rawSnodeAsJSON?.get("pubkey_x25519") as? String
|
||||
if (address != null && port != null && ed25519Key != null && x25519Key != null && address != "0.0.0.0") {
|
||||
Snode("https://$address", port, Snode.KeySet(ed25519Key, x25519Key))
|
||||
} else {
|
||||
Log.d("Loki", "Failed to parse: ${rawSnode?.prettifiedDescription()}.")
|
||||
null
|
||||
}
|
||||
}.toMutableSet()
|
||||
Log.d("Loki", "Persisting snode pool to database.")
|
||||
this.snodePool = snodePool
|
||||
try {
|
||||
deferred.resolve(snodePool.getRandomElement())
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Got an empty snode pool from: $target.")
|
||||
deferred.reject(SnodeAPI.Error.Generic)
|
||||
}
|
||||
} else {
|
||||
Log.d("Loki", "Failed to update snode pool from: ${(rawSnodes as List<*>?)?.prettifiedDescription()}.")
|
||||
deferred.reject(SnodeAPI.Error.Generic)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
deferred.reject(exception)
|
||||
}
|
||||
}
|
||||
return deferred.promise
|
||||
} else {
|
||||
return Promise.of(snodePool.getRandomElement())
|
||||
}
|
||||
}
|
||||
|
||||
public fun getSwarm(publicKey: String): Promise<Set<Snode>, Exception> {
|
||||
val cachedSwarm = database.getSwarm(publicKey)
|
||||
if (cachedSwarm != null && cachedSwarm.size >= minimumSwarmSnodeCount) {
|
||||
val cachedSwarmCopy = mutableSetOf<Snode>() // Workaround for a Kotlin compiler issue
|
||||
cachedSwarmCopy.addAll(cachedSwarm)
|
||||
return task { cachedSwarmCopy }
|
||||
} else {
|
||||
val parameters = mapOf( "pubKey" to publicKey )
|
||||
return getRandomSnode().bind {
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
SnodeAPI.shared.invoke(Snode.Method.GetSwarm, it, publicKey, parameters)
|
||||
}
|
||||
|
||||
}.map(SnodeAPI.sharedContext) {
|
||||
parseSnodes(it).toSet()
|
||||
}.success {
|
||||
database.setSwarm(publicKey, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun dropSnodeFromSwarmIfNeeded(snode: Snode, publicKey: String) {
|
||||
val swarm = database.getSwarm(publicKey)?.toMutableSet()
|
||||
if (swarm != null && swarm.contains(snode)) {
|
||||
swarm.remove(snode)
|
||||
database.setSwarm(publicKey, swarm)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getSingleTargetSnode(publicKey: String): Promise<Snode, Exception> {
|
||||
// SecureRandom() should be cryptographically secure
|
||||
return getSwarm(publicKey).map { it.shuffled(SecureRandom()).random() }
|
||||
}
|
||||
|
||||
internal fun getTargetSnodes(publicKey: String): Promise<List<Snode>, Exception> {
|
||||
// SecureRandom() should be cryptographically secure
|
||||
return getSwarm(publicKey).map { it.shuffled(SecureRandom()).take(targetSwarmSnodeCount) }
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Parsing
|
||||
private fun parseSnodes(rawResponse: Any): List<Snode> {
|
||||
val json = rawResponse as? Map<*, *>
|
||||
val rawSnodes = json?.get("snodes") as? List<*>
|
||||
if (rawSnodes != null) {
|
||||
return rawSnodes.mapNotNull { rawSnode ->
|
||||
val rawSnodeAsJSON = rawSnode as? Map<*, *>
|
||||
val address = rawSnodeAsJSON?.get("ip") as? String
|
||||
val portAsString = rawSnodeAsJSON?.get("port") as? String
|
||||
val port = portAsString?.toInt()
|
||||
val ed25519Key = rawSnodeAsJSON?.get("pubkey_ed25519") as? String
|
||||
val x25519Key = rawSnodeAsJSON?.get("pubkey_x25519") as? String
|
||||
if (address != null && port != null && ed25519Key != null && x25519Key != null && address != "0.0.0.0") {
|
||||
Snode("https://$address", port, Snode.KeySet(ed25519Key, x25519Key))
|
||||
} else {
|
||||
Log.d("Loki", "Failed to parse snode from: ${rawSnode?.prettifiedDescription()}.")
|
||||
null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d("Loki", "Failed to parse snodes from: ${rawResponse.prettifiedDescription()}.")
|
||||
return listOf()
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api.crypto
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.service.loki.api.SnodeAPI
|
||||
import java.math.BigInteger
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Based on the desktop messenger's proof of work implementation. For more information, see libloki/proof-of-work.js.
|
||||
*/
|
||||
object ProofOfWork {
|
||||
|
||||
// region Settings
|
||||
private val nonceSize = 8
|
||||
// endregion
|
||||
|
||||
// region Implementation
|
||||
@kotlin.ExperimentalUnsignedTypes
|
||||
fun calculate(data: String, hexEncodedPublicKey: String, timestamp: Long, ttl: Int): String? {
|
||||
try {
|
||||
val sha512 = MessageDigest.getInstance("SHA-512")
|
||||
val payloadAsString = timestamp.toString() + ttl.toString() + hexEncodedPublicKey + data
|
||||
val payload = payloadAsString.toByteArray()
|
||||
val target = determineTarget(ttl, payload.size)
|
||||
var currentTrialValue = ULong.MAX_VALUE
|
||||
var nonce: Long = 0
|
||||
val initialHash = sha512.digest(payload)
|
||||
while (currentTrialValue > target) {
|
||||
nonce += 1
|
||||
// This is different from bitmessage's PoW implementation
|
||||
// newHash = hash(nonce + hash(data)) → hash(nonce + initialHash)
|
||||
val newHash = sha512.digest(nonce.toByteArray() + initialHash)
|
||||
currentTrialValue = newHash.sliceArray(0 until nonceSize).toULong()
|
||||
}
|
||||
return Base64.encodeBytes(nonce.toByteArray())
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Couldn't calculate proof of work due to error: $e.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@kotlin.ExperimentalUnsignedTypes
|
||||
private fun determineTarget(ttl: Int, payloadSize: Int): ULong {
|
||||
val x1 = BigInteger.valueOf(2).pow(16) - 1.toBigInteger()
|
||||
val x2 = BigInteger.valueOf(2).pow(64) - 1.toBigInteger()
|
||||
val size = (payloadSize + nonceSize).toBigInteger()
|
||||
val ttlInSeconds = (ttl / 1000).toBigInteger()
|
||||
val x3 = (ttlInSeconds * size) / x1
|
||||
val x4 = size + x3
|
||||
val x5 = SnodeAPI.powDifficulty.toBigInteger() * x4
|
||||
return (x2 / x5).toULong()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
@kotlin.ExperimentalUnsignedTypes
|
||||
private fun BigInteger.toULong() = toLong().toULong()
|
||||
private fun Long.toByteArray() = ByteBuffer.allocate(8).putLong(this).array()
|
||||
@kotlin.ExperimentalUnsignedTypes
|
||||
private fun ByteArray.toULong() = ByteBuffer.wrap(this).long.toULong()
|
||||
// endregion
|
@ -1,77 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api.fileserver
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Request
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.api.LokiDotNetAPI
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.*
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : LokiDotNetAPI(userPublicKey, userPrivateKey, database) {
|
||||
|
||||
companion object {
|
||||
// region Settings
|
||||
internal val fileServerPublicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C"
|
||||
internal val maxRetryCount = 4
|
||||
|
||||
public val maxFileSize = 10_000_000 // 10 MB
|
||||
/**
|
||||
* The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
|
||||
* is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
|
||||
* request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
|
||||
* be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
|
||||
* uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
|
||||
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
*/
|
||||
public val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
|
||||
public val fileStorageBucketURL = "https://file-static.lokinet.org"
|
||||
// endregion
|
||||
|
||||
// region Initialization
|
||||
lateinit var shared: FileServerAPI
|
||||
|
||||
/**
|
||||
* Must be called before `LokiAPI` is used.
|
||||
*/
|
||||
fun configure(userPublicKey: String, userPrivateKey: ByteArray, database: LokiAPIDatabaseProtocol) {
|
||||
if (Companion::shared.isInitialized) { return }
|
||||
val server = "https://file.getsession.org"
|
||||
shared = FileServerAPI(server, userPublicKey, userPrivateKey, database)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Open Group Server Public Key
|
||||
fun getPublicKeyForOpenGroupServer(openGroupServer: String): Promise<String, Exception> {
|
||||
val publicKey = database.getOpenGroupPublicKey(openGroupServer)
|
||||
if (publicKey != null && PublicKeyValidation.isValid(publicKey, 64, false)) {
|
||||
return Promise.of(publicKey)
|
||||
} else {
|
||||
val url = "$server/loki/v1/getOpenGroupKey/${URL(openGroupServer).host}"
|
||||
val request = Request.Builder().url(url)
|
||||
request.addHeader("Content-Type", "application/json")
|
||||
request.addHeader("Authorization", "Bearer loki") // Tokenless request; use a dummy token
|
||||
return OnionRequestAPI.sendOnionRequest(request.build(), server, fileServerPublicKey).map { json ->
|
||||
try {
|
||||
val bodyAsString = json["data"] as String
|
||||
val body = JsonUtil.fromJson(bodyAsString)
|
||||
val base64EncodedPublicKey = body.get("data").asText()
|
||||
val prefixedPublicKey = Base64.decode(base64EncodedPublicKey)
|
||||
val hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString()
|
||||
val result = hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded()
|
||||
database.setOpenGroupPublicKey(openGroupServer, result)
|
||||
result
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse open group public key from: $json.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,460 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api.onionrequests
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.all
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Request
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.api.*
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
||||
import org.session.libsignal.service.loki.api.utilities.*
|
||||
import org.session.libsignal.service.loki.api.utilities.EncryptionResult
|
||||
import org.session.libsignal.service.loki.api.utilities.getBodyForOnionRequest
|
||||
import org.session.libsignal.service.loki.api.utilities.getHeadersForOnionRequest
|
||||
import org.session.libsignal.service.loki.utilities.*
|
||||
import org.session.libsignal.utilities.*
|
||||
|
||||
private typealias Path = List<Snode>
|
||||
|
||||
/**
|
||||
* See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
|
||||
*/
|
||||
public object OnionRequestAPI {
|
||||
private val pathFailureCount = mutableMapOf<Path, Int>()
|
||||
private val snodeFailureCount = mutableMapOf<Snode, Int>()
|
||||
public var guardSnodes = setOf<Snode>()
|
||||
public var paths: List<Path> // Not a set to ensure we consistently show the same path to the user
|
||||
get() = SnodeAPI.shared.database.getOnionRequestPaths()
|
||||
set(newValue) {
|
||||
if (newValue.isEmpty()) {
|
||||
SnodeAPI.shared.database.clearOnionRequestPaths()
|
||||
} else {
|
||||
SnodeAPI.shared.database.setOnionRequestPaths(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// region Settings
|
||||
/**
|
||||
* The number of snodes (including the guard snode) in a path.
|
||||
*/
|
||||
private val pathSize = 3
|
||||
/**
|
||||
* The number of times a path can fail before it's replaced.
|
||||
*/
|
||||
private val pathFailureThreshold = 1
|
||||
/**
|
||||
* The number of times a snode can fail before it's replaced.
|
||||
*/
|
||||
private val snodeFailureThreshold = 1
|
||||
/**
|
||||
* The number of paths to maintain.
|
||||
*/
|
||||
public val targetPathCount = 2 // A main path and a backup path for the case where the target snode is in the main path
|
||||
|
||||
/**
|
||||
* The number of guard snodes required to maintain `targetPathCount` paths.
|
||||
*/
|
||||
private val targetGuardSnodeCount
|
||||
get() = targetPathCount // One per path
|
||||
// endregion
|
||||
|
||||
class HTTPRequestFailedAtDestinationException(val statusCode: Int, val json: Map<*, *>)
|
||||
: Exception("HTTP request failed at destination with status code $statusCode.")
|
||||
class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.")
|
||||
|
||||
private data class OnionBuildingResult(
|
||||
internal val guardSnode: Snode,
|
||||
internal val finalEncryptionResult: EncryptionResult,
|
||||
internal val destinationSymmetricKey: ByteArray
|
||||
)
|
||||
|
||||
internal sealed class Destination {
|
||||
class Snode(val snode: org.session.libsignal.service.loki.api.Snode) : Destination()
|
||||
class Server(val host: String, val target: String, val x25519PublicKey: String) : Destination()
|
||||
}
|
||||
|
||||
// region Private API
|
||||
/**
|
||||
* Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise.
|
||||
*/
|
||||
private fun testSnode(snode: Snode): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
ThreadUtils.queue { // No need to block the shared context for this
|
||||
val url = "${snode.address}:${snode.port}/get_stats/v1"
|
||||
try {
|
||||
val json = HTTP.execute(HTTP.Verb.GET, url)
|
||||
val version = json["version"] as? String
|
||||
if (version == null) { deferred.reject(Exception("Missing snode version.")); return@queue }
|
||||
if (version >= "2.0.7") {
|
||||
deferred.resolve(Unit)
|
||||
} else {
|
||||
val message = "Unsupported snode version: $version."
|
||||
Log.d("Loki", message)
|
||||
deferred.reject(Exception(message))
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
deferred.reject(exception)
|
||||
}
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out if not
|
||||
* enough (reliable) snodes are available.
|
||||
*/
|
||||
private fun getGuardSnodes(reusableGuardSnodes: List<Snode>): Promise<Set<Snode>, Exception> {
|
||||
if (guardSnodes.count() >= targetGuardSnodeCount) {
|
||||
return Promise.of(guardSnodes)
|
||||
} else {
|
||||
Log.d("Loki", "Populating guard snode cache.")
|
||||
return SwarmAPI.shared.getRandomSnode().bind(SnodeAPI.sharedContext) { // Just used to populate the snode pool
|
||||
var unusedSnodes = SwarmAPI.shared.snodePool.minus(reusableGuardSnodes)
|
||||
val reusableGuardSnodeCount = reusableGuardSnodes.count()
|
||||
if (unusedSnodes.count() < (targetGuardSnodeCount - reusableGuardSnodeCount)) { throw InsufficientSnodesException() }
|
||||
fun getGuardSnode(): Promise<Snode, Exception> {
|
||||
val candidate = unusedSnodes.getRandomElementOrNull()
|
||||
?: return Promise.ofFail(InsufficientSnodesException())
|
||||
unusedSnodes = unusedSnodes.minus(candidate)
|
||||
Log.d("Loki", "Testing guard snode: $candidate.")
|
||||
// Loop until a reliable guard snode is found
|
||||
val deferred = deferred<Snode, Exception>()
|
||||
testSnode(candidate).success {
|
||||
deferred.resolve(candidate)
|
||||
}.fail {
|
||||
getGuardSnode().success {
|
||||
deferred.resolve(candidate)
|
||||
}.fail { exception ->
|
||||
if (exception is InsufficientSnodesException) {
|
||||
deferred.reject(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
val promises = (0 until (targetGuardSnodeCount - reusableGuardSnodeCount)).map { getGuardSnode() }
|
||||
all(promises).map(SnodeAPI.sharedContext) { guardSnodes ->
|
||||
val guardSnodesAsSet = (guardSnodes + reusableGuardSnodes).toSet()
|
||||
OnionRequestAPI.guardSnodes = guardSnodesAsSet
|
||||
guardSnodesAsSet
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns `targetPathCount` paths. The returned promise errors out if not
|
||||
* enough (reliable) snodes are available.
|
||||
*/
|
||||
private fun buildPaths(reusablePaths: List<Path>): Promise<List<Path>, Exception> {
|
||||
Log.d("Loki", "Building onion request paths.")
|
||||
SnodeAPI.shared.broadcaster.broadcast("buildingPaths")
|
||||
return SwarmAPI.shared.getRandomSnode().bind(SnodeAPI.sharedContext) { // Just used to populate the snode pool
|
||||
val reusableGuardSnodes = reusablePaths.map { it[0] }
|
||||
getGuardSnodes(reusableGuardSnodes).map(SnodeAPI.sharedContext) { guardSnodes ->
|
||||
var unusedSnodes = SwarmAPI.shared.snodePool.minus(guardSnodes).minus(reusablePaths.flatten())
|
||||
val reusableGuardSnodeCount = reusableGuardSnodes.count()
|
||||
val pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
|
||||
if (unusedSnodes.count() < pathSnodeCount) { throw InsufficientSnodesException() }
|
||||
// Don't test path snodes as this would reveal the user's IP to them
|
||||
guardSnodes.minus(reusableGuardSnodes).map { guardSnode ->
|
||||
val result = listOf( guardSnode ) + (0 until (pathSize - 1)).map {
|
||||
val pathSnode = unusedSnodes.getRandomElement()
|
||||
unusedSnodes = unusedSnodes.minus(pathSnode)
|
||||
pathSnode
|
||||
}
|
||||
Log.d("Loki", "Built new onion request path: $result.")
|
||||
result
|
||||
}
|
||||
}.map { paths ->
|
||||
OnionRequestAPI.paths = paths + reusablePaths
|
||||
SnodeAPI.shared.broadcaster.broadcast("pathsBuilt")
|
||||
paths
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a `Path` to be used for building an onion request. Builds new paths as needed.
|
||||
*/
|
||||
private fun getPath(snodeToExclude: Snode?): Promise<Path, Exception> {
|
||||
if (pathSize < 1) { throw Exception("Can't build path of size zero.") }
|
||||
val paths = this.paths
|
||||
val guardSnodes = mutableSetOf<Snode>()
|
||||
if (paths.isNotEmpty()) {
|
||||
guardSnodes.add(paths[0][0])
|
||||
if (paths.count() >= 2) {
|
||||
guardSnodes.add(paths[1][0])
|
||||
}
|
||||
}
|
||||
OnionRequestAPI.guardSnodes = guardSnodes
|
||||
fun getPath(paths: List<Path>): Path {
|
||||
if (snodeToExclude != null) {
|
||||
return paths.filter { !it.contains(snodeToExclude) }.getRandomElement()
|
||||
} else {
|
||||
return paths.getRandomElement()
|
||||
}
|
||||
}
|
||||
if (paths.count() >= targetPathCount) {
|
||||
return Promise.of(getPath(paths))
|
||||
} else if (paths.isNotEmpty()) {
|
||||
if (paths.any { !it.contains(snodeToExclude) }) {
|
||||
buildPaths(paths) // Re-build paths in the background
|
||||
return Promise.of(getPath(paths))
|
||||
} else {
|
||||
return buildPaths(paths).map(SnodeAPI.sharedContext) { newPaths ->
|
||||
getPath(newPaths)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return buildPaths(listOf()).map(SnodeAPI.sharedContext) { newPaths ->
|
||||
getPath(newPaths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dropGuardSnode(snode: Snode) {
|
||||
guardSnodes = guardSnodes.filter { it != snode }.toSet()
|
||||
}
|
||||
|
||||
private fun dropSnode(snode: Snode) {
|
||||
// We repair the path here because we can do it sync. In the case where we drop a whole
|
||||
// path we leave the re-building up to getPath() because re-building the path in that case
|
||||
// is async.
|
||||
snodeFailureCount[snode] = 0
|
||||
val oldPaths = paths.toMutableList()
|
||||
val pathIndex = oldPaths.indexOfFirst { it.contains(snode) }
|
||||
if (pathIndex == -1) { return }
|
||||
val path = oldPaths[pathIndex].toMutableList()
|
||||
val snodeIndex = path.indexOf(snode)
|
||||
if (snodeIndex == -1) { return }
|
||||
path.removeAt(snodeIndex)
|
||||
val unusedSnodes = SwarmAPI.shared.snodePool.minus(oldPaths.flatten())
|
||||
if (unusedSnodes.isEmpty()) { throw InsufficientSnodesException() }
|
||||
path.add(unusedSnodes.getRandomElement())
|
||||
// Don't test the new snode as this would reveal the user's IP
|
||||
oldPaths.removeAt(pathIndex)
|
||||
val newPaths = oldPaths + listOf( path )
|
||||
paths = newPaths
|
||||
}
|
||||
|
||||
private fun dropPath(path: Path) {
|
||||
pathFailureCount[path] = 0
|
||||
val paths = OnionRequestAPI.paths.toMutableList()
|
||||
val pathIndex = paths.indexOf(path)
|
||||
if (pathIndex == -1) { return }
|
||||
paths.removeAt(pathIndex)
|
||||
OnionRequestAPI.paths = paths
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an onion around `payload` and returns the result.
|
||||
*/
|
||||
private fun buildOnionForDestination(payload: Map<*, *>, destination: Destination): Promise<OnionBuildingResult, Exception> {
|
||||
lateinit var guardSnode: Snode
|
||||
lateinit var destinationSymmetricKey: ByteArray // Needed by LokiAPI to decrypt the response sent back by the destination
|
||||
lateinit var encryptionResult: EncryptionResult
|
||||
val snodeToExclude = when (destination) {
|
||||
is Destination.Snode -> destination.snode
|
||||
is Destination.Server -> null
|
||||
}
|
||||
return getPath(snodeToExclude).bind(SnodeAPI.sharedContext) { path ->
|
||||
guardSnode = path.first()
|
||||
// Encrypt in reverse order, i.e. the destination first
|
||||
OnionRequestEncryption.encryptPayloadForDestination(payload, destination).bind(SnodeAPI.sharedContext) { r ->
|
||||
destinationSymmetricKey = r.symmetricKey
|
||||
// Recursively encrypt the layers of the onion (again in reverse order)
|
||||
encryptionResult = r
|
||||
@Suppress("NAME_SHADOWING") var path = path
|
||||
var rhs = destination
|
||||
fun addLayer(): Promise<EncryptionResult, Exception> {
|
||||
if (path.isEmpty()) {
|
||||
return Promise.of(encryptionResult)
|
||||
} else {
|
||||
val lhs = Destination.Snode(path.last())
|
||||
path = path.dropLast(1)
|
||||
return OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind(SnodeAPI.sharedContext) { r ->
|
||||
encryptionResult = r
|
||||
rhs = lhs
|
||||
addLayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
addLayer()
|
||||
}
|
||||
}.map(SnodeAPI.sharedContext) { OnionBuildingResult(guardSnode, encryptionResult, destinationSymmetricKey) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an onion request to `destination`. Builds new paths as needed.
|
||||
*/
|
||||
private fun sendOnionRequest(destination: Destination, payload: Map<*, *>, isJSONRequired: Boolean = true): Promise<Map<*, *>, Exception> {
|
||||
val deferred = deferred<Map<*, *>, Exception>()
|
||||
var guardSnode: Snode? = null
|
||||
buildOnionForDestination(payload, destination).success { result ->
|
||||
guardSnode = result.guardSnode
|
||||
val url = "${guardSnode!!.address}:${guardSnode!!.port}/onion_req/v2"
|
||||
val finalEncryptionResult = result.finalEncryptionResult
|
||||
val onion = finalEncryptionResult.ciphertext
|
||||
if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerAPI.maxFileSize.toDouble()) {
|
||||
Log.d("Loki", "Approaching request size limit: ~${onion.count()} bytes.")
|
||||
}
|
||||
@Suppress("NAME_SHADOWING") val parameters = mapOf(
|
||||
"ephemeral_key" to finalEncryptionResult.ephemeralPublicKey.toHexString()
|
||||
)
|
||||
val body: ByteArray
|
||||
try {
|
||||
body = OnionRequestEncryption.encode(onion, parameters)
|
||||
} catch (exception: Exception) {
|
||||
return@success deferred.reject(exception)
|
||||
}
|
||||
val destinationSymmetricKey = result.destinationSymmetricKey
|
||||
ThreadUtils.queue {
|
||||
try {
|
||||
val json = HTTP.execute(HTTP.Verb.POST, url, body)
|
||||
val base64EncodedIVAndCiphertext = json["result"] as? String ?: return@queue deferred.reject(Exception("Invalid JSON"))
|
||||
val ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext)
|
||||
try {
|
||||
val plaintext = DecryptionUtilities.decryptUsingAESGCM(ivAndCiphertext, destinationSymmetricKey)
|
||||
try {
|
||||
@Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java)
|
||||
val statusCode = 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)
|
||||
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
|
||||
if (!isJSONRequired) {
|
||||
body = mapOf( "result" to bodyAsString )
|
||||
} else {
|
||||
body = JsonUtil.fromJson(bodyAsString, Map::class.java)
|
||||
}
|
||||
}
|
||||
if (statusCode != 200) {
|
||||
val exception = HTTPRequestFailedAtDestinationException(statusCode, body)
|
||||
return@queue deferred.reject(exception)
|
||||
}
|
||||
deferred.resolve(body)
|
||||
} else {
|
||||
if (statusCode != 200) {
|
||||
val exception = HTTPRequestFailedAtDestinationException(statusCode, json)
|
||||
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) {
|
||||
deferred.reject(exception)
|
||||
}
|
||||
}
|
||||
}.fail { exception ->
|
||||
deferred.reject(exception)
|
||||
}
|
||||
val promise = deferred.promise
|
||||
promise.fail { exception ->
|
||||
val path = if (guardSnode != null) paths.firstOrNull { it.contains(guardSnode!!) } else null
|
||||
if (exception is HTTP.HTTPRequestFailedException) {
|
||||
fun handleUnspecificError() {
|
||||
if (path == null) { return }
|
||||
var pathFailureCount = OnionRequestAPI.pathFailureCount[path] ?: 0
|
||||
pathFailureCount += 1
|
||||
if (pathFailureCount >= pathFailureThreshold) {
|
||||
dropGuardSnode(guardSnode!!)
|
||||
path.forEach { snode ->
|
||||
@Suppress("ThrowableNotThrown")
|
||||
SnodeAPI.shared.handleSnodeError(exception.statusCode, exception.json, snode, null) // Intentionally don't throw
|
||||
}
|
||||
dropPath(path)
|
||||
} else {
|
||||
OnionRequestAPI.pathFailureCount[path] = pathFailureCount
|
||||
}
|
||||
}
|
||||
val json = exception.json
|
||||
val message = json?.get("result") as? String
|
||||
val prefix = "Next node not found: "
|
||||
if (message != null && message.startsWith(prefix)) {
|
||||
val ed25519PublicKey = message.substringAfter(prefix)
|
||||
val snode = path?.firstOrNull { it.publicKeySet!!.ed25519Key == ed25519PublicKey }
|
||||
if (snode != null) {
|
||||
var snodeFailureCount = OnionRequestAPI.snodeFailureCount[snode] ?: 0
|
||||
snodeFailureCount += 1
|
||||
if (snodeFailureCount >= snodeFailureThreshold) {
|
||||
@Suppress("ThrowableNotThrown")
|
||||
SnodeAPI.shared.handleSnodeError(exception.statusCode, json, snode, null) // Intentionally don't throw
|
||||
try {
|
||||
dropSnode(snode)
|
||||
} catch (exception: Exception) {
|
||||
handleUnspecificError()
|
||||
}
|
||||
} else {
|
||||
OnionRequestAPI.snodeFailureCount[snode] = snodeFailureCount
|
||||
}
|
||||
} else {
|
||||
handleUnspecificError()
|
||||
}
|
||||
} else if (message == "Loki Server error") {
|
||||
// Do nothing
|
||||
} else {
|
||||
handleUnspecificError()
|
||||
}
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Internal API
|
||||
/**
|
||||
* Sends an onion request to `snode`. Builds new paths as needed.
|
||||
*/
|
||||
internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, publicKey: String): Promise<Map<*, *>, Exception> {
|
||||
val payload = mapOf( "method" to method.rawValue, "params" to parameters )
|
||||
return sendOnionRequest(Destination.Snode(snode), payload).recover { exception ->
|
||||
@Suppress("NAME_SHADOWING") val exception = exception as? HTTPRequestFailedAtDestinationException ?: throw exception
|
||||
throw SnodeAPI.shared.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an onion request to `server`. Builds new paths as needed.
|
||||
*
|
||||
* `publicKey` is the hex encoded public key of the user the call is associated with. This is needed for swarm cache maintenance.
|
||||
*/
|
||||
public fun sendOnionRequest(request: Request, server: String, x25519PublicKey: String, target: String = "/loki/v3/lsrpc", isJSONRequired: Boolean = true): Promise<Map<*, *>, Exception> {
|
||||
val headers = request.getHeadersForOnionRequest()
|
||||
val url = request.url()
|
||||
val urlAsString = url.toString()
|
||||
val host = url.host()
|
||||
val endpoint = when {
|
||||
server.count() < urlAsString.count() -> urlAsString.substringAfter("$server/")
|
||||
else -> ""
|
||||
}
|
||||
val body = request.getBodyForOnionRequest() ?: "null"
|
||||
val payload = mapOf(
|
||||
"body" to body,
|
||||
"endpoint" to endpoint,
|
||||
"method" to request.method(),
|
||||
"headers" to headers
|
||||
)
|
||||
val destination = Destination.Server(host, target, x25519PublicKey)
|
||||
return sendOnionRequest(destination, payload, isJSONRequired).recover { exception ->
|
||||
Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api.onionrequests
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.api.utilities.EncryptionResult
|
||||
import org.session.libsignal.service.loki.api.utilities.EncryptionUtilities
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import java.nio.Buffer
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
object OnionRequestEncryption {
|
||||
|
||||
internal fun encode(ciphertext: ByteArray, json: Map<*, *>): ByteArray {
|
||||
// The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 |
|
||||
val jsonAsData = JsonUtil.toJson(json).toByteArray()
|
||||
val ciphertextSize = ciphertext.size
|
||||
val buffer = ByteBuffer.allocate(Int.SIZE_BYTES)
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN)
|
||||
buffer.putInt(ciphertextSize)
|
||||
val ciphertextSizeAsData = ByteArray(buffer.capacity())
|
||||
// Casting here avoids an issue where this gets compiled down to incorrect byte code. See
|
||||
// https://github.com/eclipse/jetty.project/issues/3244 for more info
|
||||
(buffer as Buffer).position(0)
|
||||
buffer.get(ciphertextSizeAsData)
|
||||
return ciphertextSizeAsData + ciphertext + jsonAsData
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
val deferred = deferred<EncryptionResult, Exception>()
|
||||
ThreadUtils.queue {
|
||||
try {
|
||||
// Wrapping isn't needed for file server or open group onion requests
|
||||
when (destination) {
|
||||
is OnionRequestAPI.Destination.Snode -> {
|
||||
val snodeX25519PublicKey = destination.snode.publicKeySet!!.x25519Key
|
||||
val payloadAsData = JsonUtil.toJson(payload).toByteArray()
|
||||
val plaintext = encode(payloadAsData, mapOf( "headers" to "" ))
|
||||
val result = EncryptionUtilities.encryptForX25519PublicKey(plaintext, snodeX25519PublicKey)
|
||||
deferred.resolve(result)
|
||||
}
|
||||
is OnionRequestAPI.Destination.Server -> {
|
||||
val plaintext = JsonUtil.toJson(payload).toByteArray()
|
||||
val result = EncryptionUtilities.encryptForX25519PublicKey(plaintext, destination.x25519PublicKey)
|
||||
deferred.resolve(result)
|
||||
}
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
deferred.reject(exception)
|
||||
}
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
val deferred = deferred<EncryptionResult, Exception>()
|
||||
ThreadUtils.queue {
|
||||
try {
|
||||
val payload: MutableMap<String, Any>
|
||||
when (rhs) {
|
||||
is OnionRequestAPI.Destination.Snode -> {
|
||||
payload = mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key )
|
||||
}
|
||||
is OnionRequestAPI.Destination.Server -> {
|
||||
payload = mutableMapOf( "host" to rhs.host, "target" to rhs.target, "method" to "POST" )
|
||||
}
|
||||
}
|
||||
payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
|
||||
val x25519PublicKey: String
|
||||
when (lhs) {
|
||||
is OnionRequestAPI.Destination.Snode -> {
|
||||
x25519PublicKey = lhs.snode.publicKeySet!!.x25519Key
|
||||
}
|
||||
is OnionRequestAPI.Destination.Server -> {
|
||||
x25519PublicKey = lhs.x25519PublicKey
|
||||
}
|
||||
}
|
||||
val plaintext = encode(previousEncryptionResult.ciphertext, payload)
|
||||
val result = EncryptionUtilities.encryptForX25519PublicKey(plaintext, x25519PublicKey)
|
||||
deferred.resolve(result)
|
||||
} catch (exception: Exception) {
|
||||
deferred.reject(exception)
|
||||
}
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
package org.session.libsignal.service.loki.utilities
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import java.io.*
|
||||
|
||||
object DownloadUtilities {
|
||||
|
||||
/**
|
||||
* Blocks the calling thread.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun downloadFile(destination: File, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
|
||||
val outputStream = FileOutputStream(destination) // Throws
|
||||
var remainingAttempts = 4
|
||||
var exception: Exception? = null
|
||||
while (remainingAttempts > 0) {
|
||||
remainingAttempts -= 1
|
||||
try {
|
||||
downloadFile(outputStream, url, maxSize, listener)
|
||||
exception = null
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
exception = e
|
||||
}
|
||||
}
|
||||
if (exception != null) { throw exception }
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks the calling thread.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
|
||||
// We need to throw a PushNetworkException or NonSuccessfulResponseCodeException
|
||||
// because the underlying Signal logic requires these to work correctly
|
||||
val oldPrefixedHost = "https://" + HttpUrl.get(url).host()
|
||||
var newPrefixedHost = oldPrefixedHost
|
||||
if (oldPrefixedHost.contains(FileServerAPI.fileStorageBucketURL)) {
|
||||
newPrefixedHost = FileServerAPI.shared.server
|
||||
}
|
||||
// Edge case that needs to work: https://file-static.lokinet.org/i1pNmpInq3w9gF3TP8TFCa1rSo38J6UM
|
||||
// → https://file.getsession.org/loki/v1/f/XLxogNXVEIWHk14NVCDeppzTujPHxu35
|
||||
val fileID = url.substringAfter(oldPrefixedHost).substringAfter("/f/")
|
||||
val sanitizedURL = "$newPrefixedHost/loki/v1/f/$fileID"
|
||||
val request = Request.Builder().url(sanitizedURL).get()
|
||||
try {
|
||||
val serverPublicKey = if (newPrefixedHost.contains(FileServerAPI.shared.server)) FileServerAPI.fileServerPublicKey
|
||||
else FileServerAPI.shared.getPublicKeyForOpenGroupServer(newPrefixedHost).get()
|
||||
val json = OnionRequestAPI.sendOnionRequest(request.build(), newPrefixedHost, serverPublicKey, isJSONRequired = false).get()
|
||||
val result = json["result"] as? String
|
||||
if (result == null) {
|
||||
Log.d("Loki", "Couldn't parse attachment from: $json.")
|
||||
throw PushNetworkException("Missing response body.")
|
||||
}
|
||||
val body = Base64.decode(result)
|
||||
if (body.size > maxSize) {
|
||||
Log.d("Loki", "Attachment size limit exceeded.")
|
||||
throw PushNetworkException("Max response size exceeded.")
|
||||
}
|
||||
body.inputStream().use { input ->
|
||||
val buffer = ByteArray(32768)
|
||||
var count = 0
|
||||
var bytes = input.read(buffer)
|
||||
while (bytes >= 0) {
|
||||
outputStream.write(buffer, 0, bytes)
|
||||
count += bytes
|
||||
if (count > maxSize) {
|
||||
Log.d("Loki", "Attachment size limit exceeded.")
|
||||
throw PushNetworkException("Max response size exceeded.")
|
||||
}
|
||||
listener?.onAttachmentProgress(body.size.toLong(), count.toLong())
|
||||
bytes = input.read(buffer)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Couldn't download attachment due to error: $e.")
|
||||
throw if (e is NonSuccessfulResponseCodeException) e else PushNetworkException(e)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user