diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt index d950171421..5985d3895c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt @@ -12,6 +12,9 @@ import org.signal.core.util.Util import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.inRoundedDays import org.signal.core.util.logging.Log +import org.signal.libsignal.net.RequestResult +import org.signal.libsignal.net.RetryLaterException +import org.signal.libsignal.net.UploadTooLargeException import org.signal.protos.resumableuploads.ResumableUpload import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.attachments.Attachment @@ -171,8 +174,15 @@ class AttachmentUploadJob private constructor( try { val existingSpec = uploadSpec?.let { ResumableUploadSpec.from(it) } + val ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(databaseAttachment.size)) + val uploadForm = if (existingSpec == null) { - SignalNetwork.attachments.getAttachmentV4UploadForm().successOrThrow() + when (val result = SignalNetwork.attachments.getAttachmentV4UploadForm(ciphertextLength)) { + is RequestResult.Success -> result.result + is RequestResult.NonSuccess -> throw result.error + is RequestResult.RetryableNetworkError -> throw RetryLaterException(result.retryAfter) + is RequestResult.ApplicationError -> throw result.cause + } } else { null } @@ -288,8 +298,16 @@ class AttachmentUploadJob private constructor( database.setTransferProgressFailed(attachmentId, databaseAttachment.mmsId) } + override fun getNextRunAttemptBackoff(pastAttemptCount: Int, exception: java.lang.Exception): Long { + if (exception is RetryLaterException && exception.duration != null) { + return exception.duration.toMillis() + } + + return super.getNextRunAttemptBackoff(pastAttemptCount, exception) + } + override fun onShouldRetry(exception: Exception): Boolean { - return exception is IOException && exception !is NotPushRegisteredException + return exception is IOException && exception !is NotPushRegisteredException && exception !is UploadTooLargeException } @Throws(InvalidAttachmentException::class) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 6bf3b53ebe..8a8989d088 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -261,7 +261,6 @@ public final class JobManagerFactories { put(RemoteDeleteSendJob.KEY, new RemoteDeleteSendJob.Factory()); put(ReportSpamJob.KEY, new ReportSpamJob.Factory()); put(ResendMessageJob.KEY, new ResendMessageJob.Factory()); - put(ResumableUploadSpecJob.KEY, new ResumableUploadSpecJob.Factory()); put(RequestGroupV2InfoWorkerJob.KEY, new RequestGroupV2InfoWorkerJob.Factory()); put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory()); put(LocalBackupRestoreMediaJob.KEY, new LocalBackupRestoreMediaJob.Factory()); @@ -431,6 +430,7 @@ public final class JobManagerFactories { put("BackupRestoreJob", new FailingJob.Factory()); put("BackfillDigestsMigrationJob", new PassingMigrationJob.Factory()); put("BackfillDigestJob", new FailingJob.Factory()); + put("ResumableUploadSpecJob", new FailingJob.Factory()); }}; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java index b4948f0cfd..17e89ed477 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java @@ -35,7 +35,9 @@ import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; @@ -288,7 +290,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob { .withStream(stream) .withContentType("application/octet-stream") .withLength(length) - .withResumableUploadSpec(messageSender.getResumableUploadSpec()); + .withResumableUploadSpec(messageSender.getResumableUploadSpec(AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(length)))); messageSender.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream.build(), complete)) ); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java index 8b85eeadd4..1dcc1c4ed8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java @@ -17,7 +17,9 @@ import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.RemoteConfig; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; @@ -26,6 +28,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsO import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; +import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -88,11 +91,14 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob { out.close(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); + long dataLength = baos.toByteArray().length; + long ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(dataLength)); + ResumableUploadSpec uploadSpec = messageSender.getResumableUploadSpec(ciphertextLength); SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() .withStream(new ByteArrayInputStream(baos.toByteArray())) .withContentType("application/octet-stream") - .withLength(baos.toByteArray().length) - .withResumableUploadSpec(messageSender.getResumableUploadSpec()) + .withLength(dataLength) + .withResumableUploadSpec(uploadSpec) .build(); SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, false)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index ed113ac267..391f0c3487 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -60,7 +60,9 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.MediaUtil; +import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; import org.whispersystems.signalservice.api.messages.AttachmentTransferProgress; +import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; @@ -70,6 +72,7 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import org.whispersystems.signalservice.internal.push.BodyRange; +import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import java.io.IOException; import java.io.InputStream; @@ -171,9 +174,12 @@ public abstract class PushSendJob extends SendJob { try { if (attachment.getUri() == null || attachment.size == 0) throw new IOException("Assertion failed, outgoing attachment has no data!"); - InputStream is = PartAuthority.getAttachmentStream(context, attachment.getUri()); + InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.getUri()); + long ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size)); + ResumableUploadSpec uploadSpec = AppDependencies.getSignalServiceMessageSender().getResumableUploadSpec(ciphertextLength); + return SignalServiceAttachment.newStreamBuilder() - .withStream(is) + .withStream(inputStream) .withContentType(attachment.contentType) .withLength(attachment.size) .withFileName(attachment.fileName) @@ -185,7 +191,7 @@ public abstract class PushSendJob extends SendJob { .withHeight(attachment.height) .withCaption(attachment.caption) .withUuid(attachment.uuid) - .withResumableUploadSpec(AppDependencies.getSignalServiceMessageSender().getResumableUploadSpec()) + .withResumableUploadSpec(uploadSpec) .withListener(new SignalServiceAttachment.ProgressListener() { @Override public void onAttachmentProgress(@NonNull AttachmentTransferProgress progress) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ResumableUploadSpecJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ResumableUploadSpecJob.java deleted file mode 100644 index c0763a5a96..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ResumableUploadSpecJob.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; - -import java.io.IOException; - -/** - * No longer used. Functionality has been merged into {@link AttachmentUploadJob}. - */ -@Deprecated -public class ResumableUploadSpecJob extends BaseJob { - - private static final String TAG = Log.tag(ResumableUploadSpecJob.class); - - static final String KEY_RESUME_SPEC = "resume_spec"; - - public static final String KEY = "ResumableUploadSpecJob"; - - private ResumableUploadSpecJob(@NonNull Parameters parameters) { - super(parameters); - } - - @Override - protected void onRun() throws Exception { - ResumableUploadSpec resumableUploadSpec = AppDependencies.getSignalServiceMessageSender() - .getResumableUploadSpec(); - - setOutputData(new JsonJobData.Builder() - .putString(KEY_RESUME_SPEC, resumableUploadSpec.serialize()) - .serialize()); - } - - @Override - protected boolean onShouldRetry(@NonNull Exception e) { - return e instanceof IOException; - } - - @Override - public @Nullable byte[] serialize() { - return null; - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onFailure() { - - } - - public static class Factory implements Job.Factory { - - @Override - public @NonNull ResumableUploadSpecJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { - return new ResumableUploadSpecJob(parameters); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt index b5e7ae3eb2..4df0cd718f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt @@ -10,6 +10,7 @@ import org.signal.core.util.logging.logD import org.signal.core.util.logging.logI import org.signal.core.util.logging.logW import org.signal.core.util.toByteArray +import org.signal.libsignal.net.RequestResult import org.signal.libsignal.protocol.InvalidKeyException import org.signal.libsignal.protocol.ecc.ECPublicKey import org.thoughtcrime.securesms.attachments.AttachmentUploadUtil @@ -337,11 +338,11 @@ object LinkDeviceRepository { } Log.d(TAG, "[createAndUploadArchive] Fetching an upload form...") - val uploadForm = when (val result = NetworkResult.withRetry { SignalNetwork.attachments.getAttachmentV4UploadForm() }) { - is NetworkResult.Success -> result.result.logD(TAG, "[createAndUploadArchive] Successfully retrieved upload form.") - is NetworkResult.ApplicationError -> throw result.throwable - is NetworkResult.NetworkError -> return LinkUploadArchiveResult.NetworkError(result.exception).logW(TAG, "[createAndUploadArchive] Network error when fetching form.", result.exception) - is NetworkResult.StatusCodeError -> return LinkUploadArchiveResult.NetworkError(result.exception).logW(TAG, "[createAndUploadArchive] Status code error when fetching form.", result.exception) + val uploadForm = when (val result = SignalNetwork.attachments.getAttachmentV4UploadForm(tempBackupFile.length())) { + is RequestResult.Success -> result.result.logD(TAG, "[createAndUploadArchive] Successfully retrieved upload form.") + is RequestResult.ApplicationError -> throw result.cause + is RequestResult.RetryableNetworkError -> return LinkUploadArchiveResult.NetworkError(result.networkError).logW(TAG, "[createAndUploadArchive] Network error when fetching form.", result.networkError) + is RequestResult.NonSuccess -> return LinkUploadArchiveResult.BadRequest(result.error).logW(TAG, "[createAndUploadArchive] Upload too large when fetching form.", result.error) } if (cancellationSignal()) { diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 562a2ac764..0c0f6145ee 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -17,6 +17,7 @@ import org.signal.libsignal.net.MultiRecipientSendAuthorization; import org.signal.libsignal.net.MultiRecipientSendFailure; import org.signal.libsignal.net.RequestResult; import org.signal.libsignal.net.RequestUnauthorizedException; +import org.signal.libsignal.net.UploadTooLargeException; import org.signal.libsignal.net.RetryLaterException; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKeyPair; @@ -832,11 +833,22 @@ public class SignalServiceMessageSender { return uploadAttachmentV4(attachment, attachmentKey, attachmentData); } - public ResumableUploadSpec getResumableUploadSpec() throws IOException { + public ResumableUploadSpec getResumableUploadSpec(long uploadSizeBytes) throws IOException { Log.d(TAG, "Using pipe to retrieve attachment upload attributes..."); - AttachmentUploadForm v4UploadAttributes = NetworkResultUtil.toBasicLegacy(attachmentApi.getAttachmentV4UploadForm()); + RequestResult result = attachmentApi.getAttachmentV4UploadForm(uploadSizeBytes); - return socket.getResumableUploadSpec(v4UploadAttributes); + if (result instanceof RequestResult.Success) { + AttachmentUploadForm v4UploadAttributes = ((RequestResult.Success) result).getResult(); + return socket.getResumableUploadSpec(v4UploadAttributes); + } else if (result instanceof RequestResult.NonSuccess) { + throw ((RequestResult.NonSuccess) result).getError(); + } else if (result instanceof RequestResult.RetryableNetworkError) { + throw new PushNetworkException(((RequestResult.RetryableNetworkError) result).getNetworkError()); + } else if (result instanceof RequestResult.ApplicationError) { + throw new RuntimeException(((RequestResult.ApplicationError) result).getCause()); + } else { + throw new IOException("Unexpected RequestResult type: " + result.getClass().getSimpleName()); + } } private SignalServiceAttachmentPointer uploadAttachmentV4(SignalServiceAttachmentStream attachment, byte[] attachmentKey, PushAttachmentData attachmentData) throws IOException { diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/attachment/AttachmentApi.kt b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/attachment/AttachmentApi.kt index 4e58bb972a..28f40a92f3 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/attachment/AttachmentApi.kt +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/attachment/AttachmentApi.kt @@ -5,20 +5,25 @@ package org.whispersystems.signalservice.api.attachment +import kotlinx.coroutines.runBlocking import org.signal.core.util.logging.Log +import org.signal.libsignal.net.AuthMessagesService +import org.signal.libsignal.net.AuthenticatedChatConnection +import org.signal.libsignal.net.RequestResult +import org.signal.libsignal.net.UploadTooLargeException +import org.signal.libsignal.net.getOrError import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream import org.whispersystems.signalservice.api.websocket.SignalWebSocket import org.whispersystems.signalservice.internal.crypto.PaddingInputStream -import org.whispersystems.signalservice.internal.get import org.whispersystems.signalservice.internal.push.AttachmentUploadForm import org.whispersystems.signalservice.internal.push.PushAttachmentData import org.whispersystems.signalservice.internal.push.PushServiceSocket import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec -import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage +import java.io.IOException import java.io.InputStream import kotlin.jvm.optionals.getOrNull @@ -36,56 +41,25 @@ class AttachmentApi( /** * Gets a v4 attachment upload form, which provides the necessary information to upload an attachment. - * - * GET /v4/attachments/form/upload - * - 200: Success - * - 413: Too many attempts - * - 429: Too many attempts */ - fun getAttachmentV4UploadForm(): NetworkResult { - val request = WebSocketRequestMessage.get("/v4/attachments/form/upload") - return NetworkResult.fromWebSocketRequest(authWebSocket, request, AttachmentUploadForm::class) - } - - /** - * Uploads an attachment using the v4 upload scheme. - */ - fun uploadAttachmentV4(attachmentStream: SignalServiceAttachmentStream): NetworkResult { - if (attachmentStream.resumableUploadSpec.isEmpty) { - throw IllegalStateException("Attachment must have a resumable upload spec!") - } - - return NetworkResult.fromFetch { - val resumableUploadSpec = attachmentStream.resumableUploadSpec.get() - - val paddedLength = PaddingInputStream.getPaddedSize(attachmentStream.length) - val dataStream: InputStream = PaddingInputStream(attachmentStream.inputStream, attachmentStream.length) - val ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(paddedLength) - - val attachmentData = PushAttachmentData( - contentType = attachmentStream.contentType, - data = dataStream, - dataSize = ciphertextLength, - incremental = attachmentStream.isFaststart, - outputStreamFactory = AttachmentCipherOutputStreamFactory(resumableUploadSpec.attachmentKey, resumableUploadSpec.attachmentIv), - listener = attachmentStream.listener, - cancelationSignal = attachmentStream.cancelationSignal, - resumableUploadSpec = attachmentStream.resumableUploadSpec.get() - ) - - val digestInfo = pushServiceSocket.uploadAttachment(attachmentData) - - AttachmentUploadResult( - remoteId = SignalServiceAttachmentRemoteId.V4(resumableUploadSpec.cdnKey), - cdnNumber = resumableUploadSpec.cdnNumber, - key = resumableUploadSpec.attachmentKey, - digest = digestInfo.digest, - incrementalDigest = digestInfo.incrementalDigest, - incrementalDigestChunkSize = digestInfo.incrementalMacChunkSize, - uploadTimestamp = attachmentStream.uploadTimestamp, - dataSize = attachmentStream.length, - blurHash = attachmentStream.blurHash.getOrNull() - ) + fun getAttachmentV4UploadForm(uploadSizeBytes: Long): RequestResult { + return try { + runBlocking { + authWebSocket.runWithChatConnection { chatConnection -> + AuthMessagesService(chatConnection as AuthenticatedChatConnection).getUploadForm(uploadSizeBytes) + } + }.getOrError().map { form -> + AttachmentUploadForm( + cdn = form.cdn, + key = form.key, + headers = form.headers, + signedUploadLocation = form.signedUploadUrl.toString() + ) + } + } catch (e: IOException) { + RequestResult.RetryableNetworkError(e) + } catch (e: Throwable) { + RequestResult.ApplicationError(e) } }