diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index 40981b5cc7..8b13757f8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -108,20 +108,20 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return null // TODO: Implement } - override fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { + override fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { val database = DatabaseFactory.getAttachmentDatabase(context) val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id, - attachmentStream.contentType, - attachmentKey, - Optional.of(Util.toIntExact(attachmentStream.length)), - attachmentStream.preview, - attachmentStream.width, attachmentStream.height, - Optional.fromNullable(uploadResult.digest), - attachmentStream.fileName, - attachmentStream.voiceNote, - attachmentStream.caption, - uploadResult.url); + attachmentStream.contentType, + attachmentKey, + Optional.of(Util.toIntExact(attachmentStream.length)), + attachmentStream.preview, + attachmentStream.width, attachmentStream.height, + Optional.fromNullable(uploadResult.digest), + attachmentStream.fileName, + attachmentStream.voiceNote, + attachmentStream.caption, + uploadResult.url); val attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer), databaseAttachment.fastPreflightId).get() database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment) } diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt index b34a4ac3be..be4596d57b 100644 --- a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -29,7 +29,7 @@ interface MessageDataProvider { fun isOutgoingMessage(timestamp: Long): Boolean - fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) + fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) fun updateAttachmentAfterUploadFailed(attachmentId: Long) fun getMessageForQuote(timestamp: Long, author: Address): Pair? diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt index c8db066692..89bea6bc88 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt @@ -15,8 +15,8 @@ import org.session.libsignal.utilities.logging.Log object FileServerAPIV2 { - private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" - const val DEFAULT_SERVER = "http://88.99.175.227" + private const val SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" + const val SERVER = "http://88.99.175.227" sealed class Error(message: String) : Exception(message) { object ParsingFailed : Error("Invalid response.") @@ -43,7 +43,7 @@ object FileServerAPIV2 { } private fun send(request: Request): Promise, Exception> { - val url = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL) + val url = HttpUrl.parse(SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL) val urlBuilder = HttpUrl.Builder() .scheme(url.scheme()) .host(url.host()) @@ -64,7 +64,7 @@ object FileServerAPIV2 { HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters)) } if (request.useOnionRouting) { - return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY).fail { e -> + return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), SERVER, SERVER_PUBLIC_KEY).fail { e -> Log.e("Loki", "File server request failed.", e) } } else { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index 72c42b03a5..b601731e69 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -4,6 +4,8 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import nl.komponents.kovenant.Promise +import okhttp3.MultipartBody +import okio.Buffer import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.file_server.FileServerAPI import org.session.libsession.messaging.file_server.FileServerAPIV2 @@ -21,6 +23,7 @@ import org.session.libsignal.service.internal.push.http.DigestingRequestBody import org.session.libsignal.service.internal.util.Util import org.session.libsignal.service.loki.PlaintextOutputStreamFactory import org.session.libsignal.utilities.logging.Log +import java.util.* class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job { override var delegate: JobDelegate? = null @@ -60,7 +63,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess } handleSuccess(attachment, keyAndResult.first, keyAndResult.second) } else if (v1OpenGroup == null) { - val keyAndResult = upload(attachment, FileServerAPIV2.DEFAULT_SERVER, true) { + val keyAndResult = upload(attachment, FileServerAPIV2.SERVER, true) { FileServerAPIV2.upload(it) } handleSuccess(attachment, keyAndResult.first, keyAndResult.second) @@ -83,22 +86,46 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess } private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise): Pair { + // Key val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0) + // Length val rawLength = attachment.length - val length = if (encrypt) PaddingInputStream.getPaddedSize(rawLength) else rawLength - val stream = if (encrypt) PaddingInputStream(attachment.inputStream, rawLength) else attachment.inputStream - val ciphertextLength = if (encrypt) AttachmentCipherOutputStream.getCiphertextLength(length) else rawLength + val length = if (encrypt) { + val paddedLength = PaddingInputStream.getPaddedSize(rawLength) + AttachmentCipherOutputStream.getCiphertextLength(paddedLength) + } else { + attachment.length + } + // In & out streams + // PaddingInputStream adds padding as data is read out from it. AttachmentCipherOutputStream + // encrypts as it writes data. + val inputStream = if (encrypt) PaddingInputStream(attachment.inputStream, rawLength) else attachment.inputStream val outputStreamFactory = if (encrypt) AttachmentCipherOutputStreamFactory(key) else PlaintextOutputStreamFactory() - val pushData = PushAttachmentData(attachment.contentType, stream, ciphertextLength, outputStreamFactory, attachment.listener) - val file = DigestingRequestBody(pushData.data, outputStreamFactory, attachment.contentType, pushData.dataSize, attachment.listener) - val id = upload(pushData.data.readBytes()).get() - return Pair(key, DotNetAPI.UploadResult(id, "${server}/files/$id", file.transmittedDigest)) + // Create a multipart request body but immediately read it out to a buffer. Doing this makes + // it easier to deal with inputStream and outputStreamFactory. + val pad = PushAttachmentData(attachment.contentType, inputStream, length, outputStreamFactory, attachment.listener) + val contentType = "application/octet-stream" + val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, contentType, pad.dataSize, attachment.listener) + Log.d("Loki", "File size: ${length.toDouble() / 1000} kb.") + val mpb = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("Content-Type", contentType) + .addFormDataPart("content", UUID.randomUUID().toString(), drb) + .build() + val b = Buffer() + mpb.writeTo(b) + val data = b.readByteArray() + // Upload the data + val id = upload(data).get() + val digest = drb.transmittedDigest + // Return + return Pair(key, DotNetAPI.UploadResult(id, "${server}/files/$id", digest)) } private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { Log.d(TAG, "Attachment uploaded successfully.") delegate?.handleJobSucceeded(this) - MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult) + MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult) MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID) } diff --git a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt index 9ea2d8e3a5..fc2375c1c8 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt @@ -5,7 +5,6 @@ import okhttp3.Request import org.session.libsession.messaging.file_server.FileServerAPI import org.session.libsession.messaging.file_server.FileServerAPIV2 import org.session.libsession.snode.OnionRequestAPI -import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream import org.session.libsignal.utilities.logging.Log import org.session.libsignal.service.api.messages.SignalServiceAttachment import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException @@ -42,7 +41,7 @@ object DownloadUtilities { @JvmStatic fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) { - if (url.contains(FileServerAPIV2.DEFAULT_SERVER)) { + if (url.contains(FileServerAPIV2.SERVER)) { val httpUrl = HttpUrl.parse(url)!! val fileId = httpUrl.pathSegments().last() try { diff --git a/libsignal/src/main/java/org/session/libsignal/service/internal/push/http/AttachmentCipherOutputStreamFactory.java b/libsignal/src/main/java/org/session/libsignal/service/internal/push/http/AttachmentCipherOutputStreamFactory.java index c6137962c8..8a6599ab05 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/internal/push/http/AttachmentCipherOutputStreamFactory.java +++ b/libsignal/src/main/java/org/session/libsignal/service/internal/push/http/AttachmentCipherOutputStreamFactory.java @@ -1,6 +1,5 @@ package org.session.libsignal.service.internal.push.http; - import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream; import org.session.libsignal.service.api.crypto.DigestingOutputStream; @@ -19,5 +18,4 @@ public class AttachmentCipherOutputStreamFactory implements OutputStreamFactory public DigestingOutputStream createFor(OutputStream wrap) throws IOException { return new AttachmentCipherOutputStream(key, wrap); } - }