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