HttpUploadConnection.java

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