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.conscrypt.Conscrypt;
|
||||||
import org.session.libsession.messaging.MessagingConfiguration;
|
import org.session.libsession.messaging.MessagingConfiguration;
|
||||||
import org.session.libsession.messaging.avatars.AvatarHelper;
|
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.jobs.JobQueue;
|
||||||
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
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.ClosedGroupPoller;
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||||
import org.session.libsession.messaging.threads.Address;
|
import org.session.libsession.messaging.threads.Address;
|
||||||
|
import org.session.libsession.snode.SnodeAPI;
|
||||||
import org.session.libsession.snode.SnodeConfiguration;
|
import org.session.libsession.snode.SnodeConfiguration;
|
||||||
import org.session.libsession.utilities.IdentityKeyUtil;
|
import org.session.libsession.utilities.IdentityKeyUtil;
|
||||||
import org.session.libsession.utilities.SSKEnvironment;
|
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.dynamiclanguage.LocaleParser;
|
||||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil;
|
import org.session.libsession.utilities.preferences.ProfileKeyUtil;
|
||||||
import org.session.libsignal.service.api.util.StreamDetails;
|
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.database.LokiAPIDatabaseProtocol;
|
||||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
|
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
@ -96,6 +95,7 @@ import org.webrtc.voiceengine.WebRtcAudioUtils;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -179,11 +179,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
new SessionProtocolImpl(this));
|
new SessionProtocolImpl(this));
|
||||||
SnodeConfiguration.Companion.configure(apiDB, broadcaster);
|
SnodeConfiguration.Companion.configure(apiDB, broadcaster);
|
||||||
if (userPublicKey != null) {
|
if (userPublicKey != null) {
|
||||||
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
|
||||||
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
|
|
||||||
MentionsManager.Companion.configureIfNeeded(userPublicKey, threadDB, userDB);
|
MentionsManager.Companion.configureIfNeeded(userPublicKey, threadDB, userDB);
|
||||||
}
|
}
|
||||||
PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG);
|
|
||||||
setUpStorageAPIIfNeeded();
|
setUpStorageAPIIfNeeded();
|
||||||
resubmitProfilePictureIfNeeded();
|
resubmitProfilePictureIfNeeded();
|
||||||
publicChatManager = new PublicChatManager(this);
|
publicChatManager = new PublicChatManager(this);
|
||||||
@ -427,7 +424,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
}
|
}
|
||||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
||||||
LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||||
FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
|
||||||
org.session.libsession.messaging.fileserver.FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
org.session.libsession.messaging.fileserver.FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -458,13 +454,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||||
if (userPublicKey == null) return;
|
if (userPublicKey == null) return;
|
||||||
if (poller != null) {
|
if (poller != null) {
|
||||||
SnodeAPI.shared.setUserPublicKey(userPublicKey);
|
|
||||||
poller.setUserPublicKey(userPublicKey);
|
poller.setUserPublicKey(userPublicKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||||
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
|
||||||
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
|
|
||||||
poller = new Poller();
|
poller = new Poller();
|
||||||
closedGroupPoller = new ClosedGroupPoller();
|
closedGroupPoller = new ClosedGroupPoller();
|
||||||
}
|
}
|
||||||
@ -503,12 +496,13 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
try {
|
try {
|
||||||
File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey));
|
File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey));
|
||||||
StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length());
|
StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length());
|
||||||
FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> {
|
throw new IOException();
|
||||||
TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime());
|
// FileServerAPI.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> {
|
||||||
TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt());
|
// TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime());
|
||||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey);
|
// TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt());
|
||||||
return Unit.INSTANCE;
|
// ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey);
|
||||||
});
|
// return Unit.INSTANCE;
|
||||||
|
// });
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,12 @@ import android.widget.Toast
|
|||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import kotlinx.android.synthetic.main.activity_path.*
|
import kotlinx.android.synthetic.main.activity_path.*
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.loki.utilities.*
|
import org.thoughtcrime.securesms.loki.utilities.*
|
||||||
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
|
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
|
||||||
import org.thoughtcrime.securesms.loki.views.PathDotView
|
import org.thoughtcrime.securesms.loki.views.PathDotView
|
||||||
import org.session.libsignal.service.loki.api.Snode
|
import org.session.libsignal.service.loki.api.Snode
|
||||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
|
||||||
|
|
||||||
class PathActivity : PassphraseRequiredActionBarActivity() {
|
class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||||
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
|
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
|
||||||
|
@ -28,13 +28,13 @@ import nl.komponents.kovenant.functional.bind
|
|||||||
import nl.komponents.kovenant.task
|
import nl.komponents.kovenant.task
|
||||||
import nl.komponents.kovenant.ui.alwaysUi
|
import nl.komponents.kovenant.ui.alwaysUi
|
||||||
import org.session.libsession.messaging.avatars.AvatarHelper
|
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.opengroups.OpenGroupAPI
|
||||||
import org.session.libsession.messaging.threads.Address
|
import org.session.libsession.messaging.threads.Address
|
||||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||||
import org.session.libsignal.service.api.util.StreamDetails
|
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.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
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.jobs.MessageReceiveJob
|
||||||
import org.session.libsession.messaging.opengroups.OpenGroup
|
import org.session.libsession.messaging.opengroups.OpenGroup
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
||||||
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.service.loki.api.SnodeAPI
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
|
class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||||
@ -70,10 +71,11 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
|||||||
|
|
||||||
// Private chats
|
// Private chats
|
||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||||
val privateChatsPromise = SnodeAPI.shared.getMessages(userPublicKey).map { envelopes ->
|
val privateChatsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes ->
|
||||||
envelopes.map { envelope ->
|
throw IOException()
|
||||||
MessageReceiveJob(envelope.toByteArray(), false).executeAsync()
|
// envelopes.map { envelope ->
|
||||||
}
|
// MessageReceiveJob(envelope.toByteArray(), false).executeAsync()
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
promises.addAll(privateChatsPromise.get())
|
promises.addAll(privateChatsPromise.get())
|
||||||
|
|
||||||
|
@ -3,12 +3,12 @@ package org.thoughtcrime.securesms.loki.api
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import okhttp3.*
|
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.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
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
|
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||||
|
|
||||||
object LokiPushNotificationManager {
|
object LokiPushNotificationManager {
|
||||||
@ -16,10 +16,10 @@ object LokiPushNotificationManager {
|
|||||||
private val tokenExpirationInterval = 12 * 60 * 60 * 1000
|
private val tokenExpirationInterval = 12 * 60 * 60 * 1000
|
||||||
|
|
||||||
private val server by lazy {
|
private val server by lazy {
|
||||||
PushNotificationAPI.shared.server
|
PushNotificationAPI.server
|
||||||
}
|
}
|
||||||
private val pnServerPublicKey by lazy {
|
private val pnServerPublicKey by lazy {
|
||||||
PushNotificationAPI.pnServerPublicKey
|
PushNotificationAPI.serverPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ClosedGroupOperation {
|
enum class ClosedGroupOperation {
|
||||||
|
@ -7,7 +7,7 @@ import android.content.IntentFilter
|
|||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import com.opencsv.CSVReader
|
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 org.session.libsignal.utilities.ThreadUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
@ -11,9 +11,9 @@ import android.util.AttributeSet
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
|
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
|
||||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
|
||||||
|
|
||||||
class PathStatusView : View {
|
class PathStatusView : View {
|
||||||
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
|
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
|
||||||
|
@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms;
|
|||||||
|
|
||||||
import android.content.Context;
|
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 {
|
public class PushMediaConstraints extends MediaConstraints {
|
||||||
|
|
||||||
|
@ -4,10 +4,10 @@ import nl.komponents.kovenant.Promise
|
|||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
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.logging.Log
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
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.database.LokiAPIDatabaseProtocol
|
||||||
import org.session.libsignal.service.loki.utilities.*
|
import org.session.libsignal.service.loki.utilities.*
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
@ -8,12 +8,12 @@ import nl.komponents.kovenant.then
|
|||||||
import org.session.libsession.messaging.MessagingConfiguration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
import org.session.libsession.messaging.fileserver.FileServerAPI
|
import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
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.service.loki.utilities.retryIfNeeded
|
||||||
import org.session.libsignal.utilities.*
|
import org.session.libsignal.utilities.*
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -316,7 +316,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
Log.d("Loki", "Downloading open group profile picture from \"$url\".")
|
Log.d("Loki", "Downloading open group profile picture from \"$url\".")
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
try {
|
try {
|
||||||
DownloadUtilities.downloadFile(outputStream, url, FileServerAPI.maxFileSize, null)
|
throw IOException();
|
||||||
Log.d("Loki", "Open group profile picture was successfully loaded from \"$url\"")
|
Log.d("Loki", "Open group profile picture was successfully loaded from \"$url\"")
|
||||||
return outputStream.toByteArray()
|
return outputStream.toByteArray()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -23,7 +23,6 @@ import org.session.libsession.snode.SnodeMessage
|
|||||||
import org.session.libsession.utilities.SSKEnvironment
|
import org.session.libsession.utilities.SSKEnvironment
|
||||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
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.service.loki.utilities.hexEncodedPublicKey
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
@ -151,11 +150,9 @@ object MessageSender {
|
|||||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||||
SnodeConfiguration.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!)
|
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
|
// 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) {
|
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||||
SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
|
SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,8 @@ import okhttp3.MediaType
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import org.session.libsession.messaging.MessagingConfiguration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
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.service.loki.utilities.retryIfNeeded
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
import org.session.libsignal.utilities.logging.Log
|
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.bind
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||||
import org.session.libsession.utilities.AESGCM
|
import org.session.libsession.utilities.AESGCM
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.session.libsignal.utilities.*
|
import org.session.libsignal.utilities.*
|
||||||
import org.session.libsignal.service.loki.api.Snode
|
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.libsignal.service.loki.api.utilities.*
|
||||||
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
|
@ -8,9 +8,7 @@ data class SnodeMessage(
|
|||||||
// The time to live for the message in milliseconds.
|
// The time to live for the message in milliseconds.
|
||||||
val ttl: Long,
|
val ttl: Long,
|
||||||
// When the proof of work was calculated.
|
// When the proof of work was calculated.
|
||||||
val timestamp: Long,
|
val timestamp: Long
|
||||||
// The base 64 encoded proof of work.
|
|
||||||
val nonce: String
|
|
||||||
) {
|
) {
|
||||||
internal fun toJSON(): Map<String, String> {
|
internal fun toJSON(): Map<String, String> {
|
||||||
return mutableMapOf(
|
return mutableMapOf(
|
||||||
@ -18,6 +16,6 @@ data class SnodeMessage(
|
|||||||
"data" to data,
|
"data" to data,
|
||||||
"ttl" to ttl.toString(),
|
"ttl" to ttl.toString(),
|
||||||
"timestamp" to timestamp.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.SignalServiceAttachment.ProgressListener;
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
|
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||||
import org.session.libsignal.service.loki.utilities.DownloadUtilities;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
@ -46,8 +45,7 @@ public class SignalServiceMessageReceiver {
|
|||||||
public InputStream retrieveProfileAvatar(String path, File destination, byte[] profileKey, int maxSizeBytes)
|
public InputStream retrieveProfileAvatar(String path, File destination, byte[] profileKey, int maxSizeBytes)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
DownloadUtilities.downloadFile(destination, path, maxSizeBytes, null);
|
throw new IOException();
|
||||||
return new ProfileCipherInputStream(new FileInputStream(destination), profileKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,13 +63,6 @@ public class SignalServiceMessageReceiver {
|
|||||||
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes, ProgressListener listener)
|
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes, ProgressListener listener)
|
||||||
throws IOException, InvalidMessageException
|
throws IOException, InvalidMessageException
|
||||||
{
|
{
|
||||||
// Loki - Fetch attachment
|
throw new IOException();
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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