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

View file

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

View file

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

View file

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

View file

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

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) {
String otherBody;
if (message.hasFileOnRemoteHost()) {
otherBody = message.getFileParams().url.toString();
otherBody = message.getFileParams().url;
} else {
otherBody = message.body;
}

View file

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

View file

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

View file

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

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

View file

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

View file

@ -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);
@ -230,21 +178,17 @@ public class HttpUploadConnection implements Transferable {
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);
}
}

View file

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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