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}