pull/503/head
Niels Andriesse 3 years ago
parent 7415c728eb
commit bc66c45bca

@ -32,16 +32,13 @@ import androidx.multidex.MultiDexApplication;
import org.conscrypt.Conscrypt;
import org.session.libsession.messaging.MessagingConfiguration;
import org.session.libsession.messaging.avatars.AvatarHelper;
import org.session.libsession.messaging.fileserver.FileServerAPI;
import org.session.libsession.messaging.jobs.JobQueue;
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI;
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller;
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.snode.SnodeAPI;
import org.session.libsession.snode.SnodeConfiguration;
import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.IdentityKeyUtil;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
@ -96,7 +93,6 @@ import org.webrtc.voiceengine.WebRtcAudioUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Date;
import java.util.HashSet;
@ -177,7 +173,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
DatabaseFactory.getStorage(this),
DatabaseFactory.getAttachmentProvider(this),
new SessionProtocolImpl(this));
SnodeConfiguration.Companion.configure(apiDB, broadcaster);
SnodeModule.Companion.configure(apiDB, broadcaster);
if (userPublicKey != null) {
MentionsManager.Companion.configureIfNeeded(userPublicKey, threadDB, userDB);
}

@ -18,7 +18,7 @@ import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.snode.RawResponsePromise
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeConfiguration
import org.session.libsession.snode.SnodeModule
import org.session.libsession.snode.SnodeMessage
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsignal.service.internal.push.PushTransportDetails
@ -82,7 +82,7 @@ object MessageSender {
fun handleFailure(error: Exception) {
handleFailedMessageSend(message, error)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeConfiguration.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
SnodeModule.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
}
deferred.reject(error)
}
@ -147,12 +147,12 @@ object MessageSender {
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
// Send the result
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeConfiguration.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!)
SnodeModule.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!)
}
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
val snodeMessage = SnodeMessage(message.recipient!!, base64EncodedData, message.ttl, message.sentTimestamp!!)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
SnodeModule.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
}
SnodeAPI.sendMessage(snodeMessage).success { promises: Set<RawResponsePromise> ->
var isSuccess = false
@ -163,7 +163,7 @@ object MessageSender {
if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds
isSuccess = true
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeConfiguration.shared.broadcaster.broadcast("messageSent", message.sentTimestamp!!)
SnodeModule.shared.broadcaster.broadcast("messageSent", message.sentTimestamp!!)
}
handleSuccessfulMessageSend(message, destination, isSyncMessage)
var shouldNotify = (message is VisibleMessage && !isSyncMessage)

@ -7,7 +7,7 @@ import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeConfiguration
import org.session.libsession.snode.SnodeModule
import org.session.libsignal.service.loki.api.Snode
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.logging.Log
@ -47,9 +47,9 @@ class Poller {
private fun setUpPolling() {
if (!hasStarted) { return; }
val thread = Thread.currentThread()
SnodeAPI.getSwarm(userPublicKey).bind(SnodeAPI.messagePollingContext) {
SnodeAPI.getSwarm(userPublicKey).bind {
usedSnodes.clear()
val deferred = deferred<Unit, Exception>(SnodeAPI.messagePollingContext)
val deferred = deferred<Unit, Exception>()
pollNextSnode(deferred)
deferred.promise
}.always {
@ -63,7 +63,7 @@ class Poller {
}
private fun pollNextSnode(deferred: Deferred<Unit, Exception>) {
val swarm = SnodeConfiguration.shared.storage.getSwarm(userPublicKey) ?: setOf()
val swarm = SnodeModule.shared.storage.getSwarm(userPublicKey) ?: setOf()
val unusedSnodes = swarm.subtract(usedSnodes)
if (unusedSnodes.isNotEmpty()) {
val index = SecureRandom().nextInt(unusedSnodes.size)
@ -87,7 +87,7 @@ class Poller {
private fun poll(snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> {
if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) }
return SnodeAPI.getRawMessages(snode, userPublicKey).bind(SnodeAPI.messagePollingContext) { rawResponse ->
return SnodeAPI.getRawMessages(snode, userPublicKey).bind { rawResponse ->
isCaughtUp = true
if (deferred.promise.isDone()) {
task { Unit } // The long polling connection has been canceled; don't recurse

@ -83,7 +83,7 @@ open class DotNetAPI {
Log.d("Loki", "Requesting auth token for server: $server.")
val userKeyPair = MessagingConfiguration.shared.storage.getUserKeyPair() ?: throw Error.Generic
val parameters: Map<String, Any> = mapOf( "pubKey" to userKeyPair.first )
return execute(HTTPVerb.GET, server, "loki/v1/get_challenge", false, parameters).map(SnodeAPI.sharedContext) { json ->
return execute(HTTPVerb.GET, server, "loki/v1/get_challenge", false, parameters).map { json ->
try {
val base64EncodedChallenge = json["cipherText64"] as String
val challenge = Base64.decode(base64EncodedChallenge)

@ -17,6 +17,7 @@ import org.session.libsession.utilities.AESGCM.EncryptionResult
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsession.utilities.getBodyForOnionRequest
import org.session.libsession.utilities.getHeadersForOnionRequest
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.*
private typealias Path = List<Snode>
@ -25,16 +26,21 @@ private typealias Path = List<Snode>
* See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
*/
object OnionRequestAPI {
private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
private val broadcaster: Broadcaster
get() = SnodeModule.shared.broadcaster
private val pathFailureCount = mutableMapOf<Path, Int>()
private val snodeFailureCount = mutableMapOf<Snode, Int>()
var guardSnodes = setOf<Snode>()
var paths: List<Path> // Not a set to ensure we consistently show the same path to the user
get() = SnodeAPI.database.getOnionRequestPaths()
get() = database.getOnionRequestPaths()
set(newValue) {
if (newValue.isEmpty()) {
SnodeAPI.database.clearOnionRequestPaths()
database.clearOnionRequestPaths()
} else {
SnodeAPI.database.setOnionRequestPaths(newValue)
database.setOnionRequestPaths(newValue)
}
}
@ -51,16 +57,15 @@ object OnionRequestAPI {
* The number of times a snode can fail before it's replaced.
*/
private const val snodeFailureThreshold = 1
/**
* The number of paths to maintain.
*/
const 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
/**
* The number of paths to maintain.
*/
const val targetPathCount = 2 // A main path and a backup path for the case where the target snode is in the main path
// endregion
class HTTPRequestFailedAtDestinationException(val statusCode: Int, val json: Map<*, *>)
@ -113,7 +118,7 @@ object OnionRequestAPI {
return Promise.of(guardSnodes)
} else {
Log.d("Loki", "Populating guard snode cache.")
return SnodeAPI.getRandomSnode().bind(SnodeAPI.sharedContext) { // Just used to populate the snode pool
return SnodeAPI.getRandomSnode().bind { // Just used to populate the snode pool
var unusedSnodes = SnodeAPI.snodePool.minus(reusableGuardSnodes)
val reusableGuardSnodeCount = reusableGuardSnodes.count()
if (unusedSnodes.count() < (targetGuardSnodeCount - reusableGuardSnodeCount)) { throw InsufficientSnodesException() }
@ -138,7 +143,7 @@ object OnionRequestAPI {
return deferred.promise
}
val promises = (0 until (targetGuardSnodeCount - reusableGuardSnodeCount)).map { getGuardSnode() }
all(promises).map(SnodeAPI.sharedContext) { guardSnodes ->
all(promises).map { guardSnodes ->
val guardSnodesAsSet = (guardSnodes + reusableGuardSnodes).toSet()
OnionRequestAPI.guardSnodes = guardSnodesAsSet
guardSnodesAsSet
@ -153,10 +158,10 @@ object OnionRequestAPI {
*/
private fun buildPaths(reusablePaths: List<Path>): Promise<List<Path>, Exception> {
Log.d("Loki", "Building onion request paths.")
SnodeAPI.broadcaster.broadcast("buildingPaths")
return SnodeAPI.getRandomSnode().bind(SnodeAPI.sharedContext) { // Just used to populate the snode pool
broadcaster.broadcast("buildingPaths")
return SnodeAPI.getRandomSnode().bind { // Just used to populate the snode pool
val reusableGuardSnodes = reusablePaths.map { it[0] }
getGuardSnodes(reusableGuardSnodes).map(SnodeAPI.sharedContext) { guardSnodes ->
getGuardSnodes(reusableGuardSnodes).map { guardSnodes ->
var unusedSnodes = SnodeAPI.snodePool.minus(guardSnodes).minus(reusablePaths.flatten())
val reusableGuardSnodeCount = reusableGuardSnodes.count()
val pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
@ -173,7 +178,7 @@ object OnionRequestAPI {
}
}.map { paths ->
OnionRequestAPI.paths = paths + reusablePaths
SnodeAPI.broadcaster.broadcast("pathsBuilt")
broadcaster.broadcast("pathsBuilt")
paths
}
}
@ -207,12 +212,12 @@ object OnionRequestAPI {
buildPaths(paths) // Re-build paths in the background
return Promise.of(getPath(paths))
} else {
return buildPaths(paths).map(SnodeAPI.sharedContext) { newPaths ->
return buildPaths(paths).map { newPaths ->
getPath(newPaths)
}
}
} else {
return buildPaths(listOf()).map(SnodeAPI.sharedContext) { newPaths ->
return buildPaths(listOf()).map { newPaths ->
getPath(newPaths)
}
}
@ -263,10 +268,10 @@ object OnionRequestAPI {
is Destination.Snode -> destination.snode
is Destination.Server -> null
}
return getPath(snodeToExclude).bind(SnodeAPI.sharedContext) { path ->
return getPath(snodeToExclude).bind { path ->
guardSnode = path.first()
// Encrypt in reverse order, i.e. the destination first
OnionRequestEncryption.encryptPayloadForDestination(payload, destination).bind(SnodeAPI.sharedContext) { r ->
OnionRequestEncryption.encryptPayloadForDestination(payload, destination).bind { r ->
destinationSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r
@ -278,7 +283,7 @@ object OnionRequestAPI {
} else {
val lhs = Destination.Snode(path.last())
path = path.dropLast(1)
return OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind(SnodeAPI.sharedContext) { r ->
return OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind { r ->
encryptionResult = r
rhs = lhs
addLayer()
@ -287,7 +292,7 @@ object OnionRequestAPI {
}
addLayer()
}
}.map(SnodeAPI.sharedContext) { OnionBuildingResult(guardSnode, encryptionResult, destinationSymmetricKey) }
}.map { OnionBuildingResult(guardSnode, encryptionResult, destinationSymmetricKey) }
}
/**

@ -1,17 +1,10 @@
package org.session.libsession.snode
class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
val ip: String get() = address.removePrefix("https://")
internal enum class Method(val rawValue: String) {
/**
* Only supported by snode targets.
*/
GetSwarm("get_snodes_for_pubkey"),
/**
* Only supported by snode targets.
*/
GetMessages("retrieve"),
SendMessage("store")
}

@ -19,12 +19,10 @@ import org.session.libsignal.utilities.logging.Log
import java.security.SecureRandom
object SnodeAPI {
val database: LokiAPIDatabaseProtocol
get() = SnodeConfiguration.shared.storage
val broadcaster: Broadcaster
get() = SnodeConfiguration.shared.broadcaster
val sharedContext = Kovenant.createContext()
val messagePollingContext = Kovenant.createContext()
private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
private val broadcaster: Broadcaster
get() = SnodeModule.shared.broadcaster
internal var snodeFailureCount: MutableMap<Snode, Int> = mutableMapOf()
internal var snodePool: Set<Snode>
@ -33,30 +31,27 @@ object SnodeAPI {
// Settings
private val maxRetryCount = 6
private val minimumSnodePoolCount = 64
private val minimumSnodePoolCount = 24
private val minimumSwarmSnodeCount = 2
// 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
// Use port 4433 if the API level can handle the network security configuration and enforce pinned certificates
private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433
private val seedNodePool by lazy {
if (useTestnet) {
setOf( "http://public.loki.foundation:38157" )
} else {
setOf( "https://storage.seed1.loki.network:$seedPort", "https://storage.seed3.loki.network:$seedPort", "https://public.loki.foundation:$seedPort" )
setOf( "https://storage.seed1.loki.network:$seedNodePort", "https://storage.seed3.loki.network:$seedNodePort", "https://public.loki.foundation:$seedNodePort" )
}
}
private val snodeFailureThreshold = 4
private val targetSwarmSnodeCount = 2
private val useOnionRequests = true
internal val useTestnet = false
internal var powDifficulty = 1
internal val useTestnet = true
// Error
internal sealed class Error(val description: String) : Exception(description) {
object Generic : Error("An error occurred.")
object ClockOutOfSync : Error("The user's clock is out of sync with the service node network.")
object RandomSnodePoolUpdatingFailed : Error("Failed to update random service node pool.")
object ClockOutOfSync : Error("Your clock is out of sync with the Service Node network.")
}
// Internal API
@ -99,7 +94,7 @@ object SnodeAPI {
)
)
val deferred = deferred<Snode, Exception>()
deferred<org.session.libsignal.service.loki.api.Snode, Exception>(SnodeAPI.sharedContext)
deferred<Snode, Exception>()
ThreadUtils.queue {
try {
val json = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true)
@ -170,7 +165,7 @@ object SnodeAPI {
val parameters = mapOf( "pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey )
return getRandomSnode().bind {
invoke(Snode.Method.GetSwarm, it, publicKey, parameters)
}.map(sharedContext) {
}.map {
parseSnodes(it).toSet()
}.success {
database.setSwarm(publicKey, it)
@ -186,8 +181,8 @@ object SnodeAPI {
fun getMessages(publicKey: String): MessageListPromise {
return retryIfNeeded(maxRetryCount) {
getSingleTargetSnode(publicKey).bind(messagePollingContext) { snode ->
getRawMessages(snode, publicKey).map(messagePollingContext) { parseRawMessagesResponse(it, snode, publicKey) }
getSingleTargetSnode(publicKey).bind { snode ->
getRawMessages(snode, publicKey).map { parseRawMessagesResponse(it, snode, publicKey) }
}
}
}
@ -199,19 +194,7 @@ object SnodeAPI {
swarm.map { snode ->
val parameters = message.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
}
invoke(Snode.Method.SendMessage, snode, destination, parameters)
}
}.toSet()
}
@ -256,7 +239,6 @@ object SnodeAPI {
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()) {
@ -316,20 +298,6 @@ object SnodeAPI {
Log.d("Loki", "Got a 421 without an associated public key.")
}
}
432 -> {
// The PoW difficulty is too low
val powDifficulty = json?.get("difficulty") as? Int
if (powDifficulty != null) {
if (powDifficulty < 100) {
Log.d("Loki", "Setting proof of work difficulty to $powDifficulty (snode: $snode).")
SnodeAPI.powDifficulty = powDifficulty
} else {
handleBadSnode()
}
} else {
Log.d("Loki", "Failed to update proof of work difficulty.")
}
}
else -> {
handleBadSnode()
Log.d("Loki", "Unhandled response code: ${statusCode}.")
@ -338,8 +306,6 @@ object SnodeAPI {
}
return null
}
}
// Type Aliases

@ -12,12 +12,14 @@ data class SnodeMessage(
// When the proof of work was calculated.
val timestamp: Long
) {
internal fun toJSON(): Map<String, String> {
return mutableMapOf(
return mapOf(
"pubKey" to if (SnodeAPI.useTestnet) recipient.removing05PrefixIfNeeded() else recipient,
"data" to data,
"ttl" to ttl.toString(),
"timestamp" to timestamp.toString(),
"nonce" to "")
"nonce" to ""
)
}
}

@ -3,13 +3,14 @@ package org.session.libsession.snode
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.Broadcaster
class SnodeConfiguration(val storage: LokiAPIDatabaseProtocol, val broadcaster: Broadcaster) {
class SnodeModule(val storage: LokiAPIDatabaseProtocol, val broadcaster: Broadcaster) {
companion object {
lateinit var shared: SnodeConfiguration
lateinit var shared: SnodeModule
fun configure(storage: LokiAPIDatabaseProtocol, broadcaster: Broadcaster) {
if (Companion::shared.isInitialized) { return }
shared = SnodeConfiguration(storage, broadcaster)
shared = SnodeModule(storage, broadcaster)
}
}
}

@ -1,6 +1,7 @@
package org.session.libsession.snode
interface SnodeStorageProtocol {
fun getSnodePool(): Set<Snode>
fun setSnodePool(newValue: Set<Snode>)
fun getOnionRequestPaths(): List<List<Snode>>

Loading…
Cancel
Save