HttpUploadConnection.java

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