Detailed changes
@@ -41,7 +41,7 @@ public final class Config {
public static final String LOGTAG = BuildConfig.APP_NAME.toLowerCase(Locale.US);
- public static final boolean QUICK_LOG = false;
+ public static final boolean QUICK_LOG = true;
public static final Jid BUG_REPORTS = Jid.of("bugs@conversations.im");
public static final Uri HELP = Uri.parse("https://help.conversations.im");
@@ -117,7 +117,7 @@ public final class Config {
public static final boolean OMEMO_PADDING = false;
public static final boolean PUT_AUTH_TAG_INTO_KEY = true;
public static final boolean AUTOMATICALLY_COMPLETE_SESSIONS = true;
- public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
+ public static final boolean DISABLE_PROXY_LOOKUP = false; //disables STUN/TURN and Proxy65 look up (useful to debug IBB fallback)
public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
public static final boolean DISABLE_HTTP_UPLOAD = false;
public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
@@ -62,11 +62,13 @@ import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jingle.DescriptionTransport;
import eu.siacs.conversations.xmpp.jingle.OmemoVerification;
import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap;
import eu.siacs.conversations.xmpp.jingle.RtpContentMap;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import eu.siacs.conversations.xmpp.pep.PublishOptions;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
@@ -1262,12 +1264,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (Config.REQUIRE_RTP_VERIFICATION) {
requireVerification(session);
}
- final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
+ final ImmutableMap.Builder<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> descriptionTransportBuilder = new ImmutableMap.Builder<>();
final OmemoVerification omemoVerification = new OmemoVerification();
omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
omemoVerification.setSessionFingerprint(session.getFingerprint());
- for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : rtpContentMap.contents.entrySet()) {
- final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
+ for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : rtpContentMap.contents.entrySet()) {
+ final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport = content.getValue();
final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo;
try {
encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
@@ -1276,7 +1278,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
descriptionTransportBuilder.put(
content.getKey(),
- new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
+ new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
);
}
return Futures.immediateFuture(
@@ -1296,11 +1298,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public ListenableFuture<OmemoVerifiedPayload<RtpContentMap>> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) {
- final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
+ final ImmutableMap.Builder<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> descriptionTransportBuilder = new ImmutableMap.Builder<>();
final OmemoVerification omemoVerification = new OmemoVerification();
final ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures = new ImmutableList.Builder<>();
- for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
- final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
+ for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
+ final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport = content.getValue();
final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport;
try {
decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures);
@@ -1310,7 +1312,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
omemoVerification.setOrEnsureEqual(decryptedTransport);
descriptionTransportBuilder.put(
content.getKey(),
- new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
+ new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
);
}
processPostponed();
@@ -1376,18 +1378,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
));
}
- public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
- executor.execute(new Runnable() {
- @Override
- public void run() {
- final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
- if (buildHeader(axolotlMessage, conversation)) {
- onMessageCreatedCallback.run(axolotlMessage);
- } else {
- onMessageCreatedCallback.run(null);
- }
+ public ListenableFuture<XmppAxolotlMessage> prepareKeyTransportMessage(final Conversation conversation) {
+ return Futures.submit(()->{
+ final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
+ if (buildHeader(axolotlMessage, conversation)) {
+ return axolotlMessage;
+ } else {
+ throw new IllegalStateException("No session to decrypt to");
}
- });
+ },executor);
}
public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
@@ -27,11 +27,7 @@ public abstract class AbstractGenerator {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
private final String[] FEATURES = {
Namespace.JINGLE,
-
- //Jingle File Transfer
- FileTransferDescription.Version.FT_3.getNamespace(),
- FileTransferDescription.Version.FT_4.getNamespace(),
- FileTransferDescription.Version.FT_5.getNamespace(),
+ Namespace.JINGLE_APPS_FILE_TRANSFER,
Namespace.JINGLE_TRANSPORTS_S5B,
Namespace.JINGLE_TRANSPORTS_IBB,
Namespace.JINGLE_ENCRYPTED_TRANSPORT,
@@ -124,6 +120,7 @@ public abstract class AbstractGenerator {
if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) {
features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
features.addAll(Arrays.asList(VOIP_NAMESPACES));
+ features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL);
}
if (mXmppConnectionService.broadcastLastActivity()) {
features.add(Namespace.IDLE);
@@ -403,7 +403,12 @@ public class UnifiedPushBroker {
updateIntent.putExtra("token", target.instance);
updateIntent.putExtra("bytesMessage", payload);
updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
- // TODO add distributor verification?
+ final var distributorVerificationIntent = new Intent();
+ distributorVerificationIntent.setPackage(service.getPackageName());
+ final var pendingIntent =
+ PendingIntent.getBroadcast(
+ service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE);
+ updateIntent.putExtra("distributor", pendingIntent);
service.sendBroadcast(updateIntent);
}
@@ -1,60 +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.utils;
-
-import android.util.Base64;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-public class Checksum {
-
- public static String md5(InputStream inputStream) throws IOException {
- byte[] buffer = new byte[4096];
- MessageDigest messageDigest;
- try {
- messageDigest = MessageDigest.getInstance("MD5");
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(e);
- }
-
- int count;
- do {
- count = inputStream.read(buffer);
- if (count > 0) {
- messageDigest.update(buffer, 0, count);
- }
- } while (count != -1);
- inputStream.close();
- return Base64.encodeToString(messageDigest.digest(), Base64.NO_WRAP);
- }
-}
@@ -47,7 +47,11 @@ public final class Namespace {
public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1";
public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1";
public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1";
+ public static final String JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL = "urn:xmpp:jingle:transports:webrtc-datachannel:1";
+ public static final String JINGLE_TRANSPORT = "urn:xmpp:jingle:transports:dtls-sctp:1";
public static final String JINGLE_APPS_RTP = "urn:xmpp:jingle:apps:rtp:1";
+
+ public static final String JINGLE_APPS_FILE_TRANSFER = "urn:xmpp:jingle:apps:file-transfer:5";
public static final String JINGLE_APPS_DTLS = "urn:xmpp:jingle:apps:dtls:0";
public static final String JINGLE_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0";
public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio";
@@ -71,4 +75,5 @@ public final class Namespace {
public static final String REPORTING = "urn:xmpp:reporting:1";
public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam";
public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264";
+ public static final String HASHES = "urn:xmpp:hashes:2";
}
@@ -0,0 +1,82 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public abstract class AbstractContentMap<
+ D extends GenericDescription, T extends GenericTransportInfo> {
+
+ public final Group group;
+
+ public final Map<String, DescriptionTransport<D, T>> contents;
+
+ protected AbstractContentMap(
+ final Group group, final Map<String, DescriptionTransport<D, T>> contents) {
+ this.group = group;
+ this.contents = contents;
+ }
+
+ public static class UnsupportedApplicationException extends IllegalArgumentException {
+ UnsupportedApplicationException(String message) {
+ super(message);
+ }
+ }
+
+ public static class UnsupportedTransportException extends IllegalArgumentException {
+ UnsupportedTransportException(String message) {
+ super(message);
+ }
+ }
+
+ public Set<Content.Senders> getSenders() {
+ return ImmutableSet.copyOf(Collections2.transform(contents.values(), dt -> dt.senders));
+ }
+
+ public List<String> getNames() {
+ return ImmutableList.copyOf(contents.keySet());
+ }
+
+ JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
+ final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
+ for (final Map.Entry<String, DescriptionTransport<D, T>> entry : this.contents.entrySet()) {
+ final DescriptionTransport<D, T> descriptionTransport = entry.getValue();
+ final Content content =
+ new Content(
+ Content.Creator.INITIATOR,
+ descriptionTransport.senders,
+ entry.getKey());
+ if (descriptionTransport.description != null) {
+ content.addChild(descriptionTransport.description);
+ }
+ content.addChild(descriptionTransport.transport);
+ jinglePacket.addJingleContent(content);
+ }
+ if (this.group != null) {
+ jinglePacket.addGroup(this.group);
+ }
+ return jinglePacket;
+ }
+
+ void requireContentDescriptions() {
+ if (this.contents.size() == 0) {
+ throw new IllegalStateException("No contents available");
+ }
+ for (final Map.Entry<String, DescriptionTransport<D, T>> entry : this.contents.entrySet()) {
+ if (entry.getValue().description == null) {
+ throw new IllegalStateException(
+ String.format("%s is lacking content description", entry.getKey()));
+ }
+ }
+ }
+}
@@ -1,47 +1,352 @@
package eu.siacs.conversations.xmpp.jingle;
+import android.util.Log;
+
import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
public abstract class AbstractJingleConnection {
public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-";
public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-";
+ protected static final List<State> TERMINATED =
+ Arrays.asList(
+ State.ACCEPTED,
+ State.REJECTED,
+ State.REJECTED_RACED,
+ State.RETRACTED,
+ State.RETRACTED_RACED,
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_DECLINED_OR_BUSY,
+ State.TERMINATED_CONNECTIVITY_ERROR,
+ State.TERMINATED_CANCEL_OR_TIMEOUT,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR);
+
+ private static final Map<State, Collection<State>> VALID_TRANSITIONS;
+
+ static {
+ final ImmutableMap.Builder<State, Collection<State>> transitionBuilder =
+ new ImmutableMap.Builder<>();
+ transitionBuilder.put(
+ State.NULL,
+ ImmutableList.of(
+ State.PROPOSED,
+ State.SESSION_INITIALIZED,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR));
+ transitionBuilder.put(
+ State.PROPOSED,
+ ImmutableList.of(
+ State.ACCEPTED,
+ State.PROCEED,
+ State.REJECTED,
+ State.RETRACTED,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR,
+ State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection
+ // rebinds
+ ));
+ transitionBuilder.put(
+ State.PROCEED,
+ ImmutableList.of(
+ State.REJECTED_RACED,
+ State.RETRACTED_RACED,
+ State.SESSION_INITIALIZED_PRE_APPROVED,
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR,
+ State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error
+ // bounces of the proceed message
+ ));
+ transitionBuilder.put(
+ State.SESSION_INITIALIZED,
+ ImmutableList.of(
+ State.SESSION_ACCEPTED,
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_DECLINED_OR_BUSY,
+ State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
+ // and IQ timeouts
+ State.TERMINATED_CANCEL_OR_TIMEOUT,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR));
+ transitionBuilder.put(
+ State.SESSION_INITIALIZED_PRE_APPROVED,
+ ImmutableList.of(
+ State.SESSION_ACCEPTED,
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_DECLINED_OR_BUSY,
+ State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
+ // and IQ timeouts
+ State.TERMINATED_CANCEL_OR_TIMEOUT,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR));
+ transitionBuilder.put(
+ State.SESSION_ACCEPTED,
+ ImmutableList.of(
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_DECLINED_OR_BUSY,
+ State.TERMINATED_CONNECTIVITY_ERROR,
+ State.TERMINATED_CANCEL_OR_TIMEOUT,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR));
+ VALID_TRANSITIONS = transitionBuilder.build();
+ }
+
final JingleConnectionManager jingleConnectionManager;
protected final XmppConnectionService xmppConnectionService;
protected final Id id;
private final Jid initiator;
- AbstractJingleConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) {
+ protected State state = State.NULL;
+
+ AbstractJingleConnection(
+ final JingleConnectionManager jingleConnectionManager,
+ final Id id,
+ final Jid initiator) {
this.jingleConnectionManager = jingleConnectionManager;
this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService();
this.id = id;
this.initiator = initiator;
}
+ public Id getId() {
+ return id;
+ }
+
boolean isInitiator() {
return initiator.equals(id.account.getJid());
}
+ boolean isResponder() {
+ return !initiator.equals(id.account.getJid());
+ }
+
+ public State getState() {
+ return this.state;
+ }
+
+ protected synchronized boolean isInState(State... state) {
+ return Arrays.asList(state).contains(this.state);
+ }
+
+ protected boolean transition(final State target) {
+ return transition(target, null);
+ }
+
+ protected synchronized boolean transition(final State target, final Runnable runnable) {
+ final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
+ if (validTransitions != null && validTransitions.contains(target)) {
+ this.state = target;
+ if (runnable != null) {
+ runnable.run();
+ }
+ Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected void transitionOrThrow(final State target) {
+ if (!transition(target)) {
+ throw new IllegalStateException(
+ String.format("Unable to transition from %s to %s", this.state, target));
+ }
+ }
+
+ boolean isTerminated() {
+ return TERMINATED.contains(this.state);
+ }
+
abstract void deliverPacket(JinglePacket jinglePacket);
- public Id getId() {
- return id;
+ protected void receiveOutOfOrderAction(
+ final JinglePacket jinglePacket, final JinglePacket.Action action) {
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "%s: received %s even though we are in state %s",
+ id.account.getJid().asBareJid(), action, getState()));
+ if (isTerminated()) {
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "%s: got a reason to terminate with out-of-order. but already in state %s",
+ id.account.getJid().asBareJid(), getState()));
+ respondWithOutOfOrder(jinglePacket);
+ } else {
+ terminateWithOutOfOrder(jinglePacket);
+ }
+ }
+
+ protected void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": terminating session with out-of-order");
+ terminateTransport();
+ transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
+ respondWithOutOfOrder(jinglePacket);
+ this.finish();
}
+ protected void finish() {
+ if (isTerminated()) {
+ this.jingleConnectionManager.finishConnectionOrThrow(this);
+ } else {
+ throw new AssertionError(
+ String.format("Unable to call finish from %s", this.state));
+ }
+ }
+
+ protected abstract void terminateTransport();
+
abstract void notifyRebound();
+ protected void sendSessionTerminate(
+ final Reason reason, final String text, final Consumer<State> trigger) {
+ final State previous = this.state;
+ final State target = reasonToState(reason);
+ transitionOrThrow(target);
+ if (previous != State.NULL && trigger != null) {
+ trigger.accept(target);
+ }
+ final JinglePacket jinglePacket =
+ new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
+ jinglePacket.setReason(reason, text);
+ send(jinglePacket);
+ finish();
+ }
+
+ protected void send(final JinglePacket jinglePacket) {
+ jinglePacket.setTo(id.with);
+ xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
+ }
+
+ protected void respondOk(final JinglePacket jinglePacket) {
+ xmppConnectionService.sendIqPacket(
+ id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
+ }
+
+ protected void respondWithTieBreak(final JinglePacket jinglePacket) {
+ respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
+ }
+
+ protected void respondWithOutOfOrder(final JinglePacket jinglePacket) {
+ respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
+ }
+
+ protected void respondWithItemNotFound(final JinglePacket jinglePacket) {
+ respondWithJingleError(jinglePacket, null, "item-not-found", "cancel");
+ }
+
+ private void respondWithJingleError(
+ final IqPacket original,
+ String jingleCondition,
+ String condition,
+ String conditionType) {
+ jingleConnectionManager.respondWithJingleError(
+ id.account, original, jingleCondition, condition, conditionType);
+ }
+
+ private synchronized void handleIqResponse(final Account account, final IqPacket response) {
+ if (response.getType() == IqPacket.TYPE.ERROR) {
+ handleIqErrorResponse(response);
+ return;
+ }
+ if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+ handleIqTimeoutResponse(response);
+ }
+ }
+
+ protected void handleIqErrorResponse(final IqPacket response) {
+ Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
+ final String errorCondition = response.getErrorCondition();
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": received IQ-error from "
+ + response.getFrom()
+ + " in RTP session. "
+ + errorCondition);
+ if (isTerminated()) {
+ Log.i(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": ignoring error because session was already terminated");
+ return;
+ }
+ this.terminateTransport();
+ final State target;
+ if (Arrays.asList(
+ "service-unavailable",
+ "recipient-unavailable",
+ "remote-server-not-found",
+ "remote-server-timeout")
+ .contains(errorCondition)) {
+ target = State.TERMINATED_CONNECTIVITY_ERROR;
+ } else {
+ target = State.TERMINATED_APPLICATION_FAILURE;
+ }
+ transitionOrThrow(target);
+ this.finish();
+ }
+
+ protected void handleIqTimeoutResponse(final IqPacket response) {
+ Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT);
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": received IQ timeout in RTP session with "
+ + id.with
+ + ". terminating with connectivity error");
+ if (isTerminated()) {
+ Log.i(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": ignoring error because session was already terminated");
+ return;
+ }
+ this.terminateTransport();
+ transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
+ this.finish();
+ }
+
+ protected boolean remoteHasFeature(final String feature) {
+ final Contact contact = id.getContact();
+ final Presence presence =
+ contact.getPresences().get(Strings.nullToEmpty(id.with.getResource()));
+ final ServiceDiscoveryResult serviceDiscoveryResult =
+ presence == null ? null : presence.getServiceDiscoveryResult();
+ final List<String> features =
+ serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
+ return features != null && features.contains(feature);
+ }
public static class Id implements OngoingRtpSession {
public final Account account;
@@ -73,8 +378,7 @@ public abstract class AbstractJingleConnection {
return new Id(
message.getConversation().getAccount(),
message.getCounterpart(),
- JingleConnectionManager.nextRandomId()
- );
+ JingleConnectionManager.nextRandomId());
}
public Contact getContact() {
@@ -86,9 +390,9 @@ public abstract class AbstractJingleConnection {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Id id = (Id) o;
- return Objects.equal(account.getUuid(), id.account.getUuid()) &&
- Objects.equal(with, id.with) &&
- Objects.equal(sessionId, id.sessionId);
+ return Objects.equal(account.getUuid(), id.account.getUuid())
+ && Objects.equal(with, id.with)
+ && Objects.equal(sessionId, id.sessionId);
}
@Override
@@ -122,23 +426,36 @@ public abstract class AbstractJingleConnection {
}
}
+ protected static State reasonToState(Reason reason) {
+ return switch (reason) {
+ case SUCCESS -> State.TERMINATED_SUCCESS;
+ case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
+ case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
+ case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
+ case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State
+ .TERMINATED_APPLICATION_FAILURE;
+ default -> State.TERMINATED_CONNECTIVITY_ERROR;
+ };
+ }
public enum State {
- NULL, //default value; nothing has been sent or received yet
+ NULL, // default value; nothing has been sent or received yet
PROPOSED,
ACCEPTED,
PROCEED,
REJECTED,
- REJECTED_RACED, //used when we want to reject but havenβt received session init yet
+ REJECTED_RACED, // used when we want to reject but havenβt received session init yet
RETRACTED,
- RETRACTED_RACED, //used when receiving a retract after we already asked to proceed
- SESSION_INITIALIZED, //equal to 'PENDING'
+ RETRACTED_RACED, // used when receiving a retract after we already asked to proceed
+ SESSION_INITIALIZED, // equal to 'PENDING'
SESSION_INITIALIZED_PRE_APPROVED,
- SESSION_ACCEPTED, //equal to 'ACTIVE'
- TERMINATED_SUCCESS, //equal to 'ENDED' (after successful call) ui will just close
- TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call)
- TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button)
- TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted
+ SESSION_ACCEPTED, // equal to 'ACTIVE'
+ TERMINATED_SUCCESS, // equal to 'ENDED' (after successful call) ui will just close
+ TERMINATED_DECLINED_OR_BUSY, // equal to 'ENDED' (after other party declined the call)
+ TERMINATED_CONNECTIVITY_ERROR, // equal to 'ENDED' (but after network failures; ui will
+ // display retry button)
+ TERMINATED_CANCEL_OR_TIMEOUT, // more or less the same as retracted; caller pressed end call
+ // before session was accepted
TERMINATED_APPLICATION_FAILURE,
TERMINATED_SECURITY_ERROR
}
@@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
+import androidx.annotation.NonNull;
+
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.Collections2;
@@ -8,6 +10,8 @@ import com.google.common.collect.ImmutableSet;
import java.util.Set;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
public final class ContentAddition {
@@ -32,12 +36,13 @@ public final class ContentAddition {
Collections2.transform(
rtpContentMap.contents.entrySet(),
e -> {
- final RtpContentMap.DescriptionTransport dt = e.getValue();
+ final DescriptionTransport<RtpDescription, IceUdpTransportInfo> dt = e.getValue();
return new Summary(e.getKey(), dt.description.getMedia(), dt.senders);
}));
}
@Override
+ @NonNull
public String toString() {
return MoreObjects.toStringHelper(this)
.add("direction", direction)
@@ -77,6 +82,7 @@ public final class ContentAddition {
}
@Override
+ @NonNull
public String toString() {
return MoreObjects.toStringHelper(this)
.add("name", name)
@@ -0,0 +1,19 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
+
+public class DescriptionTransport<D extends GenericDescription, T extends GenericTransportInfo> {
+
+ public final Content.Senders senders;
+ public final D description;
+ public final T transport;
+
+ public DescriptionTransport(
+ final Content.Senders senders, final D description, final T transport) {
+ this.senders = senders;
+ this.description = description;
+ this.transport = transport;
+ }
+}
@@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
+import com.google.common.collect.ImmutableList;
+
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
@@ -15,13 +17,13 @@ import eu.siacs.conversations.xmpp.Jid;
public class DirectConnectionUtils {
- private static List<InetAddress> getLocalAddresses() {
- final List<InetAddress> addresses = new ArrayList<>();
+ public static List<InetAddress> getLocalAddresses() {
+ final ImmutableList.Builder<InetAddress> inetAddresses = new ImmutableList.Builder<>();
final Enumeration<NetworkInterface> interfaces;
try {
interfaces = NetworkInterface.getNetworkInterfaces();
- } catch (SocketException e) {
- return addresses;
+ } catch (final SocketException e) {
+ return inetAddresses.build();
}
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
@@ -34,31 +36,15 @@ public class DirectConnectionUtils {
if (inetAddress instanceof Inet6Address) {
//let's get rid of scope
try {
- addresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
+ inetAddresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
} catch (UnknownHostException e) {
//ignored
}
} else {
- addresses.add(inetAddress);
+ inetAddresses.add(inetAddress);
}
}
}
- return addresses;
+ return inetAddresses.build();
}
-
- public static List<JingleCandidate> getLocalCandidates(Jid jid) {
- SecureRandom random = new SecureRandom();
- ArrayList<JingleCandidate> candidates = new ArrayList<>();
- for (InetAddress inetAddress : getLocalAddresses()) {
- final JingleCandidate candidate = new JingleCandidate(UUID.randomUUID().toString(), true);
- candidate.setHost(inetAddress.getHostAddress());
- candidate.setPort(random.nextInt(60000) + 1024);
- candidate.setType(JingleCandidate.TYPE_DIRECT);
- candidate.setJid(jid);
- candidate.setPriority(8257536 + candidates.size());
- candidates.add(candidate);
- }
- return candidates;
- }
-
}
@@ -0,0 +1,219 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.transports.Transport;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class FileTransferContentMap
+ extends AbstractContentMap<FileTransferDescription, GenericTransportInfo> {
+
+ private static final List<Class<? extends GenericTransportInfo>> SUPPORTED_TRANSPORTS =
+ Arrays.asList(
+ SocksByteStreamsTransportInfo.class,
+ IbbTransportInfo.class,
+ WebRTCDataChannelTransportInfo.class);
+
+ protected FileTransferContentMap(
+ final Group group, final Map<String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
+ contents) {
+ super(group, contents);
+ }
+
+ public static FileTransferContentMap of(final JinglePacket jinglePacket) {
+ final Map<String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
+ contents = of(jinglePacket.getJingleContents());
+ return new FileTransferContentMap(jinglePacket.getGroup(), contents);
+ }
+
+ public static DescriptionTransport<FileTransferDescription, GenericTransportInfo> of(
+ final Content content) {
+ final GenericDescription description = content.getDescription();
+ final GenericTransportInfo transportInfo = content.getTransport();
+ final Content.Senders senders = content.getSenders();
+ final FileTransferDescription fileTransferDescription;
+ if (description == null) {
+ fileTransferDescription = null;
+ } else if (description instanceof FileTransferDescription ftDescription) {
+ fileTransferDescription = ftDescription;
+ } else {
+ throw new UnsupportedApplicationException(
+ "Content does not contain file transfer description");
+ }
+ if (!SUPPORTED_TRANSPORTS.contains(transportInfo.getClass())) {
+ throw new UnsupportedTransportException("Content does not have supported transport");
+ }
+ return new DescriptionTransport<>(senders, fileTransferDescription, transportInfo);
+ }
+
+ private static Map<String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
+ of(final Map<String, Content> contents) {
+ return ImmutableMap.copyOf(
+ Maps.transformValues(contents, content -> content == null ? null : of(content)));
+ }
+
+ public static FileTransferContentMap of(
+ final FileTransferDescription.File file, final Transport.InitialTransportInfo initialTransportInfo) {
+ // TODO copy groups
+ final var transportInfo = initialTransportInfo.transportInfo;
+ return new FileTransferContentMap(initialTransportInfo.group,
+ Map.of(
+ initialTransportInfo.contentName,
+ new DescriptionTransport<>(
+ Content.Senders.INITIATOR,
+ FileTransferDescription.of(file),
+ transportInfo)));
+ }
+
+ public FileTransferDescription.File requireOnlyFile() {
+ if (this.contents.size() != 1) {
+ throw new IllegalStateException("Only one file at a time is supported");
+ }
+ final var dt = Iterables.getOnlyElement(this.contents.values());
+ return dt.description.getFile();
+ }
+
+ public FileTransferDescription requireOnlyFileTransferDescription() {
+ if (this.contents.size() != 1) {
+ throw new IllegalStateException("Only one file at a time is supported");
+ }
+ final var dt = Iterables.getOnlyElement(this.contents.values());
+ return dt.description;
+ }
+
+ public GenericTransportInfo requireOnlyTransportInfo() {
+ if (this.contents.size() != 1) {
+ throw new IllegalStateException(
+ "We expect exactly one content with one transport info");
+ }
+ final var dt = Iterables.getOnlyElement(this.contents.values());
+ return dt.transport;
+ }
+
+ public FileTransferContentMap withTransport(final Transport.TransportInfo transportWrapper) {
+ final var transportInfo = transportWrapper.transportInfo;
+ return new FileTransferContentMap(transportWrapper.group,
+ ImmutableMap.copyOf(
+ Maps.transformValues(
+ contents,
+ content -> {
+ if (content == null) {
+ return null;
+ }
+ return new DescriptionTransport<>(
+ content.senders, content.description, transportInfo);
+ })));
+ }
+
+ public FileTransferContentMap candidateUsed(final String streamId, final String cid) {
+ return new FileTransferContentMap(null,
+ ImmutableMap.copyOf(
+ Maps.transformValues(
+ contents,
+ content -> {
+ if (content == null) {
+ return null;
+ }
+ final var transportInfo =
+ new SocksByteStreamsTransportInfo(
+ streamId, Collections.emptyList());
+ final Element candidateUsed =
+ transportInfo.addChild(
+ "candidate-used",
+ Namespace.JINGLE_TRANSPORTS_S5B);
+ candidateUsed.setAttribute("cid", cid);
+ return new DescriptionTransport<>(
+ content.senders, null, transportInfo);
+ })));
+ }
+
+ public FileTransferContentMap candidateError(final String streamId) {
+ return new FileTransferContentMap(null,
+ ImmutableMap.copyOf(
+ Maps.transformValues(
+ contents,
+ content -> {
+ if (content == null) {
+ return null;
+ }
+ final var transportInfo =
+ new SocksByteStreamsTransportInfo(
+ streamId, Collections.emptyList());
+ transportInfo.addChild(
+ "candidate-error", Namespace.JINGLE_TRANSPORTS_S5B);
+ return new DescriptionTransport<>(
+ content.senders, null, transportInfo);
+ })));
+ }
+
+ public FileTransferContentMap proxyActivated(final String streamId, final String cid) {
+ return new FileTransferContentMap(null,
+ ImmutableMap.copyOf(
+ Maps.transformValues(
+ contents,
+ content -> {
+ if (content == null) {
+ return null;
+ }
+ final var transportInfo =
+ new SocksByteStreamsTransportInfo(
+ streamId, Collections.emptyList());
+ final Element candidateUsed =
+ transportInfo.addChild(
+ "activated", Namespace.JINGLE_TRANSPORTS_S5B);
+ candidateUsed.setAttribute("cid", cid);
+ return new DescriptionTransport<>(
+ content.senders, null, transportInfo);
+ })));
+ }
+
+ FileTransferContentMap transportInfo() {
+ return new FileTransferContentMap(this.group,
+ Maps.transformValues(
+ contents,
+ dt -> new DescriptionTransport<>(dt.senders, null, dt.transport)));
+ }
+
+ FileTransferContentMap transportInfo(
+ final String contentName, final IceUdpTransportInfo.Candidate candidate) {
+ final DescriptionTransport<FileTransferDescription, GenericTransportInfo> descriptionTransport =
+ contents.get(contentName);
+ if (descriptionTransport == null) {
+ throw new IllegalArgumentException(
+ "Unable to find transport info for content name " + contentName);
+ }
+ final WebRTCDataChannelTransportInfo transportInfo;
+ if (descriptionTransport.transport instanceof WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
+ transportInfo = webRTCDataChannelTransportInfo;
+ } else {
+ throw new IllegalStateException("TransportInfo is not WebRTCDataChannel");
+ }
+ final WebRTCDataChannelTransportInfo newTransportInfo = transportInfo.cloneWrapper();
+ newTransportInfo.addCandidate(candidate);
+ return new FileTransferContentMap(
+ null,
+ ImmutableMap.of(
+ contentName,
+ new DescriptionTransport<>(
+ descriptionTransport.senders, null, newTransportInfo)));
+ }
+}
@@ -0,0 +1,98 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import android.util.Log;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.IP;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import org.webrtc.PeerConnection;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public final class IceServers {
+
+ public static List<PeerConnection.IceServer> parse(final IqPacket response) {
+ ImmutableList.Builder<PeerConnection.IceServer> listBuilder = new ImmutableList.Builder<>();
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ final Element services =
+ response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
+ final List<Element> children =
+ services == null ? Collections.emptyList() : services.getChildren();
+ for (final Element child : children) {
+ if ("service".equals(child.getName())) {
+ final String type = child.getAttribute("type");
+ final String host = child.getAttribute("host");
+ final String sport = child.getAttribute("port");
+ final Integer port = sport == null ? null : Ints.tryParse(sport);
+ final String transport = child.getAttribute("transport");
+ final String username = child.getAttribute("username");
+ final String password = child.getAttribute("password");
+ if (Strings.isNullOrEmpty(host) || port == null) {
+ continue;
+ }
+ if (port < 0 || port > 65535) {
+ continue;
+ }
+
+ if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type)
+ && Arrays.asList("udp", "tcp").contains(transport)) {
+ if (Arrays.asList("stuns", "turns").contains(type)
+ && "udp".equals(transport)) {
+ Log.w(
+ Config.LOGTAG,
+ "skipping invalid combination of udp/tls in external services");
+ continue;
+ }
+
+ // STUN URLs do not support a query section since M110
+ final String uri;
+ if (Arrays.asList("stun", "stuns").contains(type)) {
+ uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port);
+ } else {
+ uri =
+ String.format(
+ "%s:%s:%s?transport=%s",
+ type, IP.wrapIPv6(host), port, transport);
+ }
+
+ final PeerConnection.IceServer.Builder iceServerBuilder =
+ PeerConnection.IceServer.builder(uri);
+ iceServerBuilder.setTlsCertPolicy(
+ PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK);
+ if (username != null && password != null) {
+ iceServerBuilder.setUsername(username);
+ iceServerBuilder.setPassword(password);
+ } else if (Arrays.asList("turn", "turns").contains(type)) {
+ // The WebRTC spec requires throwing an
+ // InvalidAccessError when username (from libwebrtc
+ // source coder)
+ // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc
+ Log.w(
+ Config.LOGTAG,
+ "skipping "
+ + type
+ + "/"
+ + transport
+ + " without username and password");
+ continue;
+ }
+ final PeerConnection.IceServer iceServer =
+ iceServerBuilder.createIceServer();
+ Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer);
+ listBuilder.add(iceServer);
+ }
+ }
+ }
+ }
+ return listBuilder.build();
+ }
+}
@@ -1,152 +0,0 @@
-package eu.siacs.conversations.xmpp.jingle;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xmpp.InvalidJid;
-import eu.siacs.conversations.xmpp.Jid;
-
-public class JingleCandidate {
-
- public static int TYPE_UNKNOWN;
- public static int TYPE_DIRECT = 0;
- public static int TYPE_PROXY = 1;
-
- private final boolean ours;
- private boolean usedByCounterpart = false;
- private final String cid;
- private String host;
- private int port;
- private int type;
- private Jid jid;
- private int priority;
-
- public JingleCandidate(String cid, boolean ours) {
- this.ours = ours;
- this.cid = cid;
- }
-
- public String getCid() {
- return cid;
- }
-
- public void setHost(String host) {
- this.host = host;
- }
-
- public String getHost() {
- return this.host;
- }
-
- public void setJid(final Jid jid) {
- this.jid = jid;
- }
-
- public Jid getJid() {
- return this.jid;
- }
-
- public void setPort(int port) {
- this.port = port;
- }
-
- public int getPort() {
- return this.port;
- }
-
- public void setType(int type) {
- this.type = type;
- }
-
- public void setType(String type) {
- if (type == null) {
- this.type = TYPE_UNKNOWN;
- return;
- }
- switch (type) {
- case "proxy":
- this.type = TYPE_PROXY;
- break;
- case "direct":
- this.type = TYPE_DIRECT;
- break;
- default:
- this.type = TYPE_UNKNOWN;
- break;
- }
- }
-
- public void setPriority(int i) {
- this.priority = i;
- }
-
- public int getPriority() {
- return this.priority;
- }
-
- public boolean equals(JingleCandidate other) {
- return this.getCid().equals(other.getCid());
- }
-
- public boolean equalValues(JingleCandidate other) {
- return other != null && other.getHost().equals(this.getHost()) && (other.getPort() == this.getPort());
- }
-
- public boolean isOurs() {
- return ours;
- }
-
- public int getType() {
- return this.type;
- }
-
- public static List<JingleCandidate> parse(final List<Element> elements) {
- final List<JingleCandidate> candidates = new ArrayList<>();
- for (final Element element : elements) {
- if ("candidate".equals(element.getName())) {
- candidates.add(JingleCandidate.parse(element));
- }
- }
- return candidates;
- }
-
- public static JingleCandidate parse(Element element) {
- final JingleCandidate candidate = new JingleCandidate(element.getAttribute("cid"), false);
- candidate.setHost(element.getAttribute("host"));
- candidate.setJid(InvalidJid.getNullForInvalid(element.getAttributeAsJid("jid")));
- candidate.setType(element.getAttribute("type"));
- candidate.setPriority(Integer.parseInt(element.getAttribute("priority")));
- candidate.setPort(Integer.parseInt(element.getAttribute("port")));
- return candidate;
- }
-
- public Element toElement() {
- Element element = new Element("candidate");
- element.setAttribute("cid", this.getCid());
- element.setAttribute("host", this.getHost());
- element.setAttribute("port", Integer.toString(this.getPort()));
- if (jid != null) {
- element.setAttribute("jid", jid);
- }
- element.setAttribute("priority", Integer.toString(this.getPriority()));
- if (this.getType() == TYPE_DIRECT) {
- element.setAttribute("type", "direct");
- } else if (this.getType() == TYPE_PROXY) {
- element.setAttribute("type", "proxy");
- }
- return element;
- }
-
- public void flagAsUsedByCounterpart() {
- this.usedByCounterpart = true;
- }
-
- public boolean isUsedByCounterpart() {
- return this.usedByCounterpart;
- }
-
- public String toString() {
- return String.format("%s:%s (priority=%s,ours=%s)", getHost(), getPort(), getPriority(), isOurs());
- }
-}
@@ -29,10 +29,13 @@ import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
+import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport;
+import eu.siacs.conversations.xmpp.jingle.transports.Transport;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
@@ -61,8 +64,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
private final Cache<PersistableSessionId, TerminatedRtpSession> terminatedSessions =
CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build();
- private final HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>();
-
public JingleConnectionManager(XmppConnectionService service) {
super(service);
this.toneManager = new ToneManager(service);
@@ -90,7 +91,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final String descriptionNamespace =
content == null ? null : content.getDescriptionNamespace();
final AbstractJingleConnection connection;
- if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) {
+ if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(descriptionNamespace)) {
connection = new JingleFileTransferConnection(this, id, from);
} else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)
&& isUsingClearNet(account)) {
@@ -593,13 +594,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
if (old != null) {
old.cancel();
}
- final Account account = message.getConversation().getAccount();
- final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message);
final JingleFileTransferConnection connection =
- new JingleFileTransferConnection(this, id, account.getJid());
- mXmppConnectionService.markMessage(message, Message.STATUS_WAITING);
- this.connections.put(id, connection);
- connection.init(message);
+ new JingleFileTransferConnection(this, message);
+ this.connections.put(connection.getId(), connection);
+ connection.sendSessionInitialize();
}
public Optional<OngoingRtpSession> getOngoingRtpConnection(final Contact contact) {
@@ -658,60 +656,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return firedUpdates;
}
- void getPrimaryCandidate(
- final Account account,
- final boolean initiator,
- final OnPrimaryCandidateFound listener) {
- if (Config.DISABLE_PROXY_LOOKUP) {
- listener.onPrimaryCandidateFound(false, null);
- return;
- }
- if (this.primaryCandidates.containsKey(account.getJid().asBareJid())) {
- listener.onPrimaryCandidateFound(
- true, this.primaryCandidates.get(account.getJid().asBareJid()));
- return;
- }
-
- final Jid proxy =
- account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS);
- if (proxy == null) {
- listener.onPrimaryCandidateFound(false, null);
- return;
- }
- final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
- iq.setTo(proxy);
- iq.query(Namespace.BYTE_STREAMS);
- account.getXmppConnection()
- .sendIqPacket(
- iq,
- (a, response) -> {
- final Element streamhost =
- response.query()
- .findChild("streamhost", Namespace.BYTE_STREAMS);
- final String host =
- streamhost == null ? null : streamhost.getAttribute("host");
- final String port =
- streamhost == null ? null : streamhost.getAttribute("port");
- if (host != null && port != null) {
- try {
- JingleCandidate candidate =
- new JingleCandidate(nextRandomId(), true);
- candidate.setHost(host);
- candidate.setPort(Integer.parseInt(port));
- candidate.setType(JingleCandidate.TYPE_PROXY);
- candidate.setJid(proxy);
- candidate.setPriority(655360 + (initiator ? 30 : 0));
- primaryCandidates.put(a.getJid().asBareJid(), candidate);
- listener.onPrimaryCandidateFound(true, candidate);
- } catch (final NumberFormatException e) {
- listener.onPrimaryCandidateFound(false, null);
- }
- } else {
- listener.onPrimaryCandidateFound(false, null);
- }
- });
- }
-
public void retractSessionProposal(final Account account, final Jid with) {
synchronized (this.rtpSessionProposals) {
RtpSessionProposal matchingProposal = null;
@@ -810,36 +754,53 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return false;
}
- public void deliverIbbPacket(Account account, IqPacket packet) {
+ public void deliverIbbPacket(final Account account, final IqPacket packet) {
final String sid;
final Element payload;
+ final InbandBytestreamsTransport.PacketType packetType;
if (packet.hasChild("open", Namespace.IBB)) {
+ packetType = InbandBytestreamsTransport.PacketType.OPEN;
payload = packet.findChild("open", Namespace.IBB);
sid = payload.getAttribute("sid");
} else if (packet.hasChild("data", Namespace.IBB)) {
+ packetType = InbandBytestreamsTransport.PacketType.DATA;
payload = packet.findChild("data", Namespace.IBB);
sid = payload.getAttribute("sid");
} else if (packet.hasChild("close", Namespace.IBB)) {
+ packetType = InbandBytestreamsTransport.PacketType.CLOSE;
payload = packet.findChild("close", Namespace.IBB);
sid = payload.getAttribute("sid");
} else {
+ packetType = null;
payload = null;
sid = null;
}
- if (sid != null) {
- for (final AbstractJingleConnection connection : this.connections.values()) {
- if (connection instanceof JingleFileTransferConnection fileTransfer) {
- final JingleTransport transport = fileTransfer.getTransport();
- if (transport instanceof JingleInBandTransport inBandTransport) {
- if (inBandTransport.matches(account, sid)) {
- inBandTransport.deliverPayload(packet, payload);
+ if (sid == null) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet. missing sid");
+ account.getXmppConnection()
+ .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
+ return;
+ }
+ for (final AbstractJingleConnection connection : this.connections.values()) {
+ if (connection instanceof JingleFileTransferConnection fileTransfer) {
+ final Transport transport = fileTransfer.getTransport();
+ if (transport instanceof InbandBytestreamsTransport inBandTransport) {
+ if (sid.equals(inBandTransport.getStreamId())) {
+ if (inBandTransport.deliverPacket(packetType, packet.getFrom(), payload)) {
+ account.getXmppConnection()
+ .sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.RESULT), null);
+ } else {
+ account.getXmppConnection()
+ .sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.ERROR), null);
}
return;
}
}
}
}
- Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet);
+ Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet with sid="+sid);
account.getXmppConnection()
.sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
}
@@ -1,1255 +1,1454 @@
package eu.siacs.conversations.xmpp.jingle;
-import android.util.Base64;
import android.util.Log;
+import androidx.annotation.NonNull;
+
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.FluentIterable;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map.Entry;
-import java.util.concurrent.ConcurrentHashMap;
+import com.google.common.hash.Hashing;
+import com.google.common.primitives.Ints;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
import eu.siacs.conversations.Config;
-import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
-import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.Presence;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
-import eu.siacs.conversations.parser.IqParser;
-import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
-import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
-import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
-import eu.siacs.conversations.xmpp.jingle.stanzas.S5BTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport;
+import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport;
+import eu.siacs.conversations.xmpp.jingle.transports.Transport;
+import eu.siacs.conversations.xmpp.jingle.transports.WebRTCDataChannelTransport;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-public class JingleFileTransferConnection extends AbstractJingleConnection implements Transferable {
-
- private static final int JINGLE_STATUS_TRANSMITTING = 5;
- private static final String JET_OMEMO_CIPHER = "urn:xmpp:ciphers:aes-128-gcm-nopadding";
- private static final int JINGLE_STATUS_INITIATED = 0;
- private static final int JINGLE_STATUS_ACCEPTED = 1;
- private static final int JINGLE_STATUS_FINISHED = 4;
- private static final int JINGLE_STATUS_FAILED = 99;
- private static final int JINGLE_STATUS_OFFERED = -1;
-
- private static final int MAX_IBB_BLOCK_SIZE = 8192;
-
- private int ibbBlockSize = MAX_IBB_BLOCK_SIZE;
-
- private int mJingleStatus = JINGLE_STATUS_OFFERED; //migrate to enum
- private int mStatus = Transferable.STATUS_UNKNOWN;
- private Message message;
- private Jid responder;
- private final List<JingleCandidate> candidates = new ArrayList<>();
- private final ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>();
-
- private String transportId;
- private FileTransferDescription description;
- private DownloadableFile file = null;
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.io.CipherInputStream;
+import org.bouncycastle.crypto.io.CipherOutputStream;
+import org.bouncycastle.crypto.modes.AEADBlockCipher;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
+import org.webrtc.IceCandidate;
+
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+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.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.concurrent.CountDownLatch;
- private boolean proxyActivationFailed = false;
+public class JingleFileTransferConnection extends AbstractJingleConnection
+ implements Transport.Callback, Transferable {
- private String contentName;
- private Content.Creator contentCreator;
- private Content.Senders contentSenders;
- private Class<? extends GenericTransportInfo> initialTransport;
- private boolean remoteSupportsOmemoJet;
+ private final Message message;
- private int mProgress = 0;
+ private FileTransferContentMap initiatorFileTransferContentMap;
+ private FileTransferContentMap responderFileTransferContentMap;
- private boolean receivedCandidate = false;
- private boolean sentCandidate = false;
+ private Transport transport;
+ private TransportSecurity transportSecurity;
+ private AbstractFileTransceiver fileTransceiver;
+ private final Queue<IceCandidate> pendingIncomingIceCandidates = new LinkedList<>();
private boolean acceptedAutomatically = false;
- private boolean cancelled = false;
-
- private XmppAxolotlMessage mXmppAxolotlMessage;
- private JingleTransport transport = null;
+ public JingleFileTransferConnection(
+ final JingleConnectionManager jingleConnectionManager, final Message message) {
+ super(
+ jingleConnectionManager,
+ AbstractJingleConnection.Id.of(message),
+ message.getConversation().getAccount().getJid());
+ Preconditions.checkArgument(
+ message.isFileOrImage(),
+ "only file or images messages can be transported via jingle");
+ this.message = message;
+ this.message.setTransferable(this);
+ xmppConnectionService.markMessage(message, Message.STATUS_WAITING);
+ }
- private OutputStream mFileOutputStream;
- private InputStream mFileInputStream;
+ public JingleFileTransferConnection(
+ final JingleConnectionManager jingleConnectionManager,
+ final Id id,
+ final Jid initiator) {
+ super(jingleConnectionManager, id, initiator);
+ final Conversation conversation =
+ this.xmppConnectionService.findOrCreateConversation(
+ id.account, id.with.asBareJid(), false, false);
+ this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
+ this.message.setStatus(Message.STATUS_RECEIVED);
+ this.message.setErrorMessage(null);
+ this.message.setTransferable(this);
+ }
- private final OnIqPacketReceived responseListener = (account, packet) -> {
- if (packet.getType() != IqPacket.TYPE.RESULT) {
- if (mJingleStatus != JINGLE_STATUS_FAILED && mJingleStatus != JINGLE_STATUS_FINISHED) {
- fail(IqParser.extractErrorMessage(packet));
- } else {
- Log.d(Config.LOGTAG, "ignoring late delivery of jingle packet to jingle session with status=" + mJingleStatus + ": " + packet.toString());
+ @Override
+ void deliverPacket(final JinglePacket jinglePacket) {
+ switch (jinglePacket.getAction()) {
+ case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket);
+ case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket);
+ case SESSION_INFO -> receiveSessionInfo(jinglePacket);
+ case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket);
+ case TRANSPORT_ACCEPT -> receiveTransportAccept(jinglePacket);
+ case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket);
+ case TRANSPORT_REPLACE -> receiveTransportReplace(jinglePacket);
+ default -> {
+ respondOk(jinglePacket);
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "%s: received unhandled jingle action %s",
+ id.account.getJid().asBareJid(), jinglePacket.getAction()));
}
}
- };
- private byte[] expectedHash = new byte[0];
- private final OnFileTransmissionStatusChanged onFileTransmissionStatusChanged = new OnFileTransmissionStatusChanged() {
+ }
- @Override
- public void onFileTransmitted(DownloadableFile file) {
- if (responding()) {
- if (expectedHash.length > 0) {
- if (Arrays.equals(expectedHash, file.getSha1Sum())) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received file matched the expected hash");
- } else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": hashes did not match");
+ public void sendSessionInitialize() {
+ final ListenableFuture<Optional<XmppAxolotlMessage>> keyTransportMessage;
+ if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+ keyTransportMessage =
+ Futures.transform(
+ id.account
+ .getAxolotlService()
+ .prepareKeyTransportMessage(requireConversation()),
+ Optional::of,
+ MoreExecutors.directExecutor());
+ } else {
+ keyTransportMessage = Futures.immediateFuture(Optional.empty());
+ }
+ Futures.addCallback(
+ keyTransportMessage,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Optional<XmppAxolotlMessage> xmppAxolotlMessage) {
+ sendSessionInitialize(xmppAxolotlMessage.orElse(null));
}
- } else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party did not include file hash in file transfer");
- }
- sendSuccess();
- xmppConnectionService.getFileBackend().updateFileParams(message);
- xmppConnectionService.databaseBackend.createMessage(message);
- xmppConnectionService.markMessage(message, Message.STATUS_RECEIVED);
- if (acceptedAutomatically) {
- message.markUnread();
- if (message.getEncryption() == Message.ENCRYPTION_PGP) {
- id.account.getPgpDecryptionService().decrypt(message, true);
- } else {
- xmppConnectionService.getFileBackend().updateMediaScanner(file, () -> JingleFileTransferConnection.this.xmppConnectionService.getNotificationService().push(message));
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(Config.LOGTAG, "can not send message");
}
- Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
- return;
- } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
- id.account.getPgpDecryptionService().decrypt(message, true);
- }
- } else {
- if (description.getVersion() == FileTransferDescription.Version.FT_5) { //older Conversations will break when receiving a session-info
- sendHash();
- }
- if (message.getEncryption() == Message.ENCRYPTION_PGP) {
- id.account.getPgpDecryptionService().decrypt(message, false);
- }
- if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
- file.delete();
- }
- }
- Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
- if (message.getEncryption() != Message.ENCRYPTION_PGP) {
- xmppConnectionService.getFileBackend().updateMediaScanner(file);
- }
- }
+ },
+ MoreExecutors.directExecutor());
+ }
- @Override
- public void onFileTransferAborted() {
- JingleFileTransferConnection.this.sendSessionTerminate(Reason.CONNECTIVITY_ERROR);
- JingleFileTransferConnection.this.fail();
- }
- };
- private final OnTransportConnected onIbbTransportConnected = new OnTransportConnected() {
- @Override
- public void failed() {
- Log.d(Config.LOGTAG, "ibb open failed");
- }
+ private void sendSessionInitialize(final XmppAxolotlMessage xmppAxolotlMessage) {
+ this.transport = setupTransport();
+ this.transport.setTransportCallback(this);
+ final File file = xmppConnectionService.getFileBackend().getFile(message);
+ final var fileDescription =
+ new FileTransferDescription.File(
+ file.length(),
+ file.getName(),
+ message.getMimeType(),
+ Collections.emptyList());
+ final var transportInfoFuture = this.transport.asInitialTransportInfo();
+ Futures.addCallback(
+ transportInfoFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(
+ final Transport.InitialTransportInfo initialTransportInfo) {
+ final FileTransferContentMap contentMap =
+ FileTransferContentMap.of(fileDescription, initialTransportInfo);
+ sendSessionInitialize(xmppAxolotlMessage, contentMap);
+ }
- @Override
- public void established() {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ibb transport connected. sending file");
- mJingleStatus = JINGLE_STATUS_TRANSMITTING;
- JingleFileTransferConnection.this.transport.send(file, onFileTransmissionStatusChanged);
- }
- };
- private final OnProxyActivated onProxyActivated = new OnProxyActivated() {
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {}
+ },
+ MoreExecutors.directExecutor());
+ }
- @Override
- public void success() {
- if (isInitiator()) {
- Log.d(Config.LOGTAG, "we were initiating. sending file");
- transport.send(file, onFileTransmissionStatusChanged);
- } else {
- transport.receive(file, onFileTransmissionStatusChanged);
- Log.d(Config.LOGTAG, "we were responding. receiving file");
- }
+ private Conversation requireConversation() {
+ final var conversational = message.getConversation();
+ if (conversational instanceof Conversation c) {
+ return c;
+ } else {
+ throw new IllegalStateException("Message had no proper conversation attached");
}
+ }
- @Override
- public void failed() {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": proxy activation failed");
- proxyActivationFailed = true;
- if (isInitiator()) {
- sendFallbackToIbb();
+ private void sendSessionInitialize(
+ final XmppAxolotlMessage xmppAxolotlMessage, final FileTransferContentMap contentMap) {
+ if (transition(
+ State.SESSION_INITIALIZED,
+ () -> this.initiatorFileTransferContentMap = contentMap)) {
+ final var jinglePacket =
+ contentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
+ if (xmppAxolotlMessage != null) {
+ this.transportSecurity =
+ new TransportSecurity(
+ xmppAxolotlMessage.getInnerKey(), xmppAxolotlMessage.getIV());
+ jinglePacket.setSecurity(
+ Iterables.getOnlyElement(contentMap.contents.keySet()), xmppAxolotlMessage);
}
+ Log.d(Config.LOGTAG, "--> " + jinglePacket.toString());
+ jinglePacket.setTo(id.with);
+ xmppConnectionService.sendIqPacket(
+ id.account,
+ jinglePacket,
+ (a, response) -> {
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ xmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
+ return;
+ }
+ if (response.getType() == IqPacket.TYPE.ERROR) {
+ handleIqErrorResponse(response);
+ return;
+ }
+ if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+ handleIqTimeoutResponse(response);
+ }
+ });
+ this.transport.readyToSentAdditionalCandidates();
}
- };
-
- JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
- super(jingleConnectionManager, id, initiator);
}
- private static long parseLong(final Element element, final long l) {
- final String input = element == null ? null : element.getContent();
- if (input == null) {
- return l;
+ private void receiveSessionAccept(final JinglePacket jinglePacket) {
+ Log.d(Config.LOGTAG, "receive session accept " + jinglePacket);
+ if (isResponder()) {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
+ return;
}
+ final FileTransferContentMap contentMap;
try {
- return Long.parseLong(input);
- } catch (Exception e) {
- return l;
+ contentMap = FileTransferContentMap.of(jinglePacket);
+ contentMap.requireOnlyFileTransferDescription();
+ } catch (final RuntimeException e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": improperly formatted contents",
+ Throwables.getRootCause(e));
+ respondOk(jinglePacket);
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
}
+ receiveSessionAccept(jinglePacket, contentMap);
}
- //TODO get rid and use isInitiator() instead
- private boolean responding() {
- return responder != null && responder.equals(id.account.getJid());
+ private void receiveSessionAccept(
+ final JinglePacket jinglePacket, final FileTransferContentMap contentMap) {
+ if (transition(State.SESSION_ACCEPTED, () -> setRemoteContentMap(contentMap))) {
+ respondOk(jinglePacket);
+ final var transport = this.transport;
+ if (configureTransportWithPeerInfo(transport, contentMap)) {
+ transport.connect();
+ } else {
+ Log.e(
+ Config.LOGTAG,
+ "Transport in session accept did not match our session-initialize");
+ terminateTransport();
+ sendSessionTerminate(
+ Reason.FAILED_APPLICATION,
+ "Transport in session accept did not match our session-initialize");
+ }
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": receive out of order session-accept");
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
+ }
}
-
- InputStream getFileInputStream() {
- return this.mFileInputStream;
+ private static boolean configureTransportWithPeerInfo(
+ final Transport transport, final FileTransferContentMap contentMap) {
+ final GenericTransportInfo transportInfo = contentMap.requireOnlyTransportInfo();
+ if (transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport
+ && transportInfo instanceof WebRTCDataChannelTransportInfo) {
+ webRTCDataChannelTransport.setResponderDescription(SessionDescription.of(contentMap));
+ return true;
+ } else if (transport instanceof SocksByteStreamsTransport socksBytestreamsTransport
+ && transportInfo
+ instanceof SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) {
+ socksBytestreamsTransport.setTheirCandidates(
+ socksBytestreamsTransportInfo.getCandidates());
+ return true;
+ } else if (transport instanceof InbandBytestreamsTransport inbandBytestreamsTransport
+ && transportInfo instanceof IbbTransportInfo ibbTransportInfo) {
+ final var peerBlockSize = ibbTransportInfo.getBlockSize();
+ if (peerBlockSize != null) {
+ inbandBytestreamsTransport.setPeerBlockSize(peerBlockSize);
+ }
+ return true;
+ } else {
+ return false;
+ }
}
- OutputStream getFileOutputStream() throws IOException {
- if (this.file == null) {
- Log.d(Config.LOGTAG, "file object was not assigned");
- return null;
+ private void receiveSessionInitiate(final JinglePacket jinglePacket) {
+ if (isInitiator()) {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
+ return;
}
- final File parent = this.file.getParentFile();
- if (parent != null && parent.mkdirs()) {
- Log.d(Config.LOGTAG, "created parent directories for file " + file.getAbsolutePath());
+ Log.d(Config.LOGTAG, "receive session initiate " + jinglePacket);
+ final FileTransferContentMap contentMap;
+ final FileTransferDescription.File file;
+ try {
+ contentMap = FileTransferContentMap.of(jinglePacket);
+ contentMap.requireContentDescriptions();
+ file = contentMap.requireOnlyFile();
+ // TODO check is offer
+ } catch (final RuntimeException e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": improperly formatted contents",
+ Throwables.getRootCause(e));
+ respondOk(jinglePacket);
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
}
- if (this.file.createNewFile()) {
- Log.d(Config.LOGTAG, "created output file " + file.getAbsolutePath());
+ final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
+ final var security =
+ jinglePacket.getSecurity(Iterables.getOnlyElement(contentMap.contents.keySet()));
+ if (security != null) {
+ Log.d(Config.LOGTAG, "found security element!");
+ keyTransportMessage =
+ id.account
+ .getAxolotlService()
+ .processReceivingKeyTransportMessage(security, false);
+ } else {
+ keyTransportMessage = null;
}
- this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file, false, true);
- return this.mFileOutputStream;
+ receiveSessionInitiate(jinglePacket, contentMap, file, keyTransportMessage);
}
- @Override
- void deliverPacket(final JinglePacket packet) {
- final JinglePacket.Action action = packet.getAction();
- //TODO switch case
- if (action == JinglePacket.Action.SESSION_INITIATE) {
- init(packet);
- } else if (action == JinglePacket.Action.SESSION_TERMINATE) {
- final Reason reason = packet.getReason().reason;
- switch (reason) {
- case CANCEL:
- this.cancelled = true;
- this.fail();
- break;
- case SUCCESS:
- this.receiveSuccess();
- break;
- default:
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate with reason " + reason);
- this.fail();
- break;
-
- }
- } else if (action == JinglePacket.Action.SESSION_ACCEPT) {
- receiveAccept(packet);
- } else if (action == JinglePacket.Action.SESSION_INFO) {
- final Element checksum = packet.getJingleChild("checksum");
- final Element file = checksum == null ? null : checksum.findChild("file");
- final Element hash = file == null ? null : file.findChild("hash", "urn:xmpp:hashes:2");
- if (hash != null && "sha-1".equalsIgnoreCase(hash.getAttribute("algo"))) {
- try {
- this.expectedHash = Base64.decode(hash.getContent(), Base64.DEFAULT);
- } catch (Exception e) {
- this.expectedHash = new byte[0];
- }
+ private void receiveSessionInitiate(
+ final JinglePacket jinglePacket,
+ final FileTransferContentMap contentMap,
+ final FileTransferDescription.File file,
+ final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage) {
+
+ if (transition(State.SESSION_INITIALIZED, () -> setRemoteContentMap(contentMap))) {
+ respondOk(jinglePacket);
+ Log.d(Config.LOGTAG, jinglePacket.toString());
+ Log.d(
+ Config.LOGTAG,
+ "got file offer " + file + " jet=" + Objects.nonNull(keyTransportMessage));
+ setFileOffer(file);
+ if (keyTransportMessage != null) {
+ this.transportSecurity =
+ new TransportSecurity(
+ keyTransportMessage.getKey(), keyTransportMessage.getIv());
+ this.message.setFingerprint(keyTransportMessage.getFingerprint());
+ this.message.setEncryption(Message.ENCRYPTION_AXOLOTL);
+ } else {
+ this.transportSecurity = null;
+ this.message.setFingerprint(null);
}
- respondToIq(packet, true);
- } else if (action == JinglePacket.Action.TRANSPORT_INFO) {
- receiveTransportInfo(packet);
- } else if (action == JinglePacket.Action.TRANSPORT_REPLACE) {
- final Content content = packet.getJingleContent();
- final GenericTransportInfo transportInfo = content == null ? null : content.getTransport();
- if (transportInfo instanceof IbbTransportInfo) {
- receiveFallbackToIbb(packet, (IbbTransportInfo) transportInfo);
+ final var conversation = (Conversation) message.getConversation();
+ conversation.add(message);
+
+ // make auto accept decision
+ if (id.account.getRoster().getContact(id.with).showInContactList()
+ && jingleConnectionManager.hasStoragePermission()
+ && file.size <= this.jingleConnectionManager.getAutoAcceptFileSize()
+ && xmppConnectionService.isDataSaverDisabled()) {
+ Log.d(Config.LOGTAG, "auto accepting file from " + id.with);
+ this.acceptedAutomatically = true;
+ this.sendSessionAccept();
} else {
- Log.d(Config.LOGTAG, "trying to fallback to something unknown" + packet.toString());
- respondToIq(packet, false);
+ Log.d(
+ Config.LOGTAG,
+ "not auto accepting new file offer with size: "
+ + file.size
+ + " allowed size:"
+ + this.jingleConnectionManager.getAutoAcceptFileSize());
+ message.markUnread();
+ this.xmppConnectionService.updateConversationUi();
+ this.xmppConnectionService.getNotificationService().push(message);
}
- } else if (action == JinglePacket.Action.TRANSPORT_ACCEPT) {
- receiveTransportAccept(packet);
} else {
- Log.d(Config.LOGTAG, "packet arrived in connection. action was " + packet.getAction());
- respondToIq(packet, false);
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": receive out of order session-initiate");
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
}
}
- @Override
- void notifyRebound() {
- if (getJingleStatus() == JINGLE_STATUS_TRANSMITTING) {
- abort(Reason.CONNECTIVITY_ERROR);
+ private void setFileOffer(final FileTransferDescription.File file) {
+ final AbstractConnectionManager.Extension extension =
+ AbstractConnectionManager.Extension.of(file.name);
+ if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
+ this.message.setEncryption(Message.ENCRYPTION_PGP);
+ } else {
+ this.message.setEncryption(Message.ENCRYPTION_NONE);
}
+ final String ext = extension.getExtension();
+ final String filename =
+ Strings.isNullOrEmpty(ext)
+ ? message.getUuid()
+ : String.format("%s.%s", message.getUuid(), ext);
+ xmppConnectionService.getFileBackend().setupRelativeFilePath(message, filename);
}
- private void respondToIq(final IqPacket packet, final boolean result) {
- final IqPacket response;
- if (result) {
- response = packet.generateResponse(IqPacket.TYPE.RESULT);
- } else {
- response = packet.generateResponse(IqPacket.TYPE.ERROR);
- final Element error = response.addChild("error").setAttribute("type", "cancel");
- error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas");
+ public void sendSessionAccept() {
+ final FileTransferContentMap contentMap = this.initiatorFileTransferContentMap;
+ final Transport transport;
+ try {
+ transport = setupTransport(contentMap.requireOnlyTransportInfo());
+ } catch (final RuntimeException e) {
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
}
- xmppConnectionService.sendIqPacket(id.account, response, null);
- }
+ transitionOrThrow(State.SESSION_ACCEPTED);
+ this.transport = transport;
+ this.transport.setTransportCallback(this);
+ if (this.transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport) {
+ final var sessionDescription = SessionDescription.of(contentMap);
+ webRTCDataChannelTransport.setInitiatorDescription(sessionDescription);
+ }
+ final var transportInfoFuture = transport.asTransportInfo();
+ Futures.addCallback(
+ transportInfoFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Transport.TransportInfo transportInfo) {
+ final FileTransferContentMap responderContentMap =
+ contentMap.withTransport(transportInfo);
+ sendSessionAccept(responderContentMap);
+ }
- private void respondToIqWithOutOfOrder(final IqPacket packet) {
- final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
- final Element error = response.addChild("error").setAttribute("type", "wait");
- error.addChild("unexpected-request", "urn:ietf:params:xml:ns:xmpp-stanzas");
- error.addChild("out-of-order", "urn:xmpp:jingle:errors:1");
- xmppConnectionService.sendIqPacket(id.account, response, null);
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ failureToAcceptSession(throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
}
- public void init(final Message message) {
- Preconditions.checkArgument(message.isFileOrImage());
- if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
- Conversation conversation = (Conversation) message.getConversation();
- conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, xmppAxolotlMessage -> {
- if (xmppAxolotlMessage != null) {
- init(message, xmppAxolotlMessage);
- } else {
- fail();
- }
- });
- } else {
- init(message, null);
+ private void sendSessionAccept(final FileTransferContentMap contentMap) {
+ setLocalContentMap(contentMap);
+ final var jinglePacket =
+ contentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
+ Log.d(Config.LOGTAG, "--> " + jinglePacket.toString());
+ send(jinglePacket);
+ // this needs to come after session-accept or else our candidate-error might arrive first
+ this.transport.connect();
+ this.transport.readyToSentAdditionalCandidates();
+ if (this.transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport) {
+ drainPendingIncomingIceCandidates(webRTCDataChannelTransport);
}
}
- private void init(final Message message, final XmppAxolotlMessage xmppAxolotlMessage) {
- this.mXmppAxolotlMessage = xmppAxolotlMessage;
- this.contentCreator = Content.Creator.INITIATOR;
- this.contentSenders = Content.Senders.INITIATOR;
- this.contentName = JingleConnectionManager.nextRandomId();
- this.message = message;
- final List<String> remoteFeatures = getRemoteFeatures();
- final FileTransferDescription.Version remoteVersion = getAvailableFileTransferVersion(remoteFeatures);
- this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? S5BTransportInfo.class : IbbTransportInfo.class;
- this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO);
- this.message.setTransferable(this);
- this.mStatus = Transferable.STATUS_UPLOADING;
- this.responder = this.id.with;
- this.transportId = JingleConnectionManager.nextRandomId();
- this.setupDescription(remoteVersion);
- if (this.initialTransport == IbbTransportInfo.class) {
- this.sendInitRequest();
- } else {
- gatherAndConnectDirectCandidates();
- this.jingleConnectionManager.getPrimaryCandidate(id.account, isInitiator(), (success, candidate) -> {
- if (success) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
- connections.put(candidate.getCid(), socksConnection);
- socksConnection.connect(new OnTransportConnected() {
-
- @Override
- public void failed() {
- Log.d(Config.LOGTAG, String.format("connection to our own proxy65 candidate failed (%s:%d)", candidate.getHost(), candidate.getPort()));
- sendInitRequest();
- }
-
- @Override
- public void established() {
- Log.d(Config.LOGTAG, "successfully connected to our own proxy65 candidate");
- mergeCandidate(candidate);
- sendInitRequest();
- }
- });
- mergeCandidate(candidate);
- } else {
- Log.d(Config.LOGTAG, "no proxy65 candidate of our own was found");
- sendInitRequest();
- }
- });
+ private void drainPendingIncomingIceCandidates(
+ final WebRTCDataChannelTransport webRTCDataChannelTransport) {
+ while (this.pendingIncomingIceCandidates.peek() != null) {
+ final var candidate = this.pendingIncomingIceCandidates.poll();
+ if (candidate == null) {
+ continue;
+ }
+ webRTCDataChannelTransport.addIceCandidates(ImmutableList.of(candidate));
}
-
}
- private void gatherAndConnectDirectCandidates() {
- final List<JingleCandidate> directCandidates;
- if (Config.USE_DIRECT_JINGLE_CANDIDATES) {
- if (id.account.isOnion() || xmppConnectionService.useTorToConnect()) {
- directCandidates = Collections.emptyList();
- } else {
- directCandidates = DirectConnectionUtils.getLocalCandidates(id.account.getJid());
+ private Transport setupTransport(final GenericTransportInfo transportInfo) {
+ final XmppConnection xmppConnection = id.account.getXmppConnection();
+ final boolean useTor = id.account.isOnion() || xmppConnectionService.useTorToConnect();
+ if (transportInfo instanceof IbbTransportInfo ibbTransportInfo) {
+ final String streamId = ibbTransportInfo.getTransportId();
+ final Long blockSize = ibbTransportInfo.getBlockSize();
+ if (streamId == null || blockSize == null) {
+ throw new IllegalStateException("ibb transport is missing sid and/or block-size");
}
+ return new InbandBytestreamsTransport(
+ xmppConnection,
+ id.with,
+ isInitiator(),
+ streamId,
+ Ints.saturatedCast(blockSize));
+ } else if (transportInfo
+ instanceof SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) {
+ final String streamId = socksBytestreamsTransportInfo.getTransportId();
+ final String destination = socksBytestreamsTransportInfo.getDestinationAddress();
+ final List<SocksByteStreamsTransport.Candidate> candidates =
+ socksBytestreamsTransportInfo.getCandidates();
+ Log.d(Config.LOGTAG, "received socks candidates " + candidates);
+ return new SocksByteStreamsTransport(
+ xmppConnection, id, isInitiator(), useTor, streamId, candidates);
+ } else if (!useTor && transportInfo instanceof WebRTCDataChannelTransportInfo) {
+ return new WebRTCDataChannelTransport(
+ xmppConnectionService.getApplicationContext(),
+ xmppConnection,
+ id.account,
+ isInitiator());
} else {
- directCandidates = Collections.emptyList();
- }
- for (JingleCandidate directCandidate : directCandidates) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, directCandidate);
- connections.put(directCandidate.getCid(), socksConnection);
- candidates.add(directCandidate);
+ throw new IllegalArgumentException("Do not know how to create transport");
}
}
- private FileTransferDescription.Version getAvailableFileTransferVersion(List<String> remoteFeatures) {
- if (remoteFeatures.contains(FileTransferDescription.Version.FT_5.getNamespace())) {
- return FileTransferDescription.Version.FT_5;
- } else if (remoteFeatures.contains(FileTransferDescription.Version.FT_4.getNamespace())) {
- return FileTransferDescription.Version.FT_4;
- } else {
- return FileTransferDescription.Version.FT_3;
+ private Transport setupTransport() {
+ final XmppConnection xmppConnection = id.account.getXmppConnection();
+ final boolean useTor = id.account.isOnion() || xmppConnectionService.useTorToConnect();
+ if (!useTor && remoteHasFeature(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL)) {
+ return new WebRTCDataChannelTransport(
+ xmppConnectionService.getApplicationContext(),
+ xmppConnection,
+ id.account,
+ isInitiator());
}
+ if (remoteHasFeature(Namespace.JINGLE_TRANSPORTS_S5B)) {
+ return new SocksByteStreamsTransport(xmppConnection, id, isInitiator(), useTor);
+ }
+ return setupLastResortTransport();
}
- private List<String> getRemoteFeatures() {
- final String resource = Strings.nullToEmpty(this.id.with.getResource());
- final Presence presence = this.id.account.getRoster().getContact(id.with).getPresences().get(resource);
- final ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
- return result == null ? Collections.emptyList() : result.getFeatures();
+ private Transport setupLastResortTransport() {
+ final XmppConnection xmppConnection = id.account.getXmppConnection();
+ return new InbandBytestreamsTransport(xmppConnection, id.with, isInitiator());
}
- private void init(JinglePacket packet) { //should move to deliverPacket
- //TODO if not 'OFFERED' reply with out-of-order
- this.mJingleStatus = JINGLE_STATUS_INITIATED;
- final Conversation conversation = this.xmppConnectionService.findOrCreateConversation(id.account, id.with.asBareJid(), false, false);
- this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
- this.message.setStatus(Message.STATUS_RECEIVED);
- this.mStatus = Transferable.STATUS_OFFER;
- this.message.setTransferable(this);
- this.message.setCounterpart(this.id.with);
- this.responder = this.id.account.getJid();
- final Content content = packet.getJingleContent();
- final GenericTransportInfo transportInfo = content.getTransport();
- this.contentCreator = content.getCreator();
- Content.Senders senders;
- try {
- senders = content.getSenders();
- } catch (final Exception e) {
- senders = Content.Senders.INITIATOR;
- }
- this.contentSenders = senders;
- this.contentName = content.getAttribute("name");
-
- if (transportInfo instanceof S5BTransportInfo) {
- final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo;
- this.transportId = s5BTransportInfo.getTransportId();
- this.initialTransport = s5BTransportInfo.getClass();
- this.mergeCandidates(s5BTransportInfo.getCandidates());
- } else if (transportInfo instanceof IbbTransportInfo) {
- final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo;
- this.initialTransport = ibbTransportInfo.getClass();
- this.transportId = ibbTransportInfo.getTransportId();
- final int remoteBlockSize = ibbTransportInfo.getBlockSize();
- if (remoteBlockSize <= 0) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote party requested invalid ibb block size");
- respondToIq(packet, false);
- this.fail();
- }
- this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, ibbTransportInfo.getBlockSize());
- } else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote tried to use unknown transport " + transportInfo.getNamespace());
- respondToIq(packet, false);
- this.fail();
+ private void failureToAcceptSession(final Throwable throwable) {
+ if (isTerminated()) {
return;
}
+ final Throwable rootCause = Throwables.getRootCause(throwable);
+ Log.d(Config.LOGTAG, "unable to send session accept", rootCause);
+ sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
+ }
+
+ private void receiveSessionInfo(final JinglePacket jinglePacket) {
+ Log.d(Config.LOGTAG, "<-- " + jinglePacket);
+ respondOk(jinglePacket);
+ final var sessionInfo = FileTransferDescription.getSessionInfo(jinglePacket);
+ if (sessionInfo instanceof FileTransferDescription.Checksum checksum) {
+ receiveSessionInfoChecksum(checksum);
+ } else if (sessionInfo instanceof FileTransferDescription.Received received) {
+ receiveSessionInfoReceived(received);
+ }
+ }
- this.description = (FileTransferDescription) content.getDescription();
+ private void receiveSessionInfoChecksum(final FileTransferDescription.Checksum checksum) {
+ Log.d(Config.LOGTAG, "received checksum " + checksum);
+ }
- final Element fileOffer = this.description.getFileOffer();
+ private void receiveSessionInfoReceived(final FileTransferDescription.Received received) {
+ Log.d(Config.LOGTAG, "peer confirmed received " + received);
+ }
- if (fileOffer != null) {
- boolean remoteIsUsingJet = false;
- Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
- if (encrypted == null) {
- final Element security = content.findChild("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
- if (security != null && AxolotlService.PEP_PREFIX.equals(security.getAttribute("type"))) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received jingle file offer with JET");
- encrypted = security.findChild("encrypted", AxolotlService.PEP_PREFIX);
- remoteIsUsingJet = true;
- }
- }
- if (encrypted != null) {
- this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().asBareJid());
- }
- Element fileSize = fileOffer.findChild("size");
- final String path = fileOffer.findChildContent("name");
- if (path != null) {
- AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(path);
- if (VALID_IMAGE_EXTENSIONS.contains(extension.main)) {
- message.setType(Message.TYPE_IMAGE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message, message.getUuid() + "." + extension.main);
- } else if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
- if (VALID_IMAGE_EXTENSIONS.contains(extension.secondary)) {
- message.setType(Message.TYPE_IMAGE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + "." + extension.secondary);
- } else {
- message.setType(Message.TYPE_FILE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.secondary != null ? ("." + extension.secondary) : ""));
- }
- message.setEncryption(Message.ENCRYPTION_PGP);
- } else {
- message.setType(Message.TYPE_FILE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.main != null ? ("." + extension.main) : ""));
- }
- long size = parseLong(fileSize, 0);
- message.setBody(Long.toString(size));
- conversation.add(message);
- jingleConnectionManager.updateConversationUi(true);
- this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);
- if (mXmppAxolotlMessage != null) {
- XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = id.account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage, false);
- if (transportMessage != null) {
- message.setEncryption(Message.ENCRYPTION_AXOLOTL);
- this.file.setKey(transportMessage.getKey());
- this.file.setIv(transportMessage.getIv());
- message.setFingerprint(transportMessage.getFingerprint());
- } else {
- Log.d(Config.LOGTAG, "could not process KeyTransportMessage");
- }
- }
- message.resetFileParams();
- //legacy OMEMO encrypted file transfers reported the file size after encryption
- //JET reports the plain text size. however lower levels of our receiving code still
- //expect the cipher text size. so we just + 16 bytes (auth tag size) here
- this.file.setExpectedSize(size + (remoteIsUsingJet ? 16 : 0));
-
- respondToIq(packet, true);
-
- if (id.account.getRoster().getContact(id.with).showInContactList()
- && jingleConnectionManager.hasStoragePermission()
- && size < this.jingleConnectionManager.getAutoAcceptFileSize()
- && xmppConnectionService.isDataSaverDisabled()) {
- Log.d(Config.LOGTAG, "auto accepting file from " + id.with);
- this.acceptedAutomatically = true;
- this.sendAccept();
- } else {
- message.markUnread();
- Log.d(Config.LOGTAG,
- "not auto accepting new file offer with size: "
- + size
- + " allowed size:"
- + this.jingleConnectionManager
- .getAutoAcceptFileSize());
- this.xmppConnectionService.getNotificationService().push(message);
- }
- Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
- return;
- }
- respondToIq(packet, false);
+ private void receiveSessionTerminate(final JinglePacket jinglePacket) {
+ final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason();
+ final State previous = this.state;
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": received session terminate reason="
+ + wrapper.reason
+ + "("
+ + Strings.nullToEmpty(wrapper.text)
+ + ") while in state "
+ + previous);
+ if (TERMINATED.contains(previous)) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": ignoring session terminate because already in "
+ + previous);
+ return;
}
+ if (isInitiator()) {
+ this.message.setErrorMessage(
+ Strings.isNullOrEmpty(wrapper.text) ? wrapper.reason.toString() : wrapper.text);
+ }
+ terminateTransport();
+ final State target = reasonToState(wrapper.reason);
+ transitionOrThrow(target);
+ finish();
}
- private void setupDescription(final FileTransferDescription.Version version) {
- this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);
- final FileTransferDescription description;
- if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
- this.file.setKey(mXmppAxolotlMessage.getInnerKey());
- this.file.setIv(mXmppAxolotlMessage.getIV());
- //legacy OMEMO encrypted file transfer reported file size of the encrypted file
- //JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag)
- this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16));
- if (remoteSupportsOmemoJet) {
- description = FileTransferDescription.of(this.file, version, null);
- } else {
- description = FileTransferDescription.of(this.file, version, this.mXmppAxolotlMessage);
- }
- } else {
- this.file.setExpectedSize(file.getSize());
- description = FileTransferDescription.of(this.file, version, null);
- }
- this.description = description;
- }
-
- private void sendInitRequest() {
- final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET");
- final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
- security.setAttribute("name", this.contentName);
- security.setAttribute("cipher", JET_OMEMO_CIPHER);
- security.setAttribute("type", AxolotlService.PEP_PREFIX);
- security.addChild(mXmppAxolotlMessage.toElement());
- content.addChild(security);
- }
- content.setDescription(this.description);
- message.resetFileParams();
+ private void receiveTransportAccept(final JinglePacket jinglePacket) {
+ if (isResponder()) {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_ACCEPT);
+ return;
+ }
+ Log.d(Config.LOGTAG, "receive transport accept " + jinglePacket);
+ final GenericTransportInfo transportInfo;
try {
- this.mFileInputStream = new FileInputStream(file);
- } catch (FileNotFoundException e) {
- fail(e.getMessage());
+ transportInfo = FileTransferContentMap.of(jinglePacket).requireOnlyTransportInfo();
+ } catch (final RuntimeException e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": improperly formatted contents",
+ Throwables.getRootCause(e));
+ respondOk(jinglePacket);
+ sendSessionTerminate(Reason.of(e), e.getMessage());
return;
}
- if (this.initialTransport == IbbTransportInfo.class) {
- content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending IBB offer");
+ if (isInState(State.SESSION_ACCEPTED)) {
+ final var group = jinglePacket.getGroup();
+ receiveTransportAccept(jinglePacket, new Transport.TransportInfo(transportInfo, group));
} else {
- final Collection<JingleCandidate> candidates = getOurCandidates();
- content.setTransport(new S5BTransportInfo(this.transportId, candidates));
- Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size()));
- }
- packet.addJingleContent(content);
- this.sendJinglePacket(packet, (account, response) -> {
- if (response.getType() == IqPacket.TYPE.RESULT) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer");
- if (mJingleStatus == JINGLE_STATUS_OFFERED) {
- mJingleStatus = JINGLE_STATUS_INITIATED;
- xmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
- } else {
- Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus);
- }
- } else {
- fail(IqParser.extractErrorMessage(response));
- }
- });
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_ACCEPT);
+ }
+ }
+ private void receiveTransportAccept(
+ final JinglePacket jinglePacket, final Transport.TransportInfo transportInfo) {
+ final FileTransferContentMap remoteContentMap =
+ getRemoteContentMap().withTransport(transportInfo);
+ setRemoteContentMap(remoteContentMap);
+ respondOk(jinglePacket);
+ final var transport = this.transport;
+ if (configureTransportWithPeerInfo(transport, remoteContentMap)) {
+ transport.connect();
+ } else {
+ Log.e(
+ Config.LOGTAG,
+ "Transport in transport-accept did not match our transport-replace");
+ terminateTransport();
+ sendSessionTerminate(
+ Reason.FAILED_APPLICATION,
+ "Transport in transport-accept did not match our transport-replace");
+ }
}
- private void sendHash() {
- final Element checksum = new Element("checksum", description.getVersion().getNamespace());
- checksum.setAttribute("creator", "initiator");
- checksum.setAttribute("name", "a-file-offer");
- Element hash = checksum.addChild("file").addChild("hash", "urn:xmpp:hashes:2");
- hash.setAttribute("algo", "sha-1").setContent(Base64.encodeToString(file.getSha1Sum(), Base64.NO_WRAP));
+ private void receiveTransportInfo(final JinglePacket jinglePacket) {
+ final FileTransferContentMap contentMap;
+ final GenericTransportInfo transportInfo;
+ try {
+ contentMap = FileTransferContentMap.of(jinglePacket);
+ transportInfo = contentMap.requireOnlyTransportInfo();
+ } catch (final RuntimeException e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": improperly formatted contents",
+ Throwables.getRootCause(e));
+ respondOk(jinglePacket);
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
+ }
+ respondOk(jinglePacket);
+ final var transport = this.transport;
+ if (transport instanceof SocksByteStreamsTransport socksBytestreamsTransport
+ && transportInfo
+ instanceof SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) {
+ receiveTransportInfo(socksBytestreamsTransport, socksBytestreamsTransportInfo);
+ } else if (transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport
+ && transportInfo
+ instanceof WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
+ receiveTransportInfo(
+ Iterables.getOnlyElement(contentMap.contents.keySet()),
+ webRTCDataChannelTransport,
+ webRTCDataChannelTransportInfo);
+ } else if (transportInfo
+ instanceof WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
+ receiveTransportInfo(
+ Iterables.getOnlyElement(contentMap.contents.keySet()),
+ webRTCDataChannelTransportInfo);
+ } else {
+ Log.d(Config.LOGTAG, "could not deliver transport-info to transport");
+ }
+ }
- final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INFO);
- packet.addJingleChild(checksum);
- xmppConnectionService.sendIqPacket(id.account, packet, (account, response) -> {
- if (response.getType() == IqPacket.TYPE.ERROR) {
- Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring error response to our session-info (hash transmission)");
- }
- });
+ private void receiveTransportInfo(
+ final String contentName,
+ final WebRTCDataChannelTransport webRTCDataChannelTransport,
+ final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
+ final var credentials = webRTCDataChannelTransportInfo.getCredentials();
+ final var iceCandidates =
+ WebRTCDataChannelTransport.iceCandidatesOf(
+ contentName, credentials, webRTCDataChannelTransportInfo.getCandidates());
+ final var localContentMap = getLocalContentMap();
+ if (localContentMap == null) {
+ Log.d(Config.LOGTAG, "transport not ready. add pending ice candidate");
+ this.pendingIncomingIceCandidates.addAll(iceCandidates);
+ } else {
+ webRTCDataChannelTransport.addIceCandidates(iceCandidates);
+ }
}
- private Collection<JingleCandidate> getOurCandidates() {
- return Collections2.filter(this.candidates, c -> c != null && c.isOurs());
+ private void receiveTransportInfo(
+ final String contentName,
+ final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
+ final var credentials = webRTCDataChannelTransportInfo.getCredentials();
+ final var iceCandidates =
+ WebRTCDataChannelTransport.iceCandidatesOf(
+ contentName, credentials, webRTCDataChannelTransportInfo.getCandidates());
+ this.pendingIncomingIceCandidates.addAll(iceCandidates);
}
- private void sendAccept() {
- mJingleStatus = JINGLE_STATUS_ACCEPTED;
- this.mStatus = Transferable.STATUS_DOWNLOADING;
- this.jingleConnectionManager.updateConversationUi(true);
- if (initialTransport == S5BTransportInfo.class) {
- sendAcceptSocks();
- } else {
- sendAcceptIbb();
+ private void receiveTransportInfo(
+ final SocksByteStreamsTransport socksBytestreamsTransport,
+ final SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) {
+ final var transportInfo = socksBytestreamsTransportInfo.getTransportInfo();
+ if (transportInfo instanceof SocksByteStreamsTransportInfo.CandidateError) {
+ socksBytestreamsTransport.setCandidateError();
+ } else if (transportInfo
+ instanceof SocksByteStreamsTransportInfo.CandidateUsed candidateUsed) {
+ if (!socksBytestreamsTransport.setCandidateUsed(candidateUsed.cid)) {
+ sendSessionTerminate(
+ Reason.FAILED_TRANSPORT,
+ String.format(
+ "Peer is not connected to our candidate %s", candidateUsed.cid));
+ }
+ } else if (transportInfo instanceof SocksByteStreamsTransportInfo.Activated activated) {
+ socksBytestreamsTransport.setProxyActivated(activated.cid);
+ } else if (transportInfo instanceof SocksByteStreamsTransportInfo.ProxyError) {
+ socksBytestreamsTransport.setProxyError();
}
}
- private void sendAcceptSocks() {
- gatherAndConnectDirectCandidates();
- this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
- final Content content = new Content(contentCreator, contentSenders, contentName);
- content.setDescription(this.description);
- if (success && candidate != null && !equalCandidateExists(candidate)) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
- connections.put(candidate.getCid(), socksConnection);
- socksConnection.connect(new OnTransportConnected() {
+ private void receiveTransportReplace(final JinglePacket jinglePacket) {
+ if (isInitiator()) {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_REPLACE);
+ return;
+ }
+ Log.d(Config.LOGTAG, "receive transport replace " + jinglePacket);
+ final GenericTransportInfo transportInfo;
+ try {
+ transportInfo = FileTransferContentMap.of(jinglePacket).requireOnlyTransportInfo();
+ } catch (final RuntimeException e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": improperly formatted contents",
+ Throwables.getRootCause(e));
+ respondOk(jinglePacket);
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
+ }
+ if (isInState(State.SESSION_ACCEPTED)) {
+ receiveTransportReplace(jinglePacket, transportInfo);
+ } else {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_REPLACE);
+ }
+ }
+ private void receiveTransportReplace(
+ final JinglePacket jinglePacket, final GenericTransportInfo transportInfo) {
+ respondOk(jinglePacket);
+ final Transport transport;
+ try {
+ transport = setupTransport(transportInfo);
+ } catch (final RuntimeException e) {
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
+ }
+ this.transport = transport;
+ this.transport.setTransportCallback(this);
+ final var transportInfoFuture = transport.asTransportInfo();
+ Futures.addCallback(
+ transportInfoFuture,
+ new FutureCallback<>() {
@Override
- public void failed() {
- Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed");
- content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
- packet.addJingleContent(content);
- sendJinglePacket(packet);
- connectNextCandidate();
+ public void onSuccess(final Transport.TransportInfo transportWrapper) {
+ final FileTransferContentMap contentMap =
+ getLocalContentMap().withTransport(transportWrapper);
+ sendTransportAccept(contentMap);
}
@Override
- public void established() {
- Log.d(Config.LOGTAG, "connected to proxy65 candidate");
- mergeCandidate(candidate);
- content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
- packet.addJingleContent(content);
- sendJinglePacket(packet);
- connectNextCandidate();
+ public void onFailure(@NonNull Throwable throwable) {
+ // transition into application failed (analogues to failureToAccept
}
- });
- } else {
- Log.d(Config.LOGTAG, "did not find a proxy65 candidate for ourselves");
- content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
- packet.addJingleContent(content);
- sendJinglePacket(packet);
- connectNextCandidate();
- }
- });
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void sendTransportAccept(final FileTransferContentMap contentMap) {
+ setLocalContentMap(contentMap);
+ final var jinglePacket =
+ contentMap
+ .transportInfo()
+ .toJinglePacket(JinglePacket.Action.TRANSPORT_ACCEPT, id.sessionId);
+ Log.d(Config.LOGTAG, "sending transport accept " + jinglePacket);
+ send(jinglePacket);
+ transport.connect();
+ }
+
+ protected void sendSessionTerminate(final Reason reason, final String text) {
+ if (isInitiator()) {
+ this.message.setErrorMessage(Strings.isNullOrEmpty(text) ? reason.toString() : text);
+ }
+ sendSessionTerminate(reason, text, null);
+ }
+
+ private FileTransferContentMap getLocalContentMap() {
+ return isInitiator()
+ ? this.initiatorFileTransferContentMap
+ : this.responderFileTransferContentMap;
}
- private void sendAcceptIbb() {
- this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
- final Content content = new Content(contentCreator, contentSenders, contentName);
- content.setDescription(this.description);
- content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
- packet.addJingleContent(content);
- this.transport.receive(file, onFileTransmissionStatusChanged);
- this.sendJinglePacket(packet);
+ private FileTransferContentMap getRemoteContentMap() {
+ return isInitiator()
+ ? this.responderFileTransferContentMap
+ : this.initiatorFileTransferContentMap;
}
- private JinglePacket bootstrapPacket(JinglePacket.Action action) {
- final JinglePacket packet = new JinglePacket(action, this.id.sessionId);
- packet.setTo(id.with);
- return packet;
+ private void setLocalContentMap(final FileTransferContentMap contentMap) {
+ if (isInitiator()) {
+ this.initiatorFileTransferContentMap = contentMap;
+ } else {
+ this.responderFileTransferContentMap = contentMap;
+ }
}
- private void sendJinglePacket(JinglePacket packet) {
- xmppConnectionService.sendIqPacket(id.account, packet, responseListener);
+ private void setRemoteContentMap(final FileTransferContentMap contentMap) {
+ if (isInitiator()) {
+ this.responderFileTransferContentMap = contentMap;
+ } else {
+ this.initiatorFileTransferContentMap = contentMap;
+ }
}
- private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
- xmppConnectionService.sendIqPacket(id.account, packet, callback);
+ public Transport getTransport() {
+ return this.transport;
}
- private void receiveAccept(JinglePacket packet) {
- if (responding()) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept (we were responding)");
- respondToIqWithOutOfOrder(packet);
+ @Override
+ protected void terminateTransport() {
+ final var transport = this.transport;
+ if (transport == null) {
return;
}
- if (this.mJingleStatus != JINGLE_STATUS_INITIATED) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept");
- respondToIqWithOutOfOrder(packet);
+ transport.terminate();
+ this.transport = null;
+ }
+
+ @Override
+ void notifyRebound() {}
+
+ @Override
+ public void onTransportEstablished() {
+ Log.d(Config.LOGTAG, "on transport established");
+ final AbstractFileTransceiver fileTransceiver;
+ try {
+ fileTransceiver = setupTransceiver(isResponder());
+ } catch (final Exception e) {
+ Log.d(Config.LOGTAG, "failed to set up file transceiver", e);
+ sendSessionTerminate(Reason.ofThrowable(e), e.getMessage());
return;
}
- this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
- xmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
- final Content content = packet.getJingleContent();
- final GenericTransportInfo transportInfo = content.getTransport();
- //TODO we want to fail if transportInfo doesnβt match our intialTransport and/or our id
- if (transportInfo instanceof S5BTransportInfo) {
- final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo;
- respondToIq(packet, true);
- //TODO calling merge is probably a bug because that might eliminate candidates of the other party and lead to us not sending accept/deny
- //TODO: we probably just want to call add
- mergeCandidates(s5BTransportInfo.getCandidates());
- this.connectNextCandidate();
- } else if (transportInfo instanceof IbbTransportInfo) {
- final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo;
- final int remoteBlockSize = ibbTransportInfo.getBlockSize();
- if (remoteBlockSize > 0) {
- this.ibbBlockSize = Math.min(ibbBlockSize, remoteBlockSize);
- }
- respondToIq(packet, true);
- this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
- this.transport.connect(onIbbTransportConnected);
+ this.fileTransceiver = fileTransceiver;
+ final var fileTransceiverThread = new Thread(fileTransceiver);
+ fileTransceiverThread.start();
+ Futures.addCallback(
+ fileTransceiver.complete,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final List<FileTransferDescription.Hash> hashes) {
+ onFileTransmissionComplete(hashes);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ onFileTransmissionFailed(throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void onFileTransmissionComplete(final List<FileTransferDescription.Hash> hashes) {
+ // TODO if we ever support receiving files this should become isSending(); isReceiving()
+ if (isInitiator()) {
+ sendSessionInfoChecksum(hashes);
} else {
- respondToIq(packet, false);
+ Log.d(Config.LOGTAG, "file transfer complete " + hashes);
+ sendFileSessionInfoReceived();
+ terminateTransport();
+ messageReceivedSuccess();
+ sendSessionTerminate(Reason.SUCCESS, null);
}
}
- private void receiveTransportInfo(JinglePacket packet) {
- final Content content = packet.getJingleContent();
- final GenericTransportInfo transportInfo = content.getTransport();
- if (transportInfo instanceof S5BTransportInfo) {
- final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo;
- if (s5BTransportInfo.hasChild("activated")) {
- respondToIq(packet, true);
- if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
- onProxyActivated.success();
- } else {
- String cid = s5BTransportInfo.findChild("activated").getAttribute("cid");
- Log.d(Config.LOGTAG, "received proxy activated (" + cid
- + ")prior to choosing our own transport");
- JingleSocks5Transport connection = this.connections.get(cid);
- if (connection != null) {
- connection.setActivated(true);
- } else {
- Log.d(Config.LOGTAG, "activated connection not found");
- sendSessionTerminate(Reason.FAILED_TRANSPORT);
- this.fail();
- }
- }
- } else if (s5BTransportInfo.hasChild("proxy-error")) {
- respondToIq(packet, true);
- onProxyActivated.failed();
- } else if (s5BTransportInfo.hasChild("candidate-error")) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received candidate error");
- respondToIq(packet, true);
- this.receivedCandidate = true;
- if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
- this.connect();
- }
- } else if (s5BTransportInfo.hasChild("candidate-used")) {
- String cid = s5BTransportInfo.findChild("candidate-used").getAttribute("cid");
- if (cid != null) {
- Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
- JingleCandidate candidate = getCandidate(cid);
- if (candidate == null) {
- Log.d(Config.LOGTAG, "could not find candidate with cid=" + cid);
- respondToIq(packet, false);
- return;
- }
- respondToIq(packet, true);
- candidate.flagAsUsedByCounterpart();
- this.receivedCandidate = true;
- if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
- this.connect();
- } else {
- Log.d(Config.LOGTAG, "ignoring because file is already in transmission or we haven't sent our candidate yet status=" + mJingleStatus + " sentCandidate=" + sentCandidate);
- }
- } else {
- respondToIq(packet, false);
- }
+ private void messageReceivedSuccess() {
+ this.message.setTransferable(null);
+ xmppConnectionService.getFileBackend().updateFileParams(message);
+ xmppConnectionService.databaseBackend.createMessage(message);
+ final File file = xmppConnectionService.getFileBackend().getFile(message);
+ if (acceptedAutomatically) {
+ message.markUnread();
+ if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ id.account.getPgpDecryptionService().decrypt(message, true);
} else {
- respondToIq(packet, false);
+ xmppConnectionService
+ .getFileBackend()
+ .updateMediaScanner(
+ file,
+ () ->
+ JingleFileTransferConnection.this
+ .xmppConnectionService
+ .getNotificationService()
+ .push(message));
}
+ } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ id.account.getPgpDecryptionService().decrypt(message, false);
} else {
- respondToIq(packet, true);
+ xmppConnectionService.getFileBackend().updateMediaScanner(file);
}
}
- private void connect() {
- final JingleSocks5Transport connection = chooseConnection();
- this.transport = connection;
- if (connection == null) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": could not find suitable candidate");
- this.disconnectSocks5Connections();
- if (isInitiator()) {
- this.sendFallbackToIbb();
- }
+ private void onFileTransmissionFailed(final Throwable throwable) {
+ if (isTerminated()) {
+ Log.d(
+ Config.LOGTAG,
+ "file transfer failed but session is already terminated",
+ throwable);
} else {
- //TODO at this point we can already close other connections to free some resources
- final JingleCandidate candidate = connection.getCandidate();
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": elected candidate " + candidate.toString());
- this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
- if (connection.needsActivation()) {
- if (connection.getCandidate().isOurs()) {
- final String sid;
- if (description.getVersion() == FileTransferDescription.Version.FT_3) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": use session ID instead of transport ID to activate proxy");
- sid = id.sessionId;
- } else {
- sid = getTransportId();
- }
- Log.d(Config.LOGTAG, "candidate "
- + connection.getCandidate().getCid()
- + " was our proxy. going to activate");
- IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
- activation.setTo(connection.getCandidate().getJid());
- activation.query("http://jabber.org/protocol/bytestreams")
- .setAttribute("sid", sid);
- activation.query().addChild("activate")
- .setContent(this.id.with.toEscapedString());
- xmppConnectionService.sendIqPacket(this.id.account, activation, (account, response) -> {
- if (response.getType() != IqPacket.TYPE.RESULT) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": " + response.toString());
- sendProxyError();
- onProxyActivated.failed();
- } else {
- sendProxyActivated(connection.getCandidate().getCid());
- onProxyActivated.success();
- }
- });
- } else {
- Log.d(Config.LOGTAG,
- "candidate "
- + connection.getCandidate().getCid()
- + " was a proxy. waiting for other party to activate");
- }
- } else {
- if (isInitiator()) {
- Log.d(Config.LOGTAG, "we were initiating. sending file");
- connection.send(file, onFileTransmissionStatusChanged);
- } else {
- Log.d(Config.LOGTAG, "we were responding. receiving file");
- connection.receive(file, onFileTransmissionStatusChanged);
- }
- }
+ terminateTransport();
+ Log.d(Config.LOGTAG, "on file transmission failed", throwable);
+ sendSessionTerminate(Reason.CONNECTIVITY_ERROR, null);
}
}
- private JingleSocks5Transport chooseConnection() {
- final List<JingleSocks5Transport> establishedConnections = FluentIterable.from(connections.entrySet())
- .transform(Entry::getValue)
- .filter(c -> (c != null && c.isEstablished() && (c.getCandidate().isUsedByCounterpart() || !c.getCandidate().isOurs())))
- .toSortedList((a, b) -> {
- final int compare = Integer.compare(b.getCandidate().getPriority(), a.getCandidate().getPriority());
- if (compare == 0) {
- if (isInitiator()) {
- //pick the one we sent a candidate-used for (meaning not ours)
- return a.getCandidate().isOurs() ? 1 : -1;
- } else {
- //pick the one they sent a candidate-used for (meaning ours)
- return a.getCandidate().isOurs() ? -1 : 1;
- }
- }
- return compare;
- });
- return Iterables.getFirst(establishedConnections, null);
+ private AbstractFileTransceiver setupTransceiver(final boolean receiving) throws IOException {
+ final var fileDescription = getLocalContentMap().requireOnlyFile();
+ final File file = xmppConnectionService.getFileBackend().getFile(message);
+ final Runnable updateRunnable = () -> jingleConnectionManager.updateConversationUi(false);
+ if (receiving) {
+ return new FileReceiver(
+ file,
+ this.transportSecurity,
+ transport.getInputStream(),
+ transport.getTerminationLatch(),
+ fileDescription.size,
+ updateRunnable);
+ } else {
+ return new FileTransmitter(
+ file,
+ this.transportSecurity,
+ transport.getOutputStream(),
+ transport.getTerminationLatch(),
+ fileDescription.size,
+ updateRunnable);
+ }
}
- private void sendSuccess() {
- sendSessionTerminate(Reason.SUCCESS);
- this.disconnectSocks5Connections();
- this.mJingleStatus = JINGLE_STATUS_FINISHED;
- this.message.setStatus(Message.STATUS_RECEIVED);
- this.message.setTransferable(null);
- this.xmppConnectionService.updateMessage(message, false);
- this.jingleConnectionManager.finishConnection(this);
+ private void sendFileSessionInfoReceived() {
+ final var contentMap = getLocalContentMap();
+ final String name = Iterables.getOnlyElement(contentMap.contents.keySet());
+ sendSessionInfo(new FileTransferDescription.Received(name));
}
- private void sendFallbackToIbb() {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb");
- final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- this.transportId = JingleConnectionManager.nextRandomId();
- content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
- packet.addJingleContent(content);
- this.sendJinglePacket(packet);
+ private void sendSessionInfoChecksum(List<FileTransferDescription.Hash> hashes) {
+ final var contentMap = getLocalContentMap();
+ final String name = Iterables.getOnlyElement(contentMap.contents.keySet());
+ sendSessionInfo(new FileTransferDescription.Checksum(name, hashes));
}
+ private void sendSessionInfo(final FileTransferDescription.SessionInfo sessionInfo) {
+ final var jinglePacket =
+ new JinglePacket(JinglePacket.Action.SESSION_INFO, this.id.sessionId);
+ jinglePacket.addJingleChild(sessionInfo.asElement());
+ jinglePacket.setTo(this.id.with);
+ Log.d(Config.LOGTAG, "--> " + jinglePacket);
+ send(jinglePacket);
+ }
- private void receiveFallbackToIbb(final JinglePacket packet, final IbbTransportInfo transportInfo) {
- if (isInitiator()) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace (we were initiating)");
- respondToIqWithOutOfOrder(packet);
+ @Override
+ public void onTransportSetupFailed() {
+ final var transport = this.transport;
+ if (transport == null) {
+ // this really is not supposed to happen
+ sendSessionTerminate(Reason.FAILED_APPLICATION, null);
return;
}
- final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING);
- if (!validState) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace");
- respondToIqWithOutOfOrder(packet);
+ Log.d(Config.LOGTAG, "onTransportSetupFailed");
+ final var isTransportInBand = transport instanceof InbandBytestreamsTransport;
+ if (isTransportInBand) {
+ terminateTransport();
+ sendSessionTerminate(Reason.CONNECTIVITY_ERROR, "Failed to setup IBB transport");
return;
}
- this.proxyActivationFailed = false; //fallback received; now we no longer need to accept another one;
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": receiving fallback to ibb");
- final int remoteBlockSize = transportInfo.getBlockSize();
- if (remoteBlockSize > 0) {
- this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, remoteBlockSize);
+ // terminate the current transport
+ transport.terminate();
+ if (isInitiator()) {
+ this.transport = setupLastResortTransport();
+ this.transport.setTransportCallback(this);
+ final var transportInfoFuture = this.transport.asTransportInfo();
+ Futures.addCallback(
+ transportInfoFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Transport.TransportInfo transportWrapper) {
+ final FileTransferContentMap contentMap = getLocalContentMap();
+ sendTransportReplace(contentMap.withTransport(transportWrapper));
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ // TODO send application failure;
+ }
+ },
+ MoreExecutors.directExecutor());
+
} else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in transport-replace");
+ Log.d(Config.LOGTAG, "transport setup failed. waiting for initiator to replace");
}
- this.transportId = transportInfo.getTransportId(); //TODO: handle the case where this is null by the remote party
- this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
-
- final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT);
+ }
- final Content content = new Content(contentCreator, contentSenders, contentName);
- content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
- answer.addJingleContent(content);
+ private void sendTransportReplace(final FileTransferContentMap contentMap) {
+ setLocalContentMap(contentMap);
+ final var jinglePacket =
+ contentMap
+ .transportInfo()
+ .toJinglePacket(JinglePacket.Action.TRANSPORT_REPLACE, id.sessionId);
+ Log.d(Config.LOGTAG, "sending transport replace " + jinglePacket);
+ send(jinglePacket);
+ }
- respondToIq(packet, true);
+ @Override
+ public void onAdditionalCandidate(
+ final String contentName, final Transport.Candidate candidate) {
+ if (candidate instanceof IceUdpTransportInfo.Candidate iceCandidate) {
+ sendTransportInfo(contentName, iceCandidate);
+ }
+ }
- if (isInitiator()) {
- this.sendJinglePacket(answer, (account, response) -> {
- if (response.getType() == IqPacket.TYPE.RESULT) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + " recipient ACKed our transport-accept. creating ibb");
- transport.connect(onIbbTransportConnected);
- }
- });
- } else {
- this.transport.receive(file, onFileTransmissionStatusChanged);
- this.sendJinglePacket(answer);
+ public void sendTransportInfo(
+ final String contentName, final IceUdpTransportInfo.Candidate candidate) {
+ final FileTransferContentMap transportInfo;
+ try {
+ final FileTransferContentMap rtpContentMap = getLocalContentMap();
+ transportInfo = rtpContentMap.transportInfo(contentName, candidate);
+ } catch (final Exception e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": unable to prepare transport-info from candidate for content="
+ + contentName);
+ return;
}
+ final JinglePacket jinglePacket =
+ transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
+ Log.d(Config.LOGTAG, "--> " + jinglePacket);
+ send(jinglePacket);
}
- private void receiveTransportAccept(JinglePacket packet) {
- if (responding()) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept (we were responding)");
- respondToIqWithOutOfOrder(packet);
+ @Override
+ public void onCandidateUsed(
+ final String streamId, final SocksByteStreamsTransport.Candidate candidate) {
+ final FileTransferContentMap contentMap = getLocalContentMap();
+ if (contentMap == null) {
+ Log.e(Config.LOGTAG, "local content map is null on candidate used");
return;
}
- final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING);
- if (!validState) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept");
- respondToIqWithOutOfOrder(packet);
+ final var jinglePacket =
+ contentMap
+ .candidateUsed(streamId, candidate.cid)
+ .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
+ Log.d(Config.LOGTAG, "sending candidate used " + jinglePacket);
+ send(jinglePacket);
+ }
+
+ @Override
+ public void onCandidateError(final String streamId) {
+ final FileTransferContentMap contentMap = getLocalContentMap();
+ if (contentMap == null) {
+ Log.e(Config.LOGTAG, "local content map is null on candidate used");
return;
}
- this.proxyActivationFailed = false; //fallback accepted; now we no longer need to accept another one;
- final Content content = packet.getJingleContent();
- final GenericTransportInfo transportInfo = content == null ? null : content.getTransport();
- if (transportInfo instanceof IbbTransportInfo) {
- final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo;
- final int remoteBlockSize = ibbTransportInfo.getBlockSize();
- if (remoteBlockSize > 0) {
- this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, remoteBlockSize);
- }
- final String sid = ibbTransportInfo.getTransportId();
- this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
+ final var jinglePacket =
+ contentMap
+ .candidateError(streamId)
+ .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
+ Log.d(Config.LOGTAG, "sending candidate error " + jinglePacket);
+ send(jinglePacket);
+ }
- if (sid == null || !sid.equals(this.transportId)) {
- Log.w(Config.LOGTAG, String.format("%s: sid in transport-accept (%s) did not match our sid (%s) ", id.account.getJid().asBareJid(), sid, transportId));
- }
- respondToIq(packet, true);
- //might be receive instead if we are not initiating
- if (isInitiator()) {
- this.transport.connect(onIbbTransportConnected);
- }
- } else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received invalid transport-accept");
- respondToIq(packet, false);
+ @Override
+ public void onProxyActivated(String streamId, SocksByteStreamsTransport.Candidate candidate) {
+ final FileTransferContentMap contentMap = getLocalContentMap();
+ if (contentMap == null) {
+ Log.e(Config.LOGTAG, "local content map is null on candidate used");
+ return;
}
+ final var jinglePacket =
+ contentMap
+ .proxyActivated(streamId, candidate.cid)
+ .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
+ send(jinglePacket);
}
- private void receiveSuccess() {
- if (isInitiator()) {
- this.mJingleStatus = JINGLE_STATUS_FINISHED;
- this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED);
- this.disconnectSocks5Connections();
- if (this.transport instanceof JingleInBandTransport) {
- this.transport.disconnect();
+ @Override
+ protected boolean transition(final State target, final Runnable runnable) {
+ final boolean transitioned = super.transition(target, runnable);
+ if (transitioned && isInitiator()) {
+ Log.d(Config.LOGTAG, "running mark message hooks");
+ if (target == State.SESSION_ACCEPTED) {
+ xmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
+ } else if (target == State.TERMINATED_SUCCESS) {
+ xmppConnectionService.markMessage(message, Message.STATUS_SEND_RECEIVED);
+ } else if (TERMINATED.contains(target)) {
+ xmppConnectionService.markMessage(
+ message, Message.STATUS_SEND_FAILED, message.getErrorMessage());
+ } else {
+ xmppConnectionService.updateConversationUi();
}
- this.message.setTransferable(null);
- this.jingleConnectionManager.finishConnection(this);
} else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate/success while responding");
+ if (Arrays.asList(State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY)
+ .contains(target)) {
+ this.message.setTransferable(
+ new TransferablePlaceholder(Transferable.STATUS_CANCELLED));
+ } else if (target != State.TERMINATED_SUCCESS && TERMINATED.contains(target)) {
+ this.message.setTransferable(
+ new TransferablePlaceholder(Transferable.STATUS_FAILED));
+ }
+ xmppConnectionService.updateConversationUi();
}
+ return transitioned;
}
@Override
- public void cancel() {
- this.cancelled = true;
- abort(Reason.CANCEL);
- }
-
- private void abort(final Reason reason) {
- this.disconnectSocks5Connections();
- if (this.transport instanceof JingleInBandTransport) {
- this.transport.disconnect();
+ protected void finish() {
+ if (transport != null) {
+ throw new AssertionError(
+ "finish MUST not be called without terminating the transport first");
}
- sendSessionTerminate(reason);
- this.jingleConnectionManager.finishConnection(this);
- if (responding()) {
- this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
- if (this.file != null) {
- file.delete();
- }
- this.jingleConnectionManager.updateConversationUi(true);
- } else {
- this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : null);
+ // we don't want to remove TransferablePlaceholder
+ if (message.getTransferable() instanceof JingleFileTransferConnection) {
+ Log.d(Config.LOGTAG, "nulling transferable on message");
this.message.setTransferable(null);
}
+ super.finish();
}
- private void fail() {
- fail(null);
- }
-
- private void fail(String errorMessage) {
- this.mJingleStatus = JINGLE_STATUS_FAILED;
- this.disconnectSocks5Connections();
- if (this.transport instanceof JingleInBandTransport) {
- this.transport.disconnect();
- }
- FileBackend.close(mFileInputStream);
- FileBackend.close(mFileOutputStream);
- if (this.message != null) {
- if (responding()) {
- this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
- if (this.file != null) {
- file.delete();
- }
- this.jingleConnectionManager.updateConversationUi(true);
- } else {
- this.xmppConnectionService.markMessage(this.message,
- Message.STATUS_SEND_FAILED,
- cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
- this.message.setTransferable(null);
- }
+ private int getTransferableStatus() {
+ // status in file transfer is a bit weird. for sending it is mostly handled via
+ // Message.STATUS_* (offered, unsend (sic) send_received) the transferable status is just
+ // uploading
+ // for receiving the message status remains at 'received' but Transferable goes through
+ // various status
+ if (isInitiator()) {
+ return Transferable.STATUS_UPLOADING;
}
- this.jingleConnectionManager.finishConnection(this);
+ final var state = getState();
+ return switch (state) {
+ case NULL, SESSION_INITIALIZED, SESSION_INITIALIZED_PRE_APPROVED -> Transferable
+ .STATUS_OFFER;
+ case TERMINATED_APPLICATION_FAILURE,
+ TERMINATED_CONNECTIVITY_ERROR,
+ TERMINATED_DECLINED_OR_BUSY,
+ TERMINATED_SECURITY_ERROR -> Transferable.STATUS_FAILED;
+ case TERMINATED_CANCEL_OR_TIMEOUT -> Transferable.STATUS_CANCELLED;
+ case SESSION_ACCEPTED -> Transferable.STATUS_DOWNLOADING;
+ default -> Transferable.STATUS_UNKNOWN;
+ };
}
- private void sendSessionTerminate(Reason reason) {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_TERMINATE);
- packet.setReason(reason, null);
- this.sendJinglePacket(packet);
- }
+ // these methods are for interacting with 'Transferable' - we might want to remove the concept
+ // at some point
- private void connectNextCandidate() {
- for (JingleCandidate candidate : this.candidates) {
- if ((!connections.containsKey(candidate.getCid()) && (!candidate
- .isOurs()))) {
- this.connectWithCandidate(candidate);
- return;
- }
+ @Override
+ public boolean start() {
+ Log.d(Config.LOGTAG, "user pressed start()");
+ // TODO there is a 'connected' check apparently?
+ if (isInState(State.SESSION_INITIALIZED)) {
+ sendSessionAccept();
}
- this.sendCandidateError();
+ return true;
}
- private void connectWithCandidate(final JingleCandidate candidate) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
- this, candidate);
- connections.put(candidate.getCid(), socksConnection);
- socksConnection.connect(new OnTransportConnected() {
-
- @Override
- public void failed() {
- Log.d(Config.LOGTAG,
- "connection failed with " + candidate.getHost() + ":"
- + candidate.getPort());
- connectNextCandidate();
- }
-
- @Override
- public void established() {
- Log.d(Config.LOGTAG,
- "established connection with " + candidate.getHost()
- + ":" + candidate.getPort());
- sendCandidateUsed(candidate.getCid());
- }
- });
+ @Override
+ public int getStatus() {
+ return getTransferableStatus();
}
- private void disconnectSocks5Connections() {
- Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
- .entrySet().iterator();
- while (it.hasNext()) {
- Entry<String, JingleSocks5Transport> pairs = it.next();
- pairs.getValue().disconnect();
- it.remove();
+ @Override
+ public Long getFileSize() {
+ final var transceiver = this.fileTransceiver;
+ if (transceiver != null) {
+ return transceiver.total;
+ }
+ final var contentMap = this.initiatorFileTransferContentMap;
+ if (contentMap != null) {
+ return contentMap.requireOnlyFile().size;
}
+ return null;
}
- private void sendProxyActivated(String cid) {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid)));
- packet.addJingleContent(content);
- this.sendJinglePacket(packet);
+ @Override
+ public int getProgress() {
+ final var transceiver = this.fileTransceiver;
+ return transceiver != null ? transceiver.getProgress() : 0;
}
- private void sendProxyError() {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error")));
- packet.addJingleContent(content);
- this.sendJinglePacket(packet);
+ @Override
+ public void cancel() {
+ if (stopFileTransfer()) {
+ Log.d(Config.LOGTAG, "user has stopped file transfer");
+ } else {
+ Log.d(Config.LOGTAG, "user pressed cancel but file transfer was already terminated?");
+ }
}
- private void sendCandidateUsed(final String cid) {
- JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid)));
- packet.addJingleContent(content);
- this.sentCandidate = true;
- if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
- connect();
+ private boolean stopFileTransfer() {
+ if (isInitiator()) {
+ return stopFileTransfer(Reason.CANCEL);
+ } else {
+ return stopFileTransfer(Reason.DECLINE);
}
- this.sendJinglePacket(packet);
}
- private void sendCandidateError() {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error");
- JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
- Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error")));
- packet.addJingleContent(content);
- this.sentCandidate = true;
- this.sendJinglePacket(packet);
- if (receivedCandidate && mJingleStatus == JINGLE_STATUS_ACCEPTED) {
- connect();
+ private boolean stopFileTransfer(final Reason reason) {
+ final State target = reasonToState(reason);
+ if (transition(target)) {
+ // we change state before terminating transport so we don't consume the following
+ // IOException and turn it into a connectivity error
+ terminateTransport();
+ final JinglePacket jinglePacket =
+ new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
+ jinglePacket.setReason(reason, "User requested to stop file transfer");
+ send(jinglePacket);
+ finish();
+ return true;
+ } else {
+ return false;
}
}
- private int getJingleStatus() {
- return this.mJingleStatus;
- }
+ private abstract static class AbstractFileTransceiver implements Runnable {
+
+ protected final SettableFuture<List<FileTransferDescription.Hash>> complete =
+ SettableFuture.create();
+
+ protected final File file;
+ protected final TransportSecurity transportSecurity;
+
+ protected final CountDownLatch transportTerminationLatch;
+ protected final long total;
+ protected long transmitted = 0;
+ private int progress = Integer.MIN_VALUE;
+ private final Runnable updateRunnable;
+
+ private AbstractFileTransceiver(
+ final File file,
+ final TransportSecurity transportSecurity,
+ final CountDownLatch transportTerminationLatch,
+ final long total,
+ final Runnable updateRunnable) {
+ this.file = file;
+ this.transportSecurity = transportSecurity;
+ this.transportTerminationLatch = transportTerminationLatch;
+ this.total = transportSecurity == null ? total : (total + 16);
+ this.updateRunnable = updateRunnable;
+ }
- private boolean equalCandidateExists(JingleCandidate candidate) {
- for (JingleCandidate c : this.candidates) {
- if (c.equalValues(candidate)) {
- return true;
+ static void closeTransport(final Closeable stream) {
+ try {
+ stream.close();
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG, "transport has already been closed. good");
}
}
- return false;
- }
- private void mergeCandidate(JingleCandidate candidate) {
- for (JingleCandidate c : this.candidates) {
- if (c.equals(candidate)) {
- return;
- }
+ public int getProgress() {
+ return Ints.saturatedCast(Math.round((1.0 * transmitted / total) * 100));
}
- this.candidates.add(candidate);
- }
- private void mergeCandidates(List<JingleCandidate> candidates) {
- Collections.sort(candidates, (a, b) -> Integer.compare(b.getPriority(), a.getPriority()));
- for (JingleCandidate c : candidates) {
- mergeCandidate(c);
+ public void updateProgress() {
+ final int current = getProgress();
+ final boolean update;
+ synchronized (this) {
+ if (this.progress != current) {
+ this.progress = current;
+ update = true;
+ } else {
+ update = false;
+ }
+ if (update) {
+ this.updateRunnable.run();
+ }
+ }
}
- }
- private JingleCandidate getCandidate(String cid) {
- for (JingleCandidate c : this.candidates) {
- if (c.getCid().equals(cid)) {
- return c;
+ protected void awaitTransportTermination() {
+ try {
+ this.transportTerminationLatch.await();
+ } catch (final InterruptedException ignored) {
+ return;
}
+ Log.d(Config.LOGTAG, getClass().getSimpleName() + " says Goodbye!");
}
- return null;
}
- void updateProgress(int i) {
- this.mProgress = i;
- jingleConnectionManager.updateConversationUi(false);
- }
+ private static class FileTransmitter extends AbstractFileTransceiver {
- String getTransportId() {
- return this.transportId;
- }
+ private final OutputStream outputStream;
- FileTransferDescription.Version getFtVersion() {
- return this.description.getVersion();
- }
+ private FileTransmitter(
+ final File file,
+ final TransportSecurity transportSecurity,
+ final OutputStream outputStream,
+ final CountDownLatch transportTerminationLatch,
+ final long total,
+ final Runnable updateRunnable) {
+ super(file, transportSecurity, transportTerminationLatch, total, updateRunnable);
+ this.outputStream = outputStream;
+ }
- public JingleTransport getTransport() {
- return this.transport;
- }
+ private InputStream openFileInputStream() throws FileNotFoundException {
+ final var fileInputStream = new FileInputStream(this.file);
+ if (this.transportSecurity == null) {
+ return fileInputStream;
+ } else {
+ final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
+ cipher.init(
+ true,
+ new AEADParameters(
+ new KeyParameter(transportSecurity.key),
+ 128,
+ transportSecurity.iv));
+ Log.d(Config.LOGTAG, "setting up CipherInputStream");
+ return new CipherInputStream(fileInputStream, cipher);
+ }
+ }
- public boolean start() {
- if (id.account.getStatus() == Account.State.ONLINE) {
- if (mJingleStatus == JINGLE_STATUS_INITIATED) {
- new Thread(this::sendAccept).start();
+ @Override
+ public void run() {
+ Log.d(Config.LOGTAG, "file transmitter attempting to send " + total + " bytes");
+ final var sha1Hasher = Hashing.sha1().newHasher();
+ final var sha256Hasher = Hashing.sha256().newHasher();
+ try (final var fileInputStream = openFileInputStream()) {
+ final var buffer = new byte[4096];
+ while (total - transmitted > 0) {
+ final int count = fileInputStream.read(buffer);
+ if (count == -1) {
+ throw new EOFException(
+ String.format("reached EOF after %d/%d", transmitted, total));
+ }
+ outputStream.write(buffer, 0, count);
+ sha1Hasher.putBytes(buffer, 0, count);
+ sha256Hasher.putBytes(buffer, 0, count);
+ transmitted += count;
+ updateProgress();
+ }
+ outputStream.flush();
+ Log.d(
+ Config.LOGTAG,
+ "transmitted " + transmitted + " bytes from " + file.getAbsolutePath());
+ final List<FileTransferDescription.Hash> hashes =
+ ImmutableList.of(
+ new FileTransferDescription.Hash(
+ sha1Hasher.hash().asBytes(),
+ FileTransferDescription.Algorithm.SHA_1),
+ new FileTransferDescription.Hash(
+ sha256Hasher.hash().asBytes(),
+ FileTransferDescription.Algorithm.SHA_256));
+ complete.set(hashes);
+ } catch (final Exception e) {
+ complete.setException(e);
}
- return true;
- } else {
- return false;
+ // the transport implementations backed by PipedOutputStreams do not like it when
+ // the writing Thread (this thread) goes away. so we just wait until the other peer
+ // has received our file and we are shutting down the transport
+ Log.d(Config.LOGTAG, "waiting for transport to terminate before stopping thread");
+ awaitTransportTermination();
+ closeTransport(outputStream);
}
}
- @Override
- public int getStatus() {
- return this.mStatus;
- }
+ private static class FileReceiver extends AbstractFileTransceiver {
- @Override
- public Long getFileSize() {
- if (this.file != null) {
- return this.file.getExpectedSize();
- } else {
- return null;
+ private final InputStream inputStream;
+
+ private FileReceiver(
+ final File file,
+ final TransportSecurity transportSecurity,
+ final InputStream inputStream,
+ final CountDownLatch transportTerminationLatch,
+ final long total,
+ final Runnable updateRunnable) {
+ super(file, transportSecurity, transportTerminationLatch, total, updateRunnable);
+ this.inputStream = inputStream;
}
- }
- @Override
- public int getProgress() {
- return this.mProgress;
- }
+ private OutputStream openFileOutputStream() throws FileNotFoundException {
+ final var directory = this.file.getParentFile();
+ if (directory != null && directory.mkdirs()) {
+ Log.d(Config.LOGTAG, "created directory " + directory.getAbsolutePath());
+ }
+ final var fileOutputStream = new FileOutputStream(this.file);
+ if (this.transportSecurity == null) {
+ return fileOutputStream;
+ } else {
+ final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
+ cipher.init(
+ false,
+ new AEADParameters(
+ new KeyParameter(transportSecurity.key),
+ 128,
+ transportSecurity.iv));
+ Log.d(Config.LOGTAG, "setting up CipherOutputStream");
+ return new CipherOutputStream(fileOutputStream, cipher);
+ }
+ }
- AbstractConnectionManager getConnectionManager() {
- return this.jingleConnectionManager;
+ @Override
+ public void run() {
+ Log.d(Config.LOGTAG, "file receiver attempting to receive " + total + " bytes");
+ final var sha1Hasher = Hashing.sha1().newHasher();
+ final var sha256Hasher = Hashing.sha256().newHasher();
+ try (final var fileOutputStream = openFileOutputStream()) {
+ final var buffer = new byte[4096];
+ while (total - transmitted > 0) {
+ final int count = inputStream.read(buffer);
+ if (count == -1) {
+ throw new EOFException(
+ String.format("reached EOF after %d/%d", transmitted, total));
+ }
+ fileOutputStream.write(buffer, 0, count);
+ sha1Hasher.putBytes(buffer, 0, count);
+ sha256Hasher.putBytes(buffer, 0, count);
+ transmitted += count;
+ updateProgress();
+ }
+ Log.d(
+ Config.LOGTAG,
+ "written " + transmitted + " bytes to " + file.getAbsolutePath());
+ final List<FileTransferDescription.Hash> hashes =
+ ImmutableList.of(
+ new FileTransferDescription.Hash(
+ sha1Hasher.hash().asBytes(),
+ FileTransferDescription.Algorithm.SHA_1),
+ new FileTransferDescription.Hash(
+ sha256Hasher.hash().asBytes(),
+ FileTransferDescription.Algorithm.SHA_256));
+ complete.set(hashes);
+ } catch (final Exception e) {
+ complete.setException(e);
+ }
+ Log.d(Config.LOGTAG, "waiting for transport to terminate before stopping thread");
+ awaitTransportTermination();
+ closeTransport(inputStream);
+ }
}
- interface OnProxyActivated {
- void success();
+ private static final class TransportSecurity {
+ final byte[] key;
+ final byte[] iv;
- void failed();
+ private TransportSecurity(byte[] key, byte[] iv) {
+ this.key = key;
+ this.iv = iv;
+ }
}
}
@@ -1,265 +0,0 @@
-package eu.siacs.conversations.xmpp.jingle;
-
-import android.util.Base64;
-import android.util.Log;
-
-import com.google.common.base.Preconditions;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.persistance.FileBackend;
-import eu.siacs.conversations.services.AbstractConnectionManager;
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-
-public class JingleInBandTransport extends JingleTransport {
-
- private final Account account;
- private final Jid counterpart;
- private final int blockSize;
- private int seq = 0;
- private final String sessionId;
-
- private boolean established = false;
-
- private boolean connected = true;
-
- private DownloadableFile file;
- private final JingleFileTransferConnection connection;
-
- private InputStream fileInputStream = null;
- private InputStream innerInputStream = null;
- private OutputStream fileOutputStream = null;
- private long remainingSize = 0;
- private long fileSize = 0;
- private MessageDigest digest;
-
- private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
-
- private final OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
- @Override
- public void onIqPacketReceived(Account account, IqPacket packet) {
- if (!connected) {
- return;
- }
- if (packet.getType() == IqPacket.TYPE.RESULT) {
- if (remainingSize > 0) {
- sendNextBlock();
- }
- } else if (packet.getType() == IqPacket.TYPE.ERROR) {
- onFileTransmissionStatusChanged.onFileTransferAborted();
- }
- }
- };
-
- JingleInBandTransport(final JingleFileTransferConnection connection, final String sid, final int blockSize) {
- this.connection = connection;
- this.account = connection.getId().account;
- this.counterpart = connection.getId().with;
- this.blockSize = blockSize;
- this.sessionId = sid;
- }
-
- private void sendClose() {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending ibb close");
- IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
- iq.setTo(this.counterpart);
- Element close = iq.addChild("close", "http://jabber.org/protocol/ibb");
- close.setAttribute("sid", this.sessionId);
- this.account.getXmppConnection().sendIqPacket(iq, null);
- }
-
- public boolean matches(final Account account, final String sessionId) {
- return this.account == account && this.sessionId.equals(sessionId);
- }
-
- public void connect(final OnTransportConnected callback) {
- IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
- iq.setTo(this.counterpart);
- Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
- open.setAttribute("sid", this.sessionId);
- open.setAttribute("stanza", "iq");
- open.setAttribute("block-size", Integer.toString(this.blockSize));
- this.connected = true;
- this.account.getXmppConnection().sendIqPacket(iq, (account, packet) -> {
- if (packet.getType() != IqPacket.TYPE.RESULT) {
- callback.failed();
- } else {
- callback.established();
- }
- });
- }
-
- @Override
- public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
- this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback);
- this.file = file;
- try {
- this.digest = MessageDigest.getInstance("SHA-1");
- digest.reset();
- this.fileOutputStream = connection.getFileOutputStream();
- if (this.fileOutputStream == null) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not create output stream");
- callback.onFileTransferAborted();
- return;
- }
- this.remainingSize = this.fileSize = file.getExpectedSize();
- } catch (final NoSuchAlgorithmException | IOException e) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + " " + e.getMessage());
- callback.onFileTransferAborted();
- }
- }
-
- @Override
- public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
- this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback);
- this.file = file;
- try {
- this.remainingSize = this.file.getExpectedSize();
- this.fileSize = this.remainingSize;
- this.digest = MessageDigest.getInstance("SHA-1");
- this.digest.reset();
- fileInputStream = connection.getFileInputStream();
- if (fileInputStream == null) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could no create input stream");
- callback.onFileTransferAborted();
- return;
- }
- innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
- if (this.connected) {
- this.sendNextBlock();
- }
- } catch (Exception e) {
- callback.onFileTransferAborted();
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
- }
- }
-
- @Override
- public void disconnect() {
- this.connected = false;
- FileBackend.close(fileOutputStream);
- FileBackend.close(fileInputStream);
- }
-
- private void sendNextBlock() {
- byte[] buffer = new byte[this.blockSize];
- try {
- int count = innerInputStream.read(buffer);
- if (count == -1) {
- sendClose();
- file.setSha1Sum(digest.digest());
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sendNextBlock() count was -1");
- this.onFileTransmissionStatusChanged.onFileTransmitted(file);
- fileInputStream.close();
- return;
- } else if (count != buffer.length) {
- int rem = innerInputStream.read(buffer, count, buffer.length - count);
- if (rem > 0) {
- count += rem;
- }
- }
- this.remainingSize -= count;
- this.digest.update(buffer, 0, count);
- String base64 = Base64.encodeToString(buffer, 0, count, Base64.NO_WRAP);
- IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
- iq.setTo(this.counterpart);
- Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
- data.setAttribute("seq", Integer.toString(this.seq));
- data.setAttribute("block-size", Integer.toString(this.blockSize));
- data.setAttribute("sid", this.sessionId);
- data.setContent(base64);
- this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
- this.account.getXmppConnection().r(); //don't fill up stanza queue too much
- this.seq++;
- connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
- if (this.remainingSize <= 0) {
- file.setSha1Sum(digest.digest());
- this.onFileTransmissionStatusChanged.onFileTransmitted(file);
- sendClose();
- fileInputStream.close();
- }
- } catch (IOException e) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during sendNextBlock() " + e.getMessage());
- FileBackend.close(fileInputStream);
- this.onFileTransmissionStatusChanged.onFileTransferAborted();
- }
- }
-
- private void receiveNextBlock(String data) {
- try {
- byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
- if (this.remainingSize < buffer.length) {
- buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
- }
- this.remainingSize -= buffer.length;
- this.fileOutputStream.write(buffer);
- this.digest.update(buffer);
- if (this.remainingSize <= 0) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received last block. waiting for close");
- } else {
- connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
- }
- } catch (Exception e) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage(), e);
- FileBackend.close(fileOutputStream);
- this.onFileTransmissionStatusChanged.onFileTransferAborted();
- }
- }
-
- private void done() {
- try {
- file.setSha1Sum(digest.digest());
- fileOutputStream.flush();
- fileOutputStream.close();
- this.onFileTransmissionStatusChanged.onFileTransmitted(file);
- } catch (Exception e) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
- FileBackend.close(fileOutputStream);
- this.onFileTransmissionStatusChanged.onFileTransferAborted();
- }
- }
-
- void deliverPayload(IqPacket packet, Element payload) {
- if (payload.getName().equals("open")) {
- if (!established) {
- established = true;
- connected = true;
- this.receiveNextBlock("");
- this.account.getXmppConnection().sendIqPacket(
- packet.generateResponse(IqPacket.TYPE.RESULT), null);
- } else {
- this.account.getXmppConnection().sendIqPacket(
- packet.generateResponse(IqPacket.TYPE.ERROR), null);
- }
- } else if (connected && payload.getName().equals("data")) {
- this.receiveNextBlock(payload.getContent());
- this.account.getXmppConnection().sendIqPacket(
- packet.generateResponse(IqPacket.TYPE.RESULT), null);
- } else if (connected && payload.getName().equals("close")) {
- this.connected = false;
- this.account.getXmppConnection().sendIqPacket(
- packet.generateResponse(IqPacket.TYPE.RESULT), null);
- if (this.remainingSize <= 0) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close. done");
- done();
- } else {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close with " + this.remainingSize + " remaining");
- FileBackend.close(fileOutputStream);
- this.onFileTransmissionStatusChanged.onFileTransferAborted();
- }
- } else {
- this.account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
- }
- }
-}
@@ -13,7 +13,6 @@ import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
@@ -30,14 +29,10 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.RtpSessionStatus;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.utils.IP;
import eu.siacs.conversations.xml.Element;
@@ -78,96 +73,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
Arrays.asList(
State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED);
private static final long BUSY_TIME_OUT = 30;
- private static final List<State> TERMINATED =
- Arrays.asList(
- State.ACCEPTED,
- State.REJECTED,
- State.REJECTED_RACED,
- State.RETRACTED,
- State.RETRACTED_RACED,
- State.TERMINATED_SUCCESS,
- State.TERMINATED_DECLINED_OR_BUSY,
- State.TERMINATED_CONNECTIVITY_ERROR,
- State.TERMINATED_CANCEL_OR_TIMEOUT,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR);
-
- private static final Map<State, Collection<State>> VALID_TRANSITIONS;
-
- static {
- final ImmutableMap.Builder<State, Collection<State>> transitionBuilder =
- new ImmutableMap.Builder<>();
- transitionBuilder.put(
- State.NULL,
- ImmutableList.of(
- State.PROPOSED,
- State.SESSION_INITIALIZED,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR));
- transitionBuilder.put(
- State.PROPOSED,
- ImmutableList.of(
- State.ACCEPTED,
- State.PROCEED,
- State.REJECTED,
- State.RETRACTED,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR,
- State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection
- // rebinds
- ));
- transitionBuilder.put(
- State.PROCEED,
- ImmutableList.of(
- State.REJECTED_RACED,
- State.RETRACTED_RACED,
- State.SESSION_INITIALIZED_PRE_APPROVED,
- State.TERMINATED_SUCCESS,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR,
- State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error
- // bounces of the proceed message
- ));
- transitionBuilder.put(
- State.SESSION_INITIALIZED,
- ImmutableList.of(
- State.SESSION_ACCEPTED,
- State.TERMINATED_SUCCESS,
- State.TERMINATED_DECLINED_OR_BUSY,
- State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
- // and IQ timeouts
- State.TERMINATED_CANCEL_OR_TIMEOUT,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR));
- transitionBuilder.put(
- State.SESSION_INITIALIZED_PRE_APPROVED,
- ImmutableList.of(
- State.SESSION_ACCEPTED,
- State.TERMINATED_SUCCESS,
- State.TERMINATED_DECLINED_OR_BUSY,
- State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
- // and IQ timeouts
- State.TERMINATED_CANCEL_OR_TIMEOUT,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR));
- transitionBuilder.put(
- State.SESSION_ACCEPTED,
- ImmutableList.of(
- State.TERMINATED_SUCCESS,
- State.TERMINATED_DECLINED_OR_BUSY,
- State.TERMINATED_CONNECTIVITY_ERROR,
- State.TERMINATED_CANCEL_OR_TIMEOUT,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR));
- VALID_TRANSITIONS = transitionBuilder.build();
- }
private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
- private final Queue<Map.Entry<String, RtpContentMap.DescriptionTransport>>
+ private final Queue<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>>
pendingIceCandidates = new LinkedList<>();
private final OmemoVerification omemoVerification = new OmemoVerification();
private final Message message;
- private State state = State.NULL;
+
private Set<Media> proposedMedia;
private RtpContentMap initiatorRtpContentMap;
private RtpContentMap responderRtpContentMap;
@@ -192,18 +104,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
id.sessionId);
}
- private static State reasonToState(Reason reason) {
- return switch (reason) {
- case SUCCESS -> State.TERMINATED_SUCCESS;
- case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
- case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
- case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
- case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State
- .TERMINATED_APPLICATION_FAILURE;
- default -> State.TERMINATED_CONNECTIVITY_ERROR;
- };
- }
-
@Override
synchronized void deliverPacket(final JinglePacket jinglePacket) {
switch (jinglePacket.getAction()) {
@@ -233,7 +133,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
return;
}
webRTCWrapper.close();
- if (!isInitiator() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) {
+ if (isResponder() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) {
xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
}
if (isInState(
@@ -322,7 +222,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void receiveTransportInfo(
final JinglePacket jinglePacket, final RtpContentMap contentMap) {
- final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates =
+ final Set<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>> candidates =
contentMap.contents.entrySet();
final RtpContentMap remote = getRemoteContentMap();
final Set<String> remoteContentIds =
@@ -522,7 +422,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
setRemoteContentMap(modifiedContentMap);
- final SessionDescription answer = SessionDescription.of(modifiedContentMap, !isInitiator());
+ final SessionDescription answer = SessionDescription.of(modifiedContentMap, isResponder());
final org.webrtc.SessionDescription sdp =
new org.webrtc.SessionDescription(
@@ -596,7 +496,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
final SessionDescription offer;
try {
- offer = SessionDescription.of(modifiedRemoteContentMap, !isInitiator());
+ offer = SessionDescription.of(modifiedRemoteContentMap, isResponder());
} catch (final IllegalArgumentException | NullPointerException e) {
Log.d(
Config.LOGTAG,
@@ -815,7 +715,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final RtpContentMap nextRemote =
currentRemote.addContent(
patch.modifiedSenders(Content.Senders.NONE), getPeerDtlsSetup());
- return SessionDescription.of(nextRemote, !isInitiator());
+ return SessionDescription.of(nextRemote, isResponder());
}
throw new IllegalStateException(
"Unexpected rollback condition. Senders were not uniformly none");
@@ -881,7 +781,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final SessionDescription offer;
try {
- offer = SessionDescription.of(modifiedContentMap, !isInitiator());
+ offer = SessionDescription.of(modifiedContentMap, isResponder());
} catch (final IllegalArgumentException | NullPointerException e) {
Log.d(
Config.LOGTAG,
@@ -1066,7 +966,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final boolean isOffer)
throws ExecutionException, InterruptedException {
final SessionDescription sessionDescription =
- SessionDescription.of(restartContentMap, !isInitiator());
+ SessionDescription.of(restartContentMap, isResponder());
final org.webrtc.SessionDescription.Type type =
isOffer
? org.webrtc.SessionDescription.Type.OFFER
@@ -1095,14 +995,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void processCandidates(
- final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
- for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) {
+ final Set<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>> contents) {
+ for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : contents) {
processCandidate(content);
}
}
private void processCandidate(
- final Map.Entry<String, RtpContentMap.DescriptionTransport> content) {
+ final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content) {
final RtpContentMap rtpContentMap = getRemoteContentMap();
final List<String> indices = toIdentificationTags(rtpContentMap);
final String sdpMid = content.getKey(); // aka content name
@@ -1204,21 +1104,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void receiveSessionInitiate(final JinglePacket jinglePacket) {
if (isInitiator()) {
- Log.d(
- Config.LOGTAG,
- String.format(
- "%s: received session-initiate even though we were initiating",
- id.account.getJid().asBareJid()));
- if (isTerminated()) {
- Log.d(
- Config.LOGTAG,
- String.format(
- "%s: got a reason to terminate with out-of-order. but already in state %s",
- id.account.getJid().asBareJid(), getState()));
- respondWithOutOfOrder(jinglePacket);
- } else {
- terminateWithOutOfOrder(jinglePacket);
- }
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
return;
}
final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false);
@@ -1300,13 +1186,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void receiveSessionAccept(final JinglePacket jinglePacket) {
- if (!isInitiator()) {
- Log.d(
- Config.LOGTAG,
- String.format(
- "%s: received session-accept even though we were responding",
- id.account.getJid().asBareJid()));
- terminateWithOutOfOrder(jinglePacket);
+ if (isResponder()) {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
return;
}
final ListenableFuture<RtpContentMap> future =
@@ -1491,7 +1372,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void addIceCandidatesFromBlackLog() {
- Map.Entry<String, RtpContentMap.DescriptionTransport> foo;
+ Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> foo;
while ((foo = this.pendingIceCandidates.poll()) != null) {
processCandidate(foo);
Log.d(
@@ -2061,24 +1942,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
- private void sendSessionTerminate(final Reason reason) {
+ protected void sendSessionTerminate(final Reason reason) {
sendSessionTerminate(reason, null);
}
- private void sendSessionTerminate(final Reason reason, final String text) {
- final State previous = this.state;
- final State target = reasonToState(reason);
- transitionOrThrow(target);
- if (previous != State.NULL) {
- writeLogMessage(target);
- }
- final JinglePacket jinglePacket =
- new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
- jinglePacket.setReason(reason, text);
- send(jinglePacket);
- finish();
+
+ protected void sendSessionTerminate(final Reason reason, final String text) {
+ sendSessionTerminate(reason,text, this::writeLogMessage);
}
+
private void sendTransportInfo(
final String contentName, IceUdpTransportInfo.Candidate candidate) {
final RtpContentMap transportInfo;
@@ -2099,110 +1972,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
send(jinglePacket);
}
- private void send(final JinglePacket jinglePacket) {
- jinglePacket.setTo(id.with);
- xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
- }
-
- private synchronized void handleIqResponse(final Account account, final IqPacket response) {
- if (response.getType() == IqPacket.TYPE.ERROR) {
- handleIqErrorResponse(response);
- return;
- }
- if (response.getType() == IqPacket.TYPE.TIMEOUT) {
- handleIqTimeoutResponse(response);
- }
- }
-
- private void handleIqErrorResponse(final IqPacket response) {
- Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
- final String errorCondition = response.getErrorCondition();
- Log.d(
- Config.LOGTAG,
- id.account.getJid().asBareJid()
- + ": received IQ-error from "
- + response.getFrom()
- + " in RTP session. "
- + errorCondition);
- if (isTerminated()) {
- Log.i(
- Config.LOGTAG,
- id.account.getJid().asBareJid()
- + ": ignoring error because session was already terminated");
- return;
- }
- this.webRTCWrapper.close();
- final State target;
- if (Arrays.asList(
- "service-unavailable",
- "recipient-unavailable",
- "remote-server-not-found",
- "remote-server-timeout")
- .contains(errorCondition)) {
- target = State.TERMINATED_CONNECTIVITY_ERROR;
- } else {
- target = State.TERMINATED_APPLICATION_FAILURE;
- }
- transitionOrThrow(target);
- this.finish();
- }
-
- private void handleIqTimeoutResponse(final IqPacket response) {
- Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT);
- Log.d(
- Config.LOGTAG,
- id.account.getJid().asBareJid()
- + ": received IQ timeout in RTP session with "
- + id.with
- + ". terminating with connectivity error");
- if (isTerminated()) {
- Log.i(
- Config.LOGTAG,
- id.account.getJid().asBareJid()
- + ": ignoring error because session was already terminated");
- return;
- }
- this.webRTCWrapper.close();
- transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
- this.finish();
- }
-
- private void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
- Log.d(
- Config.LOGTAG,
- id.account.getJid().asBareJid() + ": terminating session with out-of-order");
- this.webRTCWrapper.close();
- transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
- respondWithOutOfOrder(jinglePacket);
- this.finish();
- }
-
- private void respondWithTieBreak(final JinglePacket jinglePacket) {
- respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
- }
-
- private void respondWithOutOfOrder(final JinglePacket jinglePacket) {
- respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
- }
-
- private void respondWithItemNotFound(final JinglePacket jinglePacket) {
- respondWithJingleError(jinglePacket, null, "item-not-found", "cancel");
- }
-
- void respondWithJingleError(
- final IqPacket original,
- String jingleCondition,
- String condition,
- String conditionType) {
- jingleConnectionManager.respondWithJingleError(
- id.account, original, jingleCondition, condition, conditionType);
- }
-
- private void respondOk(final JinglePacket jinglePacket) {
- xmppConnectionService.sendIqPacket(
- id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
- }
-
public RtpEndUserState getEndUserState() {
switch (this.state) {
case NULL, PROPOSED, SESSION_INITIALIZED -> {
@@ -2398,7 +2167,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ ": received endCall() when session has already been terminated. nothing to do");
return;
}
- if (isInState(State.PROPOSED) && !isInitiator()) {
+ if (isInState(State.PROPOSED) && isResponder()) {
rejectCallFromProposed();
return;
}
@@ -2527,22 +2296,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
sendSessionAccept();
}
- private synchronized boolean isInState(State... state) {
- return Arrays.asList(state).contains(this.state);
- }
-
- private boolean transition(final State target) {
- return transition(target, null);
- }
- private synchronized boolean transition(final State target, final Runnable runnable) {
- final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
- if (validTransitions != null && validTransitions.contains(target)) {
- this.state = target;
- if (runnable != null) {
- runnable.run();
- }
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
+ @Override
+ protected synchronized boolean transition(final State target, final Runnable runnable) {
+ if (super.transition(target, runnable)) {
updateEndUserState();
updateOngoingCallNotification();
return true;
@@ -2551,13 +2308,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
- void transitionOrThrow(final State target) {
- if (!transition(target)) {
- throw new IllegalStateException(
- String.format("Unable to transition from %s to %s", this.state, target));
- }
- }
-
@Override
public void onIceCandidate(final IceCandidate iceCandidate) {
final RtpContentMap rtpContentMap =
@@ -2893,98 +2643,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
id.account,
request,
(account, response) -> {
- ImmutableList.Builder<PeerConnection.IceServer> listBuilder =
- new ImmutableList.Builder<>();
- if (response.getType() == IqPacket.TYPE.RESULT) {
- final Element services =
- response.findChild(
- "services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
- final List<Element> children =
- services == null
- ? Collections.emptyList()
- : services.getChildren();
- for (final Element child : children) {
- if ("service".equals(child.getName())) {
- final String type = child.getAttribute("type");
- final String host = child.getAttribute("host");
- final String sport = child.getAttribute("port");
- final Integer port =
- sport == null ? null : Ints.tryParse(sport);
- final String transport = child.getAttribute("transport");
- final String username = child.getAttribute("username");
- final String password = child.getAttribute("password");
- if (Strings.isNullOrEmpty(host) || port == null) {
- continue;
- }
- if (port < 0 || port > 65535) {
- continue;
- }
-
- if (Arrays.asList("stun", "stuns", "turn", "turns")
- .contains(type)
- && Arrays.asList("udp", "tcp").contains(transport)) {
- if (Arrays.asList("stuns", "turns").contains(type)
- && "udp".equals(transport)) {
- Log.d(
- Config.LOGTAG,
- id.account.getJid().asBareJid()
- + ": skipping invalid combination of udp/tls in external services");
- continue;
- }
-
- // STUN URLs do not support a query section since M110
- final String uri;
- if (Arrays.asList("stun", "stuns").contains(type)) {
- uri =
- String.format(
- "%s:%s:%s",
- type, IP.wrapIPv6(host), port);
- } else {
- uri =
- String.format(
- "%s:%s:%s?transport=%s",
- type,
- IP.wrapIPv6(host),
- port,
- transport);
- }
-
- final PeerConnection.IceServer.Builder iceServerBuilder =
- PeerConnection.IceServer.builder(uri);
- iceServerBuilder.setTlsCertPolicy(
- PeerConnection.TlsCertPolicy
- .TLS_CERT_POLICY_INSECURE_NO_CHECK);
- if (username != null && password != null) {
- iceServerBuilder.setUsername(username);
- iceServerBuilder.setPassword(password);
- } else if (Arrays.asList("turn", "turns").contains(type)) {
- // The WebRTC spec requires throwing an
- // InvalidAccessError when username (from libwebrtc
- // source coder)
- // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc
- Log.d(
- Config.LOGTAG,
- id.account.getJid().asBareJid()
- + ": skipping "
- + type
- + "/"
- + transport
- + " without username and password");
- continue;
- }
- final PeerConnection.IceServer iceServer =
- iceServerBuilder.createIceServer();
- Log.d(
- Config.LOGTAG,
- id.account.getJid().asBareJid()
- + ": discovered ICE Server: "
- + iceServer);
- listBuilder.add(iceServer);
- }
- }
- }
- }
- final List<PeerConnection.IceServer> iceServers = listBuilder.build();
+ final var iceServers = IceServers.parse(response);
if (iceServers.size() == 0) {
Log.w(
Config.LOGTAG,
@@ -3001,13 +2660,19 @@ public class JingleRtpConnection extends AbstractJingleConnection
onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList());
}
}
+
+ @Override
+ protected void terminateTransport() {
+ this.webRTCWrapper.close();
+ }
- private void finish() {
+ @Override
+ protected void finish() {
if (isTerminated()) {
this.cancelRingingTimeout();
this.webRTCWrapper.verifyClosed();
this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
- this.jingleConnectionManager.finishConnectionOrThrow(this);
+ super.finish();
} else {
throw new IllegalStateException(
String.format("Unable to call finish from %s", this.state));
@@ -3045,14 +2710,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
- public State getState() {
- return this.state;
- }
-
- boolean isTerminated() {
- return TERMINATED.contains(this.state);
- }
-
public Optional<VideoTrack> getLocalVideoTrack() {
return webRTCWrapper.getLocalVideoTrack();
}
@@ -3091,17 +2748,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
return remoteHasFeature(Namespace.SDP_OFFER_ANSWER);
}
- private boolean remoteHasFeature(final String feature) {
- final Contact contact = id.getContact();
- final Presence presence =
- contact.getPresences().get(Strings.nullToEmpty(id.with.getResource()));
- final ServiceDiscoveryResult serviceDiscoveryResult =
- presence == null ? null : presence.getServiceDiscoveryResult();
- final List<String> features =
- serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
- return features != null && features.contains(feature);
- }
-
private interface OnIceServersDiscovered {
void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
}
@@ -1,305 +0,0 @@
-package eu.siacs.conversations.xmpp.jingle;
-
-import android.os.PowerManager;
-import android.util.Log;
-
-import com.google.common.io.ByteStreams;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.SocketAddress;
-import java.nio.ByteBuffer;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.persistance.FileBackend;
-import eu.siacs.conversations.services.AbstractConnectionManager;
-import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.utils.SocksSocketFactory;
-import eu.siacs.conversations.utils.WakeLockHelper;
-import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
-
-public class JingleSocks5Transport extends JingleTransport {
-
- private static final int SOCKET_TIMEOUT_DIRECT = 3000;
- private static final int SOCKET_TIMEOUT_PROXY = 5000;
-
- private final JingleCandidate candidate;
- private final JingleFileTransferConnection connection;
- private final String destination;
- private final Account account;
- private OutputStream outputStream;
- private InputStream inputStream;
- private boolean isEstablished = false;
- private boolean activated = false;
- private ServerSocket serverSocket;
- private Socket socket;
-
- JingleSocks5Transport(JingleFileTransferConnection jingleConnection, JingleCandidate candidate) {
- final MessageDigest messageDigest;
- try {
- messageDigest = MessageDigest.getInstance("SHA-1");
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(e);
- }
- this.candidate = candidate;
- this.connection = jingleConnection;
- this.account = jingleConnection.getId().account;
- final StringBuilder destBuilder = new StringBuilder();
- if (this.connection.getFtVersion() == FileTransferDescription.Version.FT_3) {
- Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination");
- destBuilder.append(this.connection.getId().sessionId);
- } else {
- destBuilder.append(this.connection.getTransportId());
- }
- if (candidate.isOurs()) {
- destBuilder.append(this.account.getJid());
- destBuilder.append(this.connection.getId().with);
- } else {
- destBuilder.append(this.connection.getId().with);
- destBuilder.append(this.account.getJid());
- }
- messageDigest.reset();
- this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes()));
- if (candidate.isOurs() && candidate.getType() == JingleCandidate.TYPE_DIRECT) {
- createServerSocket();
- }
- }
-
- private void createServerSocket() {
- try {
- serverSocket = new ServerSocket();
- serverSocket.bind(new InetSocketAddress(InetAddress.getByName(candidate.getHost()), candidate.getPort()));
- new Thread(() -> {
- try {
- final Socket socket = serverSocket.accept();
- new Thread(() -> {
- try {
- acceptIncomingSocketConnection(socket);
- } catch (IOException e) {
- Log.d(Config.LOGTAG, "unable to read from socket", e);
-
- }
- }).start();
- } catch (IOException e) {
- if (!serverSocket.isClosed()) {
- Log.d(Config.LOGTAG, "unable to accept socket", e);
- }
- }
- }).start();
- } catch (IOException e) {
- Log.d(Config.LOGTAG, "unable to bind server socket ", e);
- }
- }
-
- private void acceptIncomingSocketConnection(final Socket socket) throws IOException {
- Log.d(Config.LOGTAG, "accepted connection from " + socket.getInetAddress().getHostAddress());
- socket.setSoTimeout(SOCKET_TIMEOUT_DIRECT);
- final byte[] authBegin = new byte[2];
- final InputStream inputStream = socket.getInputStream();
- final OutputStream outputStream = socket.getOutputStream();
- ByteStreams.readFully(inputStream, authBegin);
- if (authBegin[0] != 0x5) {
- socket.close();
- }
- final short methodCount = authBegin[1];
- final byte[] methods = new byte[methodCount];
- ByteStreams.readFully(inputStream, methods);
- if (SocksSocketFactory.contains((byte) 0x00, methods)) {
- outputStream.write(new byte[]{0x05, 0x00});
- } else {
- outputStream.write(new byte[]{0x05, (byte) 0xff});
- }
- final byte[] connectCommand = new byte[4];
- ByteStreams.readFully(inputStream, connectCommand);
- if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) {
- int destinationCount = inputStream.read();
- final byte[] destination = new byte[destinationCount];
- ByteStreams.readFully(inputStream, destination);
- final byte[] port = new byte[2];
- ByteStreams.readFully(inputStream, port);
- final String receivedDestination = new String(destination);
- final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
- final byte[] responseHeader;
- final boolean success;
- if (receivedDestination.equals(this.destination) && this.socket == null) {
- responseHeader = new byte[]{0x05, 0x00, 0x00, 0x03};
- success = true;
- } else {
- Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": destination mismatch. received " + receivedDestination + " (expected " + this.destination + ")");
- responseHeader = new byte[]{0x05, 0x04, 0x00, 0x03};
- success = false;
- }
- response.put(responseHeader);
- response.put((byte) destination.length);
- response.put(destination);
- response.put(port);
- outputStream.write(response.array());
- outputStream.flush();
- if (success) {
- Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": successfully processed connection to candidate " + candidate.getHost() + ":" + candidate.getPort());
- socket.setSoTimeout(0);
- this.socket = socket;
- this.inputStream = inputStream;
- this.outputStream = outputStream;
- this.isEstablished = true;
- FileBackend.close(serverSocket);
- } else {
- FileBackend.close(socket);
- }
- } else {
- socket.close();
- }
- }
-
- public void connect(final OnTransportConnected callback) {
- new Thread(() -> {
- final int timeout = candidate.getType() == JingleCandidate.TYPE_DIRECT ? SOCKET_TIMEOUT_DIRECT : SOCKET_TIMEOUT_PROXY;
- try {
- final boolean useTor = this.account.isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect();
- if (useTor) {
- socket = SocksSocketFactory.createSocketOverTor(candidate.getHost(), candidate.getPort());
- } else {
- socket = new Socket();
- SocketAddress address = new InetSocketAddress(candidate.getHost(), candidate.getPort());
- socket.connect(address, timeout);
- }
- inputStream = socket.getInputStream();
- outputStream = socket.getOutputStream();
- socket.setSoTimeout(timeout);
- SocksSocketFactory.createSocksConnection(socket, destination, 0);
- socket.setSoTimeout(0);
- isEstablished = true;
- callback.established();
- } catch (final IOException e) {
- callback.failed();
- }
- }).start();
-
- }
-
- public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
- new Thread(() -> {
- InputStream fileInputStream = null;
- final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_" + connection.getId().sessionId);
- long transmitted = 0;
- try {
- wakeLock.acquire();
- MessageDigest digest = MessageDigest.getInstance("SHA-1");
- digest.reset();
- fileInputStream = connection.getFileInputStream();
- if (fileInputStream == null) {
- Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": could not create input stream");
- callback.onFileTransferAborted();
- return;
- }
- final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
- long size = file.getExpectedSize();
- int count;
- byte[] buffer = new byte[8192];
- while ((count = innerInputStream.read(buffer)) > 0) {
- outputStream.write(buffer, 0, count);
- digest.update(buffer, 0, count);
- transmitted += count;
- connection.updateProgress((int) ((((double) transmitted) / size) * 100));
- }
- outputStream.flush();
- file.setSha1Sum(digest.digest());
- if (callback != null) {
- callback.onFileTransmitted(file);
- }
- } catch (Exception e) {
- final Account account = this.account;
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed sending file after " + transmitted + "/" + file.getExpectedSize() + " (" + socket.getInetAddress() + ":" + socket.getPort() + ")", e);
- callback.onFileTransferAborted();
- } finally {
- FileBackend.close(fileInputStream);
- WakeLockHelper.release(wakeLock);
- }
- }).start();
-
- }
-
- public void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
- new Thread(() -> {
- OutputStream fileOutputStream = null;
- final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_" + connection.getId().sessionId);
- try {
- wakeLock.acquire();
- MessageDigest digest = MessageDigest.getInstance("SHA-1");
- digest.reset();
- //inputStream.skip(45);
- socket.setSoTimeout(30000);
- fileOutputStream = connection.getFileOutputStream();
- if (fileOutputStream == null) {
- callback.onFileTransferAborted();
- Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": could not create output stream");
- return;
- }
- double size = file.getExpectedSize();
- long remainingSize = file.getExpectedSize();
- byte[] buffer = new byte[8192];
- int count;
- while (remainingSize > 0) {
- count = inputStream.read(buffer);
- if (count == -1) {
- callback.onFileTransferAborted();
- Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": file ended prematurely with " + remainingSize + " bytes remaining");
- return;
- } else {
- fileOutputStream.write(buffer, 0, count);
- digest.update(buffer, 0, count);
- remainingSize -= count;
- }
- connection.updateProgress((int) (((size - remainingSize) / size) * 100));
- }
- fileOutputStream.flush();
- fileOutputStream.close();
- file.setSha1Sum(digest.digest());
- callback.onFileTransmitted(file);
- } catch (Exception e) {
- Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": " + e.getMessage());
- callback.onFileTransferAborted();
- } finally {
- WakeLockHelper.release(wakeLock);
- FileBackend.close(fileOutputStream);
- FileBackend.close(inputStream);
- }
- }).start();
- }
-
- public boolean isProxy() {
- return this.candidate.getType() == JingleCandidate.TYPE_PROXY;
- }
-
- public boolean needsActivation() {
- return (this.isProxy() && !this.activated);
- }
-
- public void disconnect() {
- FileBackend.close(inputStream);
- FileBackend.close(outputStream);
- FileBackend.close(socket);
- FileBackend.close(serverSocket);
- }
-
- public boolean isEstablished() {
- return this.isEstablished;
- }
-
- public JingleCandidate getCandidate() {
- return this.candidate;
- }
-
- public void setActivated(boolean activated) {
- this.activated = activated;
- }
-}
@@ -1,15 +0,0 @@
-package eu.siacs.conversations.xmpp.jingle;
-
-import eu.siacs.conversations.entities.DownloadableFile;
-
-public abstract class JingleTransport {
- public abstract void connect(final OnTransportConnected callback);
-
- public abstract void receive(final DownloadableFile file,
- final OnFileTransmissionStatusChanged callback);
-
- public abstract void send(final DownloadableFile file,
- final OnFileTransmissionStatusChanged callback);
-
- public abstract void disconnect();
-}
@@ -1,6 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
-import com.google.common.collect.ArrayListMultimap;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Multimap;
import java.util.List;
@@ -8,9 +9,9 @@ public class MediaBuilder {
private String media;
private int port;
private String protocol;
- private List<Integer> formats;
+ private String format;
private String connectionData;
- private ArrayListMultimap<String,String> attributes;
+ private Multimap<String, String> attributes;
public MediaBuilder setMedia(String media) {
this.media = media;
@@ -27,8 +28,13 @@ public class MediaBuilder {
return this;
}
- public MediaBuilder setFormats(List<Integer> formats) {
- this.formats = formats;
+ public MediaBuilder setFormats(final List<Integer> formats) {
+ this.format = Joiner.on(' ').join(formats);
+ return this;
+ }
+
+ public MediaBuilder setFormat(final String format) {
+ this.format = format;
return this;
}
@@ -37,12 +43,13 @@ public class MediaBuilder {
return this;
}
- public MediaBuilder setAttributes(ArrayListMultimap<String,String> attributes) {
+ public MediaBuilder setAttributes(Multimap<String, String> attributes) {
this.attributes = attributes;
return this;
}
public SessionDescription.Media createMedia() {
- return new SessionDescription.Media(media, port, protocol, formats, connectionData, attributes);
+ return new SessionDescription.Media(
+ media, port, protocol, format, connectionData, attributes);
}
-}
+}
@@ -3,12 +3,14 @@ package eu.siacs.conversations.xmpp.jingle;
import java.util.Map;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
public class OmemoVerifiedRtpContentMap extends RtpContentMap {
- public OmemoVerifiedRtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
+ public OmemoVerifiedRtpContentMap(Group group, Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
super(group, contents);
- for(final DescriptionTransport descriptionTransport : contents.values()) {
+ for(final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport : contents.values()) {
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint();
continue;
@@ -1,5 +0,0 @@
-package eu.siacs.conversations.xmpp.jingle;
-
-public interface OnPrimaryCandidateFound {
- void onPrimaryCandidateFound(boolean success, JingleCandidate canditate);
-}
@@ -6,7 +6,6 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
@@ -31,19 +30,17 @@ import java.util.Set;
import javax.annotation.Nonnull;
-public class RtpContentMap {
+public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTransportInfo> {
- public final Group group;
- public final Map<String, DescriptionTransport> contents;
-
- public RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
- this.group = group;
- this.contents = contents;
+ public RtpContentMap(
+ Group group,
+ Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
+ super(group, contents);
}
public static RtpContentMap of(final JinglePacket jinglePacket) {
- final Map<String, DescriptionTransport> contents =
- DescriptionTransport.of(jinglePacket.getJingleContents());
+ final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents =
+ of(jinglePacket.getJingleContents());
if (isOmemoVerified(contents)) {
return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
} else {
@@ -51,12 +48,15 @@ public class RtpContentMap {
}
}
- private static boolean isOmemoVerified(Map<String, DescriptionTransport> contents) {
- final Collection<DescriptionTransport> values = contents.values();
+ private static boolean isOmemoVerified(
+ Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
+ final Collection<DescriptionTransport<RtpDescription, IceUdpTransportInfo>> values =
+ contents.values();
if (values.size() == 0) {
return false;
}
- for (final DescriptionTransport descriptionTransport : values) {
+ for (final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport :
+ values) {
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
continue;
}
@@ -67,13 +67,13 @@ public class RtpContentMap {
public static RtpContentMap of(
final SessionDescription sessionDescription, final boolean isInitiator) {
- final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
- new ImmutableMap.Builder<>();
+ final ImmutableMap.Builder<
+ String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
+ contentMapBuilder = new ImmutableMap.Builder<>();
for (SessionDescription.Media media : sessionDescription.media) {
final String id = Iterables.getFirst(media.attributes.get("mid"), null);
Preconditions.checkNotNull(id, "media has no mid");
- contentMapBuilder.put(
- id, DescriptionTransport.of(sessionDescription, isInitiator, media));
+ contentMapBuilder.put(id, of(sessionDescription, isInitiator, media));
}
final String groupAttribute =
Iterables.getFirst(sessionDescription.attributes.get("group"), null);
@@ -94,26 +94,6 @@ public class RtpContentMap {
}));
}
- public Set<Content.Senders> getSenders() {
- return ImmutableSet.copyOf(Collections2.transform(contents.values(), dt -> dt.senders));
- }
-
- public List<String> getNames() {
- return ImmutableList.copyOf(contents.keySet());
- }
-
- void requireContentDescriptions() {
- if (this.contents.size() == 0) {
- throw new IllegalStateException("No contents available");
- }
- for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
- if (entry.getValue().description == null) {
- throw new IllegalStateException(
- String.format("%s is lacking content description", entry.getKey()));
- }
- }
- }
-
void requireDTLSFingerprint() {
requireDTLSFingerprint(false);
}
@@ -122,7 +102,8 @@ public class RtpContentMap {
if (this.contents.size() == 0) {
throw new IllegalStateException("No contents available");
}
- for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
+ for (Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> entry :
+ this.contents.entrySet()) {
final IceUdpTransportInfo transport = entry.getValue().transport;
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
if (fingerprint == null
@@ -146,31 +127,10 @@ public class RtpContentMap {
}
}
}
-
- JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
- final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
- if (this.group != null) {
- jinglePacket.addGroup(this.group);
- }
- for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
- final DescriptionTransport descriptionTransport = entry.getValue();
- final Content content =
- new Content(
- Content.Creator.INITIATOR,
- descriptionTransport.senders,
- entry.getKey());
- if (descriptionTransport.description != null) {
- content.addChild(descriptionTransport.description);
- }
- content.addChild(descriptionTransport.transport);
- jinglePacket.addJingleContent(content);
- }
- return jinglePacket;
- }
-
RtpContentMap transportInfo(
final String contentName, final IceUdpTransportInfo.Candidate candidate) {
- final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
+ final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
+ contents.get(contentName);
final IceUdpTransportInfo transportInfo =
descriptionTransport == null ? null : descriptionTransport.transport;
if (transportInfo == null) {
@@ -183,7 +143,7 @@ public class RtpContentMap {
null,
ImmutableMap.of(
contentName,
- new DescriptionTransport(
+ new DescriptionTransport<>(
descriptionTransport.senders, null, newTransportInfo)));
}
@@ -193,21 +153,24 @@ public class RtpContentMap {
Maps.transformValues(
contents,
dt ->
- new DescriptionTransport(
+ new DescriptionTransport<>(
dt.senders, null, dt.transport.cloneWrapper())));
}
RtpContentMap withCandidates(
ImmutableMultimap<String, IceUdpTransportInfo.Candidate> candidates) {
- final ImmutableMap.Builder<String, DescriptionTransport> contentBuilder =
- new ImmutableMap.Builder<>();
- for (final Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
+ final ImmutableMap.Builder<
+ String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
+ contentBuilder = new ImmutableMap.Builder<>();
+ for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
+ entry : this.contents.entrySet()) {
final String name = entry.getKey();
- final DescriptionTransport descriptionTransport = entry.getValue();
+ final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
+ entry.getValue();
final var transport = descriptionTransport.transport;
contentBuilder.put(
name,
- new DescriptionTransport(
+ new DescriptionTransport<>(
descriptionTransport.senders,
descriptionTransport.description,
transport.withCandidates(candidates.get(name))));
@@ -247,7 +210,7 @@ public class RtpContentMap {
}
public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
- final DescriptionTransport descriptionTransport = this.contents.get(contentName);
+ final var descriptionTransport = this.contents.get(contentName);
if (descriptionTransport == null) {
throw new IllegalArgumentException(
String.format(
@@ -287,7 +250,7 @@ public class RtpContentMap {
public boolean emptyCandidates() {
int count = 0;
- for (DescriptionTransport descriptionTransport : contents.values()) {
+ for (final var descriptionTransport : contents.values()) {
count += descriptionTransport.transport.getCandidates().size();
}
return count == 0;
@@ -300,17 +263,19 @@ public class RtpContentMap {
public RtpContentMap modifiedCredentials(
IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
- final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
- new ImmutableMap.Builder<>();
- for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
- final DescriptionTransport descriptionTransport = content.getValue();
+ final ImmutableMap.Builder<
+ String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
+ contentMapBuilder = new ImmutableMap.Builder<>();
+ for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
+ content : contents.entrySet()) {
+ final var descriptionTransport = content.getValue();
final RtpDescription rtpDescription = descriptionTransport.description;
final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
final IceUdpTransportInfo modifiedTransportInfo =
transportInfo.modifyCredentials(credentials, setup);
contentMapBuilder.put(
content.getKey(),
- new DescriptionTransport(
+ new DescriptionTransport<>(
descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
}
return new RtpContentMap(this.group, contentMapBuilder.build());
@@ -321,16 +286,18 @@ public class RtpContentMap {
this.group,
Maps.transformValues(
contents,
- dt -> new DescriptionTransport(senders, dt.description, dt.transport)));
+ dt -> new DescriptionTransport<>(senders, dt.description, dt.transport)));
}
public RtpContentMap modifiedSendersChecked(
final boolean isInitiator, final Map<String, Content.Senders> modification) {
- final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
- new ImmutableMap.Builder<>();
- for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
+ final ImmutableMap.Builder<
+ String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
+ contentMapBuilder = new ImmutableMap.Builder<>();
+ for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
+ content : contents.entrySet()) {
final String id = content.getKey();
- final DescriptionTransport descriptionTransport = content.getValue();
+ final var descriptionTransport = content.getValue();
final Content.Senders currentSenders = descriptionTransport.senders;
final Content.Senders targetSenders = modification.get(id);
if (targetSenders == null || currentSenders == targetSenders) {
@@ -339,7 +306,7 @@ public class RtpContentMap {
checkSenderModification(isInitiator, currentSenders, targetSenders);
contentMapBuilder.put(
id,
- new DescriptionTransport(
+ new DescriptionTransport<>(
targetSenders,
descriptionTransport.description,
descriptionTransport.transport));
@@ -386,7 +353,7 @@ public class RtpContentMap {
Maps.transformValues(
this.contents,
dt ->
- new DescriptionTransport(
+ new DescriptionTransport<>(
dt.senders,
RtpDescription.stub(dt.description.getMedia()),
IceUdpTransportInfo.STUB)));
@@ -415,120 +382,96 @@ public class RtpContentMap {
public RtpContentMap addContent(
final RtpContentMap modification, final IceUdpTransportInfo.Setup setupOverwrite) {
- final Map<String, DescriptionTransport> combined = merge(contents, modification.contents);
- final Map<String, DescriptionTransport> combinedFixedTransport =
- Maps.transformValues(
- combined,
- dt -> {
- final IceUdpTransportInfo iceUdpTransportInfo;
- if (dt.transport.isStub()) {
- final IceUdpTransportInfo.Credentials credentials =
- getDistinctCredentials();
- final Collection<String> iceOptions = getCombinedIceOptions();
- final DTLS dtls = getDistinctDtls();
- iceUdpTransportInfo =
- IceUdpTransportInfo.of(
- credentials,
- iceOptions,
- setupOverwrite,
- dtls.hash,
- dtls.fingerprint);
- } else {
- final IceUdpTransportInfo.Fingerprint fp =
- dt.transport.getFingerprint();
- final IceUdpTransportInfo.Setup setup = fp.getSetup();
- iceUdpTransportInfo =
- IceUdpTransportInfo.of(
- dt.transport.getCredentials(),
- dt.transport.getIceOptions(),
- setup == IceUdpTransportInfo.Setup.ACTPASS
- ? setupOverwrite
- : setup,
- fp.getHash(),
- fp.getContent());
- }
- return new DescriptionTransport(
- dt.senders, dt.description, iceUdpTransportInfo);
- });
+ final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> combined =
+ merge(contents, modification.contents);
+ final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
+ combinedFixedTransport =
+ Maps.transformValues(
+ combined,
+ dt -> {
+ final IceUdpTransportInfo iceUdpTransportInfo;
+ if (dt.transport.isStub()) {
+ final IceUdpTransportInfo.Credentials credentials =
+ getDistinctCredentials();
+ final Collection<String> iceOptions =
+ getCombinedIceOptions();
+ final DTLS dtls = getDistinctDtls();
+ iceUdpTransportInfo =
+ IceUdpTransportInfo.of(
+ credentials,
+ iceOptions,
+ setupOverwrite,
+ dtls.hash,
+ dtls.fingerprint);
+ } else {
+ final IceUdpTransportInfo.Fingerprint fp =
+ dt.transport.getFingerprint();
+ final IceUdpTransportInfo.Setup setup = fp.getSetup();
+ iceUdpTransportInfo =
+ IceUdpTransportInfo.of(
+ dt.transport.getCredentials(),
+ dt.transport.getIceOptions(),
+ setup == IceUdpTransportInfo.Setup.ACTPASS
+ ? setupOverwrite
+ : setup,
+ fp.getHash(),
+ fp.getContent());
+ }
+ return new DescriptionTransport<>(
+ dt.senders, dt.description, iceUdpTransportInfo);
+ });
return new RtpContentMap(modification.group, ImmutableMap.copyOf(combinedFixedTransport));
}
- private static Map<String, DescriptionTransport> merge(
- final Map<String, DescriptionTransport> a, final Map<String, DescriptionTransport> b) {
- final Map<String, DescriptionTransport> combined = new LinkedHashMap<>();
+ private static Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> merge(
+ final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> a,
+ final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> b) {
+ final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> combined =
+ new LinkedHashMap<>();
combined.putAll(a);
combined.putAll(b);
return ImmutableMap.copyOf(combined);
}
- public static class DescriptionTransport {
- public final Content.Senders senders;
- public final RtpDescription description;
- public final IceUdpTransportInfo transport;
-
- public DescriptionTransport(
- final Content.Senders senders,
- final RtpDescription description,
- final IceUdpTransportInfo transport) {
- this.senders = senders;
- this.description = description;
- this.transport = transport;
- }
-
- public static DescriptionTransport of(final Content content) {
- final GenericDescription description = content.getDescription();
- final GenericTransportInfo transportInfo = content.getTransport();
- final Content.Senders senders = content.getSenders();
- final RtpDescription rtpDescription;
- final IceUdpTransportInfo iceUdpTransportInfo;
- if (description == null) {
- rtpDescription = null;
- } else if (description instanceof RtpDescription) {
- rtpDescription = (RtpDescription) description;
- } else {
- throw new UnsupportedApplicationException(
- "Content does not contain rtp description");
- }
- if (transportInfo instanceof IceUdpTransportInfo) {
- iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
- } else {
- throw new UnsupportedTransportException(
- "Content does not contain ICE-UDP transport");
- }
- return new DescriptionTransport(
- senders,
- rtpDescription,
- OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
- }
-
- private static DescriptionTransport of(
- final SessionDescription sessionDescription,
- final boolean isInitiator,
- final SessionDescription.Media media) {
- final Content.Senders senders = Content.Senders.of(media, isInitiator);
- final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
- final IceUdpTransportInfo transportInfo =
- IceUdpTransportInfo.of(sessionDescription, media);
- return new DescriptionTransport(senders, rtpDescription, transportInfo);
+ public static DescriptionTransport<RtpDescription, IceUdpTransportInfo> of(
+ final Content content) {
+ final GenericDescription description = content.getDescription();
+ final GenericTransportInfo transportInfo = content.getTransport();
+ final Content.Senders senders = content.getSenders();
+ final RtpDescription rtpDescription;
+ final IceUdpTransportInfo iceUdpTransportInfo;
+ if (description == null) {
+ rtpDescription = null;
+ } else if (description instanceof RtpDescription) {
+ rtpDescription = (RtpDescription) description;
+ } else {
+ throw new UnsupportedApplicationException("Content does not contain rtp description");
}
-
- public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
- return ImmutableMap.copyOf(
- Maps.transformValues(
- contents, content -> content == null ? null : of(content)));
+ if (transportInfo instanceof IceUdpTransportInfo) {
+ iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
+ } else {
+ throw new UnsupportedTransportException("Content does not contain ICE-UDP transport");
}
+ return new DescriptionTransport<>(
+ senders,
+ rtpDescription,
+ OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
}
- public static class UnsupportedApplicationException extends IllegalArgumentException {
- UnsupportedApplicationException(String message) {
- super(message);
- }
+ private static DescriptionTransport<RtpDescription, IceUdpTransportInfo> of(
+ final SessionDescription sessionDescription,
+ final boolean isInitiator,
+ final SessionDescription.Media media) {
+ final Content.Senders senders = Content.Senders.of(media, isInitiator);
+ final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
+ final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media);
+ return new DescriptionTransport<>(senders, rtpDescription, transportInfo);
}
- public static class UnsupportedTransportException extends IllegalArgumentException {
- UnsupportedTransportException(String message) {
- super(message);
- }
+ private static Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> of(
+ final Map<String, Content> contents) {
+ return ImmutableMap.copyOf(
+ Maps.transformValues(contents, content -> content == null ? null : of(content)));
}
public static final class Diff {
@@ -10,12 +10,17 @@ import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
import java.util.Collection;
import java.util.Collections;
@@ -28,6 +33,8 @@ public class SessionDescription {
public static final String LINE_DIVIDER = "\r\n";
private static final String HARDCODED_MEDIA_PROTOCOL =
"UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint
+ private static final String HARDCODED_APPLICATION_PROTOCOL = "UDP/DTLS/SCTP";
+ private static final String FORMAT_WEBRTC_DATA_CHANNEL = "webrtc-datachannel";
private static final int HARDCODED_MEDIA_PORT = 9;
private static final Collection<String> HARDCODED_ICE_OPTIONS =
Collections.singleton("trickle");
@@ -52,9 +59,8 @@ public class SessionDescription {
this.media = media;
}
- private static void appendAttributes(
- StringBuilder s, ArrayListMultimap<String, String> attributes) {
- for (Map.Entry<String, String> attribute : attributes.entries()) {
+ private static void appendAttributes(StringBuilder s, Multimap<String, String> attributes) {
+ for (final Map.Entry<String, String> attribute : attributes.entries()) {
final String key = attribute.getKey();
final String value = attribute.getValue();
s.append("a=").append(key);
@@ -79,24 +85,20 @@ public class SessionDescription {
final char key = pair[0].charAt(0);
final String value = pair[1];
switch (key) {
- case 'v':
- sessionDescriptionBuilder.setVersion(ignorantIntParser(value));
- break;
- case 'c':
+ case 'v' -> sessionDescriptionBuilder.setVersion(ignorantIntParser(value));
+ case 'c' -> {
if (currentMediaBuilder != null) {
currentMediaBuilder.setConnectionData(value);
} else {
sessionDescriptionBuilder.setConnectionData(value);
}
- break;
- case 's':
- sessionDescriptionBuilder.setName(value);
- break;
- case 'a':
+ }
+ case 's' -> sessionDescriptionBuilder.setName(value);
+ case 'a' -> {
final Pair<String, String> attribute = parseAttribute(value);
attributeMap.put(attribute.first, attribute.second);
- break;
- case 'm':
+ }
+ case 'm' -> {
if (currentMediaBuilder == null) {
sessionDescriptionBuilder.setAttributes(attributeMap);
} else {
@@ -118,7 +120,7 @@ public class SessionDescription {
} else {
Log.d(Config.LOGTAG, "skipping media line " + line);
}
- break;
+ }
}
}
if (currentMediaBuilder != null) {
@@ -131,6 +133,56 @@ public class SessionDescription {
return sessionDescriptionBuilder.createSessionDescription();
}
+ public static SessionDescription of(final FileTransferContentMap contentMap) {
+ final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
+ final ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create();
+ final ImmutableList.Builder<Media> mediaListBuilder = new ImmutableList.Builder<>();
+
+ final Group group = contentMap.group;
+ if (group != null) {
+ final String semantics = group.getSemantics();
+ checkNoWhitespace(semantics, "group semantics value must not contain any whitespace");
+ final var idTags = group.getIdentificationTags();
+ for (final String content : idTags) {
+ checkNoWhitespace(content, "group content names must not contain any whitespace");
+ }
+ attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(idTags));
+ }
+
+ // TODO my-media-stream can be removed I think
+ attributeMap.put("msid-semantic", " WMS my-media-stream");
+
+ for (final Map.Entry<
+ String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
+ entry : contentMap.contents.entrySet()) {
+ final var dt = entry.getValue();
+ final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo;
+ if (dt.transport instanceof WebRTCDataChannelTransportInfo transportInfo) {
+ webRTCDataChannelTransportInfo = transportInfo;
+ } else {
+ throw new IllegalArgumentException("Transport is not of type WebRTCDataChannel");
+ }
+ final String name = entry.getKey();
+ checkNoWhitespace(name, "content name must not contain any whitespace");
+
+ final MediaBuilder mediaBuilder = new MediaBuilder();
+ mediaBuilder.setMedia("application");
+ mediaBuilder.setConnectionData(HARDCODED_CONNECTION);
+ mediaBuilder.setPort(HARDCODED_MEDIA_PORT);
+ mediaBuilder.setProtocol(HARDCODED_APPLICATION_PROTOCOL);
+ mediaBuilder.setAttributes(
+ transportInfoMediaAttributes(webRTCDataChannelTransportInfo));
+ mediaBuilder.setFormat(FORMAT_WEBRTC_DATA_CHANNEL);
+ mediaListBuilder.add(mediaBuilder.createMedia());
+ }
+
+ sessionDescriptionBuilder.setVersion(0);
+ sessionDescriptionBuilder.setName("-");
+ sessionDescriptionBuilder.setMedia(mediaListBuilder.build());
+ sessionDescriptionBuilder.setAttributes(attributeMap);
+ return sessionDescriptionBuilder.createSessionDescription();
+ }
+
public static SessionDescription of(
final RtpContentMap contentMap, final boolean isInitiatorContentMap) {
final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
@@ -140,58 +192,27 @@ public class SessionDescription {
if (group != null) {
final String semantics = group.getSemantics();
checkNoWhitespace(semantics, "group semantics value must not contain any whitespace");
- attributeMap.put(
- "group",
- group.getSemantics()
- + " "
- + Joiner.on(' ').join(group.getIdentificationTags()));
+ final var idTags = group.getIdentificationTags();
+ for (final String content : idTags) {
+ checkNoWhitespace(content, "group content names must not contain any whitespace");
+ }
+ attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(idTags));
}
+ // TODO my-media-stream can be removed I think
attributeMap.put("msid-semantic", " WMS my-media-stream");
- for (final Map.Entry<String, RtpContentMap.DescriptionTransport> entry :
- contentMap.contents.entrySet()) {
+ for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
+ entry : contentMap.contents.entrySet()) {
final String name = entry.getKey();
- RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue();
- RtpDescription description = descriptionTransport.description;
- IceUdpTransportInfo transport = descriptionTransport.transport;
+ checkNoWhitespace(name, "content name must not contain any whitespace");
+ final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
+ entry.getValue();
+ final RtpDescription description = descriptionTransport.description;
final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
- final String ufrag = transport.getAttribute("ufrag");
- final String pwd = transport.getAttribute("pwd");
- if (Strings.isNullOrEmpty(ufrag)) {
- throw new IllegalArgumentException(
- "Transport element is missing required ufrag attribute");
- }
- checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces");
- mediaAttributes.put("ice-ufrag", ufrag);
- if (Strings.isNullOrEmpty(pwd)) {
- throw new IllegalArgumentException(
- "Transport element is missing required pwd attribute");
- }
- checkNoWhitespace(pwd, "pwd value must not contain any whitespaces");
- mediaAttributes.put("ice-pwd", pwd);
- final List<String> negotiatedIceOptions = transport.getIceOptions();
- final Collection<String> iceOptions =
- negotiatedIceOptions.isEmpty() ? HARDCODED_ICE_OPTIONS : negotiatedIceOptions;
- mediaAttributes.put("ice-options", Joiner.on(' ').join(iceOptions));
- final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
- if (fingerprint != null) {
- final String hashFunction = fingerprint.getHash();
- final String hash = fingerprint.getContent();
- if (Strings.isNullOrEmpty(hashFunction) || Strings.isNullOrEmpty(hash)) {
- throw new IllegalArgumentException("DTLS-SRTP missing hash");
- }
- checkNoWhitespace(
- hashFunction, "DTLS-SRTP hash function must not contain whitespace");
- checkNoWhitespace(hash, "DTLS-SRTP hash must not contain whitespace");
- mediaAttributes.put("fingerprint", hashFunction + " " + hash);
- final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
- if (setup != null) {
- mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT));
- }
- }
+ mediaAttributes.putAll(transportInfoMediaAttributes(descriptionTransport.transport));
final ImmutableList.Builder<Integer> formatBuilder = new ImmutableList.Builder<>();
- for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) {
+ for (final RtpDescription.PayloadType payloadType : description.getPayloadTypes()) {
final String id = payloadType.getId();
if (Strings.isNullOrEmpty(id)) {
throw new IllegalArgumentException("Payload type is missing id");
@@ -353,6 +374,69 @@ public class SessionDescription {
return sessionDescriptionBuilder.createSessionDescription();
}
+ private static Multimap<String, String> transportInfoMediaAttributes(
+ final IceUdpTransportInfo transport) {
+ final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
+ final String ufrag = transport.getAttribute("ufrag");
+ final String pwd = transport.getAttribute("pwd");
+ if (Strings.isNullOrEmpty(ufrag)) {
+ throw new IllegalArgumentException(
+ "Transport element is missing required ufrag attribute");
+ }
+ checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces");
+ mediaAttributes.put("ice-ufrag", ufrag);
+ if (Strings.isNullOrEmpty(pwd)) {
+ throw new IllegalArgumentException(
+ "Transport element is missing required pwd attribute");
+ }
+ checkNoWhitespace(pwd, "pwd value must not contain any whitespaces");
+ mediaAttributes.put("ice-pwd", pwd);
+ final List<String> negotiatedIceOptions = transport.getIceOptions();
+ final Collection<String> iceOptions =
+ negotiatedIceOptions.isEmpty() ? HARDCODED_ICE_OPTIONS : negotiatedIceOptions;
+ mediaAttributes.put("ice-options", Joiner.on(' ').join(iceOptions));
+ final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
+ if (fingerprint != null) {
+ final String hashFunction = fingerprint.getHash();
+ final String hash = fingerprint.getContent();
+ if (Strings.isNullOrEmpty(hashFunction) || Strings.isNullOrEmpty(hash)) {
+ throw new IllegalArgumentException("DTLS-SRTP missing hash");
+ }
+ checkNoWhitespace(hashFunction, "DTLS-SRTP hash function must not contain whitespace");
+ checkNoWhitespace(hash, "DTLS-SRTP hash must not contain whitespace");
+ mediaAttributes.put("fingerprint", hashFunction + " " + hash);
+ final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
+ if (setup != null) {
+ mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT));
+ }
+ }
+ return ImmutableMultimap.copyOf(mediaAttributes);
+ }
+
+ private static Multimap<String, String> transportInfoMediaAttributes(
+ final WebRTCDataChannelTransportInfo transport) {
+ final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
+ final var iceUdpTransportInfo = transport.innerIceUdpTransportInfo();
+ if (iceUdpTransportInfo == null) {
+ throw new IllegalArgumentException(
+ "Transport element is missing inner ice-udp transport");
+ }
+ mediaAttributes.putAll(transportInfoMediaAttributes(iceUdpTransportInfo));
+ final Integer sctpPort = transport.getSctpPort();
+ if (sctpPort == null) {
+ throw new IllegalArgumentException(
+ "Transport element is missing required sctp-port attribute");
+ }
+ mediaAttributes.put("sctp-port", String.valueOf(sctpPort));
+ final Integer maxMessageSize = transport.getMaxMessageSize();
+ if (maxMessageSize == null) {
+ throw new IllegalArgumentException(
+ "Transport element is missing required max-message-size");
+ }
+ mediaAttributes.put("max-message-size", String.valueOf(maxMessageSize));
+ return ImmutableMultimap.copyOf(mediaAttributes);
+ }
+
public static String checkNoWhitespace(final String input, final String message) {
if (CharMatcher.whitespace().matchesAnyOf(input)) {
throw new IllegalArgumentException(message);
@@ -421,7 +505,7 @@ public class SessionDescription {
.append(' ')
.append(media.protocol)
.append(' ')
- .append(Joiner.on(' ').join(media.formats))
+ .append(media.format)
.append(LINE_DIVIDER);
s.append("c=").append(media.connectionData).append(LINE_DIVIDER);
appendAttributes(s, media.attributes);
@@ -433,21 +517,21 @@ public class SessionDescription {
public final String media;
public final int port;
public final String protocol;
- public final List<Integer> formats;
+ public final String format;
public final String connectionData;
- public final ArrayListMultimap<String, String> attributes;
+ public final Multimap<String, String> attributes;
public Media(
String media,
int port,
String protocol,
- List<Integer> formats,
+ String format,
String connectionData,
- ArrayListMultimap<String, String> attributes) {
+ Multimap<String, String> attributes) {
this.media = media;
this.port = port;
this.protocol = protocol;
- this.formats = formats;
+ this.format = format;
this.connectionData = connectionData;
this.attributes = attributes;
}
@@ -406,7 +406,7 @@ public class WebRTCWrapper {
}
}
- private static PeerConnection.RTCConfiguration buildConfiguration(
+ public static PeerConnection.RTCConfiguration buildConfiguration(
final List<PeerConnection.IceServer> iceServers, final boolean trickle) {
final PeerConnection.RTCConfiguration rtcConfig =
new PeerConnection.RTCConfiguration(iceServers);
@@ -774,7 +774,7 @@ public class WebRTCWrapper {
void onRenegotiationNeeded();
}
- private abstract static class SetSdpObserver implements SdpObserver {
+ public abstract static class SetSdpObserver implements SdpObserver {
@Override
public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
@@ -800,12 +800,12 @@ public class WebRTCWrapper {
public static class PeerConnectionNotInitialized extends IllegalStateException {
- private PeerConnectionNotInitialized() {
+ public PeerConnectionNotInitialized() {
super("initialize PeerConnection first");
}
}
- private static class FailureToSetDescriptionException extends IllegalArgumentException {
+ public static class FailureToSetDescriptionException extends IllegalArgumentException {
public FailureToSetDescriptionException(String message) {
super(message);
}
@@ -8,14 +8,14 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
-import java.util.Locale;
-import java.util.Set;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
+import java.util.Locale;
+import java.util.Set;
+
public class Content extends Element {
public Content(final Creator creator, final Senders senders, final String name) {
@@ -65,7 +65,7 @@ public class Content extends Element {
return null;
}
final String namespace = description.getNamespace();
- if (FileTransferDescription.NAMESPACES.contains(namespace)) {
+ if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) {
return FileTransferDescription.upgrade(description);
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
return RtpDescription.upgrade(description);
@@ -90,9 +90,11 @@ public class Content extends Element {
if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) {
return IbbTransportInfo.upgrade(transport);
} else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) {
- return S5BTransportInfo.upgrade(transport);
+ return SocksByteStreamsTransportInfo.upgrade(transport);
} else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
return IceUdpTransportInfo.upgrade(transport);
+ } else if (Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL.equals(namespace)) {
+ return WebRTCDataChannelTransportInfo.upgrade(transport);
} else if (transport != null) {
return GenericTransportInfo.upgrade(transport);
} else {
@@ -100,7 +102,6 @@ public class Content extends Element {
}
}
-
public void setTransport(GenericTransportInfo transportInfo) {
this.addChild(transportInfo);
}
@@ -141,7 +142,7 @@ public class Content extends Element {
} else if (attributes.contains("recvonly")) {
return initiator ? RESPONDER : INITIATOR;
}
- Log.w(Config.LOGTAG,"assuming default value for senders");
+ Log.w(Config.LOGTAG, "assuming default value for senders");
// If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is
// present, "sendrecv" SHOULD be assumed as the default
// https://www.rfc-editor.org/rfc/rfc4566
@@ -1,89 +1,233 @@
package eu.siacs.conversations.xmpp.jingle.stanzas;
-import com.google.common.base.Preconditions;
+import android.util.Log;
-import java.util.Arrays;
-import java.util.List;
+import androidx.annotation.NonNull;
-import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.xml.Element;
+import com.google.common.base.CaseFormat;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.BaseEncoding;
+import com.google.common.primitives.Longs;
-public class FileTransferDescription extends GenericDescription {
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
- public static List<String> NAMESPACES = Arrays.asList(
- Version.FT_3.namespace,
- Version.FT_4.namespace,
- Version.FT_5.namespace
- );
+import java.util.List;
+public class FileTransferDescription extends GenericDescription {
- private FileTransferDescription(String name, String namespace) {
- super(name, namespace);
+ private FileTransferDescription() {
+ super("description", Namespace.JINGLE_APPS_FILE_TRANSFER);
}
- public Version getVersion() {
- final String namespace = getNamespace();
- if (namespace.equals(Version.FT_3.namespace)) {
- return Version.FT_3;
- } else if (namespace.equals(Version.FT_4.namespace)) {
- return Version.FT_4;
- } else if (namespace.equals(Version.FT_5.namespace)) {
- return Version.FT_5;
- } else {
- throw new IllegalStateException("Unknown namespace");
+ public static FileTransferDescription of(final File fileDescription) {
+ final var description = new FileTransferDescription();
+ final var file = description.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ file.addChild("name").setContent(fileDescription.name);
+ file.addChild("size").setContent(Long.toString(fileDescription.size));
+ if (fileDescription.mediaType != null) {
+ file.addChild("mediaType").setContent(fileDescription.mediaType);
}
+ return description;
}
- public Element getFileOffer() {
- final Version version = getVersion();
- if (version == Version.FT_3) {
- final Element offer = this.findChild("offer");
- return offer == null ? null : offer.findChild("file");
- } else {
- return this.findChild("file");
+ public File getFile() {
+ final Element fileElement = this.findChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ if (fileElement == null) {
+ Log.d(Config.LOGTAG,"no file? "+this);
+ throw new IllegalStateException("file transfer description has no file");
}
+ final String name = fileElement.findChildContent("name");
+ final String sizeAsString = fileElement.findChildContent("size");
+ final String mediaType = fileElement.findChildContent("mediaType");
+ if (Strings.isNullOrEmpty(name) || Strings.isNullOrEmpty(sizeAsString)) {
+ throw new IllegalStateException("File definition is missing name and/or size");
+ }
+ final Long size = Longs.tryParse(sizeAsString);
+ if (size == null) {
+ throw new IllegalStateException("Invalid file size");
+ }
+ final List<Hash> hashes = findHashes(fileElement.getChildren());
+ return new File(size, name, mediaType, hashes);
}
- public static FileTransferDescription of(DownloadableFile file, Version version, XmppAxolotlMessage axolotlMessage) {
- final FileTransferDescription description = new FileTransferDescription("description", version.getNamespace());
- final Element fileElement;
- if (version == Version.FT_3) {
- Element offer = description.addChild("offer");
- fileElement = offer.addChild("file");
- } else {
- fileElement = description.addChild("file");
+ public static SessionInfo getSessionInfo(@NonNull final JinglePacket jinglePacket) {
+ Preconditions.checkNotNull(jinglePacket);
+ Preconditions.checkArgument(
+ jinglePacket.getAction() == JinglePacket.Action.SESSION_INFO,
+ "jingle packet is not a session-info");
+ final Element jingle = jinglePacket.findChild("jingle", Namespace.JINGLE);
+ if (jingle == null) {
+ return null;
}
- fileElement.addChild("size").setContent(Long.toString(file.getExpectedSize()));
- fileElement.addChild("name").setContent(file.getName());
- if (axolotlMessage != null) {
- fileElement.addChild(axolotlMessage.toElement());
+ final Element checksum = jingle.findChild("checksum", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ if (checksum != null) {
+ final Element file = checksum.findChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ final String name = checksum.getAttribute("name");
+ if (file == null || Strings.isNullOrEmpty(name)) {
+ return null;
+ }
+ return new Checksum(name, findHashes(file.getChildren()));
}
- return description;
+ final Element received = jingle.findChild("received", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ if (received != null) {
+ final String name = received.getAttribute("name");
+ if (Strings.isNullOrEmpty(name)) {
+ return new Received(name);
+ }
+ }
+ return null;
+ }
+
+ private static List<Hash> findHashes(final List<Element> elements) {
+ final ImmutableList.Builder<Hash> hashes = new ImmutableList.Builder<>();
+ for (final Element child : elements) {
+ if ("hash".equals(child.getName()) && Namespace.HASHES.equals(child.getNamespace())) {
+ final Algorithm algorithm;
+ try {
+ algorithm = Algorithm.of(child.getAttribute("algo"));
+ } catch (final IllegalArgumentException e) {
+ continue;
+ }
+ final String content = child.getContent();
+ if (Strings.isNullOrEmpty(content)) {
+ continue;
+ }
+ if (BaseEncoding.base64().canDecode(content)) {
+ hashes.add(new Hash(BaseEncoding.base64().decode(content), algorithm));
+ }
+ }
+ }
+ return hashes.build();
}
public static FileTransferDescription upgrade(final Element element) {
- Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description");
- Preconditions.checkArgument(NAMESPACES.contains(element.getNamespace()), "Element does not match a file transfer namespace");
- final FileTransferDescription description = new FileTransferDescription("description", element.getNamespace());
+ Preconditions.checkArgument(
+ "description".equals(element.getName()),
+ "Name of provided element is not description");
+ Preconditions.checkArgument(
+ element.getNamespace().equals(Namespace.JINGLE_APPS_FILE_TRANSFER),
+ "Element does not match a file transfer namespace");
+ final FileTransferDescription description = new FileTransferDescription();
description.setAttributes(element.getAttributes());
description.setChildren(element.getChildren());
return description;
}
- public enum Version {
- FT_3("urn:xmpp:jingle:apps:file-transfer:3"),
- FT_4("urn:xmpp:jingle:apps:file-transfer:4"),
- FT_5("urn:xmpp:jingle:apps:file-transfer:5");
+ public static final class Checksum extends SessionInfo {
+ public final List<Hash> hashes;
+
+ public Checksum(final String name, List<Hash> hashes) {
+ super(name);
+ this.hashes = hashes;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return MoreObjects.toStringHelper(this).add("hashes", hashes).toString();
+ }
+
+ @Override
+ public Element asElement() {
+ final var checksum = new Element("checksum", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ checksum.setAttribute("name", name);
+ final var file = checksum.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ for (final Hash hash : hashes) {
+ final var element = file.addChild("hash", Namespace.HASHES);
+ element.setAttribute(
+ "algo",
+ CaseFormat.UPPER_UNDERSCORE.to(
+ CaseFormat.LOWER_HYPHEN, hash.algorithm.toString()));
+ element.setContent(BaseEncoding.base64().encode(hash.hash));
+ }
+ return checksum;
+ }
+ }
+
+ public static final class Received extends SessionInfo {
+
+ public Received(String name) {
+ super(name);
+ }
+
+ @Override
+ public Element asElement() {
+ final var element = new Element("received", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ element.setAttribute("name", name);
+ return element;
+ }
+ }
+
+ public abstract static sealed class SessionInfo permits Checksum, Received {
- private final String namespace;
+ public final String name;
- Version(String namespace) {
- this.namespace = namespace;
+ protected SessionInfo(final String name) {
+ this.name = name;
}
- public String getNamespace() {
- return namespace;
+ public abstract Element asElement();
+ }
+
+ public static class File {
+ public final long size;
+ public final String name;
+ public final String mediaType;
+
+ public final List<Hash> hashes;
+
+ public File(long size, String name, String mediaType, List<Hash> hashes) {
+ this.size = size;
+ this.name = name;
+ this.mediaType = mediaType;
+ this.hashes = hashes;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("size", size)
+ .add("name", name)
+ .add("mediaType", mediaType)
+ .add("hashes", hashes)
+ .toString();
+ }
+ }
+
+ public static class Hash {
+ public final byte[] hash;
+ public final Algorithm algorithm;
+
+ public Hash(byte[] hash, Algorithm algorithm) {
+ this.hash = hash;
+ this.algorithm = algorithm;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("hash", hash)
+ .add("algorithm", algorithm)
+ .toString();
+ }
+ }
+
+ public enum Algorithm {
+ SHA_1,
+ SHA_256;
+
+ public static Algorithm of(final String value) {
+ if (Strings.isNullOrEmpty(value)) {
+ return null;
+ }
+ return valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value));
}
}
}
@@ -8,6 +8,7 @@ public class GenericDescription extends Element {
GenericDescription(String name, final String namespace) {
super(name, namespace);
+ Preconditions.checkArgument("description".equals(name));
}
public static GenericDescription upgrade(final Element element) {
@@ -41,7 +41,7 @@ public class Group extends Element {
}
public static Group ofSdpString(final String input) {
- ImmutableList.Builder<String> tagBuilder = new ImmutableList.Builder<>();
+ final ImmutableList.Builder<String> tagBuilder = new ImmutableList.Builder<>();
final String[] parts = input.split(" ");
if (parts.length >= 2) {
final String semantics = parts[0];
@@ -1,6 +1,8 @@
package eu.siacs.conversations.xmpp.jingle.stanzas;
import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Longs;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
@@ -23,16 +25,9 @@ public class IbbTransportInfo extends GenericTransportInfo {
return this.getAttribute("sid");
}
- public int getBlockSize() {
+ public Long getBlockSize() {
final String blockSize = this.getAttribute("block-size");
- if (blockSize == null) {
- return 0;
- }
- try {
- return Integer.parseInt(blockSize);
- } catch (NumberFormatException e) {
- return 0;
- }
+ return Strings.isNullOrEmpty(blockSize) ? null : Longs.tryParse(blockSize);
}
public static IbbTransportInfo upgrade(final Element element) {
@@ -15,11 +15,13 @@ import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
+import eu.siacs.conversations.xmpp.jingle.transports.Transport;
import java.util.Arrays;
import java.util.Collection;
@@ -195,7 +197,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
}
}
- public static class Candidate extends Element {
+ public static class Candidate extends Element implements Transport.Candidate {
private Candidate() {
super("candidate");
@@ -396,7 +398,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
return fingerprint;
}
- private static Fingerprint of(ArrayListMultimap<String, String> attributes) {
+ private static Fingerprint of(final Multimap<String, String> attributes) {
final String fingerprint = Iterables.getFirst(attributes.get("fingerprint"), null);
final String setup = Iterables.getFirst(attributes.get("setup"), null);
if (setup != null && fingerprint != null) {
@@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle.stanzas;
+import android.util.Log;
+
import androidx.annotation.NonNull;
import com.google.common.base.CaseFormat;
@@ -7,13 +9,16 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
-import java.util.Map;
-
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
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 java.util.Map;
+
public class JinglePacket extends IqPacket {
private JinglePacket() {
@@ -36,7 +41,7 @@ public class JinglePacket extends IqPacket {
return jinglePacket;
}
- //TODO deprecate this somehow and make file transfer fail if there are multiple (or something)
+ // TODO deprecate this somehow and make file transfer fail if there are multiple (or something)
public Content getJingleContent() {
final Element content = getJingleChild("content");
return content == null ? null : Content.upgrade(content);
@@ -64,7 +69,7 @@ public class JinglePacket extends IqPacket {
return builder.build();
}
- public void addJingleContent(final Content content) { //take content interface
+ public void addJingleContent(final Content content) { // take content interface
addJingleChild(content);
}
@@ -94,13 +99,13 @@ public class JinglePacket extends IqPacket {
}
}
- //RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise
+ // RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise
public void setInitiator(final Jid initiator) {
Preconditions.checkArgument(initiator.isFullJid(), "initiator should be a full JID");
findChild("jingle", Namespace.JINGLE).setAttribute("initiator", initiator);
}
- //RECOMMENDED for session-accept, NOT RECOMMENDED otherwise
+ // RECOMMENDED for session-accept, NOT RECOMMENDED otherwise
public void setResponder(Jid responder) {
Preconditions.checkArgument(responder.isFullJid(), "responder should be a full JID");
findChild("jingle", Namespace.JINGLE).setAttribute("responder", responder);
@@ -116,6 +121,39 @@ public class JinglePacket extends IqPacket {
jingle.addChild(child);
}
+ public void setSecurity(final String name, final XmppAxolotlMessage xmppAxolotlMessage) {
+ final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
+ security.setAttribute("name", name);
+ security.setAttribute("cipher", "urn:xmpp:ciphers:aes-128-gcm-nopadding");
+ security.setAttribute("type", AxolotlService.PEP_PREFIX);
+ security.addChild(xmppAxolotlMessage.toElement());
+ addJingleChild(security);
+ }
+
+ public XmppAxolotlMessage getSecurity(final String nameNeedle) {
+ final Element jingle = findChild("jingle", Namespace.JINGLE);
+ if (jingle == null) {
+ return null;
+ }
+ for (final Element child : jingle.getChildren()) {
+ if ("security".equals(child.getName())
+ && Namespace.JINGLE_ENCRYPTED_TRANSPORT.equals(child.getNamespace())) {
+ final String name = child.getAttribute("name");
+ final String type = child.getAttribute("type");
+ final String cipher = child.getAttribute("cipher");
+ if (nameNeedle.equals(name)
+ && AxolotlService.PEP_PREFIX.equals(type)
+ && "urn:xmpp:ciphers:aes-128-gcm-nopadding".equals(cipher)) {
+ final var encrypted = child.findChild("encrypted", AxolotlService.PEP_PREFIX);
+ if (encrypted != null) {
+ return XmppAxolotlMessage.fromElement(encrypted, getFrom().asBareJid());
+ }
+ }
+ }
+ }
+ return null;
+ }
+
public String getSessionId() {
return findChild("jingle", Namespace.JINGLE).getAttribute("sid");
}
@@ -142,7 +180,7 @@ public class JinglePacket extends IqPacket {
TRANSPORT_REPLACE;
public static Action of(final String value) {
- //TODO handle invalid
+ // TODO handle invalid
return Action.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value));
}
@@ -153,7 +191,6 @@ public class JinglePacket extends IqPacket {
}
}
-
public static class ReasonWrapper {
public final Reason reason;
public final String text;
@@ -18,7 +18,7 @@ public class Propose extends Element {
for (final Element child : this.children) {
if ("description".equals(child.getName())) {
final String namespace = child.getNamespace();
- if (FileTransferDescription.NAMESPACES.contains(namespace)) {
+ if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) {
builder.add(FileTransferDescription.upgrade(child));
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
builder.add(RtpDescription.upgrade(child));
@@ -1,50 +0,0 @@
-package eu.siacs.conversations.xmpp.jingle.stanzas;
-
-import com.google.common.base.Preconditions;
-
-import java.util.Collection;
-import java.util.List;
-
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.jingle.JingleCandidate;
-
-public class S5BTransportInfo extends GenericTransportInfo {
-
- private S5BTransportInfo(final String name, final String xmlns) {
- super(name, xmlns);
- }
-
- public String getTransportId() {
- return this.getAttribute("sid");
- }
-
- public S5BTransportInfo(final String transportId, final Collection<JingleCandidate> candidates) {
- super("transport", Namespace.JINGLE_TRANSPORTS_S5B);
- Preconditions.checkNotNull(transportId,"transport id must not be null");
- for(JingleCandidate candidate : candidates) {
- this.addChild(candidate.toElement());
- }
- this.setAttribute("sid", transportId);
- }
-
- public S5BTransportInfo(final String transportId, final Element child) {
- super("transport", Namespace.JINGLE_TRANSPORTS_S5B);
- Preconditions.checkNotNull(transportId,"transport id must not be null");
- this.addChild(child);
- this.setAttribute("sid", transportId);
- }
-
- public List<JingleCandidate> getCandidates() {
- return JingleCandidate.parse(this.getChildren());
- }
-
- public static S5BTransportInfo upgrade(final Element element) {
- Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport");
- Preconditions.checkArgument(Namespace.JINGLE_TRANSPORTS_S5B.equals(element.getNamespace()), "Element does not match s5b transport namespace");
- final S5BTransportInfo transportInfo = new S5BTransportInfo("transport", Namespace.JINGLE_TRANSPORTS_S5B);
- transportInfo.setAttributes(element.getAttributes());
- transportInfo.setChildren(element.getChildren());
- return transportInfo;
- }
-}
@@ -0,0 +1,117 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import android.util.Log;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport;
+
+import java.util.Collection;
+import java.util.List;
+
+public class SocksByteStreamsTransportInfo extends GenericTransportInfo {
+
+ private SocksByteStreamsTransportInfo() {
+ super("transport", Namespace.JINGLE_TRANSPORTS_S5B);
+ }
+
+ public String getTransportId() {
+ return this.getAttribute("sid");
+ }
+
+ public SocksByteStreamsTransportInfo(
+ final String transportId,
+ final Collection<SocksByteStreamsTransport.Candidate> candidates) {
+ super("transport", Namespace.JINGLE_TRANSPORTS_S5B);
+ Preconditions.checkNotNull(transportId, "transport id must not be null");
+ for (SocksByteStreamsTransport.Candidate candidate : candidates) {
+ this.addChild(candidate.asElement());
+ }
+ this.setAttribute("sid", transportId);
+ }
+
+ public TransportInfo getTransportInfo() {
+ if (hasChild("proxy-error")) {
+ return new ProxyError();
+ } else if (hasChild("candidate-error")) {
+ return new CandidateError();
+ } else if (hasChild("candidate-used")) {
+ final Element candidateUsed = findChild("candidate-used");
+ final String cid = candidateUsed == null ? null : candidateUsed.getAttribute("cid");
+ if (Strings.isNullOrEmpty(cid)) {
+ return null;
+ } else {
+ return new CandidateUsed(cid);
+ }
+ } else if (hasChild("activated")) {
+ final Element activated = findChild("activated");
+ final String cid = activated == null ? null : activated.getAttribute("cid");
+ if (Strings.isNullOrEmpty(cid)) {
+ return null;
+ } else {
+ return new Activated(cid);
+ }
+ } else {
+ return null;
+ }
+ }
+
+ public List<SocksByteStreamsTransport.Candidate> getCandidates() {
+ final ImmutableList.Builder<SocksByteStreamsTransport.Candidate> candidateBuilder =
+ new ImmutableList.Builder<>();
+ for (final Element child : this.children) {
+ if ("candidate".equals(child.getName())
+ && Namespace.JINGLE_TRANSPORTS_S5B.equals(child.getNamespace())) {
+ try {
+ candidateBuilder.add(SocksByteStreamsTransport.Candidate.of(child));
+ } catch (final Exception e) {
+ Log.d(Config.LOGTAG, "skip over broken candidate", e);
+ }
+ }
+ }
+ return candidateBuilder.build();
+ }
+
+ public static SocksByteStreamsTransportInfo upgrade(final Element element) {
+ Preconditions.checkArgument(
+ "transport".equals(element.getName()), "Name of provided element is not transport");
+ Preconditions.checkArgument(
+ Namespace.JINGLE_TRANSPORTS_S5B.equals(element.getNamespace()),
+ "Element does not match s5b transport namespace");
+ final SocksByteStreamsTransportInfo transportInfo = new SocksByteStreamsTransportInfo();
+ transportInfo.setAttributes(element.getAttributes());
+ transportInfo.setChildren(element.getChildren());
+ return transportInfo;
+ }
+
+ public String getDestinationAddress() {
+ return this.getAttribute("dstaddr");
+ }
+
+ public abstract static class TransportInfo {}
+
+ public static class CandidateUsed extends TransportInfo {
+ public final String cid;
+
+ public CandidateUsed(String cid) {
+ this.cid = cid;
+ }
+ }
+
+ public static class Activated extends TransportInfo {
+ public final String cid;
+
+ public Activated(final String cid) {
+ this.cid = cid;
+ }
+ }
+
+ public static class CandidateError extends TransportInfo {}
+
+ public static class ProxyError extends TransportInfo {}
+}
@@ -0,0 +1,111 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import com.google.common.primitives.Ints;
+
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.jingle.SessionDescription;
+import eu.siacs.conversations.xmpp.jingle.transports.Transport;
+
+public class WebRTCDataChannelTransportInfo extends GenericTransportInfo {
+
+ public static final WebRTCDataChannelTransportInfo STUB = new WebRTCDataChannelTransportInfo();
+
+ public WebRTCDataChannelTransportInfo() {
+ super("transport", Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL);
+ }
+
+ public static WebRTCDataChannelTransportInfo upgrade(final Element element) {
+ Preconditions.checkArgument(
+ "transport".equals(element.getName()), "Name of provided element is not transport");
+ Preconditions.checkArgument(
+ Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL.equals(element.getNamespace()),
+ "Element does not match ice-udp transport namespace");
+ final WebRTCDataChannelTransportInfo transportInfo = new WebRTCDataChannelTransportInfo();
+ transportInfo.setAttributes(element.getAttributes());
+ transportInfo.setChildren(element.getChildren());
+ return transportInfo;
+ }
+
+ public IceUdpTransportInfo innerIceUdpTransportInfo() {
+ final var iceUdpTransportInfo =
+ this.findChild("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP);
+ if (iceUdpTransportInfo != null) {
+ return IceUdpTransportInfo.upgrade(iceUdpTransportInfo);
+ }
+ return null;
+ }
+
+ public static Transport.InitialTransportInfo of(final SessionDescription sessionDescription) {
+ final SessionDescription.Media media = Iterables.getOnlyElement(sessionDescription.media);
+ final String id = Iterables.getFirst(media.attributes.get("mid"), null);
+ Preconditions.checkNotNull(id, "media has no mid");
+ final String maxMessageSize =
+ Iterables.getFirst(media.attributes.get("max-message-size"), null);
+ final Integer maxMessageSizeInt =
+ maxMessageSize == null ? null : Ints.tryParse(maxMessageSize);
+ final String sctpPort = Iterables.getFirst(media.attributes.get("sctp-port"), null);
+ final Integer sctpPortInt = sctpPort == null ? null : Ints.tryParse(sctpPort);
+ final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo =
+ new WebRTCDataChannelTransportInfo();
+ if (maxMessageSizeInt != null) {
+ webRTCDataChannelTransportInfo.setAttribute("max-message-size", maxMessageSizeInt);
+ }
+ if (sctpPortInt != null) {
+ webRTCDataChannelTransportInfo.setAttribute("sctp-port", sctpPortInt);
+ }
+ webRTCDataChannelTransportInfo.addChild(IceUdpTransportInfo.of(sessionDescription, media));
+
+ final String groupAttribute =
+ Iterables.getFirst(sessionDescription.attributes.get("group"), null);
+ final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
+ return new Transport.InitialTransportInfo(id, webRTCDataChannelTransportInfo, group);
+ }
+
+ public Integer getSctpPort() {
+ final var attribute = this.getAttribute("sctp-port");
+ if (attribute == null) {
+ return null;
+ }
+ return Ints.tryParse(attribute);
+ }
+
+ public Integer getMaxMessageSize() {
+ final var attribute = this.getAttribute("max-message-size");
+ if (attribute == null) {
+ return null;
+ }
+ return Ints.tryParse(attribute);
+ }
+
+ public WebRTCDataChannelTransportInfo cloneWrapper() {
+ final var iceUdpTransport = this.innerIceUdpTransportInfo();
+ final WebRTCDataChannelTransportInfo transportInfo = new WebRTCDataChannelTransportInfo();
+ transportInfo.setAttributes(new Hashtable<>(getAttributes()));
+ transportInfo.addChild(iceUdpTransport.cloneWrapper());
+ return transportInfo;
+ }
+
+ public void addCandidate(final IceUdpTransportInfo.Candidate candidate) {
+ this.innerIceUdpTransportInfo().addChild(candidate);
+ }
+
+ public List<IceUdpTransportInfo.Candidate> getCandidates() {
+ final var innerTransportInfo = this.innerIceUdpTransportInfo();
+ if (innerTransportInfo == null) {
+ return Collections.emptyList();
+ }
+ return innerTransportInfo.getCandidates();
+ }
+
+ public IceUdpTransportInfo.Credentials getCredentials() {
+ final var innerTransportInfo = this.innerIceUdpTransportInfo();
+ return innerTransportInfo == null ? null : innerTransportInfo.getCredentials();
+ }
+}
@@ -0,0 +1,321 @@
+package eu.siacs.conversations.xmpp.jingle.transports;
+
+import android.util.Log;
+
+import com.google.common.base.Strings;
+import com.google.common.io.BaseEncoding;
+import com.google.common.io.Closeables;
+import com.google.common.primitives.Ints;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class InbandBytestreamsTransport implements Transport {
+
+ private static final int DEFAULT_BLOCK_SIZE = 8192;
+
+ private final PipedInputStream pipedInputStream = new PipedInputStream(DEFAULT_BLOCK_SIZE);
+ private final PipedOutputStream pipedOutputStream = new PipedOutputStream();
+ private final CountDownLatch terminationLatch = new CountDownLatch(1);
+
+ private final XmppConnection xmppConnection;
+
+ private final Jid with;
+
+ private final boolean initiator;
+
+ private final String streamId;
+
+ private int blockSize;
+ private Callback transportCallback;
+ private final BlockSender blockSender;
+
+ private final Thread blockSenderThread;
+
+ private final AtomicBoolean isReceiving = new AtomicBoolean(false);
+
+ public InbandBytestreamsTransport(
+ final XmppConnection xmppConnection, final Jid with, final boolean initiator) {
+ this(xmppConnection, with, initiator, UUID.randomUUID().toString(), DEFAULT_BLOCK_SIZE);
+ }
+
+ public InbandBytestreamsTransport(
+ final XmppConnection xmppConnection,
+ final Jid with,
+ final boolean initiator,
+ final String streamId,
+ final int blockSize) {
+ this.xmppConnection = xmppConnection;
+ this.with = with;
+ this.initiator = initiator;
+ this.streamId = streamId;
+ this.blockSize = Math.min(DEFAULT_BLOCK_SIZE, blockSize);
+ this.blockSender =
+ new BlockSender(xmppConnection, with, streamId, this.blockSize, pipedInputStream);
+ this.blockSenderThread = new Thread(blockSender);
+ }
+
+ public void setTransportCallback(final Callback callback) {
+ this.transportCallback = callback;
+ }
+
+ public String getStreamId() {
+ return this.streamId;
+ }
+
+ public void connect() {
+ if (initiator) {
+ openInBandTransport();
+ }
+ }
+
+ @Override
+ public CountDownLatch getTerminationLatch() {
+ return this.terminationLatch;
+ }
+
+ private void openInBandTransport() {
+ final var iqPacket = new IqPacket(IqPacket.TYPE.SET);
+ iqPacket.setTo(with);
+ final var open = iqPacket.addChild("open", Namespace.IBB);
+ open.setAttribute("block-size", this.blockSize);
+ open.setAttribute("sid", this.streamId);
+ Log.d(Config.LOGTAG, "sending ibb open");
+ Log.d(Config.LOGTAG, iqPacket.toString());
+ xmppConnection.sendIqPacket(iqPacket, this::receiveResponseToOpen);
+ }
+
+ private void receiveResponseToOpen(final Account account, final IqPacket response) {
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ Log.d(Config.LOGTAG, "ibb open was accepted");
+ this.transportCallback.onTransportEstablished();
+ this.blockSenderThread.start();
+ } else {
+ this.transportCallback.onTransportSetupFailed();
+ }
+ }
+
+ public boolean deliverPacket(
+ final PacketType packetType, final Jid from, final Element payload) {
+ if (from == null || !from.equals(with)) {
+ Log.d(
+ Config.LOGTAG,
+ "ibb packet received from wrong address. was " + from + " expected " + with);
+ return false;
+ }
+ return switch (packetType) {
+ case OPEN -> receiveOpen();
+ case DATA -> receiveData(payload.getContent());
+ case CLOSE -> receiveClose();
+ default -> throw new IllegalArgumentException("Invalid packet type");
+ };
+ }
+
+ private boolean receiveData(final String encoded) {
+ final byte[] buffer;
+ if (Strings.isNullOrEmpty(encoded)) {
+ buffer = new byte[0];
+ } else {
+ buffer = BaseEncoding.base64().decode(encoded);
+ }
+ Log.d(Config.LOGTAG, "ibb received " + buffer.length + " bytes");
+ try {
+ pipedOutputStream.write(buffer);
+ pipedOutputStream.flush();
+ return true;
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG, "unable to receive ibb data", e);
+ return false;
+ }
+ }
+
+ private boolean receiveClose() {
+ if (this.isReceiving.compareAndSet(true, false)) {
+ try {
+ this.pipedOutputStream.close();
+ return true;
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG, "could not close pipedOutStream");
+ return false;
+ }
+ } else {
+ Log.d(Config.LOGTAG, "received ibb close but was not receiving");
+ return false;
+ }
+ }
+
+ private boolean receiveOpen() {
+ Log.d(Config.LOGTAG, "receiveOpen()");
+ if (this.isReceiving.get()) {
+ Log.d(Config.LOGTAG, "ibb received open even though we were already open");
+ return false;
+ }
+ this.isReceiving.set(true);
+ transportCallback.onTransportEstablished();
+ return true;
+ }
+
+ public void terminate() {
+ // TODO send close
+ Log.d(Config.LOGTAG, "IbbTransport.terminate()");
+ this.terminationLatch.countDown();
+ this.blockSender.close();
+ this.blockSenderThread.interrupt();
+ closeQuietly(this.pipedOutputStream);
+ }
+
+ private static void closeQuietly(final OutputStream outputStream) {
+ try {
+ outputStream.close();
+ } catch (final IOException ignored) {
+
+ }
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ final var outputStream = new PipedOutputStream();
+ this.pipedInputStream.connect(outputStream);
+ return outputStream;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ final var inputStream = new PipedInputStream();
+ this.pipedOutputStream.connect(inputStream);
+ return inputStream;
+ }
+
+ @Override
+ public ListenableFuture<TransportInfo> asTransportInfo() {
+ return Futures.immediateFuture(
+ new TransportInfo(new IbbTransportInfo(streamId, blockSize), null));
+ }
+
+ @Override
+ public ListenableFuture<InitialTransportInfo> asInitialTransportInfo() {
+ return Futures.immediateFuture(
+ new InitialTransportInfo(
+ UUID.randomUUID().toString(),
+ new IbbTransportInfo(streamId, blockSize),
+ null));
+ }
+
+ public void setPeerBlockSize(long peerBlockSize) {
+ this.blockSize = Math.min(Ints.saturatedCast(peerBlockSize), DEFAULT_BLOCK_SIZE);
+ if (this.blockSize < DEFAULT_BLOCK_SIZE) {
+ Log.d(Config.LOGTAG, "peer reconfigured IBB block size to " + this.blockSize);
+ }
+ this.blockSender.setBlockSize(this.blockSize);
+ }
+
+ private static class BlockSender implements Runnable, Closeable {
+
+ private final XmppConnection xmppConnection;
+
+ private final Jid with;
+ private final String streamId;
+
+ private int blockSize;
+ private final PipedInputStream inputStream;
+ private final Semaphore semaphore = new Semaphore(3);
+ private final AtomicInteger sequencer = new AtomicInteger();
+ private final AtomicBoolean isSending = new AtomicBoolean(true);
+
+ private BlockSender(
+ XmppConnection xmppConnection,
+ final Jid with,
+ String streamId,
+ int blockSize,
+ PipedInputStream inputStream) {
+ this.xmppConnection = xmppConnection;
+ this.with = with;
+ this.streamId = streamId;
+ this.blockSize = blockSize;
+ this.inputStream = inputStream;
+ }
+
+ @Override
+ public void run() {
+ final var buffer = new byte[blockSize];
+ try {
+ while (isSending.get()) {
+ final int count = this.inputStream.read(buffer);
+ if (count < 0) {
+ Log.d(Config.LOGTAG, "block sender reached EOF");
+ return;
+ }
+ this.semaphore.acquire();
+ final var block = new byte[count];
+ System.arraycopy(buffer, 0, block, 0, block.length);
+ sendIbbBlock(sequencer.getAndIncrement(), block);
+ }
+ } catch (final InterruptedException | InterruptedIOException e) {
+ if (isSending.get()) {
+ Log.w(Config.LOGTAG, "IbbBlockSender got interrupted while sending", e);
+ }
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG, "block sender terminated", e);
+ } finally {
+ Closeables.closeQuietly(inputStream);
+ }
+ }
+
+ private void sendIbbBlock(final int sequence, final byte[] block) {
+ Log.d(Config.LOGTAG, "sending ibb block #" + sequence + " " + block.length + " bytes");
+ final var iqPacket = new IqPacket(IqPacket.TYPE.SET);
+ iqPacket.setTo(with);
+ final var data = iqPacket.addChild("data", Namespace.IBB);
+ data.setAttribute("sid", this.streamId);
+ data.setAttribute("seq", sequence);
+ data.setContent(BaseEncoding.base64().encode(block));
+ this.xmppConnection.sendIqPacket(
+ iqPacket,
+ (a, response) -> {
+ if (response.getType() != IqPacket.TYPE.RESULT) {
+ Log.d(
+ Config.LOGTAG,
+ "received iq error in response to data block #" + sequence);
+ isSending.set(false);
+ }
+ semaphore.release();
+ });
+ }
+
+ @Override
+ public void close() {
+ this.isSending.set(false);
+ }
+
+ public void setBlockSize(final int blockSize) {
+ this.blockSize = blockSize;
+ }
+ }
+
+ public enum PacketType {
+ OPEN,
+ DATA,
+ CLOSE
+ }
+}
@@ -0,0 +1,870 @@
+package eu.siacs.conversations.xmpp.jingle.transports;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteStreams;
+import com.google.common.primitives.Ints;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.SocksSocketFactory;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+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.stanzas.IqPacket;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class SocksByteStreamsTransport implements Transport {
+
+ private final XmppConnection xmppConnection;
+
+ private final AbstractJingleConnection.Id id;
+
+ private final boolean initiator;
+ private final boolean useTor;
+
+ private final String streamId;
+
+ private ImmutableList<Candidate> theirCandidates;
+ private final String theirDestination;
+ private final SettableFuture<Connection> selectedByThemCandidate = SettableFuture.create();
+ private final SettableFuture<String> theirProxyActivation = SettableFuture.create();
+
+ private final CountDownLatch terminationLatch = new CountDownLatch(1);
+
+ private final ConnectionProvider connectionProvider;
+ private final ListenableFuture<Connection> ourProxyConnection;
+
+ private Connection connection;
+
+ private Callback transportCallback;
+
+ public SocksByteStreamsTransport(
+ final XmppConnection xmppConnection,
+ final AbstractJingleConnection.Id id,
+ final boolean initiator,
+ final boolean useTor,
+ final String streamId,
+ final Collection<Candidate> theirCandidates) {
+ this.xmppConnection = xmppConnection;
+ this.id = id;
+ this.initiator = initiator;
+ this.useTor = useTor;
+ this.streamId = streamId;
+ this.theirDestination =
+ Hashing.sha1()
+ .hashString(
+ Joiner.on("")
+ .join(
+ Arrays.asList(
+ streamId,
+ id.with.toEscapedString(),
+ id.account.getJid().toEscapedString())),
+ StandardCharsets.UTF_8)
+ .toString();
+ final var ourDestination =
+ Hashing.sha1()
+ .hashString(
+ Joiner.on("")
+ .join(
+ Arrays.asList(
+ streamId,
+ id.account.getJid().toEscapedString(),
+ id.with.toEscapedString())),
+ StandardCharsets.UTF_8)
+ .toString();
+
+ this.connectionProvider =
+ new ConnectionProvider(id.account.getJid(), ourDestination, useTor);
+ new Thread(connectionProvider).start();
+ this.ourProxyConnection = getOurProxyConnection(ourDestination);
+ setTheirCandidates(theirCandidates);
+ }
+
+ public SocksByteStreamsTransport(
+ final XmppConnection xmppConnection,
+ final AbstractJingleConnection.Id id,
+ final boolean initiator,
+ final boolean useTor) {
+ this(
+ xmppConnection,
+ id,
+ initiator,
+ useTor,
+ UUID.randomUUID().toString(),
+ Collections.emptyList());
+ }
+
+ public void connectTheirCandidates() {
+ Preconditions.checkState(
+ this.transportCallback != null, "transport callback needs to be set");
+ // TODO this needs to go into a variable so we can cancel it
+ final var connectionFinder =
+ new ConnectionFinder(theirCandidates, theirDestination, useTor);
+ new Thread(connectionFinder).start();
+ Futures.addCallback(
+ connectionFinder.connectionFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Connection connection) {
+ final Candidate candidate = connection.candidate;
+ transportCallback.onCandidateUsed(streamId, candidate);
+ establishTransport(connection);
+ }
+
+ @Override
+ public void onFailure(@NonNull final Throwable throwable) {
+ if (throwable instanceof CandidateErrorException) {
+ transportCallback.onCandidateError(streamId);
+ }
+ establishTransport(null);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void establishTransport(final Connection selectedByUs) {
+ Futures.addCallback(
+ selectedByThemCandidate,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Connection result) {
+ establishTransport(selectedByUs, result);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ establishTransport(selectedByUs, null);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void establishTransport(
+ final Connection selectedByUs, final Connection selectedByThem) {
+ final var selection = selectConnection(selectedByUs, selectedByThem);
+ if (selection == null) {
+ transportCallback.onTransportSetupFailed();
+ return;
+ }
+ if (selection.connection.candidate.type == CandidateType.DIRECT) {
+ Log.d(Config.LOGTAG, "final selection " + selection.connection.candidate);
+ this.connection = selection.connection;
+ this.transportCallback.onTransportEstablished();
+ } else {
+ final ListenableFuture<String> proxyActivation;
+ if (selection.owner == Owner.THEIRS) {
+ proxyActivation = this.theirProxyActivation;
+ } else {
+ proxyActivation = activateProxy(selection.connection.candidate);
+ }
+ Log.d(Config.LOGTAG, "waiting for proxy activation");
+ Futures.addCallback(
+ proxyActivation,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final String cid) {
+ // TODO compare cid to selection.connection.candidate
+ connection = selection.connection;
+ transportCallback.onTransportEstablished();
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(Config.LOGTAG, "failed to activate proxy");
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+ }
+
+ private ConnectionWithOwner selectConnection(
+ final Connection selectedByUs, final Connection selectedByThem) {
+ if (selectedByUs != null && selectedByThem != null) {
+ if (selectedByUs.candidate.priority == selectedByThem.candidate.priority) {
+ return initiator
+ ? new ConnectionWithOwner(selectedByUs, Owner.THEIRS)
+ : new ConnectionWithOwner(selectedByThem, Owner.OURS);
+ } else if (selectedByUs.candidate.priority > selectedByThem.candidate.priority) {
+ return new ConnectionWithOwner(selectedByUs, Owner.THEIRS);
+ } else {
+ return new ConnectionWithOwner(selectedByThem, Owner.OURS);
+ }
+ }
+ if (selectedByUs != null) {
+ return new ConnectionWithOwner(selectedByUs, Owner.THEIRS);
+ }
+ if (selectedByThem != null) {
+ return new ConnectionWithOwner(selectedByThem, Owner.OURS);
+ }
+ return null;
+ }
+
+ private ListenableFuture<String> activateProxy(final Candidate candidate) {
+ Log.d(Config.LOGTAG, "trying to activate our proxy " + candidate);
+ final SettableFuture<String> iqFuture = SettableFuture.create();
+ final IqPacket proxyActivation = new IqPacket(IqPacket.TYPE.SET);
+ proxyActivation.setTo(candidate.jid);
+ final Element query = proxyActivation.addChild("query", Namespace.BYTE_STREAMS);
+ query.setAttribute("sid", this.streamId);
+ final Element activate = query.addChild("activate");
+ activate.setContent(id.with.toEscapedString());
+ xmppConnection.sendIqPacket(
+ proxyActivation,
+ (a, response) -> {
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ Log.d(Config.LOGTAG, "our proxy has been activated");
+ transportCallback.onProxyActivated(this.streamId, candidate);
+ iqFuture.set(candidate.cid);
+ } else if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+ iqFuture.setException(new TimeoutException());
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ a.getJid().asBareJid()
+ + ": failed to activate proxy on "
+ + candidate.jid);
+ iqFuture.setException(new IllegalStateException("Proxy activation failed"));
+ }
+ });
+ return iqFuture;
+ }
+
+ private ListenableFuture<Connection> getOurProxyConnection(final String ourDestination) {
+ final var proxyFuture = getProxyCandidate();
+ return Futures.transformAsync(
+ proxyFuture,
+ proxy -> {
+ final var connectionFinder =
+ new ConnectionFinder(ImmutableList.of(proxy), ourDestination, useTor);
+ new Thread(connectionFinder).start();
+ return Futures.transform(
+ connectionFinder.connectionFuture,
+ c -> {
+ try {
+ c.socket.setKeepAlive(true);
+ Log.d(
+ Config.LOGTAG,
+ "set keep alive on our own proxy connection");
+ } catch (final SocketException e) {
+ throw new RuntimeException(e);
+ }
+ return c;
+ },
+ MoreExecutors.directExecutor());
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Candidate> getProxyCandidate() {
+ if (Config.DISABLE_PROXY_LOOKUP) {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException("Proxy look up is disabled"));
+ }
+ final Jid streamer = xmppConnection.findDiscoItemByFeature(Namespace.BYTE_STREAMS);
+ if (streamer == null) {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException("No proxy/streamer found"));
+ }
+ final IqPacket iqRequest = new IqPacket(IqPacket.TYPE.GET);
+ iqRequest.setTo(streamer);
+ iqRequest.query(Namespace.BYTE_STREAMS);
+ final SettableFuture<Candidate> candidateFuture = SettableFuture.create();
+ xmppConnection.sendIqPacket(
+ iqRequest,
+ (a, response) -> {
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ final Element query = response.findChild("query", Namespace.BYTE_STREAMS);
+ final Element streamHost =
+ query == null
+ ? null
+ : query.findChild("streamhost", Namespace.BYTE_STREAMS);
+ final String host =
+ streamHost == null ? null : streamHost.getAttribute("host");
+ final Integer port =
+ Ints.tryParse(
+ Strings.nullToEmpty(
+ streamHost == null
+ ? null
+ : streamHost.getAttribute("port")));
+ if (Strings.isNullOrEmpty(host) || port == null) {
+ candidateFuture.setException(
+ new IOException("Proxy response is missing attributes"));
+ return;
+ }
+ candidateFuture.set(
+ new Candidate(
+ UUID.randomUUID().toString(),
+ host,
+ streamer,
+ port,
+ 655360 + (initiator ? 0 : 15),
+ CandidateType.PROXY));
+
+ } else if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+ candidateFuture.setException(new TimeoutException());
+ } else {
+ candidateFuture.setException(
+ new IOException(
+ "received iq error in response to proxy discovery"));
+ }
+ });
+ return candidateFuture;
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ final var connection = this.connection;
+ if (connection == null) {
+ throw new IOException("No candidate has been selected yet");
+ }
+ return connection.socket.getOutputStream();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ final var connection = this.connection;
+ if (connection == null) {
+ throw new IOException("No candidate has been selected yet");
+ }
+ return connection.socket.getInputStream();
+ }
+
+ @Override
+ public ListenableFuture<TransportInfo> asTransportInfo() {
+ final ListenableFuture<Collection<Connection>> proxyConnections =
+ getOurProxyConnectionsFuture();
+ return Futures.transform(
+ proxyConnections,
+ proxies -> {
+ final var candidateBuilder = new ImmutableList.Builder<Candidate>();
+ candidateBuilder.addAll(this.connectionProvider.candidates);
+ candidateBuilder.addAll(Collections2.transform(proxies, p -> p.candidate));
+ final var transportInfo =
+ new SocksByteStreamsTransportInfo(
+ this.streamId, candidateBuilder.build());
+ return new TransportInfo(transportInfo, null);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ @Override
+ public ListenableFuture<InitialTransportInfo> asInitialTransportInfo() {
+ return Futures.transform(
+ asTransportInfo(),
+ ti ->
+ new InitialTransportInfo(
+ UUID.randomUUID().toString(), ti.transportInfo, ti.group),
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Collection<Connection>> getOurProxyConnectionsFuture() {
+ return Futures.catching(
+ Futures.transform(
+ this.ourProxyConnection,
+ Collections::singleton,
+ MoreExecutors.directExecutor()),
+ Exception.class,
+ ex -> {
+ Log.d(Config.LOGTAG, "could not find a proxy of our own", ex);
+ return Collections.emptyList();
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private Collection<Connection> getOurProxyConnections() {
+ final var future = getOurProxyConnectionsFuture();
+ if (future.isDone()) {
+ try {
+ return future.get();
+ } catch (final Exception e) {
+ return Collections.emptyList();
+ }
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ @Override
+ public void terminate() {
+ Log.d(Config.LOGTAG, "terminating socks transport");
+ this.terminationLatch.countDown();
+ final var connection = this.connection;
+ if (connection != null) {
+ closeSocket(connection.socket);
+ }
+ this.connectionProvider.close();
+ }
+
+ @Override
+ public void setTransportCallback(final Callback callback) {
+ this.transportCallback = callback;
+ }
+
+ @Override
+ public void connect() {
+ this.connectTheirCandidates();
+ }
+
+ @Override
+ public CountDownLatch getTerminationLatch() {
+ return this.terminationLatch;
+ }
+
+ public boolean setCandidateUsed(final String cid) {
+ final var ourProxyConnections = getOurProxyConnections();
+ final var proxyConnection =
+ Iterables.tryFind(ourProxyConnections, c -> c.candidate.cid.equals(cid));
+ if (proxyConnection.isPresent()) {
+ this.selectedByThemCandidate.set(proxyConnection.get());
+ return true;
+ }
+
+ // the peer selected a connection that is not our proxy. so we can close our proxies
+ closeConnections(ourProxyConnections);
+
+ final var connection = this.connectionProvider.findPeerConnection(cid);
+ if (connection.isPresent()) {
+ this.selectedByThemCandidate.set(connection.get());
+ return true;
+ } else {
+ Log.d(Config.LOGTAG, "none of the connected candidates has cid " + cid);
+ return false;
+ }
+ }
+
+ public void setCandidateError() {
+ this.selectedByThemCandidate.setException(
+ new CandidateErrorException("Remote could not connect to any of our candidates"));
+ }
+
+ public void setProxyActivated(final String cid) {
+ this.theirProxyActivation.set(cid);
+ }
+
+ public void setProxyError() {
+ this.theirProxyActivation.setException(
+ new IllegalStateException("Remote could not activate their proxy"));
+ }
+
+ public void setTheirCandidates(Collection<Candidate> candidates) {
+ this.theirCandidates =
+ Ordering.from(
+ (Comparator<Candidate>)
+ (o1, o2) -> Integer.compare(o2.priority, o1.priority))
+ .immutableSortedCopy(candidates);
+ }
+
+ private static void closeSocket(final Socket socket) {
+ try {
+ socket.close();
+ } catch (final IOException e) {
+ Log.w(Config.LOGTAG, "error closing socket", e);
+ }
+ }
+
+ private static class ConnectionProvider implements Runnable {
+
+ private final ExecutorService clientConnectionExecutorService =
+ Executors.newFixedThreadPool(4);
+
+ private final ImmutableList<Candidate> candidates;
+
+ private final int port;
+
+ private final AtomicBoolean acceptingConnections = new AtomicBoolean(true);
+
+ private ServerSocket serverSocket;
+
+ private final String destination;
+
+ private final ArrayList<Connection> peerConnections = new ArrayList<>();
+
+ private ConnectionProvider(
+ final Jid account, final String destination, final boolean useTor) {
+ final SecureRandom secureRandom = new SecureRandom();
+ this.port = secureRandom.nextInt(60_000) + 1024;
+ this.destination = destination;
+ final InetAddress[] localAddresses;
+ if (Config.USE_DIRECT_JINGLE_CANDIDATES && !useTor) {
+ localAddresses =
+ DirectConnectionUtils.getLocalAddresses().toArray(new InetAddress[0]);
+ } else {
+ localAddresses = new InetAddress[0];
+ }
+ final var candidateBuilder = new ImmutableList.Builder<Candidate>();
+ for (int i = 0; i < localAddresses.length; ++i) {
+ final var inetAddress = localAddresses[i];
+ candidateBuilder.add(
+ new Candidate(
+ UUID.randomUUID().toString(),
+ inetAddress.getHostAddress(),
+ account,
+ port,
+ 8257536 + i,
+ CandidateType.DIRECT));
+ }
+ this.candidates = candidateBuilder.build();
+ }
+
+ @Override
+ public void run() {
+ if (this.candidates.isEmpty()) {
+ Log.d(Config.LOGTAG, "no direct candidates. stopping ConnectionProvider");
+ return;
+ }
+ try (final ServerSocket serverSocket = new ServerSocket(this.port)) {
+ this.serverSocket = serverSocket;
+ while (acceptingConnections.get()) {
+ final Socket clientSocket;
+ try {
+ clientSocket = serverSocket.accept();
+ } catch (final SocketException ignored) {
+ Log.d(Config.LOGTAG, "server socket has been closed.");
+ return;
+ }
+ clientConnectionExecutorService.execute(
+ () -> acceptClientConnection(clientSocket));
+ }
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG, "could not create server socket", e);
+ }
+ }
+
+ private void acceptClientConnection(final Socket socket) {
+ final var localAddress = socket.getLocalAddress();
+ final var hostAddress = localAddress == null ? null : localAddress.getHostAddress();
+ final var candidate =
+ Iterables.tryFind(this.candidates, c -> c.host.equals(hostAddress));
+ if (candidate.isPresent()) {
+ acceptingConnections(socket, candidate.get());
+
+ } else {
+ closeSocket(socket);
+ Log.d(Config.LOGTAG, "no local candidate found for connection on " + hostAddress);
+ }
+ }
+
+ private void acceptingConnections(final Socket socket, final Candidate candidate) {
+ final var remoteAddress = socket.getRemoteSocketAddress();
+ Log.d(
+ Config.LOGTAG,
+ "accepted client connection from " + remoteAddress + " to " + candidate);
+ try {
+ socket.setSoTimeout(3000);
+ final byte[] authBegin = new byte[2];
+ final InputStream inputStream = socket.getInputStream();
+ final OutputStream outputStream = socket.getOutputStream();
+ ByteStreams.readFully(inputStream, authBegin);
+ if (authBegin[0] != 0x5) {
+ socket.close();
+ }
+ final short methodCount = authBegin[1];
+ final byte[] methods = new byte[methodCount];
+ ByteStreams.readFully(inputStream, methods);
+ if (SocksSocketFactory.contains((byte) 0x00, methods)) {
+ outputStream.write(new byte[] {0x05, 0x00});
+ } else {
+ outputStream.write(new byte[] {0x05, (byte) 0xff});
+ }
+ final byte[] connectCommand = new byte[4];
+ ByteStreams.readFully(inputStream, connectCommand);
+ if (connectCommand[0] == 0x05
+ && connectCommand[1] == 0x01
+ && connectCommand[3] == 0x03) {
+ int destinationCount = inputStream.read();
+ final byte[] destination = new byte[destinationCount];
+ ByteStreams.readFully(inputStream, destination);
+ final byte[] port = new byte[2];
+ ByteStreams.readFully(inputStream, port);
+ final String receivedDestination = new String(destination);
+ final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
+ final byte[] responseHeader;
+ final boolean success;
+ if (receivedDestination.equals(this.destination)) {
+ responseHeader = new byte[] {0x05, 0x00, 0x00, 0x03};
+ synchronized (this.peerConnections) {
+ peerConnections.add(new Connection(candidate, socket));
+ }
+ success = true;
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ "destination mismatch. received "
+ + receivedDestination
+ + " (expected "
+ + this.destination
+ + ")");
+ responseHeader = new byte[] {0x05, 0x04, 0x00, 0x03};
+ success = false;
+ }
+ response.put(responseHeader);
+ response.put((byte) destination.length);
+ response.put(destination);
+ response.put(port);
+ outputStream.write(response.array());
+ outputStream.flush();
+ if (success) {
+ Log.d(
+ Config.LOGTAG,
+ remoteAddress + " successfully connected to " + candidate);
+ } else {
+ closeSocket(socket);
+ }
+ }
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG, "failed to accept client connection to " + candidate, e);
+ closeSocket(socket);
+ }
+ }
+
+ private static void closeServerSocket(@Nullable final ServerSocket serverSocket) {
+ if (serverSocket == null) {
+ return;
+ }
+ try {
+ serverSocket.close();
+ } catch (final IOException ignored) {
+
+ }
+ }
+
+ public Optional<Connection> findPeerConnection(String cid) {
+ synchronized (this.peerConnections) {
+ return Iterables.tryFind(
+ this.peerConnections, connection -> connection.candidate.cid.equals(cid));
+ }
+ }
+
+ public void close() {
+ this.acceptingConnections.set(false); // we have probably done this earlier already
+ closeServerSocket(this.serverSocket);
+ synchronized (this.peerConnections) {
+ closeConnections(this.peerConnections);
+ this.peerConnections.clear();
+ }
+ }
+ }
+
+ private static void closeConnections(final Iterable<Connection> connections) {
+ for (final var connection : connections) {
+ closeSocket(connection.socket);
+ }
+ }
+
+ private static class ConnectionFinder implements Runnable {
+
+ private final SettableFuture<Connection> connectionFuture = SettableFuture.create();
+
+ private final ImmutableList<Candidate> candidates;
+ private final String destination;
+ private final boolean useTor;
+
+ private ConnectionFinder(
+ final ImmutableList<Candidate> candidates,
+ final String destination,
+ final boolean useTor) {
+ this.candidates = candidates;
+ this.destination = destination;
+ this.useTor = useTor;
+ }
+
+ @Override
+ public void run() {
+ for (final Candidate candidate : this.candidates) {
+ // TODO we can check if there is already something in `selectedByThemCandidate` with
+ // a higher priority and abort
+ try {
+ connectionFuture.set(connect(candidate));
+ Log.d(Config.LOGTAG, "connected to " + candidate);
+ return;
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG, "could not connect to candidate " + candidate);
+ }
+ }
+ connectionFuture.setException(
+ new CandidateErrorException(
+ String.format(
+ Locale.US,
+ "Gave up after %d candidates",
+ this.candidates.size())));
+ }
+
+ private Connection connect(final Candidate candidate) throws IOException {
+ final var timeout = 3000;
+ final Socket socket;
+ if (useTor) {
+ Log.d(Config.LOGTAG, "using Tor to connect to candidate " + candidate.host);
+ socket = SocksSocketFactory.createSocketOverTor(candidate.host, candidate.port);
+ } else {
+ socket = new Socket();
+ final SocketAddress address = new InetSocketAddress(candidate.host, candidate.port);
+ socket.connect(address, timeout);
+ }
+ socket.setSoTimeout(timeout);
+ SocksSocketFactory.createSocksConnection(socket, destination, 0);
+ socket.setSoTimeout(0);
+ return new Connection(candidate, socket);
+ }
+ }
+
+ public static class CandidateErrorException extends IllegalStateException {
+ private CandidateErrorException(final String message) {
+ super(message);
+ }
+ }
+
+ private enum Owner {
+ THEIRS,
+ OURS
+ }
+
+ public static class ConnectionWithOwner {
+ public final Connection connection;
+ public final Owner owner;
+
+ public ConnectionWithOwner(Connection connection, Owner owner) {
+ this.connection = connection;
+ this.owner = owner;
+ }
+ }
+
+ public static class Connection {
+
+ public final Candidate candidate;
+ public final Socket socket;
+
+ public Connection(Candidate candidate, Socket socket) {
+ this.candidate = candidate;
+ this.socket = socket;
+ }
+ }
+
+ public static class Candidate implements Transport.Candidate {
+ public final String cid;
+ public final String host;
+ public final Jid jid;
+ public final int port;
+ public final int priority;
+ public final CandidateType type;
+
+ public Candidate(
+ final String cid,
+ final String host,
+ final Jid jid,
+ int port,
+ int priority,
+ final CandidateType type) {
+ this.cid = cid;
+ this.host = host;
+ this.jid = jid;
+ this.port = port;
+ this.priority = priority;
+ this.type = type;
+ }
+
+ public static Candidate of(final Element element) {
+ Preconditions.checkArgument(
+ "candidate".equals(element.getName()),
+ "trying to construct candidate from non candidate element");
+ Preconditions.checkArgument(
+ Namespace.JINGLE_TRANSPORTS_S5B.equals(element.getNamespace()),
+ "candidate element is in correct namespace");
+ final String cid = element.getAttribute("cid");
+ final String host = element.getAttribute("host");
+ final String jid = element.getAttribute("jid");
+ final String port = element.getAttribute("port");
+ final String priority = element.getAttribute("priority");
+ final String type = element.getAttribute("type");
+ if (Strings.isNullOrEmpty(cid)
+ || Strings.isNullOrEmpty(host)
+ || Strings.isNullOrEmpty(jid)
+ || Strings.isNullOrEmpty(port)
+ || Strings.isNullOrEmpty(priority)
+ || Strings.isNullOrEmpty(type)) {
+ throw new IllegalArgumentException("Candidate is missing non optional attribute");
+ }
+ return new Candidate(
+ cid,
+ host,
+ Jid.ofEscaped(jid),
+ Integer.parseInt(port),
+ Integer.parseInt(priority),
+ CandidateType.valueOf(type.toUpperCase(Locale.ROOT)));
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("cid", cid)
+ .add("host", host)
+ .add("jid", jid)
+ .add("port", port)
+ .add("priority", priority)
+ .add("type", type)
+ .toString();
+ }
+
+ public Element asElement() {
+ final var element = new Element("candidate", Namespace.JINGLE_TRANSPORTS_S5B);
+ element.setAttribute("cid", this.cid);
+ element.setAttribute("host", this.host);
+ element.setAttribute("jid", this.jid);
+ element.setAttribute("port", this.port);
+ element.setAttribute("priority", this.priority);
+ element.setAttribute("type", this.type.toString().toLowerCase(Locale.ROOT));
+ return element;
+ }
+ }
+
+ public enum CandidateType {
+ DIRECT,
+ PROXY
+ }
+}
@@ -0,0 +1,80 @@
+package eu.siacs.conversations.xmpp.jingle.transports;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.CountDownLatch;
+
+public interface Transport {
+
+ OutputStream getOutputStream() throws IOException;
+
+ InputStream getInputStream() throws IOException;
+
+ ListenableFuture<TransportInfo> asTransportInfo();
+
+ ListenableFuture<InitialTransportInfo> asInitialTransportInfo();
+
+ default void readyToSentAdditionalCandidates() {}
+
+ void terminate();
+
+ void setTransportCallback(final Callback callback);
+
+ void connect();
+
+ CountDownLatch getTerminationLatch();
+
+ interface Callback {
+ void onTransportEstablished();
+
+ void onTransportSetupFailed();
+
+ void onAdditionalCandidate(final String contentName, final Candidate candidate);
+
+ void onCandidateUsed(String streamId, SocksByteStreamsTransport.Candidate candidate);
+
+ void onCandidateError(String streamId);
+
+ void onProxyActivated(String streamId, SocksByteStreamsTransport.Candidate candidate);
+ }
+
+ enum Direction {
+ SEND,
+ RECEIVE,
+ SEND_RECEIVE
+ }
+
+ class InitialTransportInfo extends TransportInfo {
+ public final String contentName;
+
+ public InitialTransportInfo(
+ String contentName, GenericTransportInfo transportInfo, Group group) {
+ super(transportInfo, group);
+ this.contentName = contentName;
+ }
+ }
+
+ class TransportInfo {
+
+ public final GenericTransportInfo transportInfo;
+ public final Group group;
+
+ public TransportInfo(final GenericTransportInfo transportInfo, final Group group) {
+ this.transportInfo = transportInfo;
+ this.group = group;
+ }
+
+ public TransportInfo(final GenericTransportInfo transportInfo) {
+ this.transportInfo = transportInfo;
+ this.group = null;
+ }
+ }
+
+ interface Candidate {}
+}
@@ -0,0 +1,617 @@
+package eu.siacs.conversations.xmpp.jingle.transports;
+
+import static eu.siacs.conversations.xmpp.jingle.WebRTCWrapper.buildConfiguration;
+import static eu.siacs.conversations.xmpp.jingle.WebRTCWrapper.logDescription;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.Closeables;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.jingle.IceServers;
+import eu.siacs.conversations.xmpp.jingle.WebRTCWrapper;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import org.webrtc.CandidatePairChangeEvent;
+import org.webrtc.DataChannel;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.SessionDescription;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nonnull;
+
+public class WebRTCDataChannelTransport implements Transport {
+
+ private static final int BUFFER_SIZE = 16_384;
+ private static final int MAX_SENT_BUFFER = 256 * 1024;
+
+ private final ExecutorService executorService = Executors.newSingleThreadExecutor();
+ private final ExecutorService localDescriptionExecutorService =
+ Executors.newSingleThreadExecutor();
+
+ private final AtomicBoolean readyToSentIceCandidates = new AtomicBoolean(false);
+ private final Queue<IceCandidate> pendingOutgoingIceCandidates = new LinkedList<>();
+
+ private final PipedOutputStream pipedOutputStream = new PipedOutputStream();
+ private final WritableByteChannel writableByteChannel = Channels.newChannel(pipedOutputStream);
+ private final PipedInputStream pipedInputStream = new PipedInputStream(BUFFER_SIZE);
+
+ private final AtomicBoolean connected = new AtomicBoolean(false);
+
+ private final CountDownLatch terminationLatch = new CountDownLatch(1);
+
+ private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
+
+ private final XmppConnection xmppConnection;
+ private final Account account;
+ private PeerConnectionFactory peerConnectionFactory;
+ private ListenableFuture<PeerConnection> peerConnectionFuture;
+
+ private ListenableFuture<SessionDescription> localDescriptionFuture;
+
+ private DataChannel dataChannel;
+
+ private Callback transportCallback;
+
+ private final PeerConnection.Observer peerConnectionObserver =
+ new PeerConnection.Observer() {
+ @Override
+ public void onSignalingChange(PeerConnection.SignalingState signalingState) {
+ Log.d(Config.LOGTAG, "onSignalChange(" + signalingState + ")");
+ }
+
+ @Override
+ public void onConnectionChange(final PeerConnection.PeerConnectionState state) {
+ stateHistory.add(state);
+ Log.d(Config.LOGTAG, "onConnectionChange(" + state + ")");
+ if (state == PeerConnection.PeerConnectionState.CONNECTED) {
+ if (connected.compareAndSet(false, true)) {
+ executorService.execute(() -> onIceConnectionConnected());
+ }
+ }
+ if (state == PeerConnection.PeerConnectionState.FAILED) {
+ final boolean neverConnected =
+ !stateHistory.contains(
+ PeerConnection.PeerConnectionState.CONNECTED);
+ // we want to terminate the connection a) to properly fail if a connection
+ // drops during a transfer and b) to avoid race conditions if we find a
+ // connection after failure while waiting for the initiator to replace
+ // transport
+ executorService.execute(() -> terminate());
+ if (neverConnected) {
+ executorService.execute(() -> onIceConnectionFailed());
+ }
+ }
+ }
+
+ @Override
+ public void onIceConnectionChange(
+ final PeerConnection.IceConnectionState newState) {}
+
+ @Override
+ public void onIceConnectionReceivingChange(boolean b) {}
+
+ @Override
+ public void onIceGatheringChange(
+ final PeerConnection.IceGatheringState iceGatheringState) {
+ Log.d(Config.LOGTAG, "onIceGatheringChange(" + iceGatheringState + ")");
+ }
+
+ @Override
+ public void onIceCandidate(final IceCandidate iceCandidate) {
+ if (readyToSentIceCandidates.get()) {
+ WebRTCDataChannelTransport.this.onIceCandidate(
+ iceCandidate.sdpMid, iceCandidate.sdp);
+ } else {
+ pendingOutgoingIceCandidates.add(iceCandidate);
+ }
+ }
+
+ @Override
+ public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {}
+
+ @Override
+ public void onAddStream(MediaStream mediaStream) {}
+
+ @Override
+ public void onRemoveStream(MediaStream mediaStream) {}
+
+ @Override
+ public void onDataChannel(final DataChannel dataChannel) {
+ Log.d(Config.LOGTAG, "onDataChannel()");
+ WebRTCDataChannelTransport.this.setDataChannel(dataChannel);
+ }
+
+ @Override
+ public void onRenegotiationNeeded() {
+ Log.d(Config.LOGTAG, "onRenegotiationNeeded");
+ }
+
+ @Override
+ public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) {
+ Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote);
+ Log.d(Config.LOGTAG, "local candidate selected: " + event.local);
+ }
+ };
+
+ private DataChannelWriter dataChannelWriter;
+
+ private void onIceConnectionConnected() {
+ this.transportCallback.onTransportEstablished();
+ }
+
+ private void onIceConnectionFailed() {
+ this.transportCallback.onTransportSetupFailed();
+ }
+
+ private void setDataChannel(final DataChannel dataChannel) {
+ Log.d(Config.LOGTAG, "the 'receiving' data channel has id " + dataChannel.id());
+ this.dataChannel = dataChannel;
+ this.dataChannel.registerObserver(
+ new OnMessageObserver() {
+ @Override
+ public void onMessage(final DataChannel.Buffer buffer) {
+ Log.d(Config.LOGTAG, "onMessage() (the other one)");
+ try {
+ WebRTCDataChannelTransport.this.writableByteChannel.write(buffer.data);
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG, "error writing to output stream");
+ }
+ }
+ });
+ }
+
+ protected void onIceCandidate(final String mid, final String sdp) {
+ final var candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(sdp, null);
+ this.transportCallback.onAdditionalCandidate(mid, candidate);
+ }
+
+ public WebRTCDataChannelTransport(
+ final Context context,
+ final XmppConnection xmppConnection,
+ final Account account,
+ final boolean initiator) {
+ PeerConnectionFactory.initialize(
+ PeerConnectionFactory.InitializationOptions.builder(context)
+ .setFieldTrials("WebRTC-BindUsingInterfaceName/Enabled/")
+ .createInitializationOptions());
+ this.peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory();
+ this.xmppConnection = xmppConnection;
+ this.account = account;
+ this.peerConnectionFuture =
+ Futures.transform(
+ getIceServers(),
+ iceServers -> createPeerConnection(iceServers, true),
+ MoreExecutors.directExecutor());
+ if (initiator) {
+ this.localDescriptionFuture = setLocalDescription();
+ }
+ }
+
+ private ListenableFuture<List<PeerConnection.IceServer>> getIceServers() {
+ if (Config.DISABLE_PROXY_LOOKUP) {
+ return Futures.immediateFuture(Collections.emptyList());
+ }
+ if (xmppConnection.getFeatures().externalServiceDiscovery()) {
+ final SettableFuture<List<PeerConnection.IceServer>> iceServerFuture =
+ SettableFuture.create();
+ final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+ request.setTo(this.account.getDomain());
+ request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
+ xmppConnection.sendIqPacket(
+ request,
+ (account, response) -> {
+ final var iceServers = IceServers.parse(response);
+ if (iceServers.size() == 0) {
+ Log.w(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": no ICE server found "
+ + response);
+ }
+ iceServerFuture.set(iceServers);
+ });
+ return iceServerFuture;
+ } else {
+ return Futures.immediateFuture(Collections.emptyList());
+ }
+ }
+
+ private PeerConnection createPeerConnection(
+ final List<PeerConnection.IceServer> iceServers, final boolean trickle) {
+ final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, trickle);
+ final PeerConnection peerConnection =
+ requirePeerConnectionFactory()
+ .createPeerConnection(rtcConfig, peerConnectionObserver);
+ if (peerConnection == null) {
+ throw new IllegalStateException("Unable to create PeerConnection");
+ }
+ final var dataChannelInit = new DataChannel.Init();
+ dataChannelInit.protocol = "xmpp-jingle";
+ final var dataChannel = peerConnection.createDataChannel("test", dataChannelInit);
+ this.dataChannelWriter = new DataChannelWriter(this.pipedInputStream, dataChannel);
+ Log.d(Config.LOGTAG, "the 'sending' data channel has id " + dataChannel.id());
+ new Thread(this.dataChannelWriter).start();
+ return peerConnection;
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ final var outputStream = new PipedOutputStream();
+ this.pipedInputStream.connect(outputStream);
+ this.dataChannelWriter.pipedInputStreamLatch.countDown();
+ return outputStream;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ final var inputStream = new PipedInputStream(BUFFER_SIZE);
+ this.pipedOutputStream.connect(inputStream);
+ return inputStream;
+ }
+
+ @Override
+ public ListenableFuture<TransportInfo> asTransportInfo() {
+ Preconditions.checkState(
+ this.localDescriptionFuture != null,
+ "Make sure you are setting initiator description first");
+ return Futures.transform(
+ asInitialTransportInfo(), info -> info, MoreExecutors.directExecutor());
+ }
+
+ @Override
+ public ListenableFuture<InitialTransportInfo> asInitialTransportInfo() {
+ return Futures.transform(
+ localDescriptionFuture,
+ sdp ->
+ WebRTCDataChannelTransportInfo.of(
+ eu.siacs.conversations.xmpp.jingle.SessionDescription.parse(
+ sdp.description)),
+ MoreExecutors.directExecutor());
+ }
+
+ @Override
+ public void readyToSentAdditionalCandidates() {
+ readyToSentIceCandidates.set(true);
+ while (this.pendingOutgoingIceCandidates.peek() != null) {
+ final var candidate = pendingOutgoingIceCandidates.poll();
+ if (candidate == null) {
+ continue;
+ }
+ onIceCandidate(candidate.sdpMid, candidate.sdp);
+ }
+ }
+
+ @Override
+ public void terminate() {
+ terminate(this.dataChannel);
+ this.dataChannel = null;
+ final var dataChannelWriter = this.dataChannelWriter;
+ if (dataChannelWriter != null) {
+ dataChannelWriter.close();
+ }
+ this.dataChannelWriter = null;
+ final var future = this.peerConnectionFuture;
+ if (future != null) {
+ future.cancel(true);
+ }
+ try {
+ final PeerConnection peerConnection = requirePeerConnection();
+ terminate(peerConnection);
+ } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
+ Log.d(Config.LOGTAG, "peer connection was not initialized during termination");
+ }
+ this.peerConnectionFuture = null;
+ final var peerConnectionFactory = this.peerConnectionFactory;
+ if (peerConnectionFactory != null) {
+ peerConnectionFactory.dispose();
+ }
+ this.peerConnectionFactory = null;
+ closeQuietly(this.pipedOutputStream);
+ this.terminationLatch.countDown();
+ Log.d(Config.LOGTAG, WebRTCDataChannelTransport.class.getSimpleName() + " terminated");
+ }
+
+ private static void closeQuietly(final OutputStream outputStream) {
+ try {
+ outputStream.close();
+ } catch (final IOException ignored) {
+
+ }
+ }
+
+ private static void terminate(final DataChannel dataChannel) {
+ if (dataChannel == null) {
+ Log.d(Config.LOGTAG, "nothing to terminate. data channel is already null");
+ return;
+ }
+ try {
+ dataChannel.close();
+ } catch (final IllegalStateException e) {
+ Log.w(Config.LOGTAG, "could not close data channel");
+ }
+ try {
+ dataChannel.dispose();
+ } catch (final IllegalStateException e) {
+ Log.w(Config.LOGTAG, "could not dispose data channel");
+ }
+ }
+
+ private static void terminate(final PeerConnection peerConnection) {
+ if (peerConnection == null) {
+ return;
+ }
+ try {
+ peerConnection.dispose();
+ Log.d(Config.LOGTAG, "terminated peer connection!");
+ } catch (final IllegalStateException e) {
+ Log.w(Config.LOGTAG, "could not dispose of peer connection");
+ }
+ }
+
+ @Override
+ public void setTransportCallback(final Callback callback) {
+ this.transportCallback = callback;
+ }
+
+ @Override
+ public void connect() {}
+
+ @Override
+ public CountDownLatch getTerminationLatch() {
+ return this.terminationLatch;
+ }
+
+ synchronized ListenableFuture<SessionDescription> setLocalDescription() {
+ return Futures.transformAsync(
+ peerConnectionFuture,
+ peerConnection -> {
+ if (peerConnection == null) {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException("PeerConnection was null"));
+ }
+ final SettableFuture<SessionDescription> future = SettableFuture.create();
+ peerConnection.setLocalDescription(
+ new WebRTCWrapper.SetSdpObserver() {
+ @Override
+ public void onSetSuccess() {
+ future.setFuture(getLocalDescriptionFuture(peerConnection));
+ }
+
+ @Override
+ public void onSetFailure(final String message) {
+ future.setException(
+ new WebRTCWrapper.FailureToSetDescriptionException(
+ message));
+ }
+ });
+ return future;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<SessionDescription> getLocalDescriptionFuture(
+ final PeerConnection peerConnection) {
+ return Futures.submit(
+ () -> {
+ final SessionDescription description = peerConnection.getLocalDescription();
+ WebRTCWrapper.logDescription(description);
+ return description;
+ },
+ localDescriptionExecutorService);
+ }
+
+ @Nonnull
+ private PeerConnectionFactory requirePeerConnectionFactory() {
+ final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
+ if (peerConnectionFactory == null) {
+ throw new IllegalStateException("Make sure PeerConnectionFactory is initialized");
+ }
+ return peerConnectionFactory;
+ }
+
+ @Nonnull
+ private PeerConnection requirePeerConnection() {
+ final var future = this.peerConnectionFuture;
+ if (future != null && future.isDone()) {
+ try {
+ return future.get();
+ } catch (final InterruptedException | ExecutionException e) {
+ throw new WebRTCWrapper.PeerConnectionNotInitialized();
+ }
+ } else {
+ throw new WebRTCWrapper.PeerConnectionNotInitialized();
+ }
+ }
+
+ public static List<IceCandidate> iceCandidatesOf(
+ final String contentName,
+ final IceUdpTransportInfo.Credentials credentials,
+ final List<IceUdpTransportInfo.Candidate> candidates) {
+ final ImmutableList.Builder<IceCandidate> iceCandidateBuilder =
+ new ImmutableList.Builder<>();
+ for (final IceUdpTransportInfo.Candidate candidate : candidates) {
+ final String sdp;
+ try {
+ sdp = candidate.toSdpAttribute(credentials.ufrag);
+ } catch (final IllegalArgumentException e) {
+ continue;
+ }
+ // TODO mLneIndex should probably not be hard coded
+ iceCandidateBuilder.add(new IceCandidate(contentName, 0, sdp));
+ }
+ return iceCandidateBuilder.build();
+ }
+
+ public void addIceCandidates(final List<IceCandidate> iceCandidates) {
+ try {
+ for (final var candidate : iceCandidates) {
+ requirePeerConnection().addIceCandidate(candidate);
+ }
+ } catch (WebRTCWrapper.PeerConnectionNotInitialized e) {
+ Log.w(Config.LOGTAG, "could not add ice candidate. peer connection is not initialized");
+ }
+ }
+
+ public void setInitiatorDescription(
+ final eu.siacs.conversations.xmpp.jingle.SessionDescription sessionDescription) {
+ final var sdp =
+ new SessionDescription(
+ SessionDescription.Type.OFFER, sessionDescription.toString());
+ final var setFuture = setRemoteDescriptionFuture(sdp);
+ this.localDescriptionFuture =
+ Futures.transformAsync(
+ setFuture, v -> setLocalDescription(), MoreExecutors.directExecutor());
+ }
+
+ public void setResponderDescription(
+ final eu.siacs.conversations.xmpp.jingle.SessionDescription sessionDescription) {
+ Log.d(Config.LOGTAG, "setResponder description");
+ final var sdp =
+ new SessionDescription(
+ SessionDescription.Type.ANSWER, sessionDescription.toString());
+ logDescription(sdp);
+ setRemoteDescriptionFuture(sdp);
+ }
+
+ synchronized ListenableFuture<Void> setRemoteDescriptionFuture(
+ final SessionDescription sessionDescription) {
+ return Futures.transformAsync(
+ this.peerConnectionFuture,
+ peerConnection -> {
+ if (peerConnection == null) {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException("PeerConnection was null"));
+ }
+ final SettableFuture<Void> future = SettableFuture.create();
+ peerConnection.setRemoteDescription(
+ new WebRTCWrapper.SetSdpObserver() {
+ @Override
+ public void onSetSuccess() {
+ future.set(null);
+ }
+
+ @Override
+ public void onSetFailure(final String message) {
+ future.setException(
+ new WebRTCWrapper.FailureToSetDescriptionException(
+ message));
+ }
+ },
+ sessionDescription);
+ return future;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private static class DataChannelWriter implements Runnable {
+
+ private final CountDownLatch pipedInputStreamLatch = new CountDownLatch(1);
+ private final CountDownLatch dataChannelLatch = new CountDownLatch(1);
+ private final AtomicBoolean isSending = new AtomicBoolean(true);
+ private final InputStream inputStream;
+ private final DataChannel dataChannel;
+
+ private DataChannelWriter(InputStream inputStream, DataChannel dataChannel) {
+ this.inputStream = inputStream;
+ this.dataChannel = dataChannel;
+ final StateChangeObserver stateChangeObserver =
+ new StateChangeObserver() {
+
+ @Override
+ public void onStateChange() {
+ if (dataChannel.state() == DataChannel.State.OPEN) {
+ dataChannelLatch.countDown();
+ }
+ }
+ };
+ this.dataChannel.registerObserver(stateChangeObserver);
+ }
+
+ public void run() {
+ try {
+ this.pipedInputStreamLatch.await();
+ this.dataChannelLatch.await();
+ final var buffer = new byte[4096];
+ while (isSending.get()) {
+ final long bufferedAmount = dataChannel.bufferedAmount();
+ if (bufferedAmount > MAX_SENT_BUFFER) {
+ Thread.sleep(50);
+ continue;
+ }
+ final int count = this.inputStream.read(buffer);
+ if (count < 0) {
+ Log.d(Config.LOGTAG, "DataChannelWriter reached EOF");
+ return;
+ }
+ dataChannel.send(
+ new DataChannel.Buffer(ByteBuffer.wrap(buffer, 0, count), true));
+ }
+ } catch (final InterruptedException | InterruptedIOException e) {
+ if (isSending.get()) {
+ Log.w(Config.LOGTAG, "DataChannelWriter got interrupted while sending", e);
+ }
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG, "DataChannelWriter terminated", e);
+ } finally {
+ Closeables.closeQuietly(inputStream);
+ }
+ }
+
+ public void close() {
+ this.isSending.set(false);
+ terminate(this.dataChannel);
+ }
+ }
+
+ private abstract static class StateChangeObserver implements DataChannel.Observer {
+
+ @Override
+ public void onBufferedAmountChange(final long change) {}
+
+ @Override
+ public void onMessage(final DataChannel.Buffer buffer) {}
+ }
+
+ private abstract static class OnMessageObserver implements DataChannel.Observer {
+
+ @Override
+ public void onBufferedAmountChange(long l) {}
+
+ @Override
+ public void onStateChange() {}
+ }
+}