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