HttpUploadConnection.java

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