|
|
|
@ -9,7 +9,6 @@ import org.session.libsignal.utilities.Base64
|
|
|
|
|
import org.session.libsignal.utilities.JsonUtil
|
|
|
|
|
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
|
|
|
|
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
|
|
|
|
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink
|
|
|
|
|
import org.session.libsignal.service.loki.utilities.*
|
|
|
|
|
import java.net.URL
|
|
|
|
|
import java.util.concurrent.ConcurrentHashMap
|
|
|
|
@ -17,21 +16,6 @@ import java.util.concurrent.ConcurrentHashMap
|
|
|
|
|
class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : DotNetAPI() {
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
// region Settings
|
|
|
|
|
/**
|
|
|
|
|
* Deprecated.
|
|
|
|
|
*/
|
|
|
|
|
private val deviceLinkType = "network.loki.messenger.devicemapping"
|
|
|
|
|
/**
|
|
|
|
|
* Deprecated.
|
|
|
|
|
*/
|
|
|
|
|
private val deviceLinkRequestCache = ConcurrentHashMap<String, Promise<Set<DeviceLink>, Exception>>()
|
|
|
|
|
/**
|
|
|
|
|
* Deprecated.
|
|
|
|
|
*/
|
|
|
|
|
private val deviceLinkUpdateInterval = 60 * 1000
|
|
|
|
|
private val lastDeviceLinkUpdate = ConcurrentHashMap<String, Long>()
|
|
|
|
|
|
|
|
|
|
internal val fileServerPublicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C"
|
|
|
|
|
internal val maxRetryCount = 4
|
|
|
|
|
|
|
|
|
@ -63,172 +47,6 @@ class FileServerAPI(public val server: String, userPublicKey: String, userPrivat
|
|
|
|
|
// endregion
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// region Device Link Update Result
|
|
|
|
|
sealed class DeviceLinkUpdateResult {
|
|
|
|
|
class Success(val publicKey: String, val deviceLinks: Set<DeviceLink>) : DeviceLinkUpdateResult()
|
|
|
|
|
class Failure(val publicKey: String, val error: Exception) : DeviceLinkUpdateResult()
|
|
|
|
|
}
|
|
|
|
|
// endregion
|
|
|
|
|
|
|
|
|
|
// region API
|
|
|
|
|
public fun hasDeviceLinkCacheExpired(referenceTime: Long = System.currentTimeMillis(), publicKey: String): Boolean {
|
|
|
|
|
return !lastDeviceLinkUpdate.containsKey(publicKey) || (referenceTime - lastDeviceLinkUpdate[publicKey]!! > deviceLinkUpdateInterval)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getDeviceLinks(publicKey: String, isForcedUpdate: Boolean = false): Promise<Set<DeviceLink>, Exception> {
|
|
|
|
|
return Promise.of(setOf())
|
|
|
|
|
/*
|
|
|
|
|
if (deviceLinkRequestCache.containsKey(publicKey) && !isForcedUpdate) {
|
|
|
|
|
val result = deviceLinkRequestCache[publicKey]
|
|
|
|
|
if (result != null) { return result } // A request was already pending
|
|
|
|
|
}
|
|
|
|
|
val promise = getDeviceLinks(setOf(publicKey), isForcedUpdate)
|
|
|
|
|
deviceLinkRequestCache[publicKey] = promise
|
|
|
|
|
promise.always {
|
|
|
|
|
deviceLinkRequestCache.remove(publicKey)
|
|
|
|
|
}
|
|
|
|
|
return promise
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getDeviceLinks(publicKeys: Set<String>, isForcedUpdate: Boolean = false): Promise<Set<DeviceLink>, Exception> {
|
|
|
|
|
return Promise.of(setOf())
|
|
|
|
|
/*
|
|
|
|
|
val validPublicKeys = publicKeys.filter { PublicKeyValidation.isValid(it) }
|
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
// IMPORTANT: Don't fetch device links for the current user (i.e. don't remove the it != userHexEncodedPublicKey) check below
|
|
|
|
|
val updatees = validPublicKeys.filter { it != userPublicKey && (hasDeviceLinkCacheExpired(now, it) || isForcedUpdate) }.toSet()
|
|
|
|
|
val cachedDeviceLinks = validPublicKeys.minus(updatees).flatMap { database.getDeviceLinks(it) }.toSet()
|
|
|
|
|
if (updatees.isEmpty()) {
|
|
|
|
|
return Promise.of(cachedDeviceLinks)
|
|
|
|
|
} else {
|
|
|
|
|
return getUserProfiles(updatees, server, true).map(SnodeAPI.sharedContext) { data ->
|
|
|
|
|
data.map dataMap@ { node ->
|
|
|
|
|
val publicKey = node["username"] as String
|
|
|
|
|
val annotations = node["annotations"] as List<Map<*, *>>
|
|
|
|
|
val deviceLinksAnnotation = annotations.find {
|
|
|
|
|
annotation -> (annotation["type"] as String) == deviceLinkType
|
|
|
|
|
} ?: return@dataMap DeviceLinkUpdateResult.Success(publicKey, setOf())
|
|
|
|
|
val value = deviceLinksAnnotation["value"] as Map<*, *>
|
|
|
|
|
val deviceLinksAsJSON = value["authorisations"] as List<Map<*, *>>
|
|
|
|
|
val deviceLinks = deviceLinksAsJSON.mapNotNull { deviceLinkAsJSON ->
|
|
|
|
|
try {
|
|
|
|
|
val masterPublicKey = deviceLinkAsJSON["primaryDevicePubKey"] as String
|
|
|
|
|
val slavePublicKey = deviceLinkAsJSON["secondaryDevicePubKey"] as String
|
|
|
|
|
var requestSignature: ByteArray? = null
|
|
|
|
|
var authorizationSignature: ByteArray? = null
|
|
|
|
|
if (deviceLinkAsJSON["requestSignature"] != null) {
|
|
|
|
|
val base64EncodedSignature = deviceLinkAsJSON["requestSignature"] as String
|
|
|
|
|
requestSignature = Base64.decode(base64EncodedSignature)
|
|
|
|
|
}
|
|
|
|
|
if (deviceLinkAsJSON["grantSignature"] != null) {
|
|
|
|
|
val base64EncodedSignature = deviceLinkAsJSON["grantSignature"] as String
|
|
|
|
|
authorizationSignature = Base64.decode(base64EncodedSignature)
|
|
|
|
|
}
|
|
|
|
|
val deviceLink = DeviceLink(masterPublicKey, slavePublicKey, requestSignature, authorizationSignature)
|
|
|
|
|
val isValid = deviceLink.verify()
|
|
|
|
|
if (!isValid) {
|
|
|
|
|
Log.d("Loki", "Ignoring invalid device link: $deviceLinkAsJSON.")
|
|
|
|
|
return@mapNotNull null
|
|
|
|
|
}
|
|
|
|
|
deviceLink
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
Log.d("Loki", "Failed to parse device links for $publicKey from $deviceLinkAsJSON due to error: $e.")
|
|
|
|
|
null
|
|
|
|
|
}
|
|
|
|
|
}.toSet()
|
|
|
|
|
DeviceLinkUpdateResult.Success(publicKey, deviceLinks)
|
|
|
|
|
}
|
|
|
|
|
}.recover { e ->
|
|
|
|
|
publicKeys.map { DeviceLinkUpdateResult.Failure(it, e) }
|
|
|
|
|
}.success { updateResults ->
|
|
|
|
|
for (updateResult in updateResults) {
|
|
|
|
|
if (updateResult is DeviceLinkUpdateResult.Success) {
|
|
|
|
|
database.clearDeviceLinks(updateResult.publicKey)
|
|
|
|
|
updateResult.deviceLinks.forEach { database.addDeviceLink(it) }
|
|
|
|
|
} else {
|
|
|
|
|
// Do nothing
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}.map(SnodeAPI.sharedContext) { updateResults ->
|
|
|
|
|
val deviceLinks = mutableListOf<DeviceLink>()
|
|
|
|
|
for (updateResult in updateResults) {
|
|
|
|
|
when (updateResult) {
|
|
|
|
|
is DeviceLinkUpdateResult.Success -> {
|
|
|
|
|
lastDeviceLinkUpdate[updateResult.publicKey] = now
|
|
|
|
|
deviceLinks.addAll(updateResult.deviceLinks)
|
|
|
|
|
}
|
|
|
|
|
is DeviceLinkUpdateResult.Failure -> {
|
|
|
|
|
if (updateResult.error is SnodeAPI.Error.ParsingFailed) {
|
|
|
|
|
lastDeviceLinkUpdate[updateResult.publicKey] = now // Don't infinitely update in case of a parsing failure
|
|
|
|
|
}
|
|
|
|
|
deviceLinks.addAll(database.getDeviceLinks(updateResult.publicKey)) // Fall back on cached device links in case of a failure
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Updatees that didn't show up in the response provided by the file server are assumed to not have any device links
|
|
|
|
|
val excludedUpdatees = updatees.filter { updatee ->
|
|
|
|
|
updateResults.find { updateResult ->
|
|
|
|
|
when (updateResult) {
|
|
|
|
|
is DeviceLinkUpdateResult.Success -> updateResult.publicKey == updatee
|
|
|
|
|
is DeviceLinkUpdateResult.Failure -> updateResult.publicKey == updatee
|
|
|
|
|
}
|
|
|
|
|
} == null
|
|
|
|
|
}
|
|
|
|
|
excludedUpdatees.forEach {
|
|
|
|
|
lastDeviceLinkUpdate[it] = now
|
|
|
|
|
}
|
|
|
|
|
deviceLinks.union(cachedDeviceLinks)
|
|
|
|
|
}.recover {
|
|
|
|
|
publicKeys.flatMap { database.getDeviceLinks(it) }.toSet()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setDeviceLinks(deviceLinks: Set<DeviceLink>): Promise<Unit, Exception> {
|
|
|
|
|
return Promise.of(Unit)
|
|
|
|
|
/*
|
|
|
|
|
val isMaster = deviceLinks.find { it.masterPublicKey == userPublicKey } != null
|
|
|
|
|
val deviceLinksAsJSON = deviceLinks.map { it.toJSON() }
|
|
|
|
|
val value = if (deviceLinks.isNotEmpty()) mapOf( "isPrimary" to isMaster, "authorisations" to deviceLinksAsJSON ) else null
|
|
|
|
|
val annotation = mapOf( "type" to deviceLinkType, "value" to value )
|
|
|
|
|
val parameters = mapOf( "annotations" to listOf( annotation ) )
|
|
|
|
|
return retryIfNeeded(maxRetryCount) {
|
|
|
|
|
execute(HTTPVerb.PATCH, server, "/users/me", parameters = parameters)
|
|
|
|
|
}.map { Unit }
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun addDeviceLink(deviceLink: DeviceLink): Promise<Unit, Exception> {
|
|
|
|
|
return Promise.of(Unit)
|
|
|
|
|
/*
|
|
|
|
|
Log.d("Loki", "Updating device links.")
|
|
|
|
|
return getDeviceLinks(userPublicKey, true).bind { deviceLinks ->
|
|
|
|
|
val mutableDeviceLinks = deviceLinks.toMutableSet()
|
|
|
|
|
mutableDeviceLinks.add(deviceLink)
|
|
|
|
|
setDeviceLinks(mutableDeviceLinks)
|
|
|
|
|
}.success {
|
|
|
|
|
database.addDeviceLink(deviceLink)
|
|
|
|
|
}.map { Unit }
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun removeDeviceLink(deviceLink: DeviceLink): Promise<Unit, Exception> {
|
|
|
|
|
return Promise.of(Unit)
|
|
|
|
|
/*
|
|
|
|
|
Log.d("Loki", "Updating device links.")
|
|
|
|
|
return getDeviceLinks(userPublicKey, true).bind { deviceLinks ->
|
|
|
|
|
val mutableDeviceLinks = deviceLinks.toMutableSet()
|
|
|
|
|
mutableDeviceLinks.remove(deviceLink)
|
|
|
|
|
setDeviceLinks(mutableDeviceLinks)
|
|
|
|
|
}.success {
|
|
|
|
|
database.removeDeviceLink(deviceLink)
|
|
|
|
|
}.map { Unit }
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
// endregion
|
|
|
|
|
|
|
|
|
|
// region Open Group Server Public Key
|
|
|
|
|
fun getPublicKeyForOpenGroupServer(openGroupServer: String): Promise<String, Exception> {
|
|
|
|
|
val publicKey = database.getOpenGroupPublicKey(openGroupServer)
|
|
|
|
|