session-android/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt

263 lines
14 KiB
Kotlin

package org.session.libsession.messaging.fileserver
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import okhttp3.Request
import org.session.libsignal.libsignal.logging.Log
import org.session.libsignal.libsignal.util.Hex
import org.session.libsignal.service.internal.util.Base64
import org.session.libsignal.service.internal.util.JsonUtil
import org.session.libsignal.service.loki.api.SnodeAPI
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.protocol.shelved.multidevice.DeviceLink
import org.session.libsignal.service.loki.utilities.*
import java.net.URL
import java.util.concurrent.ConcurrentHashMap
import kotlin.collections.set
class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : LokiDotNetAPI(userPublicKey, userPrivateKey, database) {
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
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 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)
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
}
}
}
}
}