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