This commit is contained in:
Niels Andriesse 2021-04-26 10:26:31 +10:00
parent 7415c728eb
commit bc66c45bca
10 changed files with 68 additions and 104 deletions

View File

@ -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);
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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) }
}
/**

View File

@ -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")
}

View File

@ -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
@ -94,12 +89,12 @@ object SnodeAPI {
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 )
"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<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

View File

@ -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(
"pubKey" to if (SnodeAPI.useTestnet) recipient.removing05PrefixIfNeeded() else recipient,
"data" to data,
"ttl" to ttl.toString(),
"timestamp" to timestamp.toString(),
"nonce" to "")
return mapOf(
"pubKey" to if (SnodeAPI.useTestnet) recipient.removing05PrefixIfNeeded() else recipient,
"data" to data,
"ttl" to ttl.toString(),
"timestamp" to timestamp.toString(),
"nonce" to ""
)
}
}

View File

@ -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)
}
}
}

View File

@ -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>>