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