add support for S3 file transfers

This commit is contained in:
Christian Schneppe 2018-05-26 22:56:17 +02:00
parent 369e48c220
commit a554be18a6
22 changed files with 662 additions and 186 deletions

View file

@ -64,7 +64,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() {

View file

@ -352,21 +352,37 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime, String http_upload_namespace) {
public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(host);
Element request = packet.addChild("request", http_upload_namespace);
if (http_upload_namespace == Namespace.HTTP_UPLOAD) {
request.setAttribute("filename", convertFilename(file.getName()));
request.setAttribute("size", file.getExpectedSize());
request.setAttribute("content-type", mime);
} else {
request.addChild("filename").setContent(convertFilename(file.getName()));
request.addChild("size").setContent(String.valueOf(file.getExpectedSize()));
if (mime != null) {
request.addChild("content-type").setContent(mime);
}
}
Element request = packet.addChild("request", Namespace.HTTP_UPLOAD);
request.setAttribute("filename", convertFilename(file.getName()));
request.setAttribute("size", file.getExpectedSize());
request.setAttribute("content-type", mime);
return packet;
}
public IqPacket requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(host);
Element request = packet.addChild("request", Namespace.HTTP_UPLOAD_LEGACY);
request.addChild("filename").setContent(convertFilename(file.getName()));
request.addChild("size").setContent(String.valueOf(file.getExpectedSize()));
request.addChild("content-type").setContent(mime);
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);
return packet;
}
public IqPacket requestP1S3Url(Jid host, String fileId) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(host);
packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("fileid", fileId);
return packet;
}

View file

@ -3,6 +3,7 @@ package de.pixart.messenger.generator;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@ -16,6 +17,7 @@ import de.pixart.messenger.entities.Account;
import de.pixart.messenger.entities.Contact;
import de.pixart.messenger.entities.Conversation;
import de.pixart.messenger.entities.Message;
import de.pixart.messenger.http.P1S3UrlStreamHandler;
import de.pixart.messenger.services.XmppConnectionService;
import de.pixart.messenger.utils.Namespace;
import de.pixart.messenger.xml.Element;
@ -136,8 +138,17 @@ 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);
final String file = url.getFile();
x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file);
x.setAttribute("fileid", url.getHost());
return packet;
} else {
content = url.toString();
packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
}
} else {
content = message.getBody();
}
@ -148,9 +159,17 @@ public class MessageGenerator extends AbstractGenerator {
public MessagePacket generatePgpChat(Message message) {
MessagePacket packet = preparePacket(message);
if (message.hasFileOnRemoteHost()) {
final String url = message.getFileParams().url.toString();
packet.setBody(url);
packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
Message.FileParams fileParams = message.getFileParams();
final URL url = fileParams.url;
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) {
Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER);
final String file = url.getFile();
x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file);
x.setAttribute("fileid", url.getHost());
} else {
packet.setBody(url.toString());
packet.addChild("x", Namespace.OOB).addChild("url").setContent(url.toString());
}
} else {
if (Config.supportUnencrypted()) {
packet.setBody(PGP_FALLBACK_MESSAGE);

View file

@ -3,11 +3,13 @@ package de.pixart.messenger.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;
}

View file

@ -41,11 +41,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) {

View file

@ -1,6 +1,7 @@
package de.pixart.messenger.http;
import android.os.PowerManager;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.BufferedInputStream;
@ -20,6 +21,7 @@ import javax.net.ssl.SSLHandshakeException;
import de.pixart.messenger.Config;
import de.pixart.messenger.R;
import de.pixart.messenger.entities.Account;
import de.pixart.messenger.entities.DownloadableFile;
import de.pixart.messenger.entities.Message;
import de.pixart.messenger.entities.Transferable;
@ -30,6 +32,8 @@ import de.pixart.messenger.services.XmppConnectionService;
import de.pixart.messenger.utils.CryptoHelper;
import de.pixart.messenger.utils.FileWriterException;
import de.pixart.messenger.utils.WakeLockHelper;
import de.pixart.messenger.xmpp.stanzas.IqPacket;
import rocks.xmpp.addr.Jid;
public class HttpDownloadConnection implements Transferable {
@ -42,8 +46,9 @@ public class HttpDownloadConnection implements Transferable {
private int mStatus = Transferable.STATUS_UNKNOWN;
private boolean acceptedAutomatically = false;
private int mProgress = 0;
private boolean mUseTor = false;
private final boolean mUseTor;
private boolean canceled = false;
private Method method = Method.HTTP_UPLOAD;
private final SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
@ -107,6 +112,7 @@ public class HttpDownloadConnection implements Transferable {
&& this.file.getKey() == null) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD;
checkFileSize(interactive);
} catch (MalformedURLException e) {
this.cancel();
@ -160,7 +166,7 @@ public class HttpDownloadConnection implements Transferable {
}
}
public void updateProgress(long i) {
private void updateProgress(long i) {
this.mProgress = (int) i;
mHttpConnectionManager.updateConversationUi(false);
}
@ -186,27 +192,63 @@ public class HttpDownloadConnection implements Transferable {
private class FileSizeChecker implements Runnable {
private boolean interactive = false;
private final boolean interactive;
public FileSizeChecker(boolean interactive) {
FileSizeChecker(boolean interactive) {
this.interactive = interactive;
}
@Override
public void run() {
if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) {
retrieveUrl();
} else {
check();
}
}
private void retrieveUrl() {
changeStatus(STATUS_CHECKING);
final Account account = message.getConversation().getAccount();
IqPacket request = mXmppConnectionService.getIqGenerator().requestP1S3Url(Jid.of(account.getJid().getDomain()), mUrl.getHost());
mXmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (a, packet) -> {
if (packet.getType() == IqPacket.TYPE.RESULT) {
String download = packet.query().getAttribute("download");
if (download != null) {
try {
mUrl = new URL(download);
check();
return;
} catch (MalformedURLException e) {
//fallthrough
}
}
}
Log.d(Config.LOGTAG, "unable to retrieve actual download url");
retrieveFailed(null);
});
}
private void retrieveFailed(@Nullable Exception e) {
changeStatus(STATUS_OFFER_CHECK_FILESIZE);
if (interactive) {
if (e != null) {
showToastForException(e);
}
} else {
HttpDownloadConnection.this.acceptedAutomatically = false;
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
}
cancel();
}
private void check() {
long size;
try {
size = retrieveFileSize();
} catch (Exception e) {
changeStatus(STATUS_OFFER_CHECK_FILESIZE);
Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
if (interactive) {
showToastForException(e);
} else {
HttpDownloadConnection.this.acceptedAutomatically = false;
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
}
cancel();
retrieveFailed(e);
return;
}
file.setExpectedSize(size);
@ -235,10 +277,14 @@ public class HttpDownloadConnection implements Transferable {
} else {
connection = (HttpURLConnection) mUrl.openConnection();
}
connection.setRequestMethod("HEAD");
if (method == Method.P1_S3) {
connection.setRequestMethod("GET");
connection.addRequestProperty("Range", "bytes=0-0");
} else {
connection.setRequestMethod("HEAD");
}
connection.setUseCaches(false);
Log.d(Config.LOGTAG, "url: " + connection.getURL().toString());
Log.d(Config.LOGTAG, "connection: " + connection.toString());
connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getIdentityName());
if (connection instanceof HttpsURLConnection) {
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
@ -246,7 +292,18 @@ public class HttpDownloadConnection implements Transferable {
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.CONNECT_TIMEOUT * 1000);
connection.connect();
String contentLength = connection.getHeaderField("Content-Length");
String contentLength;
if (method == Method.P1_S3) {
String contentRange = connection.getHeaderField("Content-Range");
String[] contentRangeParts = contentRange == null ? new String[0] : contentRange.split("/");
if (contentRangeParts.length != 2) {
contentLength = null;
} else {
contentLength = contentRangeParts[1];
}
} else {
contentLength = connection.getHeaderField("Content-Length");
}
connection.disconnect();
if (contentLength == null) {
throw new IOException("no content-length found in HEAD response");
@ -266,7 +323,7 @@ public class HttpDownloadConnection implements Transferable {
private class FileDownloader implements Runnable {
private boolean interactive = false;
private final boolean interactive;
private OutputStream os;
@ -385,7 +442,9 @@ public class HttpDownloadConnection implements Transferable {
message.setType(Message.TYPE_FILE);
final URL url;
final String ref = mUrl.getRef();
if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) {
if (method == Method.P1_S3) {
url = message.getFileParams().url;
} else if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) {
url = CryptoHelper.toAesGcmUrl(mUrl);
} else {
url = mUrl;

View file

@ -9,11 +9,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
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;
@ -22,47 +22,43 @@ import de.pixart.messenger.entities.Account;
import de.pixart.messenger.entities.DownloadableFile;
import de.pixart.messenger.entities.Message;
import de.pixart.messenger.entities.Transferable;
import de.pixart.messenger.parser.IqParser;
import de.pixart.messenger.persistance.FileBackend;
import de.pixart.messenger.services.AbstractConnectionManager;
import de.pixart.messenger.services.XmppConnectionService;
import de.pixart.messenger.utils.Checksum;
import de.pixart.messenger.utils.CryptoHelper;
import de.pixart.messenger.utils.Namespace;
import de.pixart.messenger.utils.WakeLockHelper;
import de.pixart.messenger.xml.Element;
import de.pixart.messenger.xmpp.stanzas.IqPacket;
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 final boolean mUseTor;
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 byte[] key = null;
private long transmitted = 0;
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 +114,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,50 +140,19 @@ public class HttpUploadConnection implements Transferable {
this.file.setExpectedSize(pair.second);
message.resetFileParams();
this.mFileInputStream = pair.first;
String http_upload_namespace = account.getXmppConnection().getFeatures().http_upload_namespace;
Jid host = account.getXmppConnection().findDiscoItemByFeature(http_upload_namespace);
IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host, file, mime, http_upload_namespace);
mXmppConnectionService.sendIqPacket(account, request, (a, packet) -> {
if (packet.getType() == IqPacket.TYPE.RESULT) {
Element slot = packet.findChild("slot", http_upload_namespace);
if (slot != null) {
try {
final Element put = slot.findChild("put");
final Element get = slot.findChild("get");
final String putUrl;
final String getUrl;
if (http_upload_namespace == Namespace.HTTP_UPLOAD) {
putUrl = put == null ? null : put.getAttribute("url");
getUrl = get == null ? null : get.getAttribute("url");
} else {
putUrl = put == null ? null : put.getContent();
getUrl = get == null ? null : get.getContent();
}
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;
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);
@ -186,11 +166,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);
@ -198,14 +178,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();
@ -222,12 +202,26 @@ 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)));
if (method == Method.P1_S3) {
get = new URL(slot.getGetUrl().toString() + "#" + CryptoHelper.bytesToHex(key));
} else {
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());

View file

@ -0,0 +1,53 @@
/*
* 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 de.pixart.messenger.http;
import de.pixart.messenger.entities.Account;
import de.pixart.messenger.xmpp.XmppConnection;
public enum Method {
P1_S3, HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
public static Method determine(Account account) {
XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures();
if (features == null) {
return HTTP_UPLOAD;
}
if (features.useLegacyHttpUpload()) {
return HTTP_UPLOAD_LEGACY;
} else if (features.httpUpload(0)) {
return HTTP_UPLOAD;
} else if (features.p1S3FileTransfer()) {
return P1_S3;
} else {
return HTTP_UPLOAD;
}
}
}

View file

@ -0,0 +1,62 @@
/*
* 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 de.pixart.messenger.http;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import de.pixart.messenger.xml.Element;
public class P1S3UrlStreamHandler extends URLStreamHandler {
public static final String PROTOCOL_NAME = "p1s3";
public static URL of(String fileId, String filename) throws MalformedURLException {
if (fileId == null || filename == null) {
throw new MalformedURLException("Paramaters must not be null");
}
return new URL(PROTOCOL_NAME + "://" + fileId + "/" + filename);
}
public static URL of(Element x) {
try {
return of(x.getAttribute("fileid"), x.getAttribute("name"));
} catch (MalformedURLException e) {
return null;
}
}
@Override
protected URLConnection openConnection(URL url) {
throw new IllegalStateException("Unable to open connection with stub protocol");
}
}

View file

@ -0,0 +1,189 @@
/*
* 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 de.pixart.messenger.http;
import android.util.Log;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import de.pixart.messenger.Config;
import de.pixart.messenger.entities.Account;
import de.pixart.messenger.entities.DownloadableFile;
import de.pixart.messenger.parser.IqParser;
import de.pixart.messenger.services.XmppConnectionService;
import de.pixart.messenger.utils.Namespace;
import de.pixart.messenger.xml.Element;
import de.pixart.messenger.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 if (method == Method.HTTP_UPLOAD_LEGACY) {
Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY);
requestHttpUploadLegacy(account, host, file, mime, callback);
} else {
requestP1S3(account, Jid.of(account.getServer()), file.getName(), md5, callback);
}
}
private void requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) {
IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime);
service.sendIqPacket(account, request, (a, packet) -> {
if (packet.getType() == IqPacket.TYPE.RESULT) {
Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY);
if (slotElement != null) {
try {
final String putUrl = slotElement.findChildContent("put");
final String getUrl = slotElement.findChildContent("get");
if (getUrl != null && putUrl != null) {
Slot slot = new Slot(new URL(putUrl));
slot.getUrl = new URL(getUrl);
slot.headers = new HashMap<>();
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 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

@ -8,6 +8,7 @@ import android.util.Pair;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@ -35,6 +36,7 @@ import de.pixart.messenger.entities.ReadByMarker;
import de.pixart.messenger.entities.ReceiptRequest;
import de.pixart.messenger.entities.ServiceDiscoveryResult;
import de.pixart.messenger.http.HttpConnectionManager;
import de.pixart.messenger.http.P1S3UrlStreamHandler;
import de.pixart.messenger.services.MessageArchiveService;
import de.pixart.messenger.services.XmppConnectionService;
import de.pixart.messenger.utils.CryptoHelper;
@ -380,6 +382,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
final Element oob = packet.findChild("x", Namespace.OOB);
final Element xP1S3 = packet.findChild("x", Namespace.P1_S3_FILE_TRANSFER);
final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3);
final String oobUrl = oob != null ? oob.findChildContent("url") : null;
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
@ -428,7 +432,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
return;
}
if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) {
if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) {
final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain());
final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
@ -472,6 +476,12 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring OTR message from " + from + " isForwarded=" + Boolean.toString(isForwarded) + ", isProperlyAddressed=" + Boolean.valueOf(isProperlyAddressed));
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}
} else if (xP1S3url != null) {
message = new Message(conversation, xP1S3url.toString(), Message.ENCRYPTION_NONE, status);
message.setOob(true);
if (CryptoHelper.isPgpEncryptedUrl(xP1S3url.toString())) {
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
}
} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
} else if (axolotlEncrypted != null && Config.supportOmemo()) {

View file

@ -86,7 +86,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) {
@ -99,16 +99,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);
}
}

View file

@ -99,7 +99,7 @@ import de.pixart.messenger.generator.AbstractGenerator;
import de.pixart.messenger.generator.IqGenerator;
import de.pixart.messenger.generator.MessageGenerator;
import de.pixart.messenger.generator.PresenceGenerator;
import de.pixart.messenger.http.AesGcmURLStreamHandlerFactory;
import de.pixart.messenger.http.CustomURLStreamHandlerFactory;
import de.pixart.messenger.http.HttpConnectionManager;
import de.pixart.messenger.parser.AbstractParser;
import de.pixart.messenger.parser.IqParser;
@ -165,7 +165,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);

View file

@ -125,7 +125,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
boolean openPaymentUrl = mAccount != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED;
final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl;
URL url = connection != null && redirectionWorthyStatus ? connection.getRedirectionUrl() : null;
if (url != null && redirectionWorthyStatus && !wasDisabled) {
if (url != null && !wasDisabled) {
try {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString())));
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
@ -1018,6 +1018,10 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
}
if (features.httpUpload(0)) {
this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
}
if (features.p1S3FileTransfer()) {
this.binding.serverInfoHttpUploadDescription.setText(R.string.p1_s3_filetransfer);
this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
} else {
this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
}

View file

@ -67,6 +67,7 @@ import de.pixart.messenger.entities.DownloadableFile;
import de.pixart.messenger.entities.Message;
import de.pixart.messenger.entities.Message.FileParams;
import de.pixart.messenger.entities.Transferable;
import de.pixart.messenger.http.P1S3UrlStreamHandler;
import de.pixart.messenger.persistance.FileBackend;
import de.pixart.messenger.services.AudioPlayer;
import de.pixart.messenger.services.MessageArchiveService;
@ -99,13 +100,10 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
private static final int SENT = 0;
private static final int RECEIVED = 1;
private static final int STATUS = 2;
private static final int DATE_SEPARATOR = 3;
boolean isResendable = false;
private List<String> highlightedTerm = null;
private static final Linkify.TransformFilter WEBURL_TRANSFORM_FILTER = (matcher, url) -> {
if (url == null) {
return null;
@ -118,6 +116,30 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
}
};
private static final Linkify.MatchFilter WEBURL_MATCH_FILTER = (cs, start, end) -> start < 1 || (cs.charAt(start - 1) != '@' && cs.charAt(start - 1) != '.' && !cs.subSequence(Math.max(0, start - 3), start).equals("://"));
private static final Linkify.MatchFilter XMPPURI_MATCH_FILTER = (s, start, end) -> {
XmppUri uri = new XmppUri(s.subSequence(start, end).toString());
return uri.isJidValid();
};
private final XmppActivity activity;
private final ListSelectionManager listSelectionManager = new ListSelectionManager();
private final AudioPlayer audioPlayer;
private List<String> highlightedTerm = null;
private DisplayMetrics metrics;
private OnContactPictureClicked mOnContactPictureClickedListener;
private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
private boolean mIndicateReceived = false;
private OnQuoteListener onQuoteListener;
public MessageAdapter(XmppActivity activity, List<Message> messages) {
super(activity, 0, messages);
this.audioPlayer = new AudioPlayer(this);
this.activity = activity;
metrics = getContext().getResources().getDisplayMetrics();
updatePreferences();
}
private static String removeTrailingBracket(final String url) {
int numOpenBrackets = 0;
for (char c : url.toCharArray()) {
@ -134,30 +156,6 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
}
}
private static final Linkify.MatchFilter WEBURL_MATCH_FILTER = (cs, start, end) -> start < 1 || (cs.charAt(start - 1) != '@' && cs.charAt(start - 1) != '.' && !cs.subSequence(Math.max(0, start - 3), start).equals("://"));
private static final Linkify.MatchFilter XMPPURI_MATCH_FILTER = (s, start, end) -> {
XmppUri uri = new XmppUri(s.subSequence(start, end).toString());
return uri.isJidValid();
};
private final XmppActivity activity;
private final ListSelectionManager listSelectionManager = new ListSelectionManager();
public final AudioPlayer audioPlayer;
private DisplayMetrics metrics;
private OnContactPictureClicked mOnContactPictureClickedListener;
private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
private boolean mIndicateReceived = false;
private OnQuoteListener onQuoteListener;
public MessageAdapter(XmppActivity activity, List<Message> messages) {
super(activity, 0, messages);
this.audioPlayer = new AudioPlayer(this);
this.activity = activity;
metrics = getContext().getResources().getDisplayMetrics();
updatePreferences();
}
public static boolean cancelPotentialWork(Message message, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
@ -183,6 +181,12 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
return null;
}
private static void resetClickListener(View... views) {
for (View view : views) {
view.setOnClickListener(null);
}
}
public void flagScreenOn() {
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
@ -957,11 +961,18 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
} else if (message.treatAsDownloadable()) {
try {
URL url = new URL(message.getBody());
displayDownloadableMessage(viewHolder,
message,
activity.getString(R.string.check_x_filesize_on_host,
UIHelper.getFileDescriptionString(activity, message),
url.getHost()));
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(url.getProtocol())) {
displayDownloadableMessage(viewHolder,
message,
activity.getString(R.string.check_x_filesize,
UIHelper.getFileDescriptionString(activity, message)));
} else {
displayDownloadableMessage(viewHolder,
message,
activity.getString(R.string.check_x_filesize_on_host,
UIHelper.getFileDescriptionString(activity, message),
url.getHost()));
}
} catch (Exception e) {
displayDownloadableMessage(viewHolder,
message,
@ -999,12 +1010,6 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
return view;
}
private static void resetClickListener(View... views) {
for (View view : views) {
view.setOnClickListener(null);
}
}
private void promptOpenKeychainInstall(View view) {
activity.showInstallPgpDialog();
}

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 de.pixart.messenger.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

@ -276,6 +276,6 @@ public final class CryptoHelper {
return false;
}
final String u = url.toLowerCase();
return !u.contains(" ") && (u.startsWith("https://") || u.startsWith("http://")) && u.endsWith(".pgp");
return !u.contains(" ") && (u.startsWith("https://") || u.startsWith("http://") || u.startsWith("p1s3://")) && u.endsWith(".pgp");
}
}

View file

@ -35,6 +35,7 @@ import java.util.regex.Pattern;
import de.pixart.messenger.entities.Message;
import de.pixart.messenger.http.AesGcmURLStreamHandler;
import de.pixart.messenger.http.P1S3UrlStreamHandler;
public class MessageUtils {
@ -77,7 +78,8 @@ public class MessageUtils {
final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
final boolean validOob = ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted) && lines.length == 1;
final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol) || P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol);
final boolean validOob = validProtocol && (oob || encrypted) && lines.length == 1;
return validAesGcm || validOob;
} catch (MalformedURLException e) {
return false;

View file

@ -21,4 +21,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";
}

View file

@ -1842,45 +1842,50 @@ 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;
} else {
List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(this.http_upload_namespace);
if (items.size() == 0) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": this server does not support the latest version of XEP-0363");
this.http_upload_namespace = Namespace.HTTP_UPLOAD_LEGACY;
items = findDiscoItemsByFeature(this.http_upload_namespace);
}
if (items.size() > 0) {
try {
long maxsize = Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(this.http_upload_namespace, "max-file-size"));
if (filesize <= maxsize) {
return true;
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": http upload is not available for files with size " + filesize + " (max is " + maxsize + ")");
return false;
for (String namespace : new String[]{Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
if (items.size() > 0) {
try {
long maxsize = Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(namespace, "max-file-size"));
if (filesize <= maxsize) {
return true;
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": http upload is not available for files with size " + filesize + " (max is " + maxsize + ")");
return false;
}
} catch (Exception e) {
//ignored
}
} catch (Exception e) {
return true;
}
} else {
return false;
}
return false;
}
}
public boolean useLegacyHttpUpload() {
return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null;
}
public long getMaxHttpUploadSize() {
List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(this.http_upload_namespace);
if (items.size() > 0) {
try {
return Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(this.http_upload_namespace, "max-file-size"));
} catch (Exception e) {
return -1;
for (String namespace : new String[]{Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
if (items.size() > 0) {
try {
return Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(namespace, "max-file-size"));
} catch (Exception e) {
//ignored
}
}
} else {
return -1;
}
return -1;
}
public boolean stanzaIds() {

View file

@ -454,6 +454,7 @@
android:layout_height="wrap_content">
<TextView
android:id="@+id/server_info_http_upload_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"

View file

@ -792,4 +792,5 @@
<string name="choose_your_server">Choose your jabber provider</string>
<string name="show_privacy">Privacy Policy</string>
<string name="show_termsofuse">Terms of Use</string>
<string name="p1_s3_filetransfer">HTTP File Sharing for S3</string>
</resources>