upload files using p1s3 - sending part

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/entities/Account.java                   |   2 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java              |   8 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java         |  42 
src/main/java/eu/siacs/conversations/http/CustomURLStreamHandlerFactory.java |   5 
src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java         |   5 
src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java          | 110 
src/main/java/eu/siacs/conversations/http/Method.java                        |  51 
src/main/java/eu/siacs/conversations/http/P1S3UrlStreamHandler.java          |  50 
src/main/java/eu/siacs/conversations/http/SlotRequester.java                 | 160 
src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java |  14 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java     |   4 
src/main/java/eu/siacs/conversations/utils/Checksum.java                     |  60 
src/main/java/eu/siacs/conversations/xml/Namespace.java                      |   1 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                |   4 
14 files changed, 434 insertions(+), 82 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/Account.java 🔗

@@ -57,7 +57,7 @@ public class Account extends AbstractEntity {
 	public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>();
 
 	public boolean httpUploadAvailable(long filesize) {
-		return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize);
+		return xmppConnection != null && (xmppConnection.getFeatures().httpUpload(filesize) || xmppConnection.getFeatures().p1S3FileTransfer());
 	}
 
 	public boolean httpUploadAvailable() {

src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -350,6 +350,14 @@ public class IqGenerator extends AbstractGenerator {
 		return packet;
 	}
 
+	public IqPacket requestP1S3Slot(Jid host, String md5) {
+		IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+		packet.setTo(host);
+		packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("md5",md5);
+		Log.d(Config.LOGTAG,packet.toString());
+		return packet;
+	}
+
 	private static String convertFilename(String name) {
 		int pos = name.indexOf('.');
 		if (pos != -1) {

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java 🔗

@@ -1,5 +1,8 @@
 package eu.siacs.conversations.generator;
 
+import android.util.Log;
+
+import java.net.URL;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -13,6 +16,7 @@ import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.http.P1S3UrlStreamHandler;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
@@ -42,7 +46,7 @@ public class MessageGenerator extends AbstractGenerator {
 		} else if (message.getType() == Message.TYPE_PRIVATE) { //TODO files and images might be private as well
 			packet.setTo(message.getCounterpart());
 			packet.setType(MessagePacket.TYPE_CHAT);
-			packet.addChild("x","http://jabber.org/protocol/muc#user");
+			packet.addChild("x", "http://jabber.org/protocol/muc#user");
 			if (this.mXmppConnectionService.indicateReceived()) {
 				packet.addChild("request", "urn:xmpp:receipts");
 			}
@@ -55,9 +59,9 @@ public class MessageGenerator extends AbstractGenerator {
 		}
 		packet.setFrom(account.getJid());
 		packet.setId(message.getUuid());
-		packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id",message.getUuid());
+		packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid());
 		if (message.edited()) {
-			packet.addChild("replace","urn:xmpp:message-correct:0").setAttribute("id",message.getEditedId());
+			packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedId());
 		}
 		return packet;
 	}
@@ -79,9 +83,9 @@ public class MessageGenerator extends AbstractGenerator {
 		packet.setAxolotlMessage(axolotlMessage.toElement());
 		packet.setBody(OMEMO_FALLBACK_MESSAGE);
 		packet.addChild("store", "urn:xmpp:hints");
-		packet.addChild("encryption","urn:xmpp:eme:0")
-				.setAttribute("name","OMEMO")
-				.setAttribute("namespace",AxolotlService.PEP_PREFIX);
+		packet.addChild("encryption", "urn:xmpp:eme:0")
+				.setAttribute("name", "OMEMO")
+				.setAttribute("namespace", AxolotlService.PEP_PREFIX);
 		return packet;
 	}
 
@@ -99,8 +103,16 @@ public class MessageGenerator extends AbstractGenerator {
 		String content;
 		if (message.hasFileOnRemoteHost()) {
 			Message.FileParams fileParams = message.getFileParams();
-			content = fileParams.url.toString();
-			packet.addChild("x",Namespace.OOB).addChild("url").setContent(content);
+			final URL url = fileParams.url;
+			if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) {
+				Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER);
+				x.setAttribute("name", url.getFile());
+				x.setAttribute("fileid", url.getHost());
+				return packet;
+			} else {
+				content = url.toString();
+				packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
+			}
 		} else {
 			content = message.getBody();
 		}
@@ -113,7 +125,7 @@ public class MessageGenerator extends AbstractGenerator {
 		if (message.hasFileOnRemoteHost()) {
 			final String url = message.getFileParams().url.toString();
 			packet.setBody(url);
-			packet.addChild("x",Namespace.OOB).addChild("url").setContent(url);
+			packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
 		} else {
 			if (Config.supportUnencrypted()) {
 				packet.setBody(PGP_FALLBACK_MESSAGE);
@@ -146,16 +158,16 @@ public class MessageGenerator extends AbstractGenerator {
 		packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
 		packet.setTo(groupChat ? to.asBareJid() : to);
 		packet.setFrom(account.getJid());
-		Element displayed = packet.addChild("displayed","urn:xmpp:chat-markers:0");
+		Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0");
 		displayed.setAttribute("id", id);
 		if (groupChat && counterpart != null) {
-			displayed.setAttribute("sender",counterpart.toString());
+			displayed.setAttribute("sender", counterpart.toString());
 		}
 		packet.addChild("store", "urn:xmpp:hints");
 		return packet;
 	}
 
-	public MessagePacket conferenceSubject(Conversation conversation,String subject) {
+	public MessagePacket conferenceSubject(Conversation conversation, String subject) {
 		MessagePacket packet = new MessagePacket();
 		packet.setType(MessagePacket.TYPE_GROUPCHAT);
 		packet.setTo(conversation.getJid().asBareJid());
@@ -175,7 +187,7 @@ public class MessageGenerator extends AbstractGenerator {
 		x.setAttribute("jid", conversation.getJid().asBareJid().toString());
 		String password = conversation.getMucOptions().getPassword();
 		if (password != null) {
-			x.setAttribute("password",password);
+			x.setAttribute("password", password);
 		}
 		return packet;
 	}
@@ -198,7 +210,7 @@ public class MessageGenerator extends AbstractGenerator {
 		receivedPacket.setType(type);
 		receivedPacket.setTo(originalMessage.getFrom());
 		receivedPacket.setFrom(account.getJid());
-		for(String namespace : namespaces) {
+		for (String namespace : namespaces) {
 			receivedPacket.addChild("received", namespace).setAttribute("id", originalMessage.getId());
 		}
 		receivedPacket.addChild("store", "urn:xmpp:hints");
@@ -209,7 +221,7 @@ public class MessageGenerator extends AbstractGenerator {
 		MessagePacket packet = new MessagePacket();
 		packet.setFrom(account.getJid());
 		packet.setTo(to);
-		packet.addChild("received","urn:xmpp:receipts").setAttribute("id",id);
+		packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id);
 		packet.addChild("store", "urn:xmpp:hints");
 		return packet;
 	}

src/main/java/eu/siacs/conversations/http/AesGcmURLStreamHandlerFactory.java → src/main/java/eu/siacs/conversations/http/CustomURLStreamHandlerFactory.java 🔗

@@ -3,11 +3,14 @@ package eu.siacs.conversations.http;
 import java.net.URLStreamHandler;
 import java.net.URLStreamHandlerFactory;
 
-public class AesGcmURLStreamHandlerFactory implements URLStreamHandlerFactory {
+public class CustomURLStreamHandlerFactory implements URLStreamHandlerFactory {
+
     @Override
     public URLStreamHandler createURLStreamHandler(String protocol) {
         if (AesGcmURLStreamHandler.PROTOCOL_NAME.equals(protocol)) {
             return new AesGcmURLStreamHandler();
+        } else if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(protocol)) {
+            return new P1S3UrlStreamHandler();
         } else {
             return null;
         }

src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java 🔗

@@ -46,11 +46,10 @@ public class HttpConnectionManager extends AbstractConnectionManager {
 		return connection;
 	}
 
-	public HttpUploadConnection createNewUploadConnection(Message message, boolean delay) {
-		HttpUploadConnection connection = new HttpUploadConnection(this);
+	public void createNewUploadConnection(Message message, boolean delay) {
+		HttpUploadConnection connection = new HttpUploadConnection(Method.determine(message.getConversation().getAccount()), this);
 		connection.init(message,delay);
 		this.uploadConnections.add(connection);
-		return connection;
 	}
 
 	public void finishConnection(HttpDownloadConnection connection) {

src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java 🔗

@@ -1,9 +1,12 @@
 package eu.siacs.conversations.http;
 
 import android.os.PowerManager;
+import android.renderscript.ScriptGroup;
 import android.util.Log;
 import android.util.Pair;
 
+import org.bouncycastle.jce.exception.ExtIOException;
+
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
@@ -14,6 +17,7 @@ import java.net.URL;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Scanner;
 
 import javax.net.ssl.HttpsURLConnection;
 
@@ -26,6 +30,7 @@ import eu.siacs.conversations.parser.IqParser;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.AbstractConnectionManager;
 import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.Checksum;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.WakeLockHelper;
 import eu.siacs.conversations.xml.Namespace;
@@ -35,24 +40,24 @@ import rocks.xmpp.addr.Jid;
 
 public class HttpUploadConnection implements Transferable {
 
-	private static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
+	public static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
 			"Authorization",
 			"Cookie",
 			"Expires"
 	);
 
-	private HttpConnectionManager mHttpConnectionManager;
-	private XmppConnectionService mXmppConnectionService;
+	private final HttpConnectionManager mHttpConnectionManager;
+	private final XmppConnectionService mXmppConnectionService;
+	private final SlotRequester mSlotRequester;
+	private final Method method;
 
 	private boolean canceled = false;
 	private boolean delayed = false;
 	private DownloadableFile file;
 	private Message message;
 	private String mime;
-	private URL mGetUrl;
-	private URL mPutUrl;
-	private HashMap<String,String> mPutHeaders;
-	private boolean mUseTor = false;
+	private SlotRequester.Slot slot;
+	private final boolean mUseTor;
 
 	private byte[] key = null;
 
@@ -60,9 +65,11 @@ public class HttpUploadConnection implements Transferable {
 
 	private InputStream mFileInputStream;
 
-	public HttpUploadConnection(HttpConnectionManager httpConnectionManager) {
+	public HttpUploadConnection(Method method, HttpConnectionManager httpConnectionManager) {
+		this.method = method;
 		this.mHttpConnectionManager = httpConnectionManager;
 		this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
+		this.mSlotRequester = new SlotRequester(this.mXmppConnectionService);
 		this.mUseTor = mXmppConnectionService.useTorToConnect();
 	}
 
@@ -118,6 +125,21 @@ public class HttpUploadConnection implements Transferable {
 			mXmppConnectionService.getRNG().nextBytes(this.key);
 			this.file.setKeyAndIv(this.key);
 		}
+
+		final String md5;
+
+		if (method == Method.P1_S3) {
+			try {
+				md5 = Checksum.md5(AbstractConnectionManager.createInputStream(file, true).first);
+			} catch (Exception e) {
+				Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to calculate md5()", e);
+				fail(e.getMessage());
+				return;
+			}
+		} else {
+			md5 = null;
+		}
+
 		Pair<InputStream,Integer> pair;
 		try {
 			pair = AbstractConnectionManager.createInputStream(file, true);
@@ -129,42 +151,20 @@ public class HttpUploadConnection implements Transferable {
 		this.file.setExpectedSize(pair.second);
 		message.resetFileParams();
 		this.mFileInputStream = pair.first;
-		Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
-		IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file,mime);
-		mXmppConnectionService.sendIqPacket(account, request, (a, packet) -> {
-			if (packet.getType() == IqPacket.TYPE.RESULT) {
-				Element slot = packet.findChild("slot", Namespace.HTTP_UPLOAD);
-				if (slot != null) {
-					try {
-						final Element put = slot.findChild("put");
-						final Element get = slot.findChild("get");
-						final String putUrl = put == null ? null : put.getAttribute("url");
-						final String getUrl = get == null ? null : get.getAttribute("url");
-						if (getUrl != null && putUrl != null) {
-							this.mGetUrl = new URL(getUrl);
-							this.mPutUrl = new URL(putUrl);
-							this.mPutHeaders = new HashMap<>();
-							for(Element child : put.getChildren()) {
-								if ("header".equals(child.getName())) {
-									final String name = child.getAttribute("name");
-									final String value = child.getContent();
-									if (WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
-										this.mPutHeaders.put(name,value.trim());
-									}
-								}
-							}
-							if (!canceled) {
-								new Thread(this::upload).start();
-							}
-							return;
-						}
-					} catch (MalformedURLException e) {
-						//fall through
-					}
+		this.mSlotRequester.request(method, account, file, mime, md5, new SlotRequester.OnSlotRequested() {
+			@Override
+			public void success(SlotRequester.Slot slot) {
+				if (!canceled) {
+					HttpUploadConnection.this.slot = slot;
+					Log.d(Config.LOGTAG,"not starting upload to "+slot.getPutUrl());
+					new Thread(HttpUploadConnection.this::upload).start();
 				}
 			}
-			Log.d(Config.LOGTAG,account.getJid().toString()+": invalid response to slot request "+packet);
-			fail(IqParser.extractErrorMessage(packet));
+
+			@Override
+			public void failure(String message) {
+				fail(message);
+			}
 		});
 		message.setTransferable(this);
 		mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
@@ -178,11 +178,11 @@ public class HttpUploadConnection implements Transferable {
 			final int expectedFileSize = (int) file.getExpectedSize();
 			final int readTimeout = (expectedFileSize / 2048) + Config.SOCKET_TIMEOUT; //assuming a minimum transfer speed of 16kbit/s
 			wakeLock.acquire(readTimeout);
-			Log.d(Config.LOGTAG, "uploading to " + mPutUrl.toString()+ " w/ read timeout of "+readTimeout+"s");
+			Log.d(Config.LOGTAG, "uploading to " + slot.getPutUrl().toString()+ " w/ read timeout of "+readTimeout+"s");
 			if (mUseTor) {
-				connection = (HttpURLConnection) mPutUrl.openConnection(HttpConnectionManager.getProxy());
+				connection = (HttpURLConnection) slot.getPutUrl().openConnection(HttpConnectionManager.getProxy());
 			} else {
-				connection = (HttpURLConnection) mPutUrl.openConnection();
+				connection = (HttpURLConnection) slot.getPutUrl().openConnection();
 			}
 			if (connection instanceof HttpsURLConnection) {
 				mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true);
@@ -190,14 +190,14 @@ public class HttpUploadConnection implements Transferable {
 			connection.setUseCaches(false);
 			connection.setRequestMethod("PUT");
 			connection.setFixedLengthStreamingMode(expectedFileSize);
-			connection.setRequestProperty("Content-Type", mime == null ? "application/octet-stream" : mime);
 			connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
-			if(mPutHeaders != null) {
-				for(HashMap.Entry<String,String> entry : mPutHeaders.entrySet()) {
+			if(slot.getHeaders() != null) {
+				for(HashMap.Entry<String,String> entry : slot.getHeaders().entrySet()) {
 					connection.setRequestProperty(entry.getKey(),entry.getValue());
 				}
 			}
 			connection.setDoOutput(true);
+			connection.setDoInput(true);
 			connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
 			connection.setReadTimeout(readTimeout * 1000);
 			connection.connect();
@@ -214,12 +214,22 @@ public class HttpUploadConnection implements Transferable {
 			os.close();
 			mFileInputStream.close();
 			int code = connection.getResponseCode();
+			InputStream is = connection.getErrorStream();
+			if (is != null) {
+				try (Scanner scanner = new Scanner(is)) {
+					scanner.useDelimiter("\\Z");
+					Log.d(Config.LOGTAG, "body: " + scanner.next());
+				}
+			}
 			if (code == 200 || code == 201) {
 				Log.d(Config.LOGTAG, "finished uploading file");
+				final URL get;
 				if (key != null) {
-					mGetUrl = CryptoHelper.toAesGcmUrl(new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key)));
+					get = CryptoHelper.toAesGcmUrl(new URL(slot.getGetUrl().toString() + "#" + CryptoHelper.bytesToHex(key)));
+				} else {
+					get = slot.getGetUrl();
 				}
-				mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl);
+				mXmppConnectionService.getFileBackend().updateFileParams(message, get);
 				mXmppConnectionService.getFileBackend().updateMediaScanner(file);
 				message.setTransferable(null);
 				message.setCounterpart(message.getConversation().getJid().asBareJid());

src/main/java/eu/siacs/conversations/http/Method.java 🔗

@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.http;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.XmppConnection;
+
+public enum  Method {
+	P1_S3, HTTP_UPLOAD;
+
+	public static Method determine(Account account) {
+		XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures();
+		if (features == null) {
+			return HTTP_UPLOAD;
+		}
+		if (features.httpUpload(0)) {
+			return HTTP_UPLOAD;
+		} else if (features.p1S3FileTransfer()) {
+			return P1_S3;
+		} else {
+			return HTTP_UPLOAD;
+		}
+	}
+}

src/main/java/eu/siacs/conversations/http/P1S3UrlStreamHandler.java 🔗

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.http;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+public class P1S3UrlStreamHandler extends URLStreamHandler {
+
+	public static final String PROTOCOL_NAME = "p1s3";
+
+	@Override
+	protected URLConnection openConnection(URL url) {
+		throw new IllegalStateException("Unable to open connection with stub protocol");
+	}
+
+	public static URL of(String fileId, String filename) throws MalformedURLException {
+		return new URL(PROTOCOL_NAME+"://" + fileId + "/" + filename);
+	}
+}

src/main/java/eu/siacs/conversations/http/SlotRequester.java 🔗

@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.http;
+
+import android.util.Log;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.parser.IqParser;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import rocks.xmpp.addr.Jid;
+
+public class SlotRequester {
+
+	private XmppConnectionService service;
+
+	public SlotRequester(XmppConnectionService service) {
+		this.service = service;
+	}
+
+	public void request(Method method, Account account, DownloadableFile file, String mime, String md5, OnSlotRequested callback) {
+		if (method == Method.HTTP_UPLOAD) {
+			Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
+			requestHttpUpload(account, host, file, mime, callback);
+		} else {
+			requestP1S3(account, Jid.of(account.getServer()), file.getName(), md5, callback);
+		}
+	}
+
+	private void requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) {
+		IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
+		service.sendIqPacket(account, request, (a, packet) -> {
+			if (packet.getType() == IqPacket.TYPE.RESULT) {
+				Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
+				if (slotElement != null) {
+					try {
+						final Element put = slotElement.findChild("put");
+						final Element get = slotElement.findChild("get");
+						final String putUrl = put == null ? null : put.getAttribute("url");
+						final String getUrl = get == null ? null : get.getAttribute("url");
+						if (getUrl != null && putUrl != null) {
+							Slot slot = new Slot(new URL(putUrl));
+							slot.getUrl = new URL(getUrl);
+							slot.headers = new HashMap<>();
+							for (Element child : put.getChildren()) {
+								if ("header".equals(child.getName())) {
+									final String name = child.getAttribute("name");
+									final String value = child.getContent();
+									if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
+										slot.headers.put(name, value.trim());
+									}
+									slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
+								}
+							}
+							callback.success(slot);
+							return;
+						}
+					} catch (MalformedURLException e) {
+						//fall through
+					}
+				}
+			}
+			Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet);
+			callback.failure(IqParser.extractErrorMessage(packet));
+		});
+
+	}
+
+	private void requestP1S3(final Account account, Jid host, String filename, String md5, OnSlotRequested callback) {
+		IqPacket request = service.getIqGenerator().requestP1S3Slot(host, md5);
+		service.sendIqPacket(account, request, (a, packet) -> {
+			if (packet.getType() == IqPacket.TYPE.RESULT) {
+				String putUrl = packet.query(Namespace.P1_S3_FILE_TRANSFER).getAttribute("upload");
+				String id = packet.query().getAttribute("fileid");
+				try {
+					if (putUrl != null && id != null) {
+						Slot slot = new Slot(new URL(putUrl));
+						slot.getUrl = P1S3UrlStreamHandler.of(id, filename);
+						slot.headers = new HashMap<>();
+						slot.headers.put("Content-MD5", md5);
+						slot.headers.put("Content-Type", " "); //required to force it to empty. otherwise library will set something
+						callback.success(slot);
+						return;
+					}
+				} catch (MalformedURLException e) {
+					//fall through;
+				}
+			}
+			callback.failure("unable to request slot");
+		});
+		Log.d(Config.LOGTAG, "requesting slot with p1. md5=" + md5);
+	}
+
+
+	public interface OnSlotRequested {
+
+		void success(Slot slot);
+
+		void failure(String message);
+
+	}
+
+	public static class Slot {
+		private final URL putUrl;
+		private URL getUrl;
+		private HashMap<String, String> headers;
+
+		private Slot(URL putUrl) {
+			this.putUrl = putUrl;
+		}
+
+		public URL getPutUrl() {
+			return putUrl;
+		}
+
+		public URL getGetUrl() {
+			return getUrl;
+		}
+
+		public HashMap<String, String> getHeaders() {
+			return headers;
+		}
+	}
+}

src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java 🔗

@@ -68,7 +68,7 @@ public class AbstractConnectionManager {
 		is = new FileInputStream(file);
 		size = (int) file.getSize();
 		if (file.getKey() == null) {
-			return new Pair<InputStream,Integer>(is,size);
+			return new Pair<>(is,size);
 		}
 		try {
 			if (gcm) {
@@ -81,16 +81,10 @@ public class AbstractConnectionManager {
 				Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
 				cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips);
 				Log.d(Config.LOGTAG, "opening encrypted input stream");
-				return new Pair<InputStream,Integer>(new CipherInputStream(is, cipher),(size / 16 + 1) * 16);
+				return new Pair<>(new CipherInputStream(is, cipher),(size / 16 + 1) * 16);
 			}
-		} catch (InvalidKeyException e) {
-			return null;
-		} catch (NoSuchAlgorithmException e) {
-			return null;
-		} catch (NoSuchPaddingException e) {
-			return null;
-		} catch (InvalidAlgorithmParameterException e) {
-			return null;
+		} catch (Exception e) {
+			throw new AssertionError(e);
 		}
 	}
 

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -88,7 +88,7 @@ import eu.siacs.conversations.generator.IqGenerator;
 import eu.siacs.conversations.generator.MessageGenerator;
 import eu.siacs.conversations.generator.PresenceGenerator;
 import eu.siacs.conversations.http.HttpConnectionManager;
-import eu.siacs.conversations.http.AesGcmURLStreamHandlerFactory;
+import eu.siacs.conversations.http.CustomURLStreamHandlerFactory;
 import eu.siacs.conversations.parser.AbstractParser;
 import eu.siacs.conversations.parser.IqParser;
 import eu.siacs.conversations.parser.MessageParser;
@@ -152,7 +152,7 @@ public class XmppConnectionService extends Service {
 	private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
 
 	static {
-		URL.setURLStreamHandlerFactory(new AesGcmURLStreamHandlerFactory());
+		URL.setURLStreamHandlerFactory(new CustomURLStreamHandlerFactory());
 	}
 
 	public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);

src/main/java/eu/siacs/conversations/utils/Checksum.java 🔗

@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.utils;
+
+import android.util.Base64;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class Checksum {
+
+	public static String md5(InputStream inputStream) throws IOException {
+		byte[] buffer = new byte[4096];
+		MessageDigest messageDigest;
+		try {
+			messageDigest = MessageDigest.getInstance("MD5");
+		} catch (NoSuchAlgorithmException e) {
+			throw new AssertionError(e);
+		}
+
+		int count;
+		do {
+			count = inputStream.read(buffer);
+			if (count > 0) {
+				messageDigest.update(buffer, 0, count);
+			}
+		} while (count != -1);
+		inputStream.close();
+		return Base64.encodeToString(messageDigest.digest(), Base64.NO_WRAP);
+	}
+}

src/main/java/eu/siacs/conversations/xml/Namespace.java 🔗

@@ -20,4 +20,5 @@ public final class Namespace {
 	public static final String NICK = "http://jabber.org/protocol/nick";
 	public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline";
 	public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind";
+	public static final String P1_S3_FILE_TRANSFER = "p1:s3filetransfer";
 }

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -1807,6 +1807,10 @@ public class XmppConnection implements Runnable {
 			this.blockListRequested = value;
 		}
 
+		public boolean p1S3FileTransfer() {
+			return hasDiscoFeature(Jid.of(account.getServer()),Namespace.P1_S3_FILE_TRANSFER);
+		}
+
 		public boolean httpUpload(long filesize) {
 			if (Config.DISABLE_HTTP_UPLOAD) {
 				return false;