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