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.MalformedURLException;
13import java.net.URL;
14import java.util.Arrays;
15import java.util.HashMap;
16import java.util.List;
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.parser.IqParser;
26import eu.siacs.conversations.persistance.FileBackend;
27import eu.siacs.conversations.services.AbstractConnectionManager;
28import eu.siacs.conversations.services.XmppConnectionService;
29import eu.siacs.conversations.utils.CryptoHelper;
30import eu.siacs.conversations.utils.WakeLockHelper;
31import eu.siacs.conversations.xml.Namespace;
32import eu.siacs.conversations.xml.Element;
33import eu.siacs.conversations.xmpp.stanzas.IqPacket;
34import rocks.xmpp.addr.Jid;
35
36public class HttpUploadConnection implements Transferable {
37
38 private static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
39 "Authorization",
40 "Cookie",
41 "Expires"
42 );
43
44 private HttpConnectionManager mHttpConnectionManager;
45 private XmppConnectionService mXmppConnectionService;
46
47 private boolean canceled = false;
48 private boolean delayed = false;
49 private DownloadableFile file;
50 private Message message;
51 private String mime;
52 private URL mGetUrl;
53 private URL mPutUrl;
54 private HashMap<String,String> mPutHeaders;
55 private boolean mUseTor = false;
56
57 private byte[] key = null;
58
59 private long transmitted = 0;
60
61 private InputStream mFileInputStream;
62
63 public HttpUploadConnection(HttpConnectionManager httpConnectionManager) {
64 this.mHttpConnectionManager = httpConnectionManager;
65 this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
66 this.mUseTor = mXmppConnectionService.useTorToConnect();
67 }
68
69 @Override
70 public boolean start() {
71 return false;
72 }
73
74 @Override
75 public int getStatus() {
76 return STATUS_UPLOADING;
77 }
78
79 @Override
80 public long getFileSize() {
81 return file == null ? 0 : file.getExpectedSize();
82 }
83
84 @Override
85 public int getProgress() {
86 if (file == null) {
87 return 0;
88 }
89 return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
90 }
91
92 @Override
93 public void cancel() {
94 this.canceled = true;
95 }
96
97 private void fail(String errorMessage) {
98 mHttpConnectionManager.finishUploadConnection(this);
99 message.setTransferable(null);
100 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, errorMessage);
101 FileBackend.close(mFileInputStream);
102 }
103
104 public void init(Message message, boolean delay) {
105 this.message = message;
106 final Account account = message.getConversation().getAccount();
107 this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
108 if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
109 this.mime = "application/pgp-encrypted";
110 } else {
111 this.mime = this.file.getMimeType();
112 }
113 this.delayed = delay;
114 if (Config.ENCRYPT_ON_HTTP_UPLOADED
115 || message.getEncryption() == Message.ENCRYPTION_AXOLOTL
116 || message.getEncryption() == Message.ENCRYPTION_OTR) {
117 this.key = new byte[48]; // todo: change this to 44 for 12-byte IV instead of 16-byte at some point in future
118 mXmppConnectionService.getRNG().nextBytes(this.key);
119 this.file.setKeyAndIv(this.key);
120 }
121 Pair<InputStream,Integer> pair;
122 try {
123 pair = AbstractConnectionManager.createInputStream(file, true);
124 } catch (FileNotFoundException e) {
125 Log.d(Config.LOGTAG, account.getJid().asBareJid()+": could not find file to upload - "+e.getMessage());
126 fail(e.getMessage());
127 return;
128 }
129 this.file.setExpectedSize(pair.second);
130 message.resetFileParams();
131 this.mFileInputStream = pair.first;
132 Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
133 IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file,mime);
134 mXmppConnectionService.sendIqPacket(account, request, (a, packet) -> {
135 if (packet.getType() == IqPacket.TYPE.RESULT) {
136 Element slot = packet.findChild("slot", Namespace.HTTP_UPLOAD);
137 if (slot != null) {
138 try {
139 final Element put = slot.findChild("put");
140 final Element get = slot.findChild("get");
141 final String putUrl = put == null ? null : put.getAttribute("url");
142 final String getUrl = get == null ? null : get.getAttribute("url");
143 if (getUrl != null && putUrl != null) {
144 this.mGetUrl = new URL(getUrl);
145 this.mPutUrl = new URL(putUrl);
146 this.mPutHeaders = new HashMap<>();
147 for(Element child : put.getChildren()) {
148 if ("header".equals(child.getName())) {
149 final String name = child.getAttribute("name");
150 final String value = child.getContent();
151 if (WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
152 this.mPutHeaders.put(name,value.trim());
153 }
154 }
155 }
156 if (!canceled) {
157 new Thread(this::upload).start();
158 }
159 return;
160 }
161 } catch (MalformedURLException e) {
162 //fall through
163 }
164 }
165 }
166 Log.d(Config.LOGTAG,account.getJid().toString()+": invalid response to slot request "+packet);
167 fail(IqParser.extractErrorMessage(packet));
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 " + mPutUrl.toString()+ " w/ read timeout of "+readTimeout+"s");
182 if (mUseTor) {
183 connection = (HttpURLConnection) mPutUrl.openConnection(HttpConnectionManager.getProxy());
184 } else {
185 connection = (HttpURLConnection) mPutUrl.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("Content-Type", mime == null ? "application/octet-stream" : mime);
194 connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
195 if(mPutHeaders != null) {
196 for(HashMap.Entry<String,String> entry : mPutHeaders.entrySet()) {
197 connection.setRequestProperty(entry.getKey(),entry.getValue());
198 }
199 }
200 connection.setDoOutput(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 if (code == 200 || code == 201) {
218 Log.d(Config.LOGTAG, "finished uploading file");
219 if (key != null) {
220 mGetUrl = CryptoHelper.toAesGcmUrl(new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key)));
221 }
222 mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl);
223 mXmppConnectionService.getFileBackend().updateMediaScanner(file);
224 message.setTransferable(null);
225 message.setCounterpart(message.getConversation().getJid().asBareJid());
226 mXmppConnectionService.resendMessage(message, delayed);
227 } else {
228 Log.d(Config.LOGTAG,"http upload failed because response code was "+code);
229 fail("http upload failed because response code was "+code);
230 }
231 } catch (IOException e) {
232 e.printStackTrace();
233 Log.d(Config.LOGTAG,"http upload failed "+e.getMessage());
234 fail(e.getMessage());
235 } finally {
236 FileBackend.close(mFileInputStream);
237 FileBackend.close(os);
238 if (connection != null) {
239 connection.disconnect();
240 }
241 WakeLockHelper.release(wakeLock);
242 }
243 }
244}