migrate to OkHttp instead of HttpUrlConnection
OkHttp gives us more fine grained control over the HTTP library and frees us from any platform bugs
This commit is contained in:
parent
b09a1432a3
commit
e217551a82
|
@ -74,6 +74,7 @@ dependencies {
|
||||||
|
|
||||||
implementation "com.squareup.retrofit2:retrofit:2.9.0"
|
implementation "com.squareup.retrofit2:retrofit:2.9.0"
|
||||||
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
|
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
|
||||||
|
//implementation "com.squareup.okhttp3:logging-interceptor:3.14.9"
|
||||||
implementation 'com.google.guava:guava:30.1-android'
|
implementation 'com.google.guava:guava:30.1-android'
|
||||||
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18'
|
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18'
|
||||||
implementation fileTree(include: ['libwebrtc-m89.aar'], dir: 'libs')
|
implementation fileTree(include: ['libwebrtc-m89.aar'], dir: 'libs')
|
||||||
|
|
|
@ -209,7 +209,7 @@ public class PgpDecryptionService {
|
||||||
message.setRelativeFilePath(path);
|
message.setRelativeFilePath(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
URL url = message.getFileParams().url;
|
final String url = message.getFileParams().url;
|
||||||
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
||||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||||
mXmppConnectionService.updateMessage(message);
|
mXmppConnectionService.updateMessage(message);
|
||||||
|
|
|
@ -75,7 +75,7 @@ public class PgpEngine {
|
||||||
params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||||
String body;
|
String body;
|
||||||
if (message.hasFileOnRemoteHost()) {
|
if (message.hasFileOnRemoteHost()) {
|
||||||
body = message.getFileParams().url.toString();
|
body = message.getFileParams().url;
|
||||||
} else {
|
} else {
|
||||||
body = message.getBody();
|
body = message.getBody();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1169,7 +1169,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
||||||
final String content;
|
final String content;
|
||||||
if (message.hasFileOnRemoteHost()) {
|
if (message.hasFileOnRemoteHost()) {
|
||||||
content = message.getFileParams().url.toString();
|
content = message.getFileParams().url;
|
||||||
} else {
|
} else {
|
||||||
content = message.getBody();
|
content = message.getBody();
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,7 +147,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean httpUploadAvailable(long filesize) {
|
public boolean httpUploadAvailable(long filesize) {
|
||||||
return xmppConnection != null && (xmppConnection.getFeatures().httpUpload(filesize) || xmppConnection.getFeatures().p1S3FileTransfer());
|
return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean httpUploadAvailable() {
|
public boolean httpUploadAvailable() {
|
||||||
|
|
|
@ -788,7 +788,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
|
if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
|
||||||
String otherBody;
|
String otherBody;
|
||||||
if (message.hasFileOnRemoteHost()) {
|
if (message.hasFileOnRemoteHost()) {
|
||||||
otherBody = message.getFileParams().url.toString();
|
otherBody = message.getFileParams().url;
|
||||||
} else {
|
} else {
|
||||||
otherBody = message.body;
|
otherBody = message.body;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,6 @@ import com.google.common.collect.ImmutableSet;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -22,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
||||||
|
import eu.siacs.conversations.http.URL;
|
||||||
import eu.siacs.conversations.services.AvatarService;
|
import eu.siacs.conversations.services.AvatarService;
|
||||||
import eu.siacs.conversations.ui.util.PresenceSelector;
|
import eu.siacs.conversations.ui.util.PresenceSelector;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
|
@ -294,7 +293,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
try {
|
try {
|
||||||
values.put(EDITED, Edit.toJson(edits));
|
values.put(EDITED, Edit.toJson(edits));
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
Log.e(Config.LOGTAG,"error persisting json for edits",e);
|
Log.e(Config.LOGTAG, "error persisting json for edits", e);
|
||||||
}
|
}
|
||||||
values.put(OOB, oob ? 1 : 0);
|
values.put(OOB, oob ? 1 : 0);
|
||||||
values.put(ERROR_MESSAGE, errorMessage);
|
values.put(ERROR_MESSAGE, errorMessage);
|
||||||
|
@ -470,7 +469,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean remoteMsgIdMatchInEdit(String id) {
|
boolean remoteMsgIdMatchInEdit(String id) {
|
||||||
for(Edit edit : this.edits) {
|
for (Edit edit : this.edits) {
|
||||||
if (id.equals(edit.getEditedId())) {
|
if (id.equals(edit.getEditedId())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -547,7 +546,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
} else {
|
} else {
|
||||||
String body, otherBody;
|
String body, otherBody;
|
||||||
if (this.hasFileOnRemoteHost()) {
|
if (this.hasFileOnRemoteHost()) {
|
||||||
body = getFileParams().url.toString();
|
body = getFileParams().url;
|
||||||
otherBody = message.body == null ? null : message.body.trim();
|
otherBody = message.body == null ? null : message.body.trim();
|
||||||
} else {
|
} else {
|
||||||
body = this.body;
|
body = this.body;
|
||||||
|
@ -757,7 +756,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
|
if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
|
||||||
return true;
|
return true;
|
||||||
} else if (presences.size() >= 1) {
|
} else if (presences.size() >= 1) {
|
||||||
counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]);
|
counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
counterpart = null;
|
counterpart = null;
|
||||||
|
@ -794,12 +793,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
if (relativeFilePath != null) {
|
if (relativeFilePath != null) {
|
||||||
extension = MimeUtils.extractRelevantExtension(relativeFilePath);
|
extension = MimeUtils.extractRelevantExtension(relativeFilePath);
|
||||||
} else {
|
} else {
|
||||||
try {
|
final String url = URL.tryParse(body.split("\n")[0]);
|
||||||
final URL url = new URL(body.split("\n")[0]);
|
if (url == null) {
|
||||||
extension = MimeUtils.extractRelevantExtension(url);
|
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
extension = MimeUtils.extractRelevantExtension(url);
|
||||||
}
|
}
|
||||||
return MimeUtils.guessMimeTypeFromExtension(extension);
|
return MimeUtils.guessMimeTypeFromExtension(extension);
|
||||||
}
|
}
|
||||||
|
@ -840,8 +838,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
case 1:
|
case 1:
|
||||||
try {
|
try {
|
||||||
fileParams.size = Long.parseLong(parts[0]);
|
fileParams.size = Long.parseLong(parts[0]);
|
||||||
} catch (NumberFormatException e) {
|
} catch (final NumberFormatException e) {
|
||||||
fileParams.url = parseUrl(parts[0]);
|
fileParams.url = URL.tryParse(parts[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
|
@ -850,7 +848,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
fileParams.width = parseInt(parts[2]);
|
fileParams.width = parseInt(parts[2]);
|
||||||
fileParams.height = parseInt(parts[3]);
|
fileParams.height = parseInt(parts[3]);
|
||||||
case 2:
|
case 2:
|
||||||
fileParams.url = parseUrl(parts[0]);
|
fileParams.url = URL.tryParse(parts[0]);
|
||||||
fileParams.size = parseLong(parts[1]);
|
fileParams.size = parseLong(parts[1]);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
|
@ -879,14 +877,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static URL parseUrl(String value) {
|
|
||||||
try {
|
|
||||||
return new URL(value);
|
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void untie() {
|
public void untie() {
|
||||||
this.mNextMessage = null;
|
this.mNextMessage = null;
|
||||||
this.mPreviousMessage = null;
|
this.mPreviousMessage = null;
|
||||||
|
@ -905,11 +895,13 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean needsUploading() {
|
public boolean needsUploading() {
|
||||||
return isFileOrImage() && getFileParams().url == null;
|
final boolean needsUploading = isFileOrImage() && getFileParams().url == null;
|
||||||
|
Log.d(Config.LOGTAG, "needs uploading " + needsUploading + " url=" + getFileParams().url);
|
||||||
|
return needsUploading;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class FileParams {
|
public static class FileParams {
|
||||||
public URL url;
|
public String url;
|
||||||
public long size = 0;
|
public long size = 0;
|
||||||
public int width = 0;
|
public int width = 0;
|
||||||
public int height = 0;
|
public int height = 0;
|
||||||
|
|
|
@ -408,20 +408,6 @@ 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);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -14,7 +14,6 @@ import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.entities.Conversational;
|
import eu.siacs.conversations.entities.Conversational;
|
||||||
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;
|
||||||
|
@ -103,18 +102,9 @@ public class MessageGenerator extends AbstractGenerator {
|
||||||
MessagePacket packet = preparePacket(message);
|
MessagePacket packet = preparePacket(message);
|
||||||
String content;
|
String content;
|
||||||
if (message.hasFileOnRemoteHost()) {
|
if (message.hasFileOnRemoteHost()) {
|
||||||
Message.FileParams fileParams = message.getFileParams();
|
final Message.FileParams fileParams = message.getFileParams();
|
||||||
final URL url = fileParams.url;
|
content = 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);
|
packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
content = message.getBody();
|
content = message.getBody();
|
||||||
}
|
}
|
||||||
|
@ -126,16 +116,9 @@ public class MessageGenerator extends AbstractGenerator {
|
||||||
MessagePacket packet = preparePacket(message);
|
MessagePacket packet = preparePacket(message);
|
||||||
if (message.hasFileOnRemoteHost()) {
|
if (message.hasFileOnRemoteHost()) {
|
||||||
Message.FileParams fileParams = message.getFileParams();
|
Message.FileParams fileParams = message.getFileParams();
|
||||||
final URL url = fileParams.url;
|
final String url = fileParams.url;
|
||||||
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) {
|
packet.setBody(url);
|
||||||
Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER);
|
packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
|
||||||
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 {
|
} else {
|
||||||
if (Config.supportUnencrypted()) {
|
if (Config.supportUnencrypted()) {
|
||||||
packet.setBody(PGP_FALLBACK_MESSAGE);
|
packet.setBody(PGP_FALLBACK_MESSAGE);
|
||||||
|
|
41
src/main/java/eu/siacs/conversations/http/AesGcmURL.java
Normal file
41
src/main/java/eu/siacs/conversations/http/AesGcmURL.java
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package eu.siacs.conversations.http;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
|
||||||
|
public final class AesGcmURL {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This matches a 48 or 44 byte IV + KEY hex combo, like used in http/aesgcm upload anchors
|
||||||
|
*/
|
||||||
|
public static final Pattern IV_KEY = Pattern.compile("([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44}");
|
||||||
|
|
||||||
|
public static final String PROTOCOL_NAME = "aesgcm";
|
||||||
|
|
||||||
|
private AesGcmURL() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toAesGcmUrl(HttpUrl url) {
|
||||||
|
if (url.isHttps()) {
|
||||||
|
return PROTOCOL_NAME + url.toString().substring(5);
|
||||||
|
} else {
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpUrl of(final String url) {
|
||||||
|
final int end = url.indexOf("://");
|
||||||
|
if (end < 0) {
|
||||||
|
throw new IllegalArgumentException("Scheme not found");
|
||||||
|
}
|
||||||
|
final String protocol = url.substring(0, end);
|
||||||
|
if (PROTOCOL_NAME.equals(protocol)) {
|
||||||
|
return HttpUrl.get("https" + url.substring(PROTOCOL_NAME.length()));
|
||||||
|
} else {
|
||||||
|
return HttpUrl.get(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
package eu.siacs.conversations.http;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.net.URLStreamHandler;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
|
|
||||||
public class AesGcmURLStreamHandler extends URLStreamHandler {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This matches a 48 or 44 byte IV + KEY hex combo, like used in http/aesgcm upload anchors
|
|
||||||
*/
|
|
||||||
public static final Pattern IV_KEY = Pattern.compile("([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44}");
|
|
||||||
|
|
||||||
public static final String PROTOCOL_NAME = "aesgcm";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected URLConnection openConnection(URL url) throws IOException {
|
|
||||||
return new URL("https"+url.toString().substring(url.getProtocol().length())).openConnection();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
package eu.siacs.conversations.http;
|
|
||||||
|
|
||||||
import java.net.URLStreamHandler;
|
|
||||||
import java.net.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,20 +4,19 @@ import android.util.Log;
|
||||||
|
|
||||||
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Proxy;
|
import java.net.Proxy;
|
||||||
import java.net.URL;
|
import java.net.UnknownHostException;
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
import javax.net.ssl.HostnameVerifier;
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
import javax.net.ssl.X509TrustManager;
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
|
@ -27,6 +26,8 @@ import eu.siacs.conversations.entities.Message;
|
||||||
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.TLSSocketFactory;
|
import eu.siacs.conversations.utils.TLSSocketFactory;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
public class HttpConnectionManager extends AbstractConnectionManager {
|
public class HttpConnectionManager extends AbstractConnectionManager {
|
||||||
|
|
||||||
|
@ -39,8 +40,12 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Proxy getProxy() throws IOException {
|
public static Proxy getProxy() {
|
||||||
|
try {
|
||||||
return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 9050));
|
return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 9050));
|
||||||
|
} catch (final UnknownHostException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createNewDownloadConnection(Message message) {
|
public void createNewDownloadConnection(Message message) {
|
||||||
|
@ -75,15 +80,6 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkConnection(Message message) {
|
|
||||||
final Account account = message.getConversation().getAccount();
|
|
||||||
final URL url = message.getFileParams().url;
|
|
||||||
if (url.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) && account.getStatus() != Account.State.ONLINE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return mXmppConnectionService.hasInternetConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
void finishConnection(HttpDownloadConnection connection) {
|
void finishConnection(HttpDownloadConnection connection) {
|
||||||
synchronized (this.downloadConnections) {
|
synchronized (this.downloadConnections) {
|
||||||
this.downloadConnections.remove(connection);
|
this.downloadConnections.remove(connection);
|
||||||
|
@ -96,7 +92,21 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) {
|
OkHttpClient buildHttpClient(final HttpUrl url, final Account account, boolean interactive) {
|
||||||
|
final String slotHostname = url.host();
|
||||||
|
final boolean onionSlot = slotHostname.endsWith(".onion");
|
||||||
|
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||||
|
//builder.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS));
|
||||||
|
builder.writeTimeout(30, TimeUnit.SECONDS);
|
||||||
|
builder.readTimeout(30, TimeUnit.SECONDS);
|
||||||
|
setupTrustManager(builder, interactive);
|
||||||
|
if (mXmppConnectionService.useTorToConnect() || account.isOnion() || onionSlot) {
|
||||||
|
builder.proxy(HttpConnectionManager.getProxy()).build();
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupTrustManager(final OkHttpClient.Builder builder, final boolean interactive) {
|
||||||
final X509TrustManager trustManager;
|
final X509TrustManager trustManager;
|
||||||
final HostnameVerifier hostnameVerifier = mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier(), interactive);
|
final HostnameVerifier hostnameVerifier = mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier(), interactive);
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
|
@ -106,8 +116,8 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
|
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
|
||||||
connection.setSSLSocketFactory(sf);
|
builder.sslSocketFactory(sf, trustManager);
|
||||||
connection.setHostnameVerifier(hostnameVerifier);
|
builder.hostnameVerifier(hostnameVerifier);
|
||||||
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
|
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,24 @@
|
||||||
package eu.siacs.conversations.http;
|
package eu.siacs.conversations.http;
|
||||||
|
|
||||||
import android.os.PowerManager;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.io.ByteStreams;
|
import com.google.common.io.ByteStreams;
|
||||||
|
import com.google.common.primitives.Longs;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.util.Locale;
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
|
||||||
import javax.net.ssl.SSLHandshakeException;
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.entities.Account;
|
|
||||||
import eu.siacs.conversations.entities.DownloadableFile;
|
import eu.siacs.conversations.entities.DownloadableFile;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.Transferable;
|
import eu.siacs.conversations.entities.Transferable;
|
||||||
|
@ -33,8 +28,11 @@ import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
import eu.siacs.conversations.utils.FileWriterException;
|
import eu.siacs.conversations.utils.FileWriterException;
|
||||||
import eu.siacs.conversations.utils.MimeUtils;
|
import eu.siacs.conversations.utils.MimeUtils;
|
||||||
import eu.siacs.conversations.utils.WakeLockHelper;
|
import okhttp3.Call;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
import okhttp3.HttpUrl;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
|
import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
|
||||||
|
|
||||||
|
@ -44,13 +42,13 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
private final boolean mUseTor;
|
private final boolean mUseTor;
|
||||||
private final HttpConnectionManager mHttpConnectionManager;
|
private final HttpConnectionManager mHttpConnectionManager;
|
||||||
private final XmppConnectionService mXmppConnectionService;
|
private final XmppConnectionService mXmppConnectionService;
|
||||||
private URL mUrl;
|
private HttpUrl mUrl;
|
||||||
private DownloadableFile file;
|
private DownloadableFile file;
|
||||||
private int mStatus = Transferable.STATUS_UNKNOWN;
|
private int mStatus = Transferable.STATUS_UNKNOWN;
|
||||||
private boolean acceptedAutomatically = false;
|
private boolean acceptedAutomatically = false;
|
||||||
private int mProgress = 0;
|
private int mProgress = 0;
|
||||||
private boolean canceled = false;
|
private boolean canceled = false;
|
||||||
private Method method = Method.HTTP_UPLOAD;
|
private Call mostRecentCall;
|
||||||
|
|
||||||
HttpDownloadConnection(Message message, HttpConnectionManager manager) {
|
HttpDownloadConnection(Message message, HttpConnectionManager manager) {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
|
@ -88,13 +86,13 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
try {
|
try {
|
||||||
final Message.FileParams fileParams = message.getFileParams();
|
final Message.FileParams fileParams = message.getFileParams();
|
||||||
if (message.hasFileOnRemoteHost()) {
|
if (message.hasFileOnRemoteHost()) {
|
||||||
mUrl = CryptoHelper.toHttpsUrl(fileParams.url);
|
mUrl = AesGcmURL.of(fileParams.url);
|
||||||
} else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
|
} else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
|
||||||
mUrl = fileParams.url;
|
mUrl = AesGcmURL.of(fileParams.url);
|
||||||
} else {
|
} else {
|
||||||
mUrl = CryptoHelper.toHttpsUrl(new URL(message.getBody().split("\n")[0]));
|
mUrl = AesGcmURL.of(message.getBody().split("\n")[0]);
|
||||||
}
|
}
|
||||||
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath());
|
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
|
||||||
if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
|
if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
|
||||||
this.message.setEncryption(Message.ENCRYPTION_PGP);
|
this.message.setEncryption(Message.ENCRYPTION_PGP);
|
||||||
} else if (message.getEncryption() != Message.ENCRYPTION_OTR
|
} else if (message.getEncryption() != Message.ENCRYPTION_OTR
|
||||||
|
@ -111,22 +109,22 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
|
if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
|
||||||
this.message.setEncryption(Message.ENCRYPTION_NONE);
|
this.message.setEncryption(Message.ENCRYPTION_NONE);
|
||||||
}
|
}
|
||||||
method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD;
|
//TODO add auth tag size to knownFileSize
|
||||||
long knownFileSize = message.getFileParams().size;
|
final long knownFileSize = message.getFileParams().size;
|
||||||
if (knownFileSize > 0 && interactive && method != Method.P1_S3) {
|
if (knownFileSize > 0 && interactive) {
|
||||||
this.file.setExpectedSize(knownFileSize);
|
this.file.setExpectedSize(knownFileSize);
|
||||||
download(true);
|
download(true);
|
||||||
} else {
|
} else {
|
||||||
checkFileSize(interactive);
|
checkFileSize(interactive);
|
||||||
}
|
}
|
||||||
} catch (MalformedURLException e) {
|
} catch (final IllegalArgumentException e) {
|
||||||
this.cancel();
|
this.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupFile() {
|
private void setupFile() {
|
||||||
final String reference = mUrl.getRef();
|
final String reference = mUrl.fragment();
|
||||||
if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) {
|
if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) {
|
||||||
this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
|
this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
|
||||||
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
|
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
|
||||||
Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
|
Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
|
||||||
|
@ -146,6 +144,10 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
@Override
|
@Override
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
this.canceled = true;
|
this.canceled = true;
|
||||||
|
final Call call = this.mostRecentCall;
|
||||||
|
if (call != null && !call.isCanceled()) {
|
||||||
|
call.cancel();
|
||||||
|
}
|
||||||
mHttpConnectionManager.finishConnection(this);
|
mHttpConnectionManager.finishConnection(this);
|
||||||
message.setTransferable(null);
|
message.setTransferable(null);
|
||||||
if (message.isFileOrImage()) {
|
if (message.isFileOrImage()) {
|
||||||
|
@ -260,34 +262,8 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) {
|
|
||||||
retrieveUrl();
|
|
||||||
} else {
|
|
||||||
check();
|
check();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void retrieveUrl() {
|
|
||||||
changeStatus(STATUS_CHECKING);
|
|
||||||
final Account account = message.getConversation().getAccount();
|
|
||||||
IqPacket request = mXmppConnectionService.getIqGenerator().requestP1S3Url(account.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) {
|
private void retrieveFailed(@Nullable Exception e) {
|
||||||
changeStatus(STATUS_OFFER_CHECK_FILESIZE);
|
changeStatus(STATUS_OFFER_CHECK_FILESIZE);
|
||||||
|
@ -330,46 +306,21 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private long retrieveFileSize() throws IOException {
|
private long retrieveFileSize() throws IOException {
|
||||||
|
final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
|
||||||
|
mUrl,
|
||||||
|
message.getConversation().getAccount(),
|
||||||
|
interactive
|
||||||
|
);
|
||||||
|
final Request request = new Request.Builder()
|
||||||
|
.url(URL.stripFragment(mUrl))
|
||||||
|
.head()
|
||||||
|
.build();
|
||||||
|
mostRecentCall = client.newCall(request);
|
||||||
try {
|
try {
|
||||||
Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive);
|
final Response response = mostRecentCall.execute();
|
||||||
changeStatus(STATUS_CHECKING);
|
final String contentLength = response.header("Content-Length");
|
||||||
HttpURLConnection connection;
|
final String contentType = response.header("Content-Type");
|
||||||
final String hostname = mUrl.getHost();
|
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
|
||||||
final boolean onion = hostname != null && hostname.endsWith(".onion");
|
|
||||||
if (mUseTor || message.getConversation().getAccount().isOnion() || onion) {
|
|
||||||
connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
|
|
||||||
} else {
|
|
||||||
connection = (HttpURLConnection) mUrl.openConnection();
|
|
||||||
}
|
|
||||||
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());
|
|
||||||
connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
|
|
||||||
if (connection instanceof HttpsURLConnection) {
|
|
||||||
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
|
|
||||||
}
|
|
||||||
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
|
|
||||||
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
|
|
||||||
connection.connect();
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
final String contentType = connection.getContentType();
|
|
||||||
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath());
|
|
||||||
if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) {
|
if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) {
|
||||||
final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
|
final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
|
||||||
if (fileExtension != null) {
|
if (fileExtension != null) {
|
||||||
|
@ -378,8 +329,7 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
setupFile();
|
setupFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
connection.disconnect();
|
if (Strings.isNullOrEmpty(contentLength)) {
|
||||||
if (contentLength == null) {
|
|
||||||
throw new IOException("no content-length found in HEAD response");
|
throw new IOException("no content-length found in HEAD response");
|
||||||
}
|
}
|
||||||
return Long.parseLong(contentLength, 10);
|
return Long.parseLong(contentLength, 10);
|
||||||
|
@ -397,8 +347,6 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
|
|
||||||
private final boolean interactive;
|
private final boolean interactive;
|
||||||
|
|
||||||
private OutputStream os;
|
|
||||||
|
|
||||||
public FileDownloader(boolean interactive) {
|
public FileDownloader(boolean interactive) {
|
||||||
this.interactive = interactive;
|
this.interactive = interactive;
|
||||||
}
|
}
|
||||||
|
@ -411,9 +359,10 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
decryptIfNeeded();
|
decryptIfNeeded();
|
||||||
updateImageBounds();
|
updateImageBounds();
|
||||||
finish();
|
finish();
|
||||||
} catch (SSLHandshakeException e) {
|
} catch (final SSLHandshakeException e) {
|
||||||
changeStatus(STATUS_OFFER);
|
changeStatus(STATUS_OFFER);
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
|
Log.d(Config.LOGTAG,"problem downloading",e);
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
showToastForException(e);
|
showToastForException(e);
|
||||||
} else {
|
} else {
|
||||||
|
@ -425,67 +374,57 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void download() throws Exception {
|
private void download() throws Exception {
|
||||||
InputStream is = null;
|
final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
|
||||||
HttpURLConnection connection = null;
|
mUrl,
|
||||||
final PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock(Thread.currentThread());
|
message.getConversation().getAccount(),
|
||||||
try {
|
interactive
|
||||||
wakeLock.acquire();
|
);
|
||||||
if (mUseTor || message.getConversation().getAccount().isOnion()) {
|
|
||||||
connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
|
final Request.Builder requestBuilder = new Request.Builder().url(URL.stripFragment(mUrl));
|
||||||
} else {
|
|
||||||
connection = (HttpURLConnection) mUrl.openConnection();
|
|
||||||
}
|
|
||||||
if (connection instanceof HttpsURLConnection) {
|
|
||||||
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
|
|
||||||
}
|
|
||||||
connection.setUseCaches(false);
|
|
||||||
connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
|
|
||||||
final long expected = file.getExpectedSize();
|
final long expected = file.getExpectedSize();
|
||||||
final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
|
final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
|
||||||
long resumeSize = 0;
|
final long resumeSize;
|
||||||
|
|
||||||
if (tryResume) {
|
if (tryResume) {
|
||||||
resumeSize = file.getSize();
|
resumeSize = file.getSize();
|
||||||
Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
|
Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
|
||||||
connection.setRequestProperty("Range", "bytes=" + resumeSize + "-");
|
requestBuilder.addHeader("Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize));
|
||||||
|
} else {
|
||||||
|
resumeSize = 0;
|
||||||
}
|
}
|
||||||
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
|
final Request request = requestBuilder.build();
|
||||||
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
|
mostRecentCall = client.newCall(request);
|
||||||
connection.connect();
|
final Response response = mostRecentCall.execute();
|
||||||
is = new BufferedInputStream(connection.getInputStream());
|
final int code = response.code();
|
||||||
final String contentRange = connection.getHeaderField("Content-Range");
|
if (code >= 200 && code <= 299) {
|
||||||
boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
|
final String contentRange = response.header("Content-Range");
|
||||||
|
final boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
|
||||||
|
final InputStream inputStream = response.body().byteStream();
|
||||||
|
final OutputStream outputStream;
|
||||||
long transmitted = 0;
|
long transmitted = 0;
|
||||||
if (tryResume && serverResumed) {
|
if (tryResume && serverResumed) {
|
||||||
Log.d(Config.LOGTAG, "server resumed");
|
Log.d(Config.LOGTAG, "server resumed");
|
||||||
transmitted = file.getSize();
|
transmitted = file.getSize();
|
||||||
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
||||||
os = AbstractConnectionManager.createOutputStream(file, true, false);
|
outputStream = AbstractConnectionManager.createOutputStream(file, true, false);
|
||||||
if (os == null) {
|
|
||||||
throw new FileWriterException();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
long reportedContentLengthOnGet;
|
final String contentLength = response.header("Content-Length");
|
||||||
try {
|
final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength);
|
||||||
reportedContentLengthOnGet = Long.parseLong(connection.getHeaderField("Content-Length"));
|
if (expected != size) {
|
||||||
} catch (NumberFormatException | NullPointerException e) {
|
Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")");
|
||||||
reportedContentLengthOnGet = 0;
|
|
||||||
}
|
|
||||||
if (expected != reportedContentLengthOnGet) {
|
|
||||||
Log.d(Config.LOGTAG, "content-length reported on GET (" + reportedContentLengthOnGet + ") did not match Content-Length reported on HEAD (" + expected + ")");
|
|
||||||
}
|
}
|
||||||
file.getParentFile().mkdirs();
|
file.getParentFile().mkdirs();
|
||||||
if (!file.exists() && !file.createNewFile()) {
|
if (!file.exists() && !file.createNewFile()) {
|
||||||
throw new FileWriterException();
|
throw new FileWriterException();
|
||||||
}
|
}
|
||||||
os = AbstractConnectionManager.createOutputStream(file, false, false);
|
outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
|
||||||
}
|
}
|
||||||
int count;
|
int count;
|
||||||
byte[] buffer = new byte[4096];
|
byte[] buffer = new byte[4096];
|
||||||
while ((count = is.read(buffer)) != -1) {
|
while ((count = inputStream.read(buffer)) != -1) {
|
||||||
transmitted += count;
|
transmitted += count;
|
||||||
try {
|
try {
|
||||||
os.write(buffer, 0, count);
|
outputStream.write(buffer, 0, count);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new FileWriterException();
|
throw new FileWriterException();
|
||||||
}
|
}
|
||||||
|
@ -494,35 +433,21 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
throw new CancellationException();
|
throw new CancellationException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
outputStream.flush();
|
||||||
os.flush();
|
} else {
|
||||||
} catch (IOException e) {
|
throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code));
|
||||||
throw new FileWriterException();
|
|
||||||
}
|
|
||||||
} catch (CancellationException | IOException e) {
|
|
||||||
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": http download failed", e);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
FileBackend.close(os);
|
|
||||||
FileBackend.close(is);
|
|
||||||
if (connection != null) {
|
|
||||||
connection.disconnect();
|
|
||||||
}
|
|
||||||
WakeLockHelper.release(wakeLock);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateImageBounds() {
|
private void updateImageBounds() {
|
||||||
final boolean privateMessage = message.isPrivateMessage();
|
final boolean privateMessage = message.isPrivateMessage();
|
||||||
message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
|
message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
|
||||||
final URL url;
|
final String url;
|
||||||
final String ref = mUrl.getRef();
|
final String ref = mUrl.fragment();
|
||||||
if (method == Method.P1_S3) {
|
if (ref != null && AesGcmURL.IV_KEY.matcher(ref).matches()) {
|
||||||
url = message.getFileParams().url;
|
url = AesGcmURL.toAesGcmUrl(mUrl);
|
||||||
} else if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) {
|
|
||||||
url = CryptoHelper.toAesGcmUrl(mUrl);
|
|
||||||
} else {
|
} else {
|
||||||
url = mUrl;
|
url = mUrl.toString();
|
||||||
}
|
}
|
||||||
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
||||||
mXmppConnectionService.updateMessage(message);
|
mXmppConnectionService.updateMessage(message);
|
||||||
|
|
|
@ -1,35 +1,30 @@
|
||||||
package eu.siacs.conversations.http;
|
package eu.siacs.conversations.http;
|
||||||
|
|
||||||
import android.os.PowerManager;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
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;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.DownloadableFile;
|
import eu.siacs.conversations.entities.DownloadableFile;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.Transferable;
|
import eu.siacs.conversations.entities.Transferable;
|
||||||
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 okhttp3.Call;
|
||||||
|
import okhttp3.Callback;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
|
public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener {
|
||||||
|
|
||||||
public class HttpUploadConnection implements Transferable {
|
|
||||||
|
|
||||||
static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
|
static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
|
@ -42,7 +37,6 @@ public class HttpUploadConnection implements Transferable {
|
||||||
private final SlotRequester mSlotRequester;
|
private final SlotRequester mSlotRequester;
|
||||||
private final Method method;
|
private final Method method;
|
||||||
private final boolean mUseTor;
|
private final boolean mUseTor;
|
||||||
private boolean cancelled = false;
|
|
||||||
private boolean delayed = false;
|
private boolean delayed = false;
|
||||||
private DownloadableFile file;
|
private DownloadableFile file;
|
||||||
private final Message message;
|
private final Message message;
|
||||||
|
@ -51,6 +45,7 @@ public class HttpUploadConnection implements Transferable {
|
||||||
private byte[] key = null;
|
private byte[] key = null;
|
||||||
|
|
||||||
private long transmitted = 0;
|
private long transmitted = 0;
|
||||||
|
private Call mostRecentCall;
|
||||||
|
|
||||||
public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) {
|
public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
|
@ -86,11 +81,16 @@ public class HttpUploadConnection implements Transferable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
this.cancelled = true;
|
final Call call = this.mostRecentCall;
|
||||||
|
if (call != null && !call.isCanceled()) {
|
||||||
|
call.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fail(String errorMessage) {
|
private void fail(String errorMessage) {
|
||||||
finish();
|
finish();
|
||||||
|
final Call call = this.mostRecentCall;
|
||||||
|
final boolean cancelled = call != null && call.isCanceled();
|
||||||
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
|
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,30 +116,14 @@ 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.upgrade(file, new FileInputStream(file)));
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to calculate md5()", e);
|
|
||||||
fail(e.getMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
md5 = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
|
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
|
||||||
message.resetFileParams();
|
message.resetFileParams();
|
||||||
this.mSlotRequester.request(method, account, file, mime, md5, new SlotRequester.OnSlotRequested() {
|
this.mSlotRequester.request(method, account, file, mime, new SlotRequester.OnSlotRequested() {
|
||||||
@Override
|
@Override
|
||||||
public void success(SlotRequester.Slot slot) {
|
public void success(final SlotRequester.Slot slot) {
|
||||||
if (!cancelled) {
|
//TODO needs to mark the message as cancelled afterwards (ie call fail())
|
||||||
HttpUploadConnection.this.slot = slot;
|
HttpUploadConnection.this.slot = slot;
|
||||||
EXECUTOR.execute(HttpUploadConnection.this::upload);
|
HttpUploadConnection.this.upload();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -152,72 +136,36 @@ public class HttpUploadConnection implements Transferable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void upload() {
|
private void upload() {
|
||||||
OutputStream os = null;
|
final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
|
||||||
InputStream fileInputStream = null;
|
slot.put,
|
||||||
HttpURLConnection connection = null;
|
message.getConversation().getAccount(),
|
||||||
final PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock(Thread.currentThread());
|
true
|
||||||
try {
|
);
|
||||||
fileInputStream = new FileInputStream(file);
|
final RequestBody requestBody = AbstractConnectionManager.requestBody(file, this);
|
||||||
final String slotHostname = slot.getPutUrl().getHost();
|
final Request request = new Request.Builder()
|
||||||
final boolean onionSlot = slotHostname != null && slotHostname.endsWith(".onion");
|
.url(slot.put)
|
||||||
final int expectedFileSize = (int) file.getExpectedSize();
|
.put(requestBody)
|
||||||
final int readTimeout = (expectedFileSize / 2048) + Config.SOCKET_TIMEOUT; //assuming a minimum transfer speed of 16kbit/s
|
.headers(slot.headers)
|
||||||
wakeLock.acquire(readTimeout);
|
.build();
|
||||||
Log.d(Config.LOGTAG, "uploading to " + slot.getPutUrl().toString()+ " w/ read timeout of "+readTimeout+"s");
|
Log.d(Config.LOGTAG, "uploading file to " + slot.put);
|
||||||
|
this.mostRecentCall = client.newCall(request);
|
||||||
|
this.mostRecentCall.enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NotNull Call call, IOException e) {
|
||||||
|
Log.d(Config.LOGTAG, "http upload failed", e);
|
||||||
|
fail(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
if (mUseTor || message.getConversation().getAccount().isOnion() || onionSlot) {
|
@Override
|
||||||
connection = (HttpURLConnection) slot.getPutUrl().openConnection(HttpConnectionManager.getProxy());
|
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
|
||||||
} else {
|
final int code = response.code();
|
||||||
connection = (HttpURLConnection) slot.getPutUrl().openConnection();
|
|
||||||
}
|
|
||||||
if (connection instanceof HttpsURLConnection) {
|
|
||||||
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true);
|
|
||||||
}
|
|
||||||
connection.setUseCaches(false);
|
|
||||||
connection.setRequestMethod("PUT");
|
|
||||||
connection.setFixedLengthStreamingMode(expectedFileSize);
|
|
||||||
connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getUserAgent());
|
|
||||||
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();
|
|
||||||
final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
|
|
||||||
os = connection.getOutputStream();
|
|
||||||
transmitted = 0;
|
|
||||||
int count;
|
|
||||||
byte[] buffer = new byte[4096];
|
|
||||||
while (((count = innerInputStream.read(buffer)) != -1) && !cancelled) {
|
|
||||||
transmitted += count;
|
|
||||||
os.write(buffer, 0, count);
|
|
||||||
mHttpConnectionManager.updateConversationUi(false);
|
|
||||||
}
|
|
||||||
os.flush();
|
|
||||||
os.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) {
|
if (code == 200 || code == 201) {
|
||||||
Log.d(Config.LOGTAG, "finished uploading file");
|
Log.d(Config.LOGTAG, "finished uploading file");
|
||||||
final URL get;
|
final String get;
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
if (method == Method.P1_S3) {
|
get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build());
|
||||||
get = new URL(slot.getGetUrl().toString()+"#"+CryptoHelper.bytesToHex(key));
|
|
||||||
} else {
|
} else {
|
||||||
get = CryptoHelper.toAesGcmUrl(new URL(slot.getGetUrl().toString() + "#" + CryptoHelper.bytesToHex(key)));
|
get = slot.get.toString();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
get = slot.getGetUrl();
|
|
||||||
}
|
}
|
||||||
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
|
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
|
||||||
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
|
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
|
||||||
|
@ -227,24 +175,20 @@ public class HttpUploadConnection implements Transferable {
|
||||||
}
|
}
|
||||||
mXmppConnectionService.resendMessage(message, delayed);
|
mXmppConnectionService.resendMessage(message, delayed);
|
||||||
} else {
|
} else {
|
||||||
Log.d(Config.LOGTAG,"http upload failed because response code was "+code);
|
Log.d(Config.LOGTAG, "http upload failed because response code was " + code);
|
||||||
fail("http upload failed because response code was "+code);
|
fail("http upload failed because response code was " + code);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
Log.d(Config.LOGTAG,"http upload failed "+e.getMessage());
|
|
||||||
fail(e.getMessage());
|
|
||||||
} finally {
|
|
||||||
FileBackend.close(fileInputStream);
|
|
||||||
FileBackend.close(os);
|
|
||||||
if (connection != null) {
|
|
||||||
connection.disconnect();
|
|
||||||
}
|
|
||||||
WakeLockHelper.release(wakeLock);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Message getMessage() {
|
public Message getMessage() {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProgress(final long progress) {
|
||||||
|
this.transmitted = progress;
|
||||||
|
mHttpConnectionManager.updateConversationUi(false);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -33,7 +33,7 @@ import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||||
|
|
||||||
public enum Method {
|
public enum Method {
|
||||||
P1_S3, HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
|
HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
|
||||||
|
|
||||||
public static Method determine(Account account) {
|
public static Method determine(Account account) {
|
||||||
XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures();
|
XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures();
|
||||||
|
@ -44,8 +44,6 @@ public enum Method {
|
||||||
return HTTP_UPLOAD_LEGACY;
|
return HTTP_UPLOAD_LEGACY;
|
||||||
} else if (features.httpUpload(0)) {
|
} else if (features.httpUpload(0)) {
|
||||||
return HTTP_UPLOAD;
|
return HTTP_UPLOAD;
|
||||||
} else if (features.p1S3FileTransfer()) {
|
|
||||||
return P1_S3;
|
|
||||||
} else {
|
} else {
|
||||||
return HTTP_UPLOAD;
|
return HTTP_UPLOAD;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.net.URLStreamHandler;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.xml.Element;
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -31,9 +31,9 @@ package eu.siacs.conversations.http;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.net.MalformedURLException;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import java.net.URL;
|
|
||||||
import java.util.HashMap;
|
import java.util.Map;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
@ -44,6 +44,8 @@ import eu.siacs.conversations.xml.Element;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||||
|
import okhttp3.Headers;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
|
||||||
public class SlotRequester {
|
public class SlotRequester {
|
||||||
|
|
||||||
|
@ -53,15 +55,13 @@ public class SlotRequester {
|
||||||
this.service = service;
|
this.service = service;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void request(Method method, Account account, DownloadableFile file, String mime, String md5, OnSlotRequested callback) {
|
public void request(Method method, Account account, DownloadableFile file, String mime, OnSlotRequested callback) {
|
||||||
if (method == Method.HTTP_UPLOAD) {
|
if (method == Method.HTTP_UPLOAD_LEGACY) {
|
||||||
Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
|
final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY);
|
||||||
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);
|
requestHttpUploadLegacy(account, host, file, mime, callback);
|
||||||
} else {
|
} else {
|
||||||
requestP1S3(account, account.getDomain(), file.getName(), md5, callback);
|
final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
|
||||||
|
requestHttpUpload(account, host, file, mime, callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,14 +75,15 @@ public class SlotRequester {
|
||||||
final String putUrl = slotElement.findChildContent("put");
|
final String putUrl = slotElement.findChildContent("put");
|
||||||
final String getUrl = slotElement.findChildContent("get");
|
final String getUrl = slotElement.findChildContent("get");
|
||||||
if (getUrl != null && putUrl != null) {
|
if (getUrl != null && putUrl != null) {
|
||||||
Slot slot = new Slot(new URL(putUrl));
|
final Slot slot = new Slot(
|
||||||
slot.getUrl = new URL(getUrl);
|
HttpUrl.get(putUrl),
|
||||||
slot.headers = new HashMap<>();
|
HttpUrl.get(getUrl),
|
||||||
slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
|
Headers.of("Content-Type", mime == null ? "application/octet-stream" : mime)
|
||||||
|
);
|
||||||
callback.success(slot);
|
callback.success(slot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (MalformedURLException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
//fall through
|
//fall through
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,7 +98,7 @@ public class SlotRequester {
|
||||||
IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
|
IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
|
||||||
service.sendIqPacket(account, request, (a, packet) -> {
|
service.sendIqPacket(account, request, (a, packet) -> {
|
||||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||||
Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
|
final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
|
||||||
if (slotElement != null) {
|
if (slotElement != null) {
|
||||||
try {
|
try {
|
||||||
final Element put = slotElement.findChild("put");
|
final Element put = slotElement.findChild("put");
|
||||||
|
@ -105,23 +106,22 @@ public class SlotRequester {
|
||||||
final String putUrl = put == null ? null : put.getAttribute("url");
|
final String putUrl = put == null ? null : put.getAttribute("url");
|
||||||
final String getUrl = get == null ? null : get.getAttribute("url");
|
final String getUrl = get == null ? null : get.getAttribute("url");
|
||||||
if (getUrl != null && putUrl != null) {
|
if (getUrl != null && putUrl != null) {
|
||||||
Slot slot = new Slot(new URL(putUrl));
|
final ImmutableMap.Builder<String, String> headers = new ImmutableMap.Builder<>();
|
||||||
slot.getUrl = new URL(getUrl);
|
|
||||||
slot.headers = new HashMap<>();
|
|
||||||
for (Element child : put.getChildren()) {
|
for (Element child : put.getChildren()) {
|
||||||
if ("header".equals(child.getName())) {
|
if ("header".equals(child.getName())) {
|
||||||
final String name = child.getAttribute("name");
|
final String name = child.getAttribute("name");
|
||||||
final String value = child.getContent();
|
final String value = child.getContent();
|
||||||
if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
|
if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
|
||||||
slot.headers.put(name, value.trim());
|
headers.put(name, value.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
|
headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
|
||||||
|
final Slot slot = new Slot(HttpUrl.get(putUrl), HttpUrl.get(getUrl), headers.build());
|
||||||
callback.success(slot);
|
callback.success(slot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (MalformedURLException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
//fall through
|
//fall through
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,59 +132,26 @@ public class SlotRequester {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
public interface OnSlotRequested {
|
||||||
|
|
||||||
void success(Slot slot);
|
void success(Slot slot);
|
||||||
|
|
||||||
void failure(String message);
|
void failure(String message);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Slot {
|
public static class Slot {
|
||||||
private final URL putUrl;
|
public final HttpUrl put;
|
||||||
private URL getUrl;
|
public final HttpUrl get;
|
||||||
private HashMap<String, String> headers;
|
public final Headers headers;
|
||||||
|
|
||||||
private Slot(URL putUrl) {
|
private Slot(HttpUrl put, HttpUrl get, Headers headers) {
|
||||||
this.putUrl = putUrl;
|
this.put = put;
|
||||||
|
this.get = get;
|
||||||
|
this.headers = headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public URL getPutUrl() {
|
private Slot(HttpUrl put, HttpUrl getUrl, Map<String, String> headers) {
|
||||||
return putUrl;
|
this.put = put;
|
||||||
}
|
this.get = getUrl;
|
||||||
|
this.headers = Headers.of(headers);
|
||||||
public URL getGetUrl() {
|
|
||||||
return getUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HashMap<String, String> getHeaders() {
|
|
||||||
return headers;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
34
src/main/java/eu/siacs/conversations/http/URL.java
Normal file
34
src/main/java/eu/siacs/conversations/http/URL.java
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package eu.siacs.conversations.http;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.http.AesGcmURL;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
|
||||||
|
public class URL {
|
||||||
|
|
||||||
|
public static final List<String> WELL_KNOWN_SCHEMES = Arrays.asList("http", "https", AesGcmURL.PROTOCOL_NAME);
|
||||||
|
|
||||||
|
|
||||||
|
public static String tryParse(String url) {
|
||||||
|
final URI uri;
|
||||||
|
try {
|
||||||
|
uri = new URI(url);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (WELL_KNOWN_SCHEMES.contains(uri.getScheme())) {
|
||||||
|
return uri.toString();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpUrl stripFragment(final HttpUrl url) {
|
||||||
|
return url.newBuilder().fragment(null).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -33,7 +33,6 @@ import eu.siacs.conversations.entities.ReadByMarker;
|
||||||
import eu.siacs.conversations.entities.ReceiptRequest;
|
import eu.siacs.conversations.entities.ReceiptRequest;
|
||||||
import eu.siacs.conversations.entities.RtpSessionStatus;
|
import eu.siacs.conversations.entities.RtpSessionStatus;
|
||||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
|
||||||
import eu.siacs.conversations.services.MessageArchiveService;
|
import eu.siacs.conversations.services.MessageArchiveService;
|
||||||
import eu.siacs.conversations.services.QuickConversationsService;
|
import eu.siacs.conversations.services.QuickConversationsService;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
|
@ -408,8 +407,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
|
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
|
||||||
final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
|
final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
|
||||||
final Element oob = packet.findChild("x", Namespace.OOB);
|
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 oobUrl = oob != null ? oob.findChildContent("url") : null;
|
||||||
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
|
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
|
||||||
final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
|
final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
|
||||||
|
@ -464,7 +461,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) {
|
if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) {
|
||||||
final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString());
|
final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString());
|
||||||
final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
|
final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
|
||||||
final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
|
final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
|
||||||
|
@ -504,13 +501,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final Message message;
|
final Message message;
|
||||||
if (xP1S3url != null) {
|
if (pgpEncrypted != null && Config.supportOpenPgp()) {
|
||||||
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);
|
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
|
||||||
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
|
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
|
||||||
Jid origin;
|
Jid origin;
|
||||||
|
|
|
@ -416,9 +416,9 @@ public class FileBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void updateFileParams(Message message, URL url, long size) {
|
public static void updateFileParams(Message message, String url, long size) {
|
||||||
final StringBuilder body = new StringBuilder();
|
final StringBuilder body = new StringBuilder();
|
||||||
body.append(url.toString()).append('|').append(size);
|
body.append(url).append('|').append(size);
|
||||||
message.setBody(body.toString());
|
message.setBody(body.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1305,7 +1305,7 @@ public class FileBackend {
|
||||||
updateFileParams(message, null);
|
updateFileParams(message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateFileParams(Message message, URL url) {
|
public void updateFileParams(Message message, String url) {
|
||||||
DownloadableFile file = getFile(message);
|
DownloadableFile file = getFile(message);
|
||||||
final String mime = file.getMimeType();
|
final String mime = file.getMimeType();
|
||||||
final boolean privateMessage = message.isPrivateMessage();
|
final boolean privateMessage = message.isPrivateMessage();
|
||||||
|
@ -1315,7 +1315,7 @@ public class FileBackend {
|
||||||
final boolean pdf = "application/pdf".equals(mime);
|
final boolean pdf = "application/pdf".equals(mime);
|
||||||
final StringBuilder body = new StringBuilder();
|
final StringBuilder body = new StringBuilder();
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
body.append(url.toString());
|
body.append(url);
|
||||||
}
|
}
|
||||||
body.append('|').append(file.getSize());
|
body.append('|').append(file.getSize());
|
||||||
if (image || video || (pdf && Compatibility.runsTwentyOne())) {
|
if (image || video || (pdf && Compatibility.runsTwentyOne())) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.siacs.conversations.services;
|
package eu.siacs.conversations.services;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.FileUtils;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -13,8 +14,10 @@ import org.bouncycastle.crypto.modes.GCMBlockCipher;
|
||||||
import org.bouncycastle.crypto.params.AEADParameters;
|
import org.bouncycastle.crypto.params.AEADParameters;
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
@ -23,12 +26,21 @@ import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.NoSuchProviderException;
|
import java.security.NoSuchProviderException;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.crypto.NoSuchPaddingException;
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.entities.DownloadableFile;
|
import eu.siacs.conversations.entities.DownloadableFile;
|
||||||
import eu.siacs.conversations.utils.Compatibility;
|
import eu.siacs.conversations.utils.Compatibility;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okio.Buffer;
|
||||||
|
import okio.BufferedSink;
|
||||||
|
import okio.ForwardingSink;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Sink;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS;
|
import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS;
|
||||||
|
|
||||||
|
@ -42,7 +54,7 @@ public class AbstractConnectionManager {
|
||||||
this.mXmppConnectionService = service;
|
this.mXmppConnectionService = service;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InputStream upgrade(DownloadableFile file, InputStream is) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, NoSuchProviderException {
|
public static InputStream upgrade(DownloadableFile file, InputStream is) {
|
||||||
if (file.getKey() != null && file.getIv() != null) {
|
if (file.getKey() != null && file.getIv() != null) {
|
||||||
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
|
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
|
||||||
cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
|
cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
|
||||||
|
@ -52,6 +64,43 @@ public class AbstractConnectionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//For progress tracking see:
|
||||||
|
//https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
|
||||||
|
|
||||||
|
public static RequestBody requestBody(final DownloadableFile file, final ProgressListener progressListener) {
|
||||||
|
return new RequestBody() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long contentLength() {
|
||||||
|
return file.getSize() + (file.getKey() != null ? 16 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public MediaType contentType() {
|
||||||
|
return MediaType.parse(file.getMimeType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(final BufferedSink sink) throws IOException {
|
||||||
|
long transmitted = 0;
|
||||||
|
try (final Source source = Okio.source(upgrade(file, new FileInputStream(file)))) {
|
||||||
|
long read;
|
||||||
|
while ((read = source.read(sink.buffer(), 8196)) != -1) {
|
||||||
|
transmitted += read;
|
||||||
|
sink.flush();
|
||||||
|
progressListener.onProgress(transmitted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ProgressListener {
|
||||||
|
void onProgress(long progress);
|
||||||
|
}
|
||||||
|
|
||||||
public static OutputStream createOutputStream(DownloadableFile file, boolean append, boolean decrypt) {
|
public static OutputStream createOutputStream(DownloadableFile file, boolean append, boolean decrypt) {
|
||||||
FileOutputStream os;
|
FileOutputStream os;
|
||||||
try {
|
try {
|
||||||
|
@ -121,6 +170,7 @@ public class AbstractConnectionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Extension of(String path) {
|
public static Extension of(String path) {
|
||||||
|
//TODO accept List<String> pathSegments
|
||||||
final int pos = path.lastIndexOf('/');
|
final int pos = path.lastIndexOf('/');
|
||||||
final String filename = path.substring(pos + 1).toLowerCase();
|
final String filename = path.substring(pos + 1).toLowerCase();
|
||||||
final String[] parts = filename.split("\\.");
|
final String[] parts = filename.split("\\.");
|
||||||
|
|
|
@ -51,13 +51,8 @@ public class ChannelDiscoveryService {
|
||||||
|
|
||||||
void initializeMuclumbusService() {
|
void initializeMuclumbusService() {
|
||||||
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||||
|
|
||||||
if (service.useTorToConnect()) {
|
if (service.useTorToConnect()) {
|
||||||
try {
|
|
||||||
builder.proxy(HttpConnectionManager.getProxy());
|
builder.proxy(HttpConnectionManager.getProxy());
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Unable to use Tor proxy", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Retrofit retrofit = new Retrofit.Builder()
|
Retrofit retrofit = new Retrofit.Builder()
|
||||||
.client(builder.build())
|
.client(builder.build())
|
||||||
|
|
|
@ -104,7 +104,6 @@ import eu.siacs.conversations.generator.AbstractGenerator;
|
||||||
import eu.siacs.conversations.generator.IqGenerator;
|
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.CustomURLStreamHandlerFactory;
|
|
||||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||||
import eu.siacs.conversations.parser.AbstractParser;
|
import eu.siacs.conversations.parser.AbstractParser;
|
||||||
import eu.siacs.conversations.parser.IqParser;
|
import eu.siacs.conversations.parser.IqParser;
|
||||||
|
@ -183,10 +182,6 @@ public class XmppConnectionService extends Service {
|
||||||
|
|
||||||
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
|
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
|
||||||
|
|
||||||
static {
|
|
||||||
URL.setURLStreamHandlerFactory(new CustomURLStreamHandlerFactory());
|
|
||||||
}
|
|
||||||
|
|
||||||
public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
|
public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
|
||||||
private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding");
|
private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding");
|
||||||
private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression");
|
private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression");
|
||||||
|
|
|
@ -1605,7 +1605,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createNewConnection(final Message message) {
|
private void createNewConnection(final Message message) {
|
||||||
if (!activity.xmppConnectionService.getHttpConnectionManager().checkConnection(message)) {
|
if (!activity.xmppConnectionService.hasInternetConnection()) {
|
||||||
Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
|
Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1071,9 +1071,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
||||||
} else {
|
} else {
|
||||||
this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
|
this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
|
||||||
}
|
}
|
||||||
} else if (features.p1S3FileTransfer()) {
|
|
||||||
this.binding.serverInfoHttpUploadDescription.setText(R.string.p1_s3_filetransfer);
|
|
||||||
this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
|
|
||||||
} else {
|
} else {
|
||||||
this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
|
this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,11 +98,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
|
||||||
config.load(ctx, getPreferences());
|
config.load(ctx, getPreferences());
|
||||||
config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE);
|
config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE);
|
||||||
if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) {
|
if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) {
|
||||||
try {
|
|
||||||
config.setHttpProxy(HttpConnectionManager.getProxy());
|
config.setHttpProxy(HttpConnectionManager.getProxy());
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Unable to configure proxy");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
|
import android.net.Uri;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
|
@ -31,6 +32,7 @@ import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -48,7 +50,6 @@ import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.Message.FileParams;
|
import eu.siacs.conversations.entities.Message.FileParams;
|
||||||
import eu.siacs.conversations.entities.RtpSessionStatus;
|
import eu.siacs.conversations.entities.RtpSessionStatus;
|
||||||
import eu.siacs.conversations.entities.Transferable;
|
import eu.siacs.conversations.entities.Transferable;
|
||||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
|
||||||
import eu.siacs.conversations.persistance.FileBackend;
|
import eu.siacs.conversations.persistance.FileBackend;
|
||||||
import eu.siacs.conversations.services.MessageArchiveService;
|
import eu.siacs.conversations.services.MessageArchiveService;
|
||||||
import eu.siacs.conversations.services.NotificationService;
|
import eu.siacs.conversations.services.NotificationService;
|
||||||
|
@ -800,21 +801,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground);
|
displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground);
|
||||||
} else if (message.treatAsDownloadable()) {
|
} else if (message.treatAsDownloadable()) {
|
||||||
try {
|
try {
|
||||||
URL url = new URL(message.getBody());
|
final URI uri = new URI(message.getBody());
|
||||||
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(url.getProtocol())) {
|
|
||||||
displayDownloadableMessage(viewHolder,
|
|
||||||
message,
|
|
||||||
activity.getString(R.string.check_x_filesize,
|
|
||||||
UIHelper.getFileDescriptionString(activity, message)),
|
|
||||||
darkBackground);
|
|
||||||
} else {
|
|
||||||
displayDownloadableMessage(viewHolder,
|
displayDownloadableMessage(viewHolder,
|
||||||
message,
|
message,
|
||||||
activity.getString(R.string.check_x_filesize_on_host,
|
activity.getString(R.string.check_x_filesize_on_host,
|
||||||
UIHelper.getFileDescriptionString(activity, message),
|
UIHelper.getFileDescriptionString(activity, message),
|
||||||
url.getHost()),
|
uri.getHost()),
|
||||||
darkBackground);
|
darkBackground);
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
displayDownloadableMessage(viewHolder,
|
displayDownloadableMessage(viewHolder,
|
||||||
message,
|
message,
|
||||||
|
@ -903,10 +896,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
|
this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface OnQuoteListener {
|
|
||||||
void onQuote(String text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnContactPictureClicked {
|
public interface OnContactPictureClicked {
|
||||||
void onContactPictureClicked(Message message);
|
void onContactPictureClicked(Message message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,10 +94,10 @@ public class ShareUtil {
|
||||||
url = message.getBody();
|
url = message.getBody();
|
||||||
} else if (message.hasFileOnRemoteHost()) {
|
} else if (message.hasFileOnRemoteHost()) {
|
||||||
resId = R.string.file_url;
|
resId = R.string.file_url;
|
||||||
url = message.getFileParams().url.toString();
|
url = message.getFileParams().url;
|
||||||
} else {
|
} else {
|
||||||
final Message.FileParams fileParams = message.getFileParams();
|
final Message.FileParams fileParams = message.getFileParams();
|
||||||
url = (fileParams != null && fileParams.url != null) ? fileParams.url.toString() : message.getBody().trim();
|
url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim();
|
||||||
resId = R.string.file_url;
|
resId = R.string.file_url;
|
||||||
}
|
}
|
||||||
if (activity.copyTextToClipboard(url, resId)) {
|
if (activity.copyTextToClipboard(url, resId)) {
|
||||||
|
|
|
@ -31,7 +31,6 @@ import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.http.AesGcmURLStreamHandler;
|
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
||||||
public final class CryptoHelper {
|
public final class CryptoHelper {
|
||||||
|
@ -278,28 +277,6 @@ public final class CryptoHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URL toAesGcmUrl(URL url) {
|
|
||||||
if (!url.getProtocol().equalsIgnoreCase("https")) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return new URL(AesGcmURLStreamHandler.PROTOCOL_NAME + url.toString().substring(url.getProtocol().length()));
|
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static URL toHttpsUrl(URL url) {
|
|
||||||
if (!url.getProtocol().equalsIgnoreCase(AesGcmURLStreamHandler.PROTOCOL_NAME)) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return new URL("https" + url.toString().substring(url.getProtocol().length()));
|
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPgpEncryptedUrl(String url) {
|
public static boolean isPgpEncryptedUrl(String url) {
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -29,16 +29,18 @@
|
||||||
|
|
||||||
package eu.siacs.conversations.utils;
|
package eu.siacs.conversations.utils;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
import java.net.MalformedURLException;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URISyntaxException;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import eu.siacs.conversations.entities.Conversational;
|
import eu.siacs.conversations.entities.Conversational;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.http.AesGcmURLStreamHandler;
|
import eu.siacs.conversations.http.AesGcmURL;
|
||||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
import eu.siacs.conversations.http.URL;
|
||||||
|
|
||||||
public class MessageUtils {
|
public class MessageUtils {
|
||||||
|
|
||||||
|
@ -82,28 +84,32 @@ public class MessageUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean treatAsDownloadable(final String body, final boolean oob) {
|
public static boolean treatAsDownloadable(final String body, final boolean oob) {
|
||||||
try {
|
|
||||||
final String[] lines = body.split("\n");
|
final String[] lines = body.split("\n");
|
||||||
if (lines.length == 0) {
|
if (lines.length == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (String line : lines) {
|
for (final String line : lines) {
|
||||||
if (line.contains("\\s+")) {
|
if (line.contains("\\s+")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final URL url = new URL(lines[0]);
|
final URI uri;
|
||||||
final String ref = url.getRef();
|
try {
|
||||||
final String protocol = url.getProtocol();
|
uri = new URI(lines[0]);
|
||||||
final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
|
} catch (final URISyntaxException e) {
|
||||||
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 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!URL.WELL_KNOWN_SCHEMES.contains(uri.getScheme())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final String ref = uri.getFragment();
|
||||||
|
final String protocol = uri.getScheme();
|
||||||
|
final boolean encrypted = ref != null && AesGcmURL.IV_KEY.matcher(ref).matches();
|
||||||
|
final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
|
||||||
|
final boolean validAesGcm = AesGcmURL.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
|
||||||
|
final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol);
|
||||||
|
final boolean validOob = validProtocol && (oob || encrypted) && lines.length == 1;
|
||||||
|
return validAesGcm || validOob;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String filterLtrRtl(String body) {
|
public static String filterLtrRtl(String body) {
|
||||||
|
|
|
@ -23,7 +23,6 @@ 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";
|
|
||||||
public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0";
|
public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0";
|
||||||
public static final String BOOKMARKS = "storage:bookmarks";
|
public static final String BOOKMARKS = "storage:bookmarks";
|
||||||
public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";
|
public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";
|
||||||
|
|
|
@ -1921,10 +1921,6 @@ public class XmppConnection implements Runnable {
|
||||||
this.blockListRequested = value;
|
this.blockListRequested = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean p1S3FileTransfer() {
|
|
||||||
return hasDiscoFeature(account.getDomain(), 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 a new issue