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