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}