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