package eu.siacs.conversations.http; import android.util.Log; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import org.checkerframework.checker.nullness.compatqual.NullableDecl; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.concurrent.Future; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener { static final List WHITE_LISTED_HEADERS = Arrays.asList( "Authorization", "Cookie", "Expires" ); private final HttpConnectionManager mHttpConnectionManager; private final XmppConnectionService mXmppConnectionService; private final Method method; private boolean delayed = false; private DownloadableFile file; private final Message message; private String mime; private SlotRequester.Slot slot; private byte[] key = null; private long transmitted = 0; private Call mostRecentCall; private ListenableFuture slotFuture; public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) { this.message = message; this.method = method; this.mHttpConnectionManager = httpConnectionManager; this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService(); } @Override public boolean start() { return false; } @Override public int getStatus() { return STATUS_UPLOADING; } @Override public long getFileSize() { return file == null ? 0 : file.getExpectedSize(); } @Override public int getProgress() { if (file == null) { return 0; } return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); } @Override public void cancel() { final ListenableFuture slotFuture = this.slotFuture; if (slotFuture != null && !slotFuture.isDone()) { slotFuture.cancel(true); } final Call call = this.mostRecentCall; if (call != null && !call.isCanceled()) { call.cancel(); } } private void fail(String errorMessage) { finish(); final Call call = this.mostRecentCall; final Future slotFuture = this.slotFuture; final boolean cancelled = (call != null && call.isCanceled()) || (slotFuture != null && slotFuture.isCancelled()); mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage); } private void markAsCancelled() { finish(); mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, Message.ERROR_MESSAGE_CANCELLED); } private void finish() { mHttpConnectionManager.finishUploadConnection(this); message.setTransferable(null); } public void init(boolean delay) { final Account account = message.getConversation().getAccount(); this.file = mXmppConnectionService.getFileBackend().getFile(message, false); if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { this.mime = "application/pgp-encrypted"; } else { this.mime = this.file.getMimeType(); } final long originalFileSize = file.getSize(); this.delayed = delay; if (Config.ENCRYPT_ON_HTTP_UPLOADED || message.getEncryption() == Message.ENCRYPTION_AXOLOTL || message.getEncryption() == Message.ENCRYPTION_OTR) { this.key = new byte[44]; mXmppConnectionService.getRNG().nextBytes(this.key); this.file.setKeyAndIv(this.key); } this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); message.resetFileParams(); this.slotFuture = new SlotRequester(mXmppConnectionService).request(method, account, file, mime); Futures.addCallback(this.slotFuture, new FutureCallback() { @Override public void onSuccess(@NullableDecl SlotRequester.Slot result) { HttpUploadConnection.this.slot = result; HttpUploadConnection.this.upload(); } @Override public void onFailure(@NotNull final Throwable throwable) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable); fail(throwable.getMessage()); } }, MoreExecutors.directExecutor()); message.setTransferable(this); mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); } private void upload() { final OkHttpClient client = mHttpConnectionManager.buildHttpClient( slot.put, message.getConversation().getAccount(), true ); final RequestBody requestBody = AbstractConnectionManager.requestBody(file, this); final Request request = new Request.Builder() .url(slot.put) .put(requestBody) .headers(slot.headers) .build(); Log.d(Config.LOGTAG, "uploading file to " + slot.put); this.mostRecentCall = client.newCall(request); this.mostRecentCall.enqueue(new Callback() { @Override public void onFailure(@NotNull Call call, IOException e) { Log.d(Config.LOGTAG, "http upload failed", e); fail(e.getMessage()); } @Override public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { final int code = response.code(); if (code == 200 || code == 201) { Log.d(Config.LOGTAG, "finished uploading file"); final String get; if (key != null) { get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build()); } else { get = slot.get.toString(); } mXmppConnectionService.getFileBackend().updateFileParams(message, get); mXmppConnectionService.getFileBackend().updateMediaScanner(file); finish(); if (!message.isPrivateMessage()) { message.setCounterpart(message.getConversation().getJid().asBareJid()); } mXmppConnectionService.resendMessage(message, delayed); } else { Log.d(Config.LOGTAG, "http upload failed because response code was " + code); fail("http upload failed because response code was " + code); } } }); } public Message getMessage() { return message; } @Override public void onProgress(final long progress) { this.transmitted = progress; mHttpConnectionManager.updateConversationUi(false); } }