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