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}