1package eu.siacs.conversations.http;
2
3import android.content.Intent;
4import android.net.Uri;
5import android.os.PowerManager;
6import android.util.Log;
7
8import java.io.BufferedInputStream;
9import java.io.IOException;
10import java.io.InputStream;
11import java.io.OutputStream;
12import java.net.HttpURLConnection;
13import java.net.InetAddress;
14import java.net.InetSocketAddress;
15import java.net.MalformedURLException;
16import java.net.Proxy;
17import java.net.URL;
18import java.util.Arrays;
19
20import javax.net.ssl.HttpsURLConnection;
21import javax.net.ssl.SSLHandshakeException;
22
23import eu.siacs.conversations.Config;
24import eu.siacs.conversations.R;
25import eu.siacs.conversations.entities.DownloadableFile;
26import eu.siacs.conversations.entities.Message;
27import eu.siacs.conversations.entities.Transferable;
28import eu.siacs.conversations.entities.TransferablePlaceholder;
29import eu.siacs.conversations.persistance.FileBackend;
30import eu.siacs.conversations.services.AbstractConnectionManager;
31import eu.siacs.conversations.services.XmppConnectionService;
32import eu.siacs.conversations.utils.CryptoHelper;
33
34public class HttpDownloadConnection implements Transferable {
35
36 private HttpConnectionManager mHttpConnectionManager;
37 private XmppConnectionService mXmppConnectionService;
38
39 private URL mUrl;
40 private Message message;
41 private DownloadableFile file;
42 private int mStatus = Transferable.STATUS_UNKNOWN;
43 private boolean acceptedAutomatically = false;
44 private int mProgress = 0;
45 private boolean mUseTor = false;
46
47 public HttpDownloadConnection(HttpConnectionManager manager) {
48 this.mHttpConnectionManager = manager;
49 this.mXmppConnectionService = manager.getXmppConnectionService();
50 this.mUseTor = mXmppConnectionService.useTorToConnect();
51 }
52
53 @Override
54 public boolean start() {
55 if (mXmppConnectionService.hasInternetConnection()) {
56 if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) {
57 checkFileSize(true);
58 } else {
59 new Thread(new FileDownloader(true)).start();
60 }
61 return true;
62 } else {
63 return false;
64 }
65 }
66
67 public void init(Message message) {
68 init(message, false);
69 }
70
71 public void init(Message message, boolean interactive) {
72 this.message = message;
73 this.message.setTransferable(this);
74 try {
75 if (message.hasFileOnRemoteHost()) {
76 mUrl = message.getFileParams().url;
77 } else {
78 mUrl = new URL(message.getBody());
79 }
80 String[] parts = mUrl.getPath().toLowerCase().split("\\.");
81 String lastPart = parts.length >= 1 ? parts[parts.length - 1] : null;
82 String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null;
83 if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) {
84 this.message.setEncryption(Message.ENCRYPTION_PGP);
85 } else if (message.getEncryption() != Message.ENCRYPTION_OTR
86 && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
87 this.message.setEncryption(Message.ENCRYPTION_NONE);
88 }
89 String extension;
90 if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(lastPart)) {
91 extension = secondToLast;
92 } else {
93 extension = lastPart;
94 }
95 message.setRelativeFilePath(message.getUuid()+"."+extension);
96 this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
97 String reference = mUrl.getRef();
98 if (reference != null && reference.length() == 96) {
99 this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
100 }
101
102 if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
103 || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL)
104 && this.file.getKey() == null) {
105 this.message.setEncryption(Message.ENCRYPTION_NONE);
106 }
107 checkFileSize(interactive);
108 } catch (MalformedURLException e) {
109 this.cancel();
110 }
111 }
112
113 private void checkFileSize(boolean interactive) {
114 new Thread(new FileSizeChecker(interactive)).start();
115 }
116
117 public void cancel() {
118 mHttpConnectionManager.finishConnection(this);
119 if (message.isFileOrImage()) {
120 message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
121 } else {
122 message.setTransferable(null);
123 }
124 mXmppConnectionService.updateConversationUi();
125 }
126
127 private void finish() {
128 Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
129 intent.setData(Uri.fromFile(file));
130 mXmppConnectionService.sendBroadcast(intent);
131 message.setTransferable(null);
132 mHttpConnectionManager.finishConnection(this);
133 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
134 message.getConversation().getAccount().getPgpDecryptionService().add(message);
135 }
136 mXmppConnectionService.updateConversationUi();
137 if (acceptedAutomatically) {
138 mXmppConnectionService.getNotificationService().push(message);
139 }
140 }
141
142 private void changeStatus(int status) {
143 this.mStatus = status;
144 mXmppConnectionService.updateConversationUi();
145 }
146
147 private void showToastForException(Exception e) {
148 e.printStackTrace();
149 if (e instanceof java.net.UnknownHostException) {
150 mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
151 } else if (e instanceof java.net.ConnectException) {
152 mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
153 } else {
154 mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
155 }
156 }
157
158 private class FileSizeChecker implements Runnable {
159
160 private boolean interactive = false;
161
162 public FileSizeChecker(boolean interactive) {
163 this.interactive = interactive;
164 }
165
166 @Override
167 public void run() {
168 long size;
169 try {
170 size = retrieveFileSize();
171 } catch (SSLHandshakeException e) {
172 changeStatus(STATUS_OFFER_CHECK_FILESIZE);
173 HttpDownloadConnection.this.acceptedAutomatically = false;
174 HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
175 return;
176 } catch (IOException e) {
177 Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
178 if (interactive) {
179 showToastForException(e);
180 }
181 cancel();
182 return;
183 }
184 file.setExpectedSize(size);
185 if (mHttpConnectionManager.hasStoragePermission() && size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
186 HttpDownloadConnection.this.acceptedAutomatically = true;
187 new Thread(new FileDownloader(interactive)).start();
188 } else {
189 changeStatus(STATUS_OFFER);
190 HttpDownloadConnection.this.acceptedAutomatically = false;
191 HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
192 }
193 }
194
195 private long retrieveFileSize() throws IOException {
196 try {
197 Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive));
198 changeStatus(STATUS_CHECKING);
199 HttpURLConnection connection;
200 if (mUseTor) {
201 connection = (HttpURLConnection) mUrl.openConnection(mHttpConnectionManager.getProxy());
202 } else {
203 connection = (HttpURLConnection) mUrl.openConnection();
204 }
205 connection.setRequestMethod("HEAD");
206 Log.d(Config.LOGTAG,"url: "+connection.getURL().toString());
207 Log.d(Config.LOGTAG,"connection: "+connection.toString());
208 connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getIdentityName());
209 if (connection instanceof HttpsURLConnection) {
210 mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
211 }
212 connection.connect();
213 String contentLength = connection.getHeaderField("Content-Length");
214 connection.disconnect();
215 if (contentLength == null) {
216 throw new IOException();
217 }
218 return Long.parseLong(contentLength, 10);
219 } catch (IOException e) {
220 throw e;
221 } catch (NumberFormatException e) {
222 throw new IOException();
223 }
224 }
225
226 }
227
228 private class FileDownloader implements Runnable {
229
230 private boolean interactive = false;
231
232 private OutputStream os;
233
234 public FileDownloader(boolean interactive) {
235 this.interactive = interactive;
236 }
237
238 @Override
239 public void run() {
240 try {
241 changeStatus(STATUS_DOWNLOADING);
242 download();
243 updateImageBounds();
244 finish();
245 } catch (SSLHandshakeException e) {
246 changeStatus(STATUS_OFFER);
247 } catch (IOException e) {
248 if (interactive) {
249 showToastForException(e);
250 }
251 cancel();
252 }
253 }
254
255 private void download() throws IOException {
256 InputStream is = null;
257 PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid());
258 try {
259 wakeLock.acquire();
260 HttpURLConnection connection;
261 if (mUseTor) {
262 connection = (HttpURLConnection) mUrl.openConnection(mHttpConnectionManager.getProxy());
263 } else {
264 connection = (HttpURLConnection) mUrl.openConnection();
265 }
266 if (connection instanceof HttpsURLConnection) {
267 mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
268 }
269 connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
270 connection.connect();
271 is = new BufferedInputStream(connection.getInputStream());
272 file.getParentFile().mkdirs();
273 file.createNewFile();
274 os = AbstractConnectionManager.createOutputStream(file, true);
275 long transmitted = 0;
276 long expected = file.getExpectedSize();
277 int count = -1;
278 byte[] buffer = new byte[1024];
279 while ((count = is.read(buffer)) != -1) {
280 transmitted += count;
281 os.write(buffer, 0, count);
282 updateProgress((int) ((((double) transmitted) / expected) * 100));
283 }
284 os.flush();
285 } catch (IOException e) {
286 throw e;
287 } finally {
288 FileBackend.close(os);
289 FileBackend.close(is);
290 wakeLock.release();
291 }
292 }
293
294 private void updateImageBounds() {
295 message.setType(Message.TYPE_FILE);
296 mXmppConnectionService.getFileBackend().updateFileParams(message, mUrl);
297 mXmppConnectionService.updateMessage(message);
298 }
299
300 }
301
302 public void updateProgress(int i) {
303 this.mProgress = i;
304 mXmppConnectionService.updateConversationUi();
305 }
306
307 @Override
308 public int getStatus() {
309 return this.mStatus;
310 }
311
312 @Override
313 public long getFileSize() {
314 if (this.file != null) {
315 return this.file.getExpectedSize();
316 } else {
317 return 0;
318 }
319 }
320
321 @Override
322 public int getProgress() {
323 return this.mProgress;
324 }
325}