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