|
|
|
@ -21,11 +21,13 @@ import org.session.libsignal.utilities.Log
|
|
|
|
|
import org.session.libsignal.utilities.SessionId
|
|
|
|
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
|
|
|
|
|
|
|
|
class InvalidDestination: Exception("Trying to push configs somewhere other than our swarm or a closed group")
|
|
|
|
|
class InvalidContactDestination: Exception("Trying to push to non-user config swarm")
|
|
|
|
|
class InvalidDestination :
|
|
|
|
|
Exception("Trying to push configs somewhere other than our swarm or a closed group")
|
|
|
|
|
|
|
|
|
|
class InvalidContactDestination : Exception("Trying to push to non-user config swarm")
|
|
|
|
|
|
|
|
|
|
// only contact (self) and closed group destinations will be supported
|
|
|
|
|
data class ConfigurationSyncJob(val destination: Destination): Job {
|
|
|
|
|
data class ConfigurationSyncJob(val destination: Destination) : Job {
|
|
|
|
|
|
|
|
|
|
override var delegate: JobDelegate? = null
|
|
|
|
|
override var id: String? = null
|
|
|
|
@ -34,65 +36,106 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
|
|
|
|
|
|
|
|
|
|
val shouldRunAgain = AtomicBoolean(false)
|
|
|
|
|
|
|
|
|
|
data class ConfigMessageInformation(val batch: SnodeBatchRequestInfo, val config: Config, val seqNo: Long?) // seqNo will be null for keys type
|
|
|
|
|
|
|
|
|
|
data class SyncInformation(val configs: List<ConfigMessageInformation>, val toDelete: List<String>)
|
|
|
|
|
|
|
|
|
|
private fun destinationConfigs(delegate: JobDelegate,
|
|
|
|
|
dispatcherName: String,
|
|
|
|
|
configFactoryProtocol: ConfigFactoryProtocol): SyncInformation {
|
|
|
|
|
data class ConfigMessageInformation(
|
|
|
|
|
val batch: SnodeBatchRequestInfo,
|
|
|
|
|
val config: Config,
|
|
|
|
|
val seqNo: Long?
|
|
|
|
|
) // seqNo will be null for keys type
|
|
|
|
|
|
|
|
|
|
data class SyncInformation(
|
|
|
|
|
val configs: List<ConfigMessageInformation>,
|
|
|
|
|
val toDelete: List<String>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private fun destinationConfigs(
|
|
|
|
|
delegate: JobDelegate,
|
|
|
|
|
dispatcherName: String,
|
|
|
|
|
configFactoryProtocol: ConfigFactoryProtocol
|
|
|
|
|
): SyncInformation {
|
|
|
|
|
val toDelete = mutableListOf<String>()
|
|
|
|
|
val configsRequiringPush = if (destination is Destination.ClosedGroup) {
|
|
|
|
|
val sentTimestamp = SnodeAPI.nowWithOffset
|
|
|
|
|
// destination is a closed group, get all configs requiring push here
|
|
|
|
|
val groupId = SessionId.from(destination.publicKey)
|
|
|
|
|
|
|
|
|
|
val signingKey = configFactoryProtocol.userGroups!!.getClosedGroup(destination.publicKey)!!.signingKey()
|
|
|
|
|
|
|
|
|
|
val keys = configFactoryProtocol.getGroupKeysConfig(groupId)!!
|
|
|
|
|
val info = configFactoryProtocol.getGroupInfoConfig(groupId)!!
|
|
|
|
|
val members = configFactoryProtocol.getGroupMemberConfig(groupId)!!
|
|
|
|
|
|
|
|
|
|
val requiringPush = listOf(keys, info, members).filter {
|
|
|
|
|
when (it) {
|
|
|
|
|
is GroupKeysConfig -> it.pendingConfig()?.isNotEmpty() == true
|
|
|
|
|
is ConfigBase -> it.needsPush()
|
|
|
|
|
else -> false
|
|
|
|
|
val configsRequiringPush =
|
|
|
|
|
if (destination is Destination.ClosedGroup) {
|
|
|
|
|
val sentTimestamp = SnodeAPI.nowWithOffset
|
|
|
|
|
// destination is a closed group, get all configs requiring push here
|
|
|
|
|
val groupId = SessionId.from(destination.publicKey)
|
|
|
|
|
|
|
|
|
|
val signingKey =
|
|
|
|
|
configFactoryProtocol.userGroups!!.getClosedGroup(
|
|
|
|
|
destination.publicKey
|
|
|
|
|
)!!
|
|
|
|
|
.signingKey()
|
|
|
|
|
|
|
|
|
|
val keys = configFactoryProtocol.getGroupKeysConfig(groupId)!!
|
|
|
|
|
val info = configFactoryProtocol.getGroupInfoConfig(groupId)!!
|
|
|
|
|
val members = configFactoryProtocol.getGroupMemberConfig(groupId)!!
|
|
|
|
|
|
|
|
|
|
val requiringPush =
|
|
|
|
|
listOf(keys, info, members).filter {
|
|
|
|
|
when (it) {
|
|
|
|
|
is GroupKeysConfig -> it.pendingConfig()?.isNotEmpty() == true
|
|
|
|
|
is ConfigBase -> it.needsPush()
|
|
|
|
|
else -> false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// free the objects that were created but won't be used after this point
|
|
|
|
|
// in case any of the configs don't need pushing, they won't be freed later
|
|
|
|
|
(listOf(keys, info, members) subtract requiringPush).forEach(Config::free)
|
|
|
|
|
|
|
|
|
|
requiringPush.map { config ->
|
|
|
|
|
val (push, seqNo, obsoleteHashes) =
|
|
|
|
|
if (config is GroupKeysConfig) {
|
|
|
|
|
ConfigPush(
|
|
|
|
|
config.pendingConfig()!!,
|
|
|
|
|
0,
|
|
|
|
|
emptyList()
|
|
|
|
|
) // should not be null from filter step previous
|
|
|
|
|
} else if (config is ConfigBase) {
|
|
|
|
|
config.push()
|
|
|
|
|
} else
|
|
|
|
|
throw IllegalArgumentException(
|
|
|
|
|
"Got a non group keys or config base object for config sync"
|
|
|
|
|
)
|
|
|
|
|
toDelete += obsoleteHashes
|
|
|
|
|
val message =
|
|
|
|
|
SnodeMessage(
|
|
|
|
|
destination.publicKey,
|
|
|
|
|
Base64.encodeBytes(push),
|
|
|
|
|
SnodeMessage.CONFIG_TTL,
|
|
|
|
|
sentTimestamp
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ConfigMessageInformation(
|
|
|
|
|
SnodeAPI.buildAuthenticatedStoreBatchInfo(
|
|
|
|
|
config.namespace(),
|
|
|
|
|
message,
|
|
|
|
|
signingKey
|
|
|
|
|
),
|
|
|
|
|
config,
|
|
|
|
|
seqNo
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// assume our own user as check already takes place in `execute` for our own key
|
|
|
|
|
// if contact
|
|
|
|
|
configFactoryProtocol.getUserConfigs().filter { it.needsPush() }.map { config ->
|
|
|
|
|
val (bytes, seqNo, obsoleteHashes) = config.push()
|
|
|
|
|
toDelete += obsoleteHashes
|
|
|
|
|
val message =
|
|
|
|
|
messageForConfig(config, bytes, seqNo)
|
|
|
|
|
?: throw NullPointerException(
|
|
|
|
|
"SnodeBatchRequest message was null, check group keys exists"
|
|
|
|
|
)
|
|
|
|
|
ConfigMessageInformation(message, config, seqNo)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// free the objects that were created but won't be used after this point
|
|
|
|
|
// in case any of the configs don't need pushing, they won't be freed later
|
|
|
|
|
(listOf(keys,info,members) subtract requiringPush).forEach(Config::free)
|
|
|
|
|
|
|
|
|
|
requiringPush.map { config ->
|
|
|
|
|
val (push, seqNo, obsoleteHashes) = if (config is GroupKeysConfig) {
|
|
|
|
|
ConfigPush(config.pendingConfig()!!, 0, emptyList()) // should not be null from filter step previous
|
|
|
|
|
} else if (config is ConfigBase) {
|
|
|
|
|
config.push()
|
|
|
|
|
} else throw IllegalArgumentException("Got a non group keys or config base object for config sync")
|
|
|
|
|
toDelete += obsoleteHashes
|
|
|
|
|
val message = SnodeMessage(destination.publicKey, Base64.encodeBytes(push), SnodeMessage.CONFIG_TTL, sentTimestamp)
|
|
|
|
|
|
|
|
|
|
ConfigMessageInformation(SnodeAPI.buildAuthenticatedStoreBatchInfo(config.namespace(), message, signingKey), config, seqNo)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// assume our own user as check already takes place in `execute` for our own key if contact
|
|
|
|
|
configFactoryProtocol.getUserConfigs().filter { it.needsPush() }.map { config ->
|
|
|
|
|
val (bytes, seqNo, obsoleteHashes) = config.push()
|
|
|
|
|
toDelete += obsoleteHashes
|
|
|
|
|
val message = messageForConfig(config, bytes, seqNo)
|
|
|
|
|
?: throw NullPointerException("SnodeBatchRequest message was null, check group keys exists")
|
|
|
|
|
ConfigMessageInformation(message, config, seqNo)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return SyncInformation(configsRequiringPush, toDelete)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun messageForConfig(
|
|
|
|
|
config: ConfigBase,
|
|
|
|
|
bytes: ByteArray,
|
|
|
|
|
seqNo: Long
|
|
|
|
|
config: ConfigBase,
|
|
|
|
|
bytes: ByteArray,
|
|
|
|
|
seqNo: Long
|
|
|
|
|
): SnodeBatchRequestInfo? {
|
|
|
|
|
val message = SharedConfigurationMessage(config.protoKindFor(), bytes, seqNo)
|
|
|
|
|
val snodeMessage = MessageSender.buildWrappedMessageToSnode(destination, message, true)
|
|
|
|
@ -104,22 +147,32 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
|
|
|
|
|
|
|
|
|
|
val userPublicKey = storage.getUserPublicKey()
|
|
|
|
|
val delegate = delegate ?: return Log.e("ConfigurationSyncJob", "No Delegate")
|
|
|
|
|
if (destination !is Destination.ClosedGroup && (destination !is Destination.Contact || destination.publicKey != userPublicKey)) {
|
|
|
|
|
if (destination !is Destination.ClosedGroup &&
|
|
|
|
|
(destination !is Destination.Contact ||
|
|
|
|
|
destination.publicKey != userPublicKey)
|
|
|
|
|
) {
|
|
|
|
|
return delegate.handleJobFailedPermanently(this, dispatcherName, InvalidDestination())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// configFactory singleton instance will come in handy for modifying hashes and fetching configs for namespace etc
|
|
|
|
|
// configFactory singleton instance will come in handy for modifying hashes and fetching
|
|
|
|
|
// configs for namespace etc
|
|
|
|
|
val configFactory = MessagingModuleConfiguration.shared.configFactory
|
|
|
|
|
|
|
|
|
|
// allow null results here so the list index matches configsRequiringPush
|
|
|
|
|
val (batchObjects, toDeleteHashes) = destinationConfigs(delegate, dispatcherName, configFactory)
|
|
|
|
|
val (batchObjects, toDeleteHashes) =
|
|
|
|
|
destinationConfigs(delegate, dispatcherName, configFactory)
|
|
|
|
|
|
|
|
|
|
if (batchObjects.isEmpty()) return delegate.handleJobSucceeded(this, dispatcherName)
|
|
|
|
|
|
|
|
|
|
val toDeleteRequest = toDeleteHashes.let { toDeleteFromAllNamespaces ->
|
|
|
|
|
if (toDeleteFromAllNamespaces.isEmpty()) null
|
|
|
|
|
else SnodeAPI.buildAuthenticatedDeleteBatchInfo(destination.destinationPublicKey(), toDeleteFromAllNamespaces)
|
|
|
|
|
}
|
|
|
|
|
val toDeleteRequest =
|
|
|
|
|
toDeleteHashes.let { toDeleteFromAllNamespaces ->
|
|
|
|
|
if (toDeleteFromAllNamespaces.isEmpty()) null
|
|
|
|
|
else
|
|
|
|
|
SnodeAPI.buildAuthenticatedDeleteBatchInfo(
|
|
|
|
|
destination.destinationPublicKey(),
|
|
|
|
|
toDeleteFromAllNamespaces
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val allRequests = mutableListOf<SnodeBatchRequestInfo>()
|
|
|
|
|
allRequests += batchObjects.map { (request) -> request }
|
|
|
|
@ -129,14 +182,15 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
|
|
|
|
|
Log.d(TAG, "Including delete request for current hashes")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val batchResponse = SnodeAPI.getSingleTargetSnode(destination.destinationPublicKey()).bind { snode ->
|
|
|
|
|
SnodeAPI.getRawBatchResponse(
|
|
|
|
|
snode,
|
|
|
|
|
destination.destinationPublicKey(),
|
|
|
|
|
allRequests,
|
|
|
|
|
sequence = true
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
val batchResponse =
|
|
|
|
|
SnodeAPI.getSingleTargetSnode(destination.destinationPublicKey()).bind { snode ->
|
|
|
|
|
SnodeAPI.getRawBatchResponse(
|
|
|
|
|
snode,
|
|
|
|
|
destination.destinationPublicKey(),
|
|
|
|
|
allRequests,
|
|
|
|
|
sequence = true
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
val rawResponses = batchResponse.get()
|
|
|
|
@ -147,25 +201,32 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
|
|
|
|
|
batchObjects.forEachIndexed { index, (message, config, seqNo) ->
|
|
|
|
|
val response = responseList[index]
|
|
|
|
|
val responseBody = response["body"] as? RawResponse
|
|
|
|
|
val insertHash = responseBody?.get("hash") as? String ?: run {
|
|
|
|
|
Log.w(TAG, "No hash returned for the configuration in namespace ${config.namespace()}")
|
|
|
|
|
return@forEachIndexed
|
|
|
|
|
}
|
|
|
|
|
val insertHash =
|
|
|
|
|
responseBody?.get("hash") as? String
|
|
|
|
|
?: run {
|
|
|
|
|
Log.w(
|
|
|
|
|
TAG,
|
|
|
|
|
"No hash returned for the configuration in namespace ${config.namespace()}"
|
|
|
|
|
)
|
|
|
|
|
return@forEachIndexed
|
|
|
|
|
}
|
|
|
|
|
Log.d(TAG, "Hash ${insertHash.take(4)} returned from store request for new config")
|
|
|
|
|
|
|
|
|
|
// confirm pushed seqno
|
|
|
|
|
if (config is ConfigBase) {
|
|
|
|
|
seqNo?.let {
|
|
|
|
|
config.confirmPushed(it, insertHash)
|
|
|
|
|
}
|
|
|
|
|
seqNo?.let { config.confirmPushed(it, insertHash) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "Successfully removed the deleted hashes from ${config.javaClass.simpleName}")
|
|
|
|
|
Log.d(
|
|
|
|
|
TAG,
|
|
|
|
|
"Successfully removed the deleted hashes from ${config.javaClass.simpleName}"
|
|
|
|
|
)
|
|
|
|
|
// dump and write config after successful
|
|
|
|
|
if (config is ConfigBase && config.needsDump()) { // usually this will be true?
|
|
|
|
|
configFactory.persist(config, (message.params["timestamp"] as String).toLong())
|
|
|
|
|
} else if (config is GroupKeysConfig && config.needsDump()) {
|
|
|
|
|
Log.d("Loki", "Should persist the GroupKeysConfig")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (destination is Destination.ClosedGroup) {
|
|
|
|
|
config.free() // after they are used, free the temporary group configs
|
|
|
|
|
}
|
|
|
|
@ -181,22 +242,24 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun Destination.destinationPublicKey(): String = when (this) {
|
|
|
|
|
is Destination.Contact -> publicKey
|
|
|
|
|
is Destination.ClosedGroup -> publicKey
|
|
|
|
|
else -> throw NullPointerException("Not public key for this destination")
|
|
|
|
|
}
|
|
|
|
|
fun Destination.destinationPublicKey(): String =
|
|
|
|
|
when (this) {
|
|
|
|
|
is Destination.Contact -> publicKey
|
|
|
|
|
is Destination.ClosedGroup -> publicKey
|
|
|
|
|
else -> throw NullPointerException("Not public key for this destination")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun serialize(): Data {
|
|
|
|
|
val (type, address) = when (destination) {
|
|
|
|
|
is Destination.Contact -> CONTACT_TYPE to destination.publicKey
|
|
|
|
|
is Destination.ClosedGroup -> GROUP_TYPE to destination.publicKey
|
|
|
|
|
else -> return Data.EMPTY
|
|
|
|
|
}
|
|
|
|
|
val (type, address) =
|
|
|
|
|
when (destination) {
|
|
|
|
|
is Destination.Contact -> CONTACT_TYPE to destination.publicKey
|
|
|
|
|
is Destination.ClosedGroup -> GROUP_TYPE to destination.publicKey
|
|
|
|
|
else -> return Data.EMPTY
|
|
|
|
|
}
|
|
|
|
|
return Data.Builder()
|
|
|
|
|
.putInt(DESTINATION_TYPE_KEY, type)
|
|
|
|
|
.putString(DESTINATION_ADDRESS_KEY, address)
|
|
|
|
|
.build()
|
|
|
|
|
.putInt(DESTINATION_TYPE_KEY, type)
|
|
|
|
|
.putString(DESTINATION_ADDRESS_KEY, address)
|
|
|
|
|
.build()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun getFactoryKey(): String = KEY
|
|
|
|
@ -212,21 +275,23 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
|
|
|
|
|
// type mappings
|
|
|
|
|
const val CONTACT_TYPE = 1
|
|
|
|
|
const val GROUP_TYPE = 2
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Factory: Job.Factory<ConfigurationSyncJob> {
|
|
|
|
|
class Factory : Job.Factory<ConfigurationSyncJob> {
|
|
|
|
|
override fun create(data: Data): ConfigurationSyncJob? {
|
|
|
|
|
if (!data.hasInt(DESTINATION_TYPE_KEY) || !data.hasString(DESTINATION_ADDRESS_KEY)) return null
|
|
|
|
|
if (!data.hasInt(DESTINATION_TYPE_KEY) || !data.hasString(DESTINATION_ADDRESS_KEY))
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
val address = data.getString(DESTINATION_ADDRESS_KEY)
|
|
|
|
|
val destination = when (data.getInt(DESTINATION_TYPE_KEY)) {
|
|
|
|
|
CONTACT_TYPE -> Destination.Contact(address)
|
|
|
|
|
GROUP_TYPE -> Destination.ClosedGroup(address)
|
|
|
|
|
else -> return null
|
|
|
|
|
}
|
|
|
|
|
val destination =
|
|
|
|
|
when (data.getInt(DESTINATION_TYPE_KEY)) {
|
|
|
|
|
CONTACT_TYPE -> Destination.Contact(address)
|
|
|
|
|
GROUP_TYPE -> Destination.ClosedGroup(address)
|
|
|
|
|
else -> return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ConfigurationSyncJob(destination)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|