upload files using p1s3 - sending part

This commit is contained in:
Daniel Gultsch 2018-05-25 12:24:23 +02:00
parent e4ca8961aa
commit 70d95c7903
14 changed files with 434 additions and 82 deletions

View file

@ -57,7 +57,7 @@ public class Account extends AbstractEntity {
public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>(); public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>();
public boolean httpUploadAvailable(long filesize) { 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() { public boolean httpUploadAvailable() {

View file

@ -350,6 +350,14 @@ public class IqGenerator extends AbstractGenerator {
return packet; 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) { private static String convertFilename(String name) {
int pos = name.indexOf('.'); int pos = name.indexOf('.');
if (pos != -1) { if (pos != -1) {

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}
}

View file

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

View file

@ -88,7 +88,7 @@ import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.MessageGenerator; import eu.siacs.conversations.generator.MessageGenerator;
import eu.siacs.conversations.generator.PresenceGenerator; import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.http.HttpConnectionManager; 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.AbstractParser;
import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.parser.MessageParser; 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"; private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
static { static {
URL.setURLStreamHandlerFactory(new AesGcmURLStreamHandlerFactory()); URL.setURLStreamHandlerFactory(new CustomURLStreamHandlerFactory());
} }
public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1); public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);

View file

@ -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);
}
}

View file

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

View file

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