upload files using p1s3 - sending part
This commit is contained in:
parent
e4ca8961aa
commit
70d95c7903
|
@ -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() {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
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);
|
packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
content = message.getBody();
|
content = message.getBody();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
|
||||||
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) {
|
if (!canceled) {
|
||||||
new Thread(this::upload).start();
|
HttpUploadConnection.this.slot = slot;
|
||||||
}
|
Log.d(Config.LOGTAG,"not starting upload to "+slot.getPutUrl());
|
||||||
return;
|
new Thread(HttpUploadConnection.this::upload).start();
|
||||||
}
|
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
//fall through
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void failure(String message) {
|
||||||
|
fail(message);
|
||||||
}
|
}
|
||||||
Log.d(Config.LOGTAG,account.getJid().toString()+": invalid response to slot request "+packet);
|
|
||||||
fail(IqParser.extractErrorMessage(packet));
|
|
||||||
});
|
});
|
||||||
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());
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue