Detailed changes
@@ -7,7 +7,6 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
@@ -16,14 +15,11 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.pep.Avatar;
import im.conversations.android.xmpp.model.stanza.Iq;
-import im.conversations.android.xmpp.model.upload.Request;
-import java.nio.ByteBuffer;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
-import java.util.UUID;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyRecord;
@@ -262,35 +258,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public Iq requestHttpUploadSlot(
- final Jid host, final DownloadableFile file, final String mime) {
- final Iq packet = new Iq(Iq.Type.GET);
- packet.setTo(host);
- final var request = packet.addExtension(new Request());
- request.setFilename(convertFilename(file.getName()));
- request.setSize(file.getExpectedSize());
- return packet;
- }
-
- private static String convertFilename(String name) {
- int pos = name.indexOf('.');
- if (pos != -1) {
- try {
- UUID uuid = UUID.fromString(name.substring(0, pos));
- ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
- bb.putLong(uuid.getMostSignificantBits());
- bb.putLong(uuid.getLeastSignificantBits());
- return Base64.encodeToString(
- bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP)
- + name.substring(pos);
- } catch (Exception e) {
- return name;
- }
- } else {
- return name;
- }
- }
-
public static Iq generateCreateAccountWithCaptcha(
final Account account, final String id, final Data data) {
final Iq register = new Iq(Iq.Type.SET);
@@ -17,6 +17,7 @@ import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@@ -39,12 +40,12 @@ public class HttpUploadConnection
private boolean delayed = false;
private DownloadableFile file;
private final Message message;
- private SlotRequester.Slot slot;
+ private HttpUploadManager.Slot slot;
private byte[] key = null;
private long transmitted = 0;
private Call mostRecentCall;
- private ListenableFuture<SlotRequester.Slot> slotFuture;
+ private ListenableFuture<HttpUploadManager.Slot> slotFuture;
public HttpUploadConnection(
final Message message, final HttpConnectionManager httpConnectionManager) {
@@ -78,7 +79,7 @@ public class HttpUploadConnection
@Override
public void cancel() {
- final ListenableFuture<SlotRequester.Slot> slotFuture = this.slotFuture;
+ final ListenableFuture<HttpUploadManager.Slot> slotFuture = this.slotFuture;
if (slotFuture != null && !slotFuture.isDone()) {
if (slotFuture.cancel(true)) {
Log.d(Config.LOGTAG, "cancelled slot requester");
@@ -94,7 +95,7 @@ public class HttpUploadConnection
private void fail(String errorMessage) {
finish();
final Call call = this.mostRecentCall;
- final Future<SlotRequester.Slot> slotFuture = this.slotFuture;
+ final Future<HttpUploadManager.Slot> slotFuture = this.slotFuture;
final boolean cancelled =
(call != null && call.isCanceled())
|| (slotFuture != null && slotFuture.isCancelled());
@@ -109,8 +110,9 @@ public class HttpUploadConnection
message.setTransferable(null);
}
- public void init(boolean delay) {
+ public void init(final boolean delay) {
final Account account = message.getConversation().getAccount();
+ final var connection = account.getXmppConnection();
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
final String mime;
if (message.getEncryption() == Message.ENCRYPTION_PGP
@@ -129,12 +131,12 @@ public class HttpUploadConnection
}
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
message.resetFileParams();
- this.slotFuture = new SlotRequester(mXmppConnectionService).request(account, file, mime);
+ this.slotFuture = connection.getManager(HttpUploadManager.class).request(file, mime);
Futures.addCallback(
this.slotFuture,
new FutureCallback<>() {
@Override
- public void onSuccess(@Nullable SlotRequester.Slot result) {
+ public void onSuccess(@Nullable HttpUploadManager.Slot result) {
HttpUploadConnection.this.slot = result;
try {
HttpUploadConnection.this.upload();
@@ -1,126 +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 android.util.Log;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.MoreExecutors;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.Jid;
-import im.conversations.android.xmpp.model.stanza.Iq;
-import im.conversations.android.xmpp.model.upload.Header;
-import im.conversations.android.xmpp.model.upload.Slot;
-import java.util.Map;
-import okhttp3.Headers;
-import okhttp3.HttpUrl;
-
-public class SlotRequester {
-
- private final XmppConnectionService service;
-
- public SlotRequester(XmppConnectionService service) {
- this.service = service;
- }
-
- public ListenableFuture<Slot> request(
- final Account account, final DownloadableFile file, final String mime) {
- final var result =
- account.getXmppConnection()
- .getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
- if (result == null) {
- return Futures.immediateFailedFuture(
- new IllegalStateException("No HTTP upload host found"));
- }
- return requestHttpUpload(account, result.getKey(), file, mime);
- }
-
- private ListenableFuture<Slot> requestHttpUpload(
- final Account account, final Jid host, final DownloadableFile file, final String mime) {
- final Iq request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
- final var iqFuture = service.sendIqPacket(account, request);
- return Futures.transform(
- iqFuture,
- response -> {
- final var slot =
- response.getExtension(
- im.conversations.android.xmpp.model.upload.Slot.class);
- if (slot == null) {
- Log.d(Config.LOGTAG, "-->" + response);
- throw new IllegalStateException("Slot not found in IQ response");
- }
- final var getUrl = slot.getGetUrl();
- final var put = slot.getPut();
- if (getUrl == null || put == null) {
- throw new IllegalStateException("Missing get or put in slot response");
- }
- final var putUrl = put.getUrl();
- if (putUrl == null) {
- throw new IllegalStateException("Missing put url");
- }
- final var headers = new ImmutableMap.Builder<String, String>();
- for (final Header header : put.getHeaders()) {
- final String name = header.getHeaderName();
- final String value = header.getContent();
- if (Strings.isNullOrEmpty(value) || value.contains("\n")) {
- continue;
- }
- headers.put(name, value.trim());
- }
- headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
- return new Slot(putUrl, getUrl, headers.buildKeepingLast());
- },
- MoreExecutors.directExecutor());
- }
-
- public static class Slot {
- public final HttpUrl put;
- public final HttpUrl get;
- public final Headers headers;
-
- private Slot(HttpUrl put, HttpUrl get, Headers headers) {
- this.put = put;
- this.get = get;
- this.headers = headers;
- }
-
- private Slot(HttpUrl put, HttpUrl getUrl, Map<String, String> headers) {
- this.put = put;
- this.get = getUrl;
- this.headers = Headers.of(headers);
- }
- }
-}
@@ -11,6 +11,7 @@ import eu.siacs.conversations.xmpp.manager.BookmarkManager;
import eu.siacs.conversations.xmpp.manager.CarbonsManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
import eu.siacs.conversations.xmpp.manager.EntityTimeManager;
+import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager;
import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager;
import eu.siacs.conversations.xmpp.manager.NickManager;
@@ -39,6 +40,7 @@ public class Managers {
.put(CarbonsManager.class, new CarbonsManager(context, connection))
.put(DiscoManager.class, new DiscoManager(context, connection))
.put(EntityTimeManager.class, new EntityTimeManager(context, connection))
+ .put(HttpUploadManager.class, new HttpUploadManager(context, connection))
.put(LegacyBookmarkManager.class, new LegacyBookmarkManager(context, connection))
.put(
MessageDisplayedSynchronizationManager.class,
@@ -2730,29 +2730,6 @@ public class XmppConnection implements Runnable {
return this.managers.getInstance(clazz);
}
- private List<Entry<Jid, InfoQuery>> findDiscoItemsByFeature(final String feature) {
- final List<Entry<Jid, InfoQuery>> items = new ArrayList<>();
- for (final Entry<Jid, InfoQuery> cursor :
- getManager(DiscoManager.class).getServerItems().entrySet()) {
- if (cursor.getValue().getFeatureStrings().contains(feature)) {
- items.add(cursor);
- }
- }
- return items;
- }
-
- public Entry<Jid, InfoQuery> getServiceDiscoveryResultByFeature(final String feature) {
- return Iterables.getFirst(findDiscoItemsByFeature(feature), null);
- }
-
- public Jid findDiscoItemByFeature(final String feature) {
- final var items = findDiscoItemsByFeature(feature);
- if (items.isEmpty()) {
- return null;
- }
- return Iterables.getFirst(items, null).getKey();
- }
-
public List<String> getMucServersWithholdAccount() {
final List<String> servers = getMucServers();
servers.remove(account.getDomain().toString());
@@ -3207,7 +3184,8 @@ public class XmppConnection implements Runnable {
if (Config.DISABLE_HTTP_UPLOAD) {
return false;
}
- final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
+ final var result =
+ getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
if (result == null) {
return false;
}
@@ -3238,7 +3216,8 @@ public class XmppConnection implements Runnable {
}
public long getMaxHttpUploadSize() {
- final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
+ final var result =
+ getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
if (result == null) {
return -1;
}
@@ -29,6 +29,7 @@ import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.DirectConnectionUtils;
import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
import im.conversations.android.xmpp.model.stanza.Iq;
import java.io.IOException;
import java.io.InputStream;
@@ -306,14 +307,18 @@ public class SocksByteStreamsTransport implements Transport {
return Futures.immediateFailedFuture(
new IllegalStateException("Proxy look up is disabled"));
}
- final Jid streamer = xmppConnection.findDiscoItemByFeature(Namespace.BYTE_STREAMS);
+ final var streamer =
+ xmppConnection
+ .getManager(DiscoManager.class)
+ .findDiscoItemByFeature(Namespace.BYTE_STREAMS);
if (streamer == null) {
return Futures.immediateFailedFuture(
new IllegalStateException("No proxy/streamer found"));
}
final Iq iqRequest = new Iq(Iq.Type.GET);
- iqRequest.setTo(streamer);
+ iqRequest.setTo(streamer.getKey());
// TODO urgent refactor to extension
+ // TODO and maybe move to Manager
iqRequest.query(Namespace.BYTE_STREAMS);
final SettableFuture<Candidate> candidateFuture = SettableFuture.create();
xmppConnection.sendIqPacket(
@@ -342,7 +347,7 @@ public class SocksByteStreamsTransport implements Transport {
new Candidate(
UUID.randomUUID().toString(),
host,
- streamer,
+ streamer.getKey(),
port,
655360 + (initiator ? 0 : 15),
CandidateType.PROXY));
@@ -7,6 +7,8 @@ import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
import com.google.common.io.BaseEncoding;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -467,6 +469,15 @@ public class DiscoManager extends AbstractManager {
}
}
+ public Map<Jid, InfoQuery> findDiscoItemsByFeature(final String feature) {
+ return Maps.filterValues(getServerItems(), v -> v.hasFeature(feature));
+ }
+
+ public Map.Entry<Jid, InfoQuery> findDiscoItemByFeature(final String feature) {
+ final var items = findDiscoItemsByFeature(feature);
+ return Iterables.getFirst(items.entrySet(), null);
+ }
+
public static final class CapsHashMismatchException extends IllegalStateException {
public CapsHashMismatchException(final String message) {
super(message);
@@ -0,0 +1,108 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import android.util.Base64;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.upload.Request;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.UUID;
+import okhttp3.Headers;
+import okhttp3.HttpUrl;
+
+public class HttpUploadManager extends AbstractManager {
+
+ public HttpUploadManager(final Context context, final XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public ListenableFuture<Slot> request(final DownloadableFile file, final String mime) {
+ final var result =
+ getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
+ if (result == null) {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException("No HTTP upload host found"));
+ }
+ return requestHttpUpload(result.getKey(), file, mime);
+ }
+
+ private ListenableFuture<Slot> requestHttpUpload(
+ final Jid host, final DownloadableFile file, final String mime) {
+ final Iq iq = new Iq(Iq.Type.GET);
+ iq.setTo(host);
+ final var request = iq.addExtension(new Request());
+ request.setFilename(convertFilename(file.getName()));
+ request.setSize(file.getExpectedSize());
+ request.setContentType(mime);
+ final var iqFuture = this.connection.sendIqPacket(iq);
+ return Futures.transform(
+ iqFuture,
+ response -> {
+ final var slot =
+ response.getExtension(
+ im.conversations.android.xmpp.model.upload.Slot.class);
+ if (slot == null) {
+ throw new IllegalStateException("Slot not found in IQ response");
+ }
+ final var getUrl = slot.getGetUrl();
+ final var put = slot.getPut();
+ if (getUrl == null || put == null) {
+ throw new IllegalStateException("Missing get or put in slot response");
+ }
+ final var putUrl = put.getUrl();
+ if (putUrl == null) {
+ throw new IllegalStateException("Missing put url");
+ }
+ final var contentType = mime == null ? "application/octet-stream" : mime;
+ final var headers =
+ new ImmutableMap.Builder<String, String>()
+ .putAll(put.getHeadersAllowList())
+ .put("Content-Type", contentType)
+ .buildKeepingLast();
+ return new Slot(putUrl, getUrl, headers);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private static String convertFilename(final String name) {
+ int pos = name.indexOf('.');
+ if (pos < 0) {
+ return name;
+ }
+ try {
+ UUID uuid = UUID.fromString(name.substring(0, pos));
+ ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
+ bb.putLong(uuid.getMostSignificantBits());
+ bb.putLong(uuid.getLeastSignificantBits());
+ return Base64.encodeToString(
+ bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP)
+ + name.substring(pos);
+ } catch (final Exception e) {
+ return name;
+ }
+ }
+
+ public static class Slot {
+ public final HttpUrl put;
+ public final HttpUrl get;
+ public final Headers headers;
+
+ private Slot(final HttpUrl put, final HttpUrl get, final Headers headers) {
+ this.put = put;
+ this.get = get;
+ this.headers = headers;
+ }
+
+ private Slot(final HttpUrl put, final HttpUrl get, final Map<String, String> headers) {
+ this(put, get, Headers.of(headers));
+ }
+ }
+}
@@ -37,11 +37,13 @@ public class VCardManager extends AbstractManager {
vCard -> {
final var photo = vCard.getPhoto();
if (photo == null) {
- throw new IllegalStateException("No photo in vCard");
+ throw new IllegalStateException(
+ String.format("No photo in vCard of %s", address));
}
final var binaryValue = photo.getBinaryValue();
if (binaryValue == null) {
- throw new IllegalStateException("Photo has no binary value");
+ throw new IllegalStateException(
+ String.format("Photo has no binary value in vCard of %s", address));
}
return binaryValue.asBytes();
},
@@ -1,14 +1,21 @@
package im.conversations.android.xmpp.model.upload;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
+import java.util.Map;
import okhttp3.HttpUrl;
@XmlElement
public class Put extends Extension {
+ private static final List<String> HEADER_ALLOW_LIST =
+ Arrays.asList("Authorization", "Cookie", "Expires");
+
public Put() {
super(Put.class);
}
@@ -24,4 +31,19 @@ public class Put extends Extension {
public Collection<Header> getHeaders() {
return this.getExtensions(Header.class);
}
+
+ public Map<String, String> getHeadersAllowList() {
+ final var headers = new ImmutableMap.Builder<String, String>();
+ for (final Header header : this.getHeaders()) {
+ final String name = header.getHeaderName();
+ final String value = Strings.nullToEmpty(header.getContent()).trim();
+ if (Strings.isNullOrEmpty(value) || value.contains("\n")) {
+ continue;
+ }
+ if (HEADER_ALLOW_LIST.contains(name)) {
+ headers.put(name, value);
+ }
+ }
+ return headers.buildKeepingLast();
+ }
}