HttpDownloadConnection.java

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