From a14fc0503f448b6989cc49cd4cbc1edf5df74ad3 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 27 Apr 2021 14:48:57 +1000 Subject: [PATCH] Take care of remaining loose ends --- .../securesms/jobs/AvatarDownloadJob.java | 20 ++++- .../jobs/RetrieveProfileAvatarJob.java | 6 +- .../messaging/open_groups/OpenGroupAPI.kt | 4 +- .../libsession/utilities/DownloadUtilities.kt | 88 +++++++++++++++++++ 4 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java index b31029c354..34faaf48a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java @@ -4,6 +4,8 @@ import android.graphics.Bitmap; import androidx.annotation.NonNull; import org.session.libsession.messaging.jobs.Data; +import org.session.libsession.utilities.DownloadUtilities; +import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.session.libsession.messaging.threads.GroupRecord; @@ -22,6 +24,7 @@ import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -91,9 +94,20 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType { attachment = File.createTempFile("avatar", "tmp", context.getCacheDir()); attachment.deleteOnExit(); - SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), url); - InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE); - Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500); + SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), url); + + if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL."); + DownloadUtilities.downloadFile(attachment, pointer.getUrl(), MAX_AVATAR_SIZE, null); + + // Assume we're retrieving an attachment for an open group server if the digest is not set + InputStream inputStream; + if (!pointer.getDigest().isPresent()) { + inputStream = new FileInputStream(attachment); + } else { + inputStream = AttachmentCipherInputStream.createForAttachment(attachment, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get()); + } + + Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500); database.updateProfilePicture(groupId, avatar); inputStream.close(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index d89db00604..8211032292 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -10,9 +10,11 @@ import org.session.libsession.messaging.avatars.AvatarHelper; import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.recipients.Recipient; +import org.session.libsession.utilities.DownloadUtilities; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; import org.session.libsignal.service.api.SignalServiceMessageReceiver; +import org.session.libsignal.service.api.crypto.ProfileCipherInputStream; import org.session.libsignal.service.api.push.exceptions.PushNetworkException; import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -22,6 +24,7 @@ import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -102,7 +105,8 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType File downloadDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir()); try { - InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES); + DownloadUtilities.downloadFile(downloadDestination, profileAvatar, MAX_PROFILE_SIZE_BYTES, null); + InputStream avatarStream = new ProfileCipherInputStream(new FileInputStream(downloadDestination), profileKey); File decryptDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir()); Util.copy(avatarStream, new FileOutputStream(decryptDestination)); diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPI.kt index 211bf33643..bd70118abd 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPI.kt @@ -6,7 +6,9 @@ import nl.komponents.kovenant.deferred import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.then import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.file_server.FileServerAPI import org.session.libsession.messaging.utilities.DotNetAPI +import org.session.libsession.utilities.DownloadUtilities import org.session.libsignal.service.loki.utilities.retryIfNeeded import org.session.libsignal.utilities.* import org.session.libsignal.utilities.Base64 @@ -315,7 +317,7 @@ object OpenGroupAPI: DotNetAPI() { Log.d("Loki", "Downloading open group profile picture from \"$url\".") val outputStream = ByteArrayOutputStream() try { - throw IOException(); + DownloadUtilities.downloadFile(outputStream, url, FileServerAPI.maxFileSize, null) Log.d("Loki", "Open group profile picture was successfully loaded from \"$url\"") return outputStream.toByteArray() } catch (e: Exception) { diff --git a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt new file mode 100644 index 0000000000..1c0e29f045 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt @@ -0,0 +1,88 @@ +package org.session.libsession.utilities + +import okhttp3.HttpUrl +import okhttp3.Request +import org.session.libsession.messaging.file_server.FileServerAPI +import org.session.libsession.snode.OnionRequestAPI +import org.session.libsignal.utilities.logging.Log +import org.session.libsignal.service.api.messages.SignalServiceAttachment +import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException +import org.session.libsignal.service.api.push.exceptions.PushNetworkException +import org.session.libsignal.utilities.Base64 +import java.io.* + +object DownloadUtilities { + + /** + * Blocks the calling thread. + */ + @JvmStatic + fun downloadFile(destination: File, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) { + val outputStream = FileOutputStream(destination) // Throws + var remainingAttempts = 4 + var exception: Exception? = null + while (remainingAttempts > 0) { + remainingAttempts -= 1 + try { + downloadFile(outputStream, url, maxSize, listener) + exception = null + break + } catch (e: Exception) { + exception = e + } + } + if (exception != null) { throw exception } + } + + /** + * Blocks the calling thread. + */ + @JvmStatic + fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) { + // We need to throw a PushNetworkException or NonSuccessfulResponseCodeException + // because the underlying Signal logic requires these to work correctly + val oldPrefixedHost = "https://" + HttpUrl.get(url).host() + var newPrefixedHost = oldPrefixedHost + if (oldPrefixedHost.contains(FileServerAPI.fileStorageBucketURL)) { + newPrefixedHost = FileServerAPI.shared.server + } + // Edge case that needs to work: https://file-static.lokinet.org/i1pNmpInq3w9gF3TP8TFCa1rSo38J6UM + // → https://file.getsession.org/loki/v1/f/XLxogNXVEIWHk14NVCDeppzTujPHxu35 + val fileID = url.substringAfter(oldPrefixedHost).substringAfter("/f/") + val sanitizedURL = "$newPrefixedHost/loki/v1/f/$fileID" + val request = Request.Builder().url(sanitizedURL).get() + try { + val serverPublicKey = if (newPrefixedHost.contains(FileServerAPI.shared.server)) FileServerAPI.fileServerPublicKey + else FileServerAPI.shared.getPublicKeyForOpenGroupServer(newPrefixedHost).get() + val json = OnionRequestAPI.sendOnionRequest(request.build(), newPrefixedHost, serverPublicKey, isJSONRequired = false).get() + val result = json["result"] as? String + if (result == null) { + Log.d("Loki", "Couldn't parse attachment from: $json.") + throw PushNetworkException("Missing response body.") + } + val body = Base64.decode(result) + if (body.size > maxSize) { + Log.d("Loki", "Attachment size limit exceeded.") + throw PushNetworkException("Max response size exceeded.") + } + body.inputStream().use { input -> + val buffer = ByteArray(32768) + var count = 0 + var bytes = input.read(buffer) + while (bytes >= 0) { + outputStream.write(buffer, 0, bytes) + count += bytes + if (count > maxSize) { + Log.d("Loki", "Attachment size limit exceeded.") + throw PushNetworkException("Max response size exceeded.") + } + listener?.onAttachmentProgress(body.size.toLong(), count.toLong()) + bytes = input.read(buffer) + } + } + } catch (e: Exception) { + Log.d("Loki", "Couldn't download attachment due to error: $e.") + throw if (e is NonSuccessfulResponseCodeException) e else PushNetworkException(e) + } + } +} \ No newline at end of file