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:
Daniel Gultsch 2021-03-19 14:57:15 +01:00
parent b09a1432a3
commit e217551a82
33 changed files with 1566 additions and 1797 deletions

View file

@ -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')

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -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) {

View file

@ -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())) { packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER);
final String file = url.getFile();
x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file);
x.setAttribute("fileid", url.getHost());
return packet;
} else {
content = url.toString();
packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
}
} else { } 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);
@ -225,7 +208,7 @@ public class MessageGenerator extends AbstractGenerator {
return packet; return packet;
} }
public MessagePacket received(Account account, final Jid from, final String id, ArrayList<String> namespaces, int type) { public MessagePacket received(Account account, final Jid from, final String id, ArrayList<String> namespaces, int type) {
final MessagePacket receivedPacket = new MessagePacket(); final MessagePacket receivedPacket = new MessagePacket();
receivedPacket.setType(type); receivedPacket.setType(type);
receivedPacket.setTo(from); receivedPacket.setTo(from);

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

View file

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

View file

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

View file

@ -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() {
return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 9050)); 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) { 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) {
} }
} }

View file

@ -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,33 +262,7 @@ public class HttpDownloadConnection implements Transferable {
@Override @Override
public void run() { public void run() {
if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) { check();
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) { private void retrieveFailed(@Nullable Exception e) {
@ -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());
} 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 boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
long resumeSize = 0;
if (tryResume) { final Request.Builder requestBuilder = new Request.Builder().url(URL.stripFragment(mUrl));
resumeSize = file.getSize();
Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected); final long expected = file.getExpectedSize();
connection.setRequestProperty("Range", "bytes=" + resumeSize + "-"); final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
} final long resumeSize;
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); if (tryResume) {
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); resumeSize = file.getSize();
connection.connect(); Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
is = new BufferedInputStream(connection.getInputStream()); requestBuilder.addHeader("Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize));
final String contentRange = connection.getHeaderField("Content-Range"); } else {
boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-"); resumeSize = 0;
}
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; 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);

View file

@ -1,250 +1,194 @@
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(
"Authorization",
"Cookie",
"Expires"
);
static final List<String> WHITE_LISTED_HEADERS = Arrays.asList( private final HttpConnectionManager mHttpConnectionManager;
"Authorization", private final XmppConnectionService mXmppConnectionService;
"Cookie", private final SlotRequester mSlotRequester;
"Expires" private final Method method;
); private final boolean mUseTor;
private boolean delayed = false;
private DownloadableFile file;
private final Message message;
private String mime;
private SlotRequester.Slot slot;
private byte[] key = null;
private final HttpConnectionManager mHttpConnectionManager; private long transmitted = 0;
private final XmppConnectionService mXmppConnectionService; private Call mostRecentCall;
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;
private String mime;
private SlotRequester.Slot slot;
private byte[] key = null;
private long transmitted = 0; public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) {
this.message = message;
this.method = method;
this.mHttpConnectionManager = httpConnectionManager;
this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
this.mSlotRequester = new SlotRequester(this.mXmppConnectionService);
this.mUseTor = mXmppConnectionService.useTorToConnect();
}
public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) { @Override
this.message = message; public boolean start() {
this.method = method; return false;
this.mHttpConnectionManager = httpConnectionManager; }
this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
this.mSlotRequester = new SlotRequester(this.mXmppConnectionService);
this.mUseTor = mXmppConnectionService.useTorToConnect();
}
@Override @Override
public boolean start() { public int getStatus() {
return false; return STATUS_UPLOADING;
} }
@Override @Override
public int getStatus() { public long getFileSize() {
return STATUS_UPLOADING; return file == null ? 0 : file.getExpectedSize();
} }
@Override @Override
public long getFileSize() { public int getProgress() {
return file == null ? 0 : file.getExpectedSize(); if (file == null) {
} return 0;
}
return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
}
@Override @Override
public int getProgress() { public void cancel() {
if (file == null) { final Call call = this.mostRecentCall;
return 0; if (call != null && !call.isCanceled()) {
} call.cancel();
return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); }
} }
@Override private void fail(String errorMessage) {
public void cancel() { finish();
this.cancelled = true; 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);
}
private void fail(String errorMessage) { private void finish() {
finish(); mHttpConnectionManager.finishUploadConnection(this);
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage); message.setTransferable(null);
} }
private void finish() { public void init(boolean delay) {
mHttpConnectionManager.finishUploadConnection(this); final Account account = message.getConversation().getAccount();
message.setTransferable(null); this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
} if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
this.mime = "application/pgp-encrypted";
} else {
this.mime = this.file.getMimeType();
}
final long originalFileSize = file.getSize();
this.delayed = delay;
if (Config.ENCRYPT_ON_HTTP_UPLOADED
|| message.getEncryption() == Message.ENCRYPTION_AXOLOTL
|| message.getEncryption() == Message.ENCRYPTION_OTR) {
this.key = new byte[44];
mXmppConnectionService.getRNG().nextBytes(this.key);
this.file.setKeyAndIv(this.key);
}
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
message.resetFileParams();
this.mSlotRequester.request(method, account, file, mime, new SlotRequester.OnSlotRequested() {
@Override
public void success(final SlotRequester.Slot slot) {
//TODO needs to mark the message as cancelled afterwards (ie call fail())
HttpUploadConnection.this.slot = slot;
HttpUploadConnection.this.upload();
}
public void init(boolean delay) { @Override
final Account account = message.getConversation().getAccount(); public void failure(String message) {
this.file = mXmppConnectionService.getFileBackend().getFile(message, false); fail(message);
if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { }
this.mime = "application/pgp-encrypted"; });
} else { message.setTransferable(this);
this.mime = this.file.getMimeType(); mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
} }
final long originalFileSize = file.getSize();
this.delayed = delay;
if (Config.ENCRYPT_ON_HTTP_UPLOADED
|| message.getEncryption() == Message.ENCRYPTION_AXOLOTL
|| message.getEncryption() == Message.ENCRYPTION_OTR) {
this.key = new byte[44];
mXmppConnectionService.getRNG().nextBytes(this.key);
this.file.setKeyAndIv(this.key);
}
final String md5; private void upload() {
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 (method == Method.P1_S3) { @Override
try { public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
md5 = Checksum.md5(AbstractConnectionManager.upgrade(file, new FileInputStream(file))); final int code = response.code();
} catch (Exception e) { if (code == 200 || code == 201) {
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to calculate md5()", e); Log.d(Config.LOGTAG, "finished uploading file");
fail(e.getMessage()); final String get;
return; if (key != null) {
} get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build());
} else { } else {
md5 = null; get = slot.get.toString();
} }
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
finish();
if (!message.isPrivateMessage()) {
message.setCounterpart(message.getConversation().getJid().asBareJid());
}
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);
}
}
});
}
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); public Message getMessage() {
message.resetFileParams(); return message;
this.mSlotRequester.request(method, account, file, mime, md5, new SlotRequester.OnSlotRequested() { }
@Override
public void success(SlotRequester.Slot slot) {
if (!cancelled) {
HttpUploadConnection.this.slot = slot;
EXECUTOR.execute(HttpUploadConnection.this::upload);
}
}
@Override @Override
public void failure(String message) { public void onProgress(final long progress) {
fail(message); this.transmitted = progress;
} mHttpConnectionManager.updateConversationUi(false);
}); }
message.setTransferable(this);
mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
}
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");
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());
}
}
if (code == 200 || code == 201) {
Log.d(Config.LOGTAG, "finished uploading file");
final URL get;
if (key != null) {
if (method == Method.P1_S3) {
get = new URL(slot.getGetUrl().toString()+"#"+CryptoHelper.bytesToHex(key));
} else {
get = CryptoHelper.toAesGcmUrl(new URL(slot.getGetUrl().toString() + "#" + CryptoHelper.bytesToHex(key)));
}
} else {
get = slot.getGetUrl();
}
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
finish();
if (!message.isPrivateMessage()) {
message.setCounterpart(message.getConversation().getJid().asBareJid());
}
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);
}
} 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;
}
} }

View file

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

View file

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

View file

@ -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,147 +44,114 @@ 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 {
private final XmppConnectionService service; private final XmppConnectionService service;
public SlotRequester(XmppConnectionService service) { public SlotRequester(XmppConnectionService service) {
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); requestHttpUploadLegacy(account, host, file, mime, callback);
} else if (method == Method.HTTP_UPLOAD_LEGACY) { } else {
Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY); final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
requestHttpUploadLegacy(account, host, file, mime, callback); requestHttpUpload(account, host, file, mime, callback);
} else { }
requestP1S3(account, account.getDomain(), file.getName(), md5, callback); }
}
}
private void requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) { private void requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) {
IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime); IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(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_LEGACY); Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY);
if (slotElement != null) { if (slotElement != null) {
try { try {
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); );
return; callback.success(slot);
} return;
} catch (MalformedURLException e) { }
//fall through } catch (IllegalArgumentException e) {
} //fall through
} }
} }
Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet); }
callback.failure(IqParser.extractErrorMessage(packet)); Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet);
}); callback.failure(IqParser.extractErrorMessage(packet));
});
} }
private void requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) { private void requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) {
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");
final Element get = slotElement.findChild("get"); final Element get = slotElement.findChild("get");
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); for (Element child : put.getChildren()) {
slot.headers = new HashMap<>(); if ("header".equals(child.getName())) {
for (Element child : put.getChildren()) { final String name = child.getAttribute("name");
if ("header".equals(child.getName())) { final String value = child.getContent();
final String name = child.getAttribute("name"); if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
final String value = child.getContent(); headers.put(name, value.trim());
if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) { }
slot.headers.put(name, value.trim()); }
} }
} headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
} final Slot slot = new Slot(HttpUrl.get(putUrl), HttpUrl.get(getUrl), headers.build());
slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime); callback.success(slot);
callback.success(slot); return;
return; }
} } catch (IllegalArgumentException e) {
} catch (MalformedURLException e) { //fall through
//fall through }
} }
} }
} Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet);
Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet); callback.failure(IqParser.extractErrorMessage(packet));
callback.failure(IqParser.extractErrorMessage(packet)); });
});
} }
private void requestP1S3(final Account account, Jid host, String filename, String md5, OnSlotRequested callback) { public interface OnSlotRequested {
IqPacket request = service.getIqGenerator().requestP1S3Slot(host, md5); void success(Slot slot);
service.sendIqPacket(account, request, (a, packet) -> { void failure(String message);
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 static class Slot {
public final HttpUrl put;
public final HttpUrl get;
public final Headers headers;
public interface OnSlotRequested { private Slot(HttpUrl put, HttpUrl get, Headers headers) {
this.put = put;
this.get = get;
this.headers = headers;
}
void success(Slot slot); private Slot(HttpUrl put, HttpUrl getUrl, Map<String, String> headers) {
this.put = put;
void failure(String message); this.get = getUrl;
this.headers = Headers.of(headers);
} }
}
public static class Slot {
private final URL putUrl;
private URL getUrl;
private HashMap<String, String> headers;
private Slot(URL putUrl) {
this.putUrl = putUrl;
}
public URL getPutUrl() {
return putUrl;
}
public URL getGetUrl() {
return getUrl;
}
public HashMap<String, String> getHeaders() {
return headers;
}
}
} }

View file

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

View file

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

View file

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

View file

@ -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("\\.");

View file

@ -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())

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)) {

View file

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

View file

@ -29,88 +29,94 @@
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 {
private static final Pattern LTR_RTL = Pattern.compile("(\\u200E[^\\u200F]*\\u200F){3,}"); private static final Pattern LTR_RTL = Pattern.compile("(\\u200E[^\\u200F]*\\u200F){3,}");
private static final String EMPTY_STRING = ""; private static final String EMPTY_STRING = "";
public static String prepareQuote(Message message) { public static String prepareQuote(Message message) {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
final String body; final String body;
if (message.hasMeCommand()) { if (message.hasMeCommand()) {
final String nick; final String nick;
if (message.getStatus() == Message.STATUS_RECEIVED) { if (message.getStatus() == Message.STATUS_RECEIVED) {
if (message.getConversation().getMode() == Conversational.MODE_MULTI) { if (message.getConversation().getMode() == Conversational.MODE_MULTI) {
nick = Strings.nullToEmpty(message.getCounterpart().getResource()); nick = Strings.nullToEmpty(message.getCounterpart().getResource());
} else { } else {
nick = message.getContact().getPublicDisplayName(); nick = message.getContact().getPublicDisplayName();
} }
} else { } else {
nick = UIHelper.getMessageDisplayName(message); nick = UIHelper.getMessageDisplayName(message);
} }
body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length()); body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length());
} else { } else {
body = message.getMergedBody().toString(); body = message.getMergedBody().toString();
} }
for (String line : body.split("\n")) { for (String line : body.split("\n")) {
if (line.length() <= 0) { if (line.length() <= 0) {
continue; continue;
} }
final char c = line.charAt(0); final char c = line.charAt(0);
if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0) if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0)
|| (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) { || (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) {
continue; continue;
} }
if (builder.length() != 0) { if (builder.length() != 0) {
builder.append('\n'); builder.append('\n');
} }
builder.append(line.trim()); builder.append(line.trim());
} }
return builder.toString(); return builder.toString();
} }
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 (final String line : lines) {
for (String line : lines) { if (line.contains("\\s+")) {
if (line.contains("\\s+")) { return false;
return false; }
} }
} final URI uri;
final URL url = new URL(lines[0]); try {
final String ref = url.getRef(); uri = new URI(lines[0]);
final String protocol = url.getProtocol(); } catch (final URISyntaxException e) {
final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches(); return false;
final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:"); }
final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri); if (!URL.WELL_KNOWN_SCHEMES.contains(uri.getScheme())) {
final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol) || P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol); return false;
final boolean validOob = validProtocol && (oob || encrypted) && lines.length == 1; }
return validAesGcm || validOob; final String ref = uri.getFragment();
} catch (MalformedURLException e) { final String protocol = uri.getScheme();
return false; 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) {
return LTR_RTL.matcher(body).replaceFirst(EMPTY_STRING); return LTR_RTL.matcher(body).replaceFirst(EMPTY_STRING);
} }
public static boolean unInitiatedButKnownSize(Message message) { public static boolean unInitiatedButKnownSize(Message message) {
return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size > 0 && message.getFileParams().url != null; return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size > 0 && message.getFileParams().url != null;
} }
} }

View file

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

View file

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