HttpUploadConnection.java

  1package eu.siacs.conversations.http;
  2
  3import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
  4
  5import android.util.Log;
  6import androidx.annotation.NonNull;
  7import androidx.annotation.Nullable;
  8import com.google.common.util.concurrent.FutureCallback;
  9import com.google.common.util.concurrent.Futures;
 10import com.google.common.util.concurrent.ListenableFuture;
 11import com.google.common.util.concurrent.MoreExecutors;
 12import eu.siacs.conversations.Config;
 13import eu.siacs.conversations.entities.Account;
 14import eu.siacs.conversations.entities.DownloadableFile;
 15import eu.siacs.conversations.entities.Message;
 16import eu.siacs.conversations.entities.Transferable;
 17import eu.siacs.conversations.services.AbstractConnectionManager;
 18import eu.siacs.conversations.services.XmppConnectionService;
 19import eu.siacs.conversations.utils.CryptoHelper;
 20import java.io.IOException;
 21import java.util.Arrays;
 22import java.util.List;
 23import java.util.concurrent.Future;
 24import okhttp3.Call;
 25import okhttp3.Callback;
 26import okhttp3.OkHttpClient;
 27import okhttp3.Request;
 28import okhttp3.RequestBody;
 29import okhttp3.Response;
 30
 31public class HttpUploadConnection
 32        implements Transferable, AbstractConnectionManager.ProgressListener {
 33
 34    static final List<String> WHITE_LISTED_HEADERS =
 35            Arrays.asList("Authorization", "Cookie", "Expires");
 36
 37    private final HttpConnectionManager mHttpConnectionManager;
 38    private final XmppConnectionService mXmppConnectionService;
 39    private boolean delayed = false;
 40    private DownloadableFile file;
 41    private final Message message;
 42    private SlotRequester.Slot slot;
 43    private byte[] key = null;
 44
 45    private long transmitted = 0;
 46    private Call mostRecentCall;
 47    private ListenableFuture<SlotRequester.Slot> slotFuture;
 48
 49    public HttpUploadConnection(
 50            final Message message, final HttpConnectionManager httpConnectionManager) {
 51        this.message = message;
 52        this.mHttpConnectionManager = httpConnectionManager;
 53        this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
 54    }
 55
 56    @Override
 57    public boolean start() {
 58        return false;
 59    }
 60
 61    @Override
 62    public int getStatus() {
 63        return STATUS_UPLOADING;
 64    }
 65
 66    @Override
 67    public Long getFileSize() {
 68        return file == null ? null : file.getExpectedSize();
 69    }
 70
 71    @Override
 72    public int getProgress() {
 73        if (file == null) {
 74            return 0;
 75        }
 76        return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
 77    }
 78
 79    @Override
 80    public void cancel() {
 81        final ListenableFuture<SlotRequester.Slot> slotFuture = this.slotFuture;
 82        if (slotFuture != null && !slotFuture.isDone()) {
 83            if (slotFuture.cancel(true)) {
 84                Log.d(Config.LOGTAG, "cancelled slot requester");
 85            }
 86        }
 87        final Call call = this.mostRecentCall;
 88        if (call != null && !call.isCanceled()) {
 89            call.cancel();
 90            Log.d(Config.LOGTAG, "cancelled HTTP request");
 91        }
 92    }
 93
 94    private void fail(String errorMessage) {
 95        finish();
 96        final Call call = this.mostRecentCall;
 97        final Future<SlotRequester.Slot> slotFuture = this.slotFuture;
 98        final boolean cancelled =
 99                (call != null && call.isCanceled())
100                        || (slotFuture != null && slotFuture.isCancelled());
101        mXmppConnectionService.markMessage(
102                message,
103                Message.STATUS_SEND_FAILED,
104                cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
105    }
106
107    private void finish() {
108        mHttpConnectionManager.finishUploadConnection(this);
109        message.setTransferable(null);
110    }
111
112    public void init(boolean delay) {
113        final Account account = message.getConversation().getAccount();
114        this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
115        final String mime;
116        if (message.getEncryption() == Message.ENCRYPTION_PGP
117                || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
118            mime = "application/pgp-encrypted";
119        } else {
120            mime = this.file.getMimeType();
121        }
122        final long originalFileSize = file.getSize();
123        this.delayed = delay;
124        if (Config.ENCRYPT_ON_HTTP_UPLOADED
125                || message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
126            this.key = new byte[44];
127            SECURE_RANDOM.nextBytes(this.key);
128            this.file.setKeyAndIv(this.key);
129        }
130        this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
131        message.resetFileParams();
132        this.slotFuture = new SlotRequester(mXmppConnectionService).request(account, file, mime);
133        Futures.addCallback(
134                this.slotFuture,
135                new FutureCallback<>() {
136                    @Override
137                    public void onSuccess(@Nullable SlotRequester.Slot result) {
138                        HttpUploadConnection.this.slot = result;
139                        try {
140                            HttpUploadConnection.this.upload();
141                        } catch (final Exception e) {
142                            fail(e.getMessage());
143                        }
144                    }
145
146                    @Override
147                    public void onFailure(@NonNull final Throwable throwable) {
148                        Log.d(
149                                Config.LOGTAG,
150                                account.getJid().asBareJid() + ": unable to request slot",
151                                throwable);
152                        // TODO consider fall back to jingle in 1-on-1 chats with exactly one online
153                        // presence
154                        fail(throwable.getMessage());
155                    }
156                },
157                MoreExecutors.directExecutor());
158        message.setTransferable(this);
159        mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
160    }
161
162    private void upload() {
163        final OkHttpClient client =
164                mHttpConnectionManager.buildHttpClient(
165                        slot.put, message.getConversation().getAccount(), 0, true);
166        final RequestBody requestBody = AbstractConnectionManager.requestBody(file, this);
167        final Request request =
168                new Request.Builder().url(slot.put).put(requestBody).headers(slot.headers).build();
169        Log.d(Config.LOGTAG, "uploading file to " + slot.put);
170        this.mostRecentCall = client.newCall(request);
171        this.mostRecentCall.enqueue(
172                new Callback() {
173                    @Override
174                    public void onFailure(@NonNull Call call, IOException e) {
175                        Log.d(Config.LOGTAG, "http upload failed", e);
176                        fail(e.getMessage());
177                    }
178
179                    @Override
180                    public void onResponse(@NonNull Call call, @NonNull Response response) {
181                        final int code = response.code();
182                        if (code == 200 || code == 201) {
183                            Log.d(Config.LOGTAG, "finished uploading file");
184                            final String get;
185                            if (key != null) {
186                                get =
187                                        AesGcmURL.toAesGcmUrl(
188                                                slot.get
189                                                        .newBuilder()
190                                                        .fragment(CryptoHelper.bytesToHex(key))
191                                                        .build());
192                            } else {
193                                get = slot.get.toString();
194                            }
195                            mXmppConnectionService.getFileBackend().updateFileParams(message, get);
196                            mXmppConnectionService.getFileBackend().updateMediaScanner(file);
197                            finish();
198                            if (!message.isPrivateMessage()) {
199                                message.setCounterpart(
200                                        message.getConversation().getJid().asBareJid());
201                            }
202                            mXmppConnectionService.resendMessage(message, delayed);
203                        } else {
204                            Log.d(
205                                    Config.LOGTAG,
206                                    "http upload failed because response code was " + code);
207                            fail("http upload failed because response code was " + code);
208                        }
209                    }
210                });
211    }
212
213    public Message getMessage() {
214        return message;
215    }
216
217    @Override
218    public void onProgress(final long progress) {
219        this.transmitted = progress;
220        mHttpConnectionManager.updateConversationUi(false);
221    }
222}