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