HttpDownloadConnection.java

  1package eu.siacs.conversations.http;
  2
  3import android.content.Intent;
  4import android.net.Uri;
  5import android.util.Log;
  6
  7import org.bouncycastle.crypto.engines.AESEngine;
  8import org.bouncycastle.crypto.io.CipherOutputStream;
  9import org.bouncycastle.crypto.modes.AEADBlockCipher;
 10import org.bouncycastle.crypto.modes.GCMBlockCipher;
 11import org.bouncycastle.crypto.params.AEADParameters;
 12import org.bouncycastle.crypto.params.KeyParameter;
 13
 14import java.io.BufferedInputStream;
 15import java.io.FileOutputStream;
 16import java.io.IOException;
 17import java.io.OutputStream;
 18import java.net.HttpURLConnection;
 19import java.net.MalformedURLException;
 20import java.net.URL;
 21import java.util.Arrays;
 22
 23import javax.net.ssl.HttpsURLConnection;
 24import javax.net.ssl.SSLHandshakeException;
 25
 26import eu.siacs.conversations.Config;
 27import eu.siacs.conversations.R;
 28import eu.siacs.conversations.entities.DownloadableFile;
 29import eu.siacs.conversations.entities.Message;
 30import eu.siacs.conversations.entities.Transferable;
 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
 46	public HttpDownloadConnection(HttpConnectionManager manager) {
 47		this.mHttpConnectionManager = manager;
 48		this.mXmppConnectionService = manager.getXmppConnectionService();
 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			mUrl = new URL(message.getBody());
 74			String[] parts = mUrl.getPath().toLowerCase().split("\\.");
 75			String lastPart = parts.length >= 1 ? parts[parts.length - 1] : null;
 76			String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null;
 77			if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) {
 78				this.message.setEncryption(Message.ENCRYPTION_PGP);
 79			} else if (message.getEncryption() != Message.ENCRYPTION_OTR
 80					&& message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
 81				this.message.setEncryption(Message.ENCRYPTION_NONE);
 82			}
 83			String extension;
 84			if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(lastPart)) {
 85				extension = secondToLast;
 86			} else {
 87				extension = lastPart;
 88			}
 89			message.setRelativeFilePath(message.getUuid()+"."+extension);
 90			this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
 91			String reference = mUrl.getRef();
 92			if (reference != null && reference.length() == 96) {
 93				this.file.setKey(CryptoHelper.hexToBytes(reference));
 94			}
 95
 96			if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
 97					|| this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL)
 98					&& this.file.getKey() == null) {
 99				this.message.setEncryption(Message.ENCRYPTION_NONE);
100					}
101			checkFileSize(interactive);
102		} catch (MalformedURLException e) {
103			this.cancel();
104		}
105	}
106
107	private void checkFileSize(boolean interactive) {
108		new Thread(new FileSizeChecker(interactive)).start();
109	}
110
111	public void cancel() {
112		mHttpConnectionManager.finishConnection(this);
113		message.setTransferable(null);
114		mXmppConnectionService.updateConversationUi();
115	}
116
117	private void finish() {
118		Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
119		intent.setData(Uri.fromFile(file));
120		mXmppConnectionService.sendBroadcast(intent);
121		message.setTransferable(null);
122		mHttpConnectionManager.finishConnection(this);
123		mXmppConnectionService.updateConversationUi();
124		if (acceptedAutomatically) {
125			mXmppConnectionService.getNotificationService().push(message);
126		}
127	}
128
129	private void changeStatus(int status) {
130		this.mStatus = status;
131		mXmppConnectionService.updateConversationUi();
132	}
133
134	private class FileSizeChecker implements Runnable {
135
136		private boolean interactive = false;
137
138		public FileSizeChecker(boolean interactive) {
139			this.interactive = interactive;
140		}
141
142		@Override
143		public void run() {
144			long size;
145			try {
146				size = retrieveFileSize();
147			} catch (SSLHandshakeException e) {
148				changeStatus(STATUS_OFFER_CHECK_FILESIZE);
149				HttpDownloadConnection.this.acceptedAutomatically = false;
150				HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
151				return;
152			} catch (IOException e) {
153				Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
154				if (interactive) {
155					mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host);
156				}
157				cancel();
158				return;
159			}
160			file.setExpectedSize(size);
161			if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
162				HttpDownloadConnection.this.acceptedAutomatically = true;
163				new Thread(new FileDownloader(interactive)).start();
164			} else {
165				changeStatus(STATUS_OFFER);
166				HttpDownloadConnection.this.acceptedAutomatically = false;
167				HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
168			}
169		}
170
171		private long retrieveFileSize() throws IOException {
172			Log.d(Config.LOGTAG,"retrieve file size. interactive:"+String.valueOf(interactive));
173			changeStatus(STATUS_CHECKING);
174			HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
175			connection.setRequestMethod("HEAD");
176			if (connection instanceof HttpsURLConnection) {
177				mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
178			}
179			connection.connect();
180			String contentLength = connection.getHeaderField("Content-Length");
181			if (contentLength == null) {
182				throw new IOException();
183			}
184			try {
185				return Long.parseLong(contentLength, 10);
186			} catch (NumberFormatException e) {
187				throw new IOException();
188			}
189		}
190
191	}
192
193	private class FileDownloader implements Runnable {
194
195		private boolean interactive = false;
196
197		public FileDownloader(boolean interactive) {
198			this.interactive = interactive;
199		}
200
201		@Override
202		public void run() {
203			try {
204				changeStatus(STATUS_DOWNLOADING);
205				download();
206				updateImageBounds();
207				finish();
208			} catch (SSLHandshakeException e) {
209				changeStatus(STATUS_OFFER);
210			} catch (IOException e) {
211				mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host);
212				cancel();
213			}
214		}
215
216		private void download() throws IOException {
217			HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
218			if (connection instanceof HttpsURLConnection) {
219				mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
220			}
221			connection.connect();
222			BufferedInputStream is = new BufferedInputStream(connection.getInputStream());
223			file.getParentFile().mkdirs();
224			file.createNewFile();
225			OutputStream os;
226			if (file.getKey() != null) {
227				AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
228				cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
229				os = new CipherOutputStream(new FileOutputStream(file), cipher);
230			} else {
231				os = new FileOutputStream(file);
232			}
233			long transmitted = 0;
234			long expected = file.getExpectedSize();
235			int count = -1;
236			byte[] buffer = new byte[1024];
237			while ((count = is.read(buffer)) != -1) {
238				transmitted += count;
239				os.write(buffer, 0, count);
240				updateProgress((int) ((((double) transmitted) / expected) * 100));
241			}
242			os.flush();
243			os.close();
244			is.close();
245		}
246
247		private void updateImageBounds() {
248			message.setType(Message.TYPE_FILE);
249			mXmppConnectionService.getFileBackend().updateFileParams(message, mUrl);
250			mXmppConnectionService.updateMessage(message);
251		}
252
253	}
254
255	public void updateProgress(int i) {
256		this.mProgress = i;
257		mXmppConnectionService.updateConversationUi();
258	}
259
260	@Override
261	public int getStatus() {
262		return this.mStatus;
263	}
264
265	@Override
266	public long getFileSize() {
267		if (this.file != null) {
268			return this.file.getExpectedSize();
269		} else {
270			return 0;
271		}
272	}
273
274	@Override
275	public int getProgress() {
276		return this.mProgress;
277	}
278}