1package eu.siacs.conversations.http;
2
3import android.os.PowerManager;
4import android.util.Log;
5import android.util.Pair;
6
7import java.io.FileNotFoundException;
8import java.io.IOException;
9import java.io.InputStream;
10import java.io.OutputStream;
11import java.net.HttpURLConnection;
12import java.net.URL;
13import java.util.Arrays;
14import java.util.HashMap;
15import java.util.List;
16import java.util.Scanner;
17
18import javax.net.ssl.HttpsURLConnection;
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.persistance.FileBackend;
26import eu.siacs.conversations.services.AbstractConnectionManager;
27import eu.siacs.conversations.services.XmppConnectionService;
28import eu.siacs.conversations.utils.Checksum;
29import eu.siacs.conversations.utils.CryptoHelper;
30import eu.siacs.conversations.utils.WakeLockHelper;
31
32public class HttpUploadConnection implements Transferable {
33
34 public static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
35 "Authorization",
36 "Cookie",
37 "Expires"
38 );
39
40 private final HttpConnectionManager mHttpConnectionManager;
41 private final XmppConnectionService mXmppConnectionService;
42 private final SlotRequester mSlotRequester;
43 private final Method method;
44 private final boolean mUseTor;
45 private boolean canceled = false;
46 private boolean delayed = false;
47 private DownloadableFile file;
48 private Message message;
49 private String mime;
50 private SlotRequester.Slot slot;
51 private byte[] key = null;
52
53 private long transmitted = 0;
54
55 private InputStream mFileInputStream;
56
57 public HttpUploadConnection(Method method, HttpConnectionManager httpConnectionManager) {
58 this.method = method;
59 this.mHttpConnectionManager = httpConnectionManager;
60 this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
61 this.mSlotRequester = new SlotRequester(this.mXmppConnectionService);
62 this.mUseTor = mXmppConnectionService.useTorToConnect();
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 ? 0 : 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 this.canceled = true;
91 }
92
93 private void fail(String errorMessage) {
94 mHttpConnectionManager.finishUploadConnection(this);
95 message.setTransferable(null);
96 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, errorMessage);
97 FileBackend.close(mFileInputStream);
98 }
99
100 public void init(Message message, boolean delay) {
101 this.message = message;
102 final Account account = message.getConversation().getAccount();
103 this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
104 if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
105 this.mime = "application/pgp-encrypted";
106 } else {
107 this.mime = this.file.getMimeType();
108 }
109 this.delayed = delay;
110 if (Config.ENCRYPT_ON_HTTP_UPLOADED
111 || message.getEncryption() == Message.ENCRYPTION_AXOLOTL
112 || message.getEncryption() == Message.ENCRYPTION_OTR) {
113 this.key = new byte[48]; // todo: change this to 44 for 12-byte IV instead of 16-byte at some point in future
114 mXmppConnectionService.getRNG().nextBytes(this.key);
115 this.file.setKeyAndIv(this.key);
116 }
117
118 final String md5;
119
120 if (method == Method.P1_S3) {
121 try {
122 md5 = Checksum.md5(AbstractConnectionManager.createInputStream(file, true).first);
123 } catch (Exception e) {
124 Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to calculate md5()", e);
125 fail(e.getMessage());
126 return;
127 }
128 } else {
129 md5 = null;
130 }
131
132 Pair<InputStream,Integer> pair;
133 try {
134 pair = AbstractConnectionManager.createInputStream(file, true);
135 } catch (FileNotFoundException e) {
136 Log.d(Config.LOGTAG, account.getJid().asBareJid()+": could not find file to upload - "+e.getMessage());
137 fail(e.getMessage());
138 return;
139 }
140 this.file.setExpectedSize(pair.second);
141 message.resetFileParams();
142 this.mFileInputStream = pair.first;
143 this.mSlotRequester.request(method, account, file, mime, md5, new SlotRequester.OnSlotRequested() {
144 @Override
145 public void success(SlotRequester.Slot slot) {
146 if (!canceled) {
147 HttpUploadConnection.this.slot = slot;
148 new Thread(HttpUploadConnection.this::upload).start();
149 }
150 }
151
152 @Override
153 public void failure(String message) {
154 fail(message);
155 }
156 });
157 message.setTransferable(this);
158 mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
159 }
160
161 private void upload() {
162 OutputStream os = null;
163 HttpURLConnection connection = null;
164 PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_upload_"+message.getUuid());
165 try {
166 final int expectedFileSize = (int) file.getExpectedSize();
167 final int readTimeout = (expectedFileSize / 2048) + Config.SOCKET_TIMEOUT; //assuming a minimum transfer speed of 16kbit/s
168 wakeLock.acquire(readTimeout);
169 Log.d(Config.LOGTAG, "uploading to " + slot.getPutUrl().toString()+ " w/ read timeout of "+readTimeout+"s");
170 if (mUseTor || message.getConversation().getAccount().isOnion()) {
171 connection = (HttpURLConnection) slot.getPutUrl().openConnection(HttpConnectionManager.getProxy());
172 } else {
173 connection = (HttpURLConnection) slot.getPutUrl().openConnection();
174 }
175 if (connection instanceof HttpsURLConnection) {
176 mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true);
177 }
178 connection.setUseCaches(false);
179 connection.setRequestMethod("PUT");
180 connection.setFixedLengthStreamingMode(expectedFileSize);
181 connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
182 if(slot.getHeaders() != null) {
183 for(HashMap.Entry<String,String> entry : slot.getHeaders().entrySet()) {
184 connection.setRequestProperty(entry.getKey(),entry.getValue());
185 }
186 }
187 connection.setDoOutput(true);
188 connection.setDoInput(true);
189 connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
190 connection.setReadTimeout(readTimeout * 1000);
191 connection.connect();
192 os = connection.getOutputStream();
193 transmitted = 0;
194 int count;
195 byte[] buffer = new byte[4096];
196 while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) {
197 transmitted += count;
198 os.write(buffer, 0, count);
199 mHttpConnectionManager.updateConversationUi(false);
200 }
201 os.flush();
202 os.close();
203 mFileInputStream.close();
204 int code = connection.getResponseCode();
205 InputStream is = connection.getErrorStream();
206 if (is != null) {
207 try (Scanner scanner = new Scanner(is)) {
208 scanner.useDelimiter("\\Z");
209 Log.d(Config.LOGTAG, "body: " + scanner.next());
210 }
211 }
212 if (code == 200 || code == 201) {
213 Log.d(Config.LOGTAG, "finished uploading file");
214 final URL get;
215 if (key != null) {
216 if (method == Method.P1_S3) {
217 get = new URL(slot.getGetUrl().toString()+"#"+CryptoHelper.bytesToHex(key));
218 } else {
219 get = CryptoHelper.toAesGcmUrl(new URL(slot.getGetUrl().toString() + "#" + CryptoHelper.bytesToHex(key)));
220 }
221 } else {
222 get = slot.getGetUrl();
223 }
224 mXmppConnectionService.getFileBackend().updateFileParams(message, get);
225 mXmppConnectionService.getFileBackend().updateMediaScanner(file);
226 message.setTransferable(null);
227 message.setCounterpart(message.getConversation().getJid().asBareJid());
228 mXmppConnectionService.resendMessage(message, delayed);
229 } else {
230 Log.d(Config.LOGTAG,"http upload failed because response code was "+code);
231 fail("http upload failed because response code was "+code);
232 }
233 } catch (Exception e) {
234 e.printStackTrace();
235 Log.d(Config.LOGTAG,"http upload failed "+e.getMessage());
236 fail(e.getMessage());
237 } finally {
238 FileBackend.close(mFileInputStream);
239 FileBackend.close(os);
240 if (connection != null) {
241 connection.disconnect();
242 }
243 WakeLockHelper.release(wakeLock);
244 }
245 }
246}