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:converter-gson:2.9.0"
|
||||
//implementation "com.squareup.okhttp3:logging-interceptor:3.14.9"
|
||||
implementation 'com.google.guava:guava:30.1-android'
|
||||
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18'
|
||||
implementation fileTree(include: ['libwebrtc-m89.aar'], dir: 'libs')
|
||||
|
|
|
@ -209,7 +209,7 @@ public class PgpDecryptionService {
|
|||
message.setRelativeFilePath(path);
|
||||
}
|
||||
}
|
||||
URL url = message.getFileParams().url;
|
||||
final String url = message.getFileParams().url;
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
|
|
|
@ -75,7 +75,7 @@ public class PgpEngine {
|
|||
params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
String body;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
body = message.getFileParams().url.toString();
|
||||
body = message.getFileParams().url;
|
||||
} else {
|
||||
body = message.getBody();
|
||||
}
|
||||
|
|
|
@ -1169,7 +1169,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
||||
final String content;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
content = message.getFileParams().url.toString();
|
||||
content = message.getFileParams().url;
|
||||
} else {
|
||||
content = message.getBody();
|
||||
}
|
||||
|
|
|
@ -147,7 +147,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
}
|
||||
|
||||
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() {
|
||||
|
|
|
@ -788,7 +788,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
|
||||
String otherBody;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
otherBody = message.getFileParams().url.toString();
|
||||
otherBody = message.getFileParams().url;
|
||||
} else {
|
||||
otherBody = message.body;
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@ import com.google.common.collect.ImmutableSet;
|
|||
import org.json.JSONException;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -22,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
||||
import eu.siacs.conversations.http.URL;
|
||||
import eu.siacs.conversations.services.AvatarService;
|
||||
import eu.siacs.conversations.ui.util.PresenceSelector;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
|
@ -294,7 +293,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
try {
|
||||
values.put(EDITED, Edit.toJson(edits));
|
||||
} 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(ERROR_MESSAGE, errorMessage);
|
||||
|
@ -470,7 +469,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
}
|
||||
|
||||
boolean remoteMsgIdMatchInEdit(String id) {
|
||||
for(Edit edit : this.edits) {
|
||||
for (Edit edit : this.edits) {
|
||||
if (id.equals(edit.getEditedId())) {
|
||||
return true;
|
||||
}
|
||||
|
@ -547,7 +546,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
} else {
|
||||
String body, otherBody;
|
||||
if (this.hasFileOnRemoteHost()) {
|
||||
body = getFileParams().url.toString();
|
||||
body = getFileParams().url;
|
||||
otherBody = message.body == null ? null : message.body.trim();
|
||||
} else {
|
||||
body = this.body;
|
||||
|
@ -757,7 +756,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
|
||||
return true;
|
||||
} else if (presences.size() >= 1) {
|
||||
counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]);
|
||||
counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
|
||||
return true;
|
||||
} else {
|
||||
counterpart = null;
|
||||
|
@ -794,12 +793,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
if (relativeFilePath != null) {
|
||||
extension = MimeUtils.extractRelevantExtension(relativeFilePath);
|
||||
} else {
|
||||
try {
|
||||
final URL url = new URL(body.split("\n")[0]);
|
||||
extension = MimeUtils.extractRelevantExtension(url);
|
||||
} catch (MalformedURLException e) {
|
||||
final String url = URL.tryParse(body.split("\n")[0]);
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
extension = MimeUtils.extractRelevantExtension(url);
|
||||
}
|
||||
return MimeUtils.guessMimeTypeFromExtension(extension);
|
||||
}
|
||||
|
@ -840,8 +838,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
case 1:
|
||||
try {
|
||||
fileParams.size = Long.parseLong(parts[0]);
|
||||
} catch (NumberFormatException e) {
|
||||
fileParams.url = parseUrl(parts[0]);
|
||||
} catch (final NumberFormatException e) {
|
||||
fileParams.url = URL.tryParse(parts[0]);
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
|
@ -850,7 +848,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
fileParams.width = parseInt(parts[2]);
|
||||
fileParams.height = parseInt(parts[3]);
|
||||
case 2:
|
||||
fileParams.url = parseUrl(parts[0]);
|
||||
fileParams.url = URL.tryParse(parts[0]);
|
||||
fileParams.size = parseLong(parts[1]);
|
||||
break;
|
||||
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() {
|
||||
this.mNextMessage = null;
|
||||
this.mPreviousMessage = null;
|
||||
|
@ -905,11 +895,13 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
}
|
||||
|
||||
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 URL url;
|
||||
public String url;
|
||||
public long size = 0;
|
||||
public int width = 0;
|
||||
public int height = 0;
|
||||
|
|
|
@ -408,20 +408,6 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket requestP1S3Slot(Jid host, String md5) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
packet.setTo(host);
|
||||
packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("md5", md5);
|
||||
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) {
|
||||
int pos = name.indexOf('.');
|
||||
if (pos != -1) {
|
||||
|
|
|
@ -14,7 +14,6 @@ import eu.siacs.conversations.entities.Account;
|
|||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Conversational;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
|
@ -103,18 +102,9 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
MessagePacket packet = preparePacket(message);
|
||||
String content;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
Message.FileParams fileParams = message.getFileParams();
|
||||
final URL url = fileParams.url;
|
||||
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) {
|
||||
Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER);
|
||||
final String file = url.getFile();
|
||||
x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file);
|
||||
x.setAttribute("fileid", url.getHost());
|
||||
return packet;
|
||||
} else {
|
||||
content = url.toString();
|
||||
final Message.FileParams fileParams = message.getFileParams();
|
||||
content = fileParams.url;
|
||||
packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
|
||||
}
|
||||
} else {
|
||||
content = message.getBody();
|
||||
}
|
||||
|
@ -126,16 +116,9 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
MessagePacket packet = preparePacket(message);
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
Message.FileParams fileParams = message.getFileParams();
|
||||
final URL url = fileParams.url;
|
||||
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) {
|
||||
Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER);
|
||||
final String file = url.getFile();
|
||||
x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file);
|
||||
x.setAttribute("fileid", url.getHost());
|
||||
} else {
|
||||
packet.setBody(url.toString());
|
||||
packet.addChild("x", Namespace.OOB).addChild("url").setContent(url.toString());
|
||||
}
|
||||
final String url = fileParams.url;
|
||||
packet.setBody(url);
|
||||
packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
|
||||
} else {
|
||||
if (Config.supportUnencrypted()) {
|
||||
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 java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
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.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.TLSSocketFactory;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class HttpConnectionManager extends AbstractConnectionManager {
|
||||
|
||||
|
@ -39,8 +40,12 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
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));
|
||||
} catch (final UnknownHostException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
synchronized (this.downloadConnections) {
|
||||
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 HostnameVerifier hostnameVerifier = mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier(), interactive);
|
||||
if (interactive) {
|
||||
|
@ -106,8 +116,8 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
}
|
||||
try {
|
||||
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
|
||||
connection.setSSLSocketFactory(sf);
|
||||
connection.setHostnameVerifier(hostnameVerifier);
|
||||
builder.sslSocketFactory(sf, trustManager);
|
||||
builder.hostnameVerifier(hostnameVerifier);
|
||||
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,24 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
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.FileWriterException;
|
||||
import eu.siacs.conversations.utils.MimeUtils;
|
||||
import eu.siacs.conversations.utils.WakeLockHelper;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
|
||||
|
||||
|
@ -44,13 +42,13 @@ public class HttpDownloadConnection implements Transferable {
|
|||
private final boolean mUseTor;
|
||||
private final HttpConnectionManager mHttpConnectionManager;
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
private URL mUrl;
|
||||
private HttpUrl mUrl;
|
||||
private DownloadableFile file;
|
||||
private int mStatus = Transferable.STATUS_UNKNOWN;
|
||||
private boolean acceptedAutomatically = false;
|
||||
private int mProgress = 0;
|
||||
private boolean canceled = false;
|
||||
private Method method = Method.HTTP_UPLOAD;
|
||||
private Call mostRecentCall;
|
||||
|
||||
HttpDownloadConnection(Message message, HttpConnectionManager manager) {
|
||||
this.message = message;
|
||||
|
@ -88,13 +86,13 @@ public class HttpDownloadConnection implements Transferable {
|
|||
try {
|
||||
final Message.FileParams fileParams = message.getFileParams();
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
mUrl = CryptoHelper.toHttpsUrl(fileParams.url);
|
||||
mUrl = AesGcmURL.of(fileParams.url);
|
||||
} else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
|
||||
mUrl = fileParams.url;
|
||||
mUrl = AesGcmURL.of(fileParams.url);
|
||||
} 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)) {
|
||||
this.message.setEncryption(Message.ENCRYPTION_PGP);
|
||||
} 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) {
|
||||
this.message.setEncryption(Message.ENCRYPTION_NONE);
|
||||
}
|
||||
method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD;
|
||||
long knownFileSize = message.getFileParams().size;
|
||||
if (knownFileSize > 0 && interactive && method != Method.P1_S3) {
|
||||
//TODO add auth tag size to knownFileSize
|
||||
final long knownFileSize = message.getFileParams().size;
|
||||
if (knownFileSize > 0 && interactive) {
|
||||
this.file.setExpectedSize(knownFileSize);
|
||||
download(true);
|
||||
} else {
|
||||
checkFileSize(interactive);
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
} catch (final IllegalArgumentException e) {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupFile() {
|
||||
final String reference = mUrl.getRef();
|
||||
if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) {
|
||||
final String reference = mUrl.fragment();
|
||||
if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) {
|
||||
this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
|
||||
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
|
||||
Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
|
||||
|
@ -146,6 +144,10 @@ public class HttpDownloadConnection implements Transferable {
|
|||
@Override
|
||||
public void cancel() {
|
||||
this.canceled = true;
|
||||
final Call call = this.mostRecentCall;
|
||||
if (call != null && !call.isCanceled()) {
|
||||
call.cancel();
|
||||
}
|
||||
mHttpConnectionManager.finishConnection(this);
|
||||
message.setTransferable(null);
|
||||
if (message.isFileOrImage()) {
|
||||
|
@ -260,34 +262,8 @@ public class HttpDownloadConnection implements Transferable {
|
|||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) {
|
||||
retrieveUrl();
|
||||
} else {
|
||||
check();
|
||||
}
|
||||
}
|
||||
|
||||
private void retrieveUrl() {
|
||||
changeStatus(STATUS_CHECKING);
|
||||
final Account account = message.getConversation().getAccount();
|
||||
IqPacket request = mXmppConnectionService.getIqGenerator().requestP1S3Url(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) {
|
||||
changeStatus(STATUS_OFFER_CHECK_FILESIZE);
|
||||
|
@ -330,46 +306,21 @@ public class HttpDownloadConnection implements Transferable {
|
|||
}
|
||||
|
||||
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 {
|
||||
Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive);
|
||||
changeStatus(STATUS_CHECKING);
|
||||
HttpURLConnection connection;
|
||||
final String hostname = mUrl.getHost();
|
||||
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());
|
||||
final Response response = mostRecentCall.execute();
|
||||
final String contentLength = response.header("Content-Length");
|
||||
final String contentType = response.header("Content-Type");
|
||||
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
|
||||
if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) {
|
||||
final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
|
||||
if (fileExtension != null) {
|
||||
|
@ -378,8 +329,7 @@ public class HttpDownloadConnection implements Transferable {
|
|||
setupFile();
|
||||
}
|
||||
}
|
||||
connection.disconnect();
|
||||
if (contentLength == null) {
|
||||
if (Strings.isNullOrEmpty(contentLength)) {
|
||||
throw new IOException("no content-length found in HEAD response");
|
||||
}
|
||||
return Long.parseLong(contentLength, 10);
|
||||
|
@ -397,8 +347,6 @@ public class HttpDownloadConnection implements Transferable {
|
|||
|
||||
private final boolean interactive;
|
||||
|
||||
private OutputStream os;
|
||||
|
||||
public FileDownloader(boolean interactive) {
|
||||
this.interactive = interactive;
|
||||
}
|
||||
|
@ -411,9 +359,10 @@ public class HttpDownloadConnection implements Transferable {
|
|||
decryptIfNeeded();
|
||||
updateImageBounds();
|
||||
finish();
|
||||
} catch (SSLHandshakeException e) {
|
||||
} catch (final SSLHandshakeException e) {
|
||||
changeStatus(STATUS_OFFER);
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
Log.d(Config.LOGTAG,"problem downloading",e);
|
||||
if (interactive) {
|
||||
showToastForException(e);
|
||||
} else {
|
||||
|
@ -425,67 +374,57 @@ public class HttpDownloadConnection implements Transferable {
|
|||
}
|
||||
|
||||
private void download() throws Exception {
|
||||
InputStream is = null;
|
||||
HttpURLConnection connection = null;
|
||||
final PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock(Thread.currentThread());
|
||||
try {
|
||||
wakeLock.acquire();
|
||||
if (mUseTor || message.getConversation().getAccount().isOnion()) {
|
||||
connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
|
||||
} 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 OkHttpClient client = mHttpConnectionManager.buildHttpClient(
|
||||
mUrl,
|
||||
message.getConversation().getAccount(),
|
||||
interactive
|
||||
);
|
||||
|
||||
final Request.Builder requestBuilder = new Request.Builder().url(URL.stripFragment(mUrl));
|
||||
|
||||
final long expected = file.getExpectedSize();
|
||||
final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
|
||||
long resumeSize = 0;
|
||||
|
||||
final long resumeSize;
|
||||
if (tryResume) {
|
||||
resumeSize = file.getSize();
|
||||
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);
|
||||
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.connect();
|
||||
is = new BufferedInputStream(connection.getInputStream());
|
||||
final String contentRange = connection.getHeaderField("Content-Range");
|
||||
boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
|
||||
final Request request = requestBuilder.build();
|
||||
mostRecentCall = client.newCall(request);
|
||||
final Response response = mostRecentCall.execute();
|
||||
final int code = response.code();
|
||||
if (code >= 200 && code <= 299) {
|
||||
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;
|
||||
if (tryResume && serverResumed) {
|
||||
Log.d(Config.LOGTAG, "server resumed");
|
||||
transmitted = file.getSize();
|
||||
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
||||
os = AbstractConnectionManager.createOutputStream(file, true, false);
|
||||
if (os == null) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
outputStream = AbstractConnectionManager.createOutputStream(file, true, false);
|
||||
} else {
|
||||
long reportedContentLengthOnGet;
|
||||
try {
|
||||
reportedContentLengthOnGet = Long.parseLong(connection.getHeaderField("Content-Length"));
|
||||
} catch (NumberFormatException | NullPointerException e) {
|
||||
reportedContentLengthOnGet = 0;
|
||||
}
|
||||
if (expected != reportedContentLengthOnGet) {
|
||||
Log.d(Config.LOGTAG, "content-length reported on GET (" + reportedContentLengthOnGet + ") did not match Content-Length reported on HEAD (" + expected + ")");
|
||||
final String contentLength = response.header("Content-Length");
|
||||
final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength);
|
||||
if (expected != size) {
|
||||
Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")");
|
||||
}
|
||||
file.getParentFile().mkdirs();
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
os = AbstractConnectionManager.createOutputStream(file, false, false);
|
||||
outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
|
||||
}
|
||||
int count;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((count = is.read(buffer)) != -1) {
|
||||
while ((count = inputStream.read(buffer)) != -1) {
|
||||
transmitted += count;
|
||||
try {
|
||||
os.write(buffer, 0, count);
|
||||
outputStream.write(buffer, 0, count);
|
||||
} catch (IOException e) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
|
@ -494,35 +433,21 @@ public class HttpDownloadConnection implements Transferable {
|
|||
throw new CancellationException();
|
||||
}
|
||||
}
|
||||
try {
|
||||
os.flush();
|
||||
} catch (IOException e) {
|
||||
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);
|
||||
outputStream.flush();
|
||||
} else {
|
||||
throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateImageBounds() {
|
||||
final boolean privateMessage = message.isPrivateMessage();
|
||||
message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
|
||||
final URL url;
|
||||
final String ref = mUrl.getRef();
|
||||
if (method == Method.P1_S3) {
|
||||
url = message.getFileParams().url;
|
||||
} else if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) {
|
||||
url = CryptoHelper.toAesGcmUrl(mUrl);
|
||||
final String url;
|
||||
final String ref = mUrl.fragment();
|
||||
if (ref != null && AesGcmURL.IV_KEY.matcher(ref).matches()) {
|
||||
url = AesGcmURL.toAesGcmUrl(mUrl);
|
||||
} else {
|
||||
url = mUrl;
|
||||
url = mUrl.toString();
|
||||
}
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
|
|
|
@ -1,35 +1,30 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
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.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Transferable;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.Checksum;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.WakeLockHelper;
|
||||
import 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 {
|
||||
public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener {
|
||||
|
||||
static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
|
||||
"Authorization",
|
||||
|
@ -42,7 +37,6 @@ public class HttpUploadConnection implements Transferable {
|
|||
private final SlotRequester mSlotRequester;
|
||||
private final Method method;
|
||||
private final boolean mUseTor;
|
||||
private boolean cancelled = false;
|
||||
private boolean delayed = false;
|
||||
private DownloadableFile file;
|
||||
private final Message message;
|
||||
|
@ -51,6 +45,7 @@ public class HttpUploadConnection implements Transferable {
|
|||
private byte[] key = null;
|
||||
|
||||
private long transmitted = 0;
|
||||
private Call mostRecentCall;
|
||||
|
||||
public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) {
|
||||
this.message = message;
|
||||
|
@ -86,11 +81,16 @@ public class HttpUploadConnection implements Transferable {
|
|||
|
||||
@Override
|
||||
public void cancel() {
|
||||
this.cancelled = true;
|
||||
final Call call = this.mostRecentCall;
|
||||
if (call != null && !call.isCanceled()) {
|
||||
call.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(String errorMessage) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -116,30 +116,14 @@ public class HttpUploadConnection implements Transferable {
|
|||
mXmppConnectionService.getRNG().nextBytes(this.key);
|
||||
this.file.setKeyAndIv(this.key);
|
||||
}
|
||||
|
||||
final String md5;
|
||||
|
||||
if (method == Method.P1_S3) {
|
||||
try {
|
||||
md5 = Checksum.md5(AbstractConnectionManager.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));
|
||||
message.resetFileParams();
|
||||
this.mSlotRequester.request(method, account, file, mime, md5, new SlotRequester.OnSlotRequested() {
|
||||
this.mSlotRequester.request(method, account, file, mime, new SlotRequester.OnSlotRequested() {
|
||||
@Override
|
||||
public void success(SlotRequester.Slot slot) {
|
||||
if (!cancelled) {
|
||||
public void success(final SlotRequester.Slot slot) {
|
||||
//TODO needs to mark the message as cancelled afterwards (ie call fail())
|
||||
HttpUploadConnection.this.slot = slot;
|
||||
EXECUTOR.execute(HttpUploadConnection.this::upload);
|
||||
}
|
||||
HttpUploadConnection.this.upload();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -152,72 +136,36 @@ public class HttpUploadConnection implements Transferable {
|
|||
}
|
||||
|
||||
private void upload() {
|
||||
OutputStream os = null;
|
||||
InputStream fileInputStream = null;
|
||||
HttpURLConnection connection = null;
|
||||
final PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock(Thread.currentThread());
|
||||
try {
|
||||
fileInputStream = new FileInputStream(file);
|
||||
final String slotHostname = slot.getPutUrl().getHost();
|
||||
final boolean onionSlot = slotHostname != null && slotHostname.endsWith(".onion");
|
||||
final int expectedFileSize = (int) file.getExpectedSize();
|
||||
final int readTimeout = (expectedFileSize / 2048) + Config.SOCKET_TIMEOUT; //assuming a minimum transfer speed of 16kbit/s
|
||||
wakeLock.acquire(readTimeout);
|
||||
Log.d(Config.LOGTAG, "uploading to " + slot.getPutUrl().toString()+ " w/ read timeout of "+readTimeout+"s");
|
||||
final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
|
||||
slot.put,
|
||||
message.getConversation().getAccount(),
|
||||
true
|
||||
);
|
||||
final RequestBody requestBody = AbstractConnectionManager.requestBody(file, this);
|
||||
final Request request = new Request.Builder()
|
||||
.url(slot.put)
|
||||
.put(requestBody)
|
||||
.headers(slot.headers)
|
||||
.build();
|
||||
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) {
|
||||
connection = (HttpURLConnection) slot.getPutUrl().openConnection(HttpConnectionManager.getProxy());
|
||||
} else {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
|
||||
final int code = response.code();
|
||||
if (code == 200 || code == 201) {
|
||||
Log.d(Config.LOGTAG, "finished uploading file");
|
||||
final URL get;
|
||||
final String get;
|
||||
if (key != null) {
|
||||
if (method == Method.P1_S3) {
|
||||
get = new URL(slot.getGetUrl().toString()+"#"+CryptoHelper.bytesToHex(key));
|
||||
get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build());
|
||||
} else {
|
||||
get = CryptoHelper.toAesGcmUrl(new URL(slot.getGetUrl().toString() + "#" + CryptoHelper.bytesToHex(key)));
|
||||
}
|
||||
} else {
|
||||
get = slot.getGetUrl();
|
||||
get = slot.get.toString();
|
||||
}
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
|
||||
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
|
||||
|
@ -227,24 +175,20 @@ public class HttpUploadConnection implements Transferable {
|
|||
}
|
||||
mXmppConnectionService.resendMessage(message, delayed);
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,"http upload failed because response code was "+code);
|
||||
fail("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);
|
||||
}
|
||||
} 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() {
|
||||
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;
|
||||
|
||||
public enum Method {
|
||||
P1_S3, HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
|
||||
HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
|
||||
|
||||
public static Method determine(Account account) {
|
||||
XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures();
|
||||
|
@ -44,8 +44,6 @@ public enum Method {
|
|||
return HTTP_UPLOAD_LEGACY;
|
||||
} else if (features.httpUpload(0)) {
|
||||
return HTTP_UPLOAD;
|
||||
} else if (features.p1S3FileTransfer()) {
|
||||
return P1_S3;
|
||||
} else {
|
||||
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 java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
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.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class SlotRequester {
|
||||
|
||||
|
@ -53,15 +55,13 @@ public class SlotRequester {
|
|||
this.service = service;
|
||||
}
|
||||
|
||||
public void request(Method method, Account account, DownloadableFile file, String mime, String md5, OnSlotRequested callback) {
|
||||
if (method == Method.HTTP_UPLOAD) {
|
||||
Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
|
||||
requestHttpUpload(account, host, file, mime, callback);
|
||||
} else if (method == Method.HTTP_UPLOAD_LEGACY) {
|
||||
Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY);
|
||||
public void request(Method method, Account account, DownloadableFile file, String mime, OnSlotRequested callback) {
|
||||
if (method == Method.HTTP_UPLOAD_LEGACY) {
|
||||
final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY);
|
||||
requestHttpUploadLegacy(account, host, file, mime, callback);
|
||||
} 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 getUrl = slotElement.findChildContent("get");
|
||||
if (getUrl != null && putUrl != null) {
|
||||
Slot slot = new Slot(new URL(putUrl));
|
||||
slot.getUrl = new URL(getUrl);
|
||||
slot.headers = new HashMap<>();
|
||||
slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
|
||||
final Slot slot = new Slot(
|
||||
HttpUrl.get(putUrl),
|
||||
HttpUrl.get(getUrl),
|
||||
Headers.of("Content-Type", mime == null ? "application/octet-stream" : mime)
|
||||
);
|
||||
callback.success(slot);
|
||||
return;
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
} catch (IllegalArgumentException e) {
|
||||
//fall through
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +98,7 @@ public class SlotRequester {
|
|||
IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
|
||||
service.sendIqPacket(account, request, (a, packet) -> {
|
||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||
Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
|
||||
final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
|
||||
if (slotElement != null) {
|
||||
try {
|
||||
final Element put = slotElement.findChild("put");
|
||||
|
@ -105,23 +106,22 @@ public class SlotRequester {
|
|||
final String putUrl = put == null ? null : put.getAttribute("url");
|
||||
final String getUrl = get == null ? null : get.getAttribute("url");
|
||||
if (getUrl != null && putUrl != null) {
|
||||
Slot slot = new Slot(new URL(putUrl));
|
||||
slot.getUrl = new URL(getUrl);
|
||||
slot.headers = new HashMap<>();
|
||||
final ImmutableMap.Builder<String, String> headers = new ImmutableMap.Builder<>();
|
||||
for (Element child : put.getChildren()) {
|
||||
if ("header".equals(child.getName())) {
|
||||
final String name = child.getAttribute("name");
|
||||
final String value = child.getContent();
|
||||
if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
|
||||
slot.headers.put(name, value.trim());
|
||||
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);
|
||||
return;
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
} catch (IllegalArgumentException e) {
|
||||
//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 {
|
||||
|
||||
void success(Slot slot);
|
||||
|
||||
void failure(String message);
|
||||
|
||||
}
|
||||
|
||||
public static class Slot {
|
||||
private final URL putUrl;
|
||||
private URL getUrl;
|
||||
private HashMap<String, String> headers;
|
||||
public final HttpUrl put;
|
||||
public final HttpUrl get;
|
||||
public final Headers headers;
|
||||
|
||||
private Slot(URL putUrl) {
|
||||
this.putUrl = putUrl;
|
||||
private Slot(HttpUrl put, HttpUrl get, Headers headers) {
|
||||
this.put = put;
|
||||
this.get = get;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public URL getPutUrl() {
|
||||
return putUrl;
|
||||
}
|
||||
|
||||
public URL getGetUrl() {
|
||||
return getUrl;
|
||||
}
|
||||
|
||||
public HashMap<String, String> getHeaders() {
|
||||
return headers;
|
||||
private Slot(HttpUrl put, HttpUrl getUrl, Map<String, String> headers) {
|
||||
this.put = put;
|
||||
this.get = getUrl;
|
||||
this.headers = Headers.of(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.RtpSessionStatus;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
||||
import eu.siacs.conversations.services.MessageArchiveService;
|
||||
import eu.siacs.conversations.services.QuickConversationsService;
|
||||
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 Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
|
||||
final Element oob = packet.findChild("x", Namespace.OOB);
|
||||
final Element xP1S3 = packet.findChild("x", Namespace.P1_S3_FILE_TRANSFER);
|
||||
final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3);
|
||||
final String oobUrl = oob != null ? oob.findChildContent("url") : null;
|
||||
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
|
||||
final Element axolotlEncrypted = packet.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 Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
|
||||
final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
|
||||
|
@ -504,13 +501,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
}
|
||||
}
|
||||
final Message message;
|
||||
if (xP1S3url != null) {
|
||||
message = new Message(conversation, xP1S3url.toString(), Message.ENCRYPTION_NONE, status);
|
||||
message.setOob(true);
|
||||
if (CryptoHelper.isPgpEncryptedUrl(xP1S3url.toString())) {
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
}
|
||||
} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
|
||||
if (pgpEncrypted != null && Config.supportOpenPgp()) {
|
||||
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
|
||||
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
|
||||
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();
|
||||
body.append(url.toString()).append('|').append(size);
|
||||
body.append(url).append('|').append(size);
|
||||
message.setBody(body.toString());
|
||||
}
|
||||
|
||||
|
@ -1305,7 +1305,7 @@ public class FileBackend {
|
|||
updateFileParams(message, null);
|
||||
}
|
||||
|
||||
public void updateFileParams(Message message, URL url) {
|
||||
public void updateFileParams(Message message, String url) {
|
||||
DownloadableFile file = getFile(message);
|
||||
final String mime = file.getMimeType();
|
||||
final boolean privateMessage = message.isPrivateMessage();
|
||||
|
@ -1315,7 +1315,7 @@ public class FileBackend {
|
|||
final boolean pdf = "application/pdf".equals(mime);
|
||||
final StringBuilder body = new StringBuilder();
|
||||
if (url != null) {
|
||||
body.append(url.toString());
|
||||
body.append(url);
|
||||
}
|
||||
body.append('|').append(file.getSize());
|
||||
if (image || video || (pdf && Compatibility.runsTwentyOne())) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package eu.siacs.conversations.services;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.FileUtils;
|
||||
import android.os.PowerManager;
|
||||
import android.os.SystemClock;
|
||||
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.KeyParameter;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
|
@ -23,12 +26,21 @@ import java.security.NoSuchAlgorithmException;
|
|||
import java.security.NoSuchProviderException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
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;
|
||||
|
||||
|
@ -42,7 +54,7 @@ public class AbstractConnectionManager {
|
|||
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) {
|
||||
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
|
||||
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) {
|
||||
FileOutputStream os;
|
||||
try {
|
||||
|
@ -121,6 +170,7 @@ public class AbstractConnectionManager {
|
|||
}
|
||||
|
||||
public static Extension of(String path) {
|
||||
//TODO accept List<String> pathSegments
|
||||
final int pos = path.lastIndexOf('/');
|
||||
final String filename = path.substring(pos + 1).toLowerCase();
|
||||
final String[] parts = filename.split("\\.");
|
||||
|
|
|
@ -51,13 +51,8 @@ public class ChannelDiscoveryService {
|
|||
|
||||
void initializeMuclumbusService() {
|
||||
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||
|
||||
if (service.useTorToConnect()) {
|
||||
try {
|
||||
builder.proxy(HttpConnectionManager.getProxy());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unable to use Tor proxy", e);
|
||||
}
|
||||
}
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.client(builder.build())
|
||||
|
|
|
@ -104,7 +104,6 @@ import eu.siacs.conversations.generator.AbstractGenerator;
|
|||
import eu.siacs.conversations.generator.IqGenerator;
|
||||
import eu.siacs.conversations.generator.MessageGenerator;
|
||||
import eu.siacs.conversations.generator.PresenceGenerator;
|
||||
import eu.siacs.conversations.http.CustomURLStreamHandlerFactory;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.parser.AbstractParser;
|
||||
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";
|
||||
|
||||
static {
|
||||
URL.setURLStreamHandlerFactory(new CustomURLStreamHandlerFactory());
|
||||
}
|
||||
|
||||
public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
|
||||
private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding");
|
||||
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) {
|
||||
if (!activity.xmppConnectionService.getHttpConnectionManager().checkConnection(message)) {
|
||||
if (!activity.xmppConnectionService.hasInternetConnection()) {
|
||||
Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1071,9 +1071,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
} else {
|
||||
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 {
|
||||
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.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE);
|
||||
if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) {
|
||||
try {
|
||||
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.pm.PackageManager;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
|
@ -31,6 +32,7 @@ import androidx.core.content.ContextCompat;
|
|||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
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.RtpSessionStatus;
|
||||
import eu.siacs.conversations.entities.Transferable;
|
||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.MessageArchiveService;
|
||||
import eu.siacs.conversations.services.NotificationService;
|
||||
|
@ -800,21 +801,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground);
|
||||
} else if (message.treatAsDownloadable()) {
|
||||
try {
|
||||
URL url = new URL(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 {
|
||||
final URI uri = new URI(message.getBody());
|
||||
displayDownloadableMessage(viewHolder,
|
||||
message,
|
||||
activity.getString(R.string.check_x_filesize_on_host,
|
||||
UIHelper.getFileDescriptionString(activity, message),
|
||||
url.getHost()),
|
||||
uri.getHost()),
|
||||
darkBackground);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
displayDownloadableMessage(viewHolder,
|
||||
message,
|
||||
|
@ -903,10 +896,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
|
||||
}
|
||||
|
||||
public interface OnQuoteListener {
|
||||
void onQuote(String text);
|
||||
}
|
||||
|
||||
public interface OnContactPictureClicked {
|
||||
void onContactPictureClicked(Message message);
|
||||
}
|
||||
|
|
|
@ -94,10 +94,10 @@ public class ShareUtil {
|
|||
url = message.getBody();
|
||||
} else if (message.hasFileOnRemoteHost()) {
|
||||
resId = R.string.file_url;
|
||||
url = message.getFileParams().url.toString();
|
||||
url = message.getFileParams().url;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
if (activity.copyTextToClipboard(url, resId)) {
|
||||
|
|
|
@ -31,7 +31,6 @@ import eu.siacs.conversations.Config;
|
|||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.http.AesGcmURLStreamHandler;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
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) {
|
||||
if (url == null) {
|
||||
return false;
|
||||
|
|
|
@ -29,16 +29,18 @@
|
|||
|
||||
package eu.siacs.conversations.utils;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import eu.siacs.conversations.entities.Conversational;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.http.AesGcmURLStreamHandler;
|
||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
||||
import eu.siacs.conversations.http.AesGcmURL;
|
||||
import eu.siacs.conversations.http.URL;
|
||||
|
||||
public class MessageUtils {
|
||||
|
||||
|
@ -82,28 +84,32 @@ public class MessageUtils {
|
|||
}
|
||||
|
||||
public static boolean treatAsDownloadable(final String body, final boolean oob) {
|
||||
try {
|
||||
final String[] lines = body.split("\n");
|
||||
if (lines.length == 0) {
|
||||
return false;
|
||||
}
|
||||
for (String line : lines) {
|
||||
for (final String line : lines) {
|
||||
if (line.contains("\\s+")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final URL url = new URL(lines[0]);
|
||||
final String ref = url.getRef();
|
||||
final String protocol = url.getProtocol();
|
||||
final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
|
||||
final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
|
||||
final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
|
||||
final boolean 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) {
|
||||
final URI uri;
|
||||
try {
|
||||
uri = new URI(lines[0]);
|
||||
} catch (final URISyntaxException e) {
|
||||
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) {
|
||||
|
|
|
@ -23,7 +23,6 @@ public final class Namespace {
|
|||
public static final String NICK = "http://jabber.org/protocol/nick";
|
||||
public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline";
|
||||
public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind";
|
||||
public static final String P1_S3_FILE_TRANSFER = "p1:s3filetransfer";
|
||||
public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0";
|
||||
public static final String BOOKMARKS = "storage:bookmarks";
|
||||
public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";
|
||||
|
|
|
@ -1921,10 +1921,6 @@ public class XmppConnection implements Runnable {
|
|||
this.blockListRequested = value;
|
||||
}
|
||||
|
||||
public boolean p1S3FileTransfer() {
|
||||
return hasDiscoFeature(account.getDomain(), Namespace.P1_S3_FILE_TRANSFER);
|
||||
}
|
||||
|
||||
public boolean httpUpload(long filesize) {
|
||||
if (Config.DISABLE_HTTP_UPLOAD) {
|
||||
return false;
|
||||
|
|
Loading…
Reference in a new issue