refactor Jingle File Transfer. add WebRTCDatachannel transport

This commit is contained in:
Daniel Gultsch 2023-12-19 17:25:48 +01:00
parent 8208724172
commit d3b38a5273
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
41 changed files with 4930 additions and 2911 deletions

View file

@ -41,7 +41,7 @@ public final class Config {
public static final String LOGTAG = BuildConfig.APP_NAME.toLowerCase(Locale.US); 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 Jid BUG_REPORTS = Jid.of("bugs@conversations.im");
public static final Uri HELP = Uri.parse("https://help.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 OMEMO_PADDING = false;
public static final boolean PUT_AUTH_TAG_INTO_KEY = true; public static final boolean PUT_AUTH_TAG_INTO_KEY = true;
public static final boolean AUTOMATICALLY_COMPLETE_SESSIONS = 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 USE_DIRECT_JINGLE_CANDIDATES = true;
public static final boolean DISABLE_HTTP_UPLOAD = false; public static final boolean DISABLE_HTTP_UPLOAD = false;
public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts

View file

@ -62,11 +62,13 @@ import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
import eu.siacs.conversations.xmpp.OnIqPacketReceived; 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.OmemoVerification;
import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap; import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap;
import eu.siacs.conversations.xmpp.jingle.RtpContentMap; import eu.siacs.conversations.xmpp.jingle.RtpContentMap;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; 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.pep.PublishOptions;
import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
@ -1262,12 +1264,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (Config.REQUIRE_RTP_VERIFICATION) { if (Config.REQUIRE_RTP_VERIFICATION) {
requireVerification(session); 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(); final OmemoVerification omemoVerification = new OmemoVerification();
omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId()); omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
omemoVerification.setSessionFingerprint(session.getFingerprint()); omemoVerification.setSessionFingerprint(session.getFingerprint());
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : rtpContentMap.contents.entrySet()) { for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : rtpContentMap.contents.entrySet()) {
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue(); final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport = content.getValue();
final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo; final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo;
try { try {
encryptedTransportInfo = encrypt(descriptionTransport.transport, session); encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
@ -1276,7 +1278,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} }
descriptionTransportBuilder.put( descriptionTransportBuilder.put(
content.getKey(), content.getKey(),
new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo) new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
); );
} }
return Futures.immediateFuture( return Futures.immediateFuture(
@ -1296,11 +1298,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} }
public ListenableFuture<OmemoVerifiedPayload<RtpContentMap>> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { 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 OmemoVerification omemoVerification = new OmemoVerification();
final ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures = new ImmutableList.Builder<>(); final ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures = new ImmutableList.Builder<>();
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : omemoVerifiedRtpContentMap.contents.entrySet()) { for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue(); final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport = content.getValue();
final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport; final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport;
try { try {
decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures); decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures);
@ -1310,7 +1312,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
omemoVerification.setOrEnsureEqual(decryptedTransport); omemoVerification.setOrEnsureEqual(decryptedTransport);
descriptionTransportBuilder.put( descriptionTransportBuilder.put(
content.getKey(), content.getKey(),
new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload) new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
); );
} }
processPostponed(); processPostponed();
@ -1376,18 +1378,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
)); ));
} }
public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) { public ListenableFuture<XmppAxolotlMessage> prepareKeyTransportMessage(final Conversation conversation) {
executor.execute(new Runnable() { return Futures.submit(()->{
@Override final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
public void run() { if (buildHeader(axolotlMessage, conversation)) {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); return axolotlMessage;
if (buildHeader(axolotlMessage, conversation)) { } else {
onMessageCreatedCallback.run(axolotlMessage); throw new IllegalStateException("No session to decrypt to");
} else {
onMessageCreatedCallback.run(null);
}
} }
}); },executor);
} }
public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {

View file

@ -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 static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
private final String[] FEATURES = { private final String[] FEATURES = {
Namespace.JINGLE, Namespace.JINGLE,
Namespace.JINGLE_APPS_FILE_TRANSFER,
//Jingle File Transfer
FileTransferDescription.Version.FT_3.getNamespace(),
FileTransferDescription.Version.FT_4.getNamespace(),
FileTransferDescription.Version.FT_5.getNamespace(),
Namespace.JINGLE_TRANSPORTS_S5B, Namespace.JINGLE_TRANSPORTS_S5B,
Namespace.JINGLE_TRANSPORTS_IBB, Namespace.JINGLE_TRANSPORTS_IBB,
Namespace.JINGLE_ENCRYPTED_TRANSPORT, Namespace.JINGLE_ENCRYPTED_TRANSPORT,
@ -124,6 +120,7 @@ public abstract class AbstractGenerator {
if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) { if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) {
features.addAll(Arrays.asList(PRIVACY_SENSITIVE)); features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
features.addAll(Arrays.asList(VOIP_NAMESPACES)); features.addAll(Arrays.asList(VOIP_NAMESPACES));
features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL);
} }
if (mXmppConnectionService.broadcastLastActivity()) { if (mXmppConnectionService.broadcastLastActivity()) {
features.add(Namespace.IDLE); features.add(Namespace.IDLE);

View file

@ -403,7 +403,12 @@ public class UnifiedPushBroker {
updateIntent.putExtra("token", target.instance); updateIntent.putExtra("token", target.instance);
updateIntent.putExtra("bytesMessage", payload); updateIntent.putExtra("bytesMessage", payload);
updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8)); 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); service.sendBroadcast(updateIntent);
} }

View file

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

View file

@ -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_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_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_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_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_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_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0";
public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; 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 = "urn:xmpp:reporting:1";
public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam"; 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 SDP_OFFER_ANSWER = "urn:ietf:rfc:3264";
public static final String HASHES = "urn:xmpp:hashes:2";
} }

View file

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

View file

@ -1,47 +1,352 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.common.base.Preconditions; 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.Account;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Message; 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.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; 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 abstract class AbstractJingleConnection {
public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-"; public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-";
public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-"; 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; final JingleConnectionManager jingleConnectionManager;
protected final XmppConnectionService xmppConnectionService; protected final XmppConnectionService xmppConnectionService;
protected final Id id; protected final Id id;
private final Jid initiator; 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.jingleConnectionManager = jingleConnectionManager;
this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService(); this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService();
this.id = id; this.id = id;
this.initiator = initiator; this.initiator = initiator;
} }
boolean isInitiator() {
return initiator.equals(id.account.getJid());
}
abstract void deliverPacket(JinglePacket jinglePacket);
public Id getId() { public Id getId() {
return id; 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);
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(); 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 static class Id implements OngoingRtpSession {
public final Account account; public final Account account;
@ -73,8 +378,7 @@ public abstract class AbstractJingleConnection {
return new Id( return new Id(
message.getConversation().getAccount(), message.getConversation().getAccount(),
message.getCounterpart(), message.getCounterpart(),
JingleConnectionManager.nextRandomId() JingleConnectionManager.nextRandomId());
);
} }
public Contact getContact() { public Contact getContact() {
@ -86,9 +390,9 @@ public abstract class AbstractJingleConnection {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
Id id = (Id) o; Id id = (Id) o;
return Objects.equal(account.getUuid(), id.account.getUuid()) && return Objects.equal(account.getUuid(), id.account.getUuid())
Objects.equal(with, id.with) && && Objects.equal(with, id.with)
Objects.equal(sessionId, id.sessionId); && Objects.equal(sessionId, id.sessionId);
} }
@Override @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 { public enum State {
NULL, //default value; nothing has been sent or received yet NULL, // default value; nothing has been sent or received yet
PROPOSED, PROPOSED,
ACCEPTED, ACCEPTED,
PROCEED, PROCEED,
REJECTED, REJECTED,
REJECTED_RACED, //used when we want to reject but havent received session init yet REJECTED_RACED, // used when we want to reject but havent received session init yet
RETRACTED, RETRACTED,
RETRACTED_RACED, //used when receiving a retract after we already asked to proceed RETRACTED_RACED, // used when receiving a retract after we already asked to proceed
SESSION_INITIALIZED, //equal to 'PENDING' SESSION_INITIALIZED, // equal to 'PENDING'
SESSION_INITIALIZED_PRE_APPROVED, SESSION_INITIALIZED_PRE_APPROVED,
SESSION_ACCEPTED, //equal to 'ACTIVE' SESSION_ACCEPTED, // equal to 'ACTIVE'
TERMINATED_SUCCESS, //equal to 'ENDED' (after successful call) ui will just close 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_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_CONNECTIVITY_ERROR, // equal to 'ENDED' (but after network failures; ui will
TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted // 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_APPLICATION_FAILURE,
TERMINATED_SECURITY_ERROR TERMINATED_SECURITY_ERROR
} }

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
@ -8,6 +10,8 @@ import com.google.common.collect.ImmutableSet;
import java.util.Set; import java.util.Set;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content; 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 { public final class ContentAddition {
@ -32,12 +36,13 @@ public final class ContentAddition {
Collections2.transform( Collections2.transform(
rtpContentMap.contents.entrySet(), rtpContentMap.contents.entrySet(),
e -> { e -> {
final RtpContentMap.DescriptionTransport dt = e.getValue(); final DescriptionTransport<RtpDescription, IceUdpTransportInfo> dt = e.getValue();
return new Summary(e.getKey(), dt.description.getMedia(), dt.senders); return new Summary(e.getKey(), dt.description.getMedia(), dt.senders);
})); }));
} }
@Override @Override
@NonNull
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("direction", direction) .add("direction", direction)
@ -77,6 +82,7 @@ public final class ContentAddition {
} }
@Override @Override
@NonNull
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("name", name) .add("name", name)

View file

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

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import com.google.common.collect.ImmutableList;
import java.net.Inet6Address; import java.net.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.NetworkInterface; import java.net.NetworkInterface;
@ -15,13 +17,13 @@ import eu.siacs.conversations.xmpp.Jid;
public class DirectConnectionUtils { public class DirectConnectionUtils {
private static List<InetAddress> getLocalAddresses() { public static List<InetAddress> getLocalAddresses() {
final List<InetAddress> addresses = new ArrayList<>(); final ImmutableList.Builder<InetAddress> inetAddresses = new ImmutableList.Builder<>();
final Enumeration<NetworkInterface> interfaces; final Enumeration<NetworkInterface> interfaces;
try { try {
interfaces = NetworkInterface.getNetworkInterfaces(); interfaces = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e) { } catch (final SocketException e) {
return addresses; return inetAddresses.build();
} }
while (interfaces.hasMoreElements()) { while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement(); NetworkInterface networkInterface = interfaces.nextElement();
@ -34,31 +36,15 @@ public class DirectConnectionUtils {
if (inetAddress instanceof Inet6Address) { if (inetAddress instanceof Inet6Address) {
//let's get rid of scope //let's get rid of scope
try { try {
addresses.add(Inet6Address.getByAddress(inetAddress.getAddress())); inetAddresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
//ignored //ignored
} }
} else { } 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;
}
} }

View file

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

View file

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

View file

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

View file

@ -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.Content;
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; 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.JinglePacket;
import eu.siacs.conversations.xmpp.jingle.stanzas.Propose; import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; 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.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
@ -61,8 +64,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
private final Cache<PersistableSessionId, TerminatedRtpSession> terminatedSessions = private final Cache<PersistableSessionId, TerminatedRtpSession> terminatedSessions =
CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build(); CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build();
private final HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>();
public JingleConnectionManager(XmppConnectionService service) { public JingleConnectionManager(XmppConnectionService service) {
super(service); super(service);
this.toneManager = new ToneManager(service); this.toneManager = new ToneManager(service);
@ -90,7 +91,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final String descriptionNamespace = final String descriptionNamespace =
content == null ? null : content.getDescriptionNamespace(); content == null ? null : content.getDescriptionNamespace();
final AbstractJingleConnection connection; final AbstractJingleConnection connection;
if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(descriptionNamespace)) {
connection = new JingleFileTransferConnection(this, id, from); connection = new JingleFileTransferConnection(this, id, from);
} else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)
&& isUsingClearNet(account)) { && isUsingClearNet(account)) {
@ -593,13 +594,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
if (old != null) { if (old != null) {
old.cancel(); old.cancel();
} }
final Account account = message.getConversation().getAccount();
final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message);
final JingleFileTransferConnection connection = final JingleFileTransferConnection connection =
new JingleFileTransferConnection(this, id, account.getJid()); new JingleFileTransferConnection(this, message);
mXmppConnectionService.markMessage(message, Message.STATUS_WAITING); this.connections.put(connection.getId(), connection);
this.connections.put(id, connection); connection.sendSessionInitialize();
connection.init(message);
} }
public Optional<OngoingRtpSession> getOngoingRtpConnection(final Contact contact) { public Optional<OngoingRtpSession> getOngoingRtpConnection(final Contact contact) {
@ -658,60 +656,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return firedUpdates; 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) { public void retractSessionProposal(final Account account, final Jid with) {
synchronized (this.rtpSessionProposals) { synchronized (this.rtpSessionProposals) {
RtpSessionProposal matchingProposal = null; RtpSessionProposal matchingProposal = null;
@ -810,36 +754,53 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return false; return false;
} }
public void deliverIbbPacket(Account account, IqPacket packet) { public void deliverIbbPacket(final Account account, final IqPacket packet) {
final String sid; final String sid;
final Element payload; final Element payload;
final InbandBytestreamsTransport.PacketType packetType;
if (packet.hasChild("open", Namespace.IBB)) { if (packet.hasChild("open", Namespace.IBB)) {
packetType = InbandBytestreamsTransport.PacketType.OPEN;
payload = packet.findChild("open", Namespace.IBB); payload = packet.findChild("open", Namespace.IBB);
sid = payload.getAttribute("sid"); sid = payload.getAttribute("sid");
} else if (packet.hasChild("data", Namespace.IBB)) { } else if (packet.hasChild("data", Namespace.IBB)) {
packetType = InbandBytestreamsTransport.PacketType.DATA;
payload = packet.findChild("data", Namespace.IBB); payload = packet.findChild("data", Namespace.IBB);
sid = payload.getAttribute("sid"); sid = payload.getAttribute("sid");
} else if (packet.hasChild("close", Namespace.IBB)) { } else if (packet.hasChild("close", Namespace.IBB)) {
packetType = InbandBytestreamsTransport.PacketType.CLOSE;
payload = packet.findChild("close", Namespace.IBB); payload = packet.findChild("close", Namespace.IBB);
sid = payload.getAttribute("sid"); sid = payload.getAttribute("sid");
} else { } else {
packetType = null;
payload = null; payload = null;
sid = null; sid = null;
} }
if (sid != null) { if (sid == null) {
for (final AbstractJingleConnection connection : this.connections.values()) { Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet. missing sid");
if (connection instanceof JingleFileTransferConnection fileTransfer) { account.getXmppConnection()
final JingleTransport transport = fileTransfer.getTransport(); .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
if (transport instanceof JingleInBandTransport inBandTransport) { return;
if (inBandTransport.matches(account, sid)) { }
inBandTransport.deliverPayload(packet, payload); 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; 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() account.getXmppConnection()
.sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
} }

View file

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

View file

@ -13,7 +13,6 @@ import com.google.common.base.Strings;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables; 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.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException; import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; 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.Conversation;
import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.utils.IP; import eu.siacs.conversations.utils.IP;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
@ -78,96 +73,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
Arrays.asList( Arrays.asList(
State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED); State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED);
private static final long BUSY_TIME_OUT = 30; 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 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<>(); pendingIceCandidates = new LinkedList<>();
private final OmemoVerification omemoVerification = new OmemoVerification(); private final OmemoVerification omemoVerification = new OmemoVerification();
private final Message message; private final Message message;
private State state = State.NULL;
private Set<Media> proposedMedia; private Set<Media> proposedMedia;
private RtpContentMap initiatorRtpContentMap; private RtpContentMap initiatorRtpContentMap;
private RtpContentMap responderRtpContentMap; private RtpContentMap responderRtpContentMap;
@ -192,18 +104,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
id.sessionId); 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 @Override
synchronized void deliverPacket(final JinglePacket jinglePacket) { synchronized void deliverPacket(final JinglePacket jinglePacket) {
switch (jinglePacket.getAction()) { switch (jinglePacket.getAction()) {
@ -233,7 +133,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
return; return;
} }
webRTCWrapper.close(); webRTCWrapper.close();
if (!isInitiator() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) { if (isResponder() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) {
xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
} }
if (isInState( if (isInState(
@ -322,7 +222,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void receiveTransportInfo( private void receiveTransportInfo(
final JinglePacket jinglePacket, final RtpContentMap contentMap) { 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(); contentMap.contents.entrySet();
final RtpContentMap remote = getRemoteContentMap(); final RtpContentMap remote = getRemoteContentMap();
final Set<String> remoteContentIds = final Set<String> remoteContentIds =
@ -522,7 +422,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
setRemoteContentMap(modifiedContentMap); setRemoteContentMap(modifiedContentMap);
final SessionDescription answer = SessionDescription.of(modifiedContentMap, !isInitiator()); final SessionDescription answer = SessionDescription.of(modifiedContentMap, isResponder());
final org.webrtc.SessionDescription sdp = final org.webrtc.SessionDescription sdp =
new org.webrtc.SessionDescription( new org.webrtc.SessionDescription(
@ -596,7 +496,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
final SessionDescription offer; final SessionDescription offer;
try { try {
offer = SessionDescription.of(modifiedRemoteContentMap, !isInitiator()); offer = SessionDescription.of(modifiedRemoteContentMap, isResponder());
} catch (final IllegalArgumentException | NullPointerException e) { } catch (final IllegalArgumentException | NullPointerException e) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
@ -815,7 +715,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final RtpContentMap nextRemote = final RtpContentMap nextRemote =
currentRemote.addContent( currentRemote.addContent(
patch.modifiedSenders(Content.Senders.NONE), getPeerDtlsSetup()); patch.modifiedSenders(Content.Senders.NONE), getPeerDtlsSetup());
return SessionDescription.of(nextRemote, !isInitiator()); return SessionDescription.of(nextRemote, isResponder());
} }
throw new IllegalStateException( throw new IllegalStateException(
"Unexpected rollback condition. Senders were not uniformly none"); "Unexpected rollback condition. Senders were not uniformly none");
@ -881,7 +781,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final SessionDescription offer; final SessionDescription offer;
try { try {
offer = SessionDescription.of(modifiedContentMap, !isInitiator()); offer = SessionDescription.of(modifiedContentMap, isResponder());
} catch (final IllegalArgumentException | NullPointerException e) { } catch (final IllegalArgumentException | NullPointerException e) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
@ -1066,7 +966,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final boolean isOffer) final boolean isOffer)
throws ExecutionException, InterruptedException { throws ExecutionException, InterruptedException {
final SessionDescription sessionDescription = final SessionDescription sessionDescription =
SessionDescription.of(restartContentMap, !isInitiator()); SessionDescription.of(restartContentMap, isResponder());
final org.webrtc.SessionDescription.Type type = final org.webrtc.SessionDescription.Type type =
isOffer isOffer
? org.webrtc.SessionDescription.Type.OFFER ? org.webrtc.SessionDescription.Type.OFFER
@ -1095,14 +995,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
private void processCandidates( private void processCandidates(
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) { final Set<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>> contents) {
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) { for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : contents) {
processCandidate(content); processCandidate(content);
} }
} }
private void processCandidate( private void processCandidate(
final Map.Entry<String, RtpContentMap.DescriptionTransport> content) { final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content) {
final RtpContentMap rtpContentMap = getRemoteContentMap(); final RtpContentMap rtpContentMap = getRemoteContentMap();
final List<String> indices = toIdentificationTags(rtpContentMap); final List<String> indices = toIdentificationTags(rtpContentMap);
final String sdpMid = content.getKey(); // aka content name final String sdpMid = content.getKey(); // aka content name
@ -1204,21 +1104,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void receiveSessionInitiate(final JinglePacket jinglePacket) { private void receiveSessionInitiate(final JinglePacket jinglePacket) {
if (isInitiator()) { if (isInitiator()) {
Log.d( receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
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);
}
return; return;
} }
final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false); final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false);
@ -1300,13 +1186,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
private void receiveSessionAccept(final JinglePacket jinglePacket) { private void receiveSessionAccept(final JinglePacket jinglePacket) {
if (!isInitiator()) { if (isResponder()) {
Log.d( receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
Config.LOGTAG,
String.format(
"%s: received session-accept even though we were responding",
id.account.getJid().asBareJid()));
terminateWithOutOfOrder(jinglePacket);
return; return;
} }
final ListenableFuture<RtpContentMap> future = final ListenableFuture<RtpContentMap> future =
@ -1491,7 +1372,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
private void addIceCandidatesFromBlackLog() { private void addIceCandidatesFromBlackLog() {
Map.Entry<String, RtpContentMap.DescriptionTransport> foo; Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> foo;
while ((foo = this.pendingIceCandidates.poll()) != null) { while ((foo = this.pendingIceCandidates.poll()) != null) {
processCandidate(foo); processCandidate(foo);
Log.d( 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); sendSessionTerminate(reason, null);
} }
private void sendSessionTerminate(final Reason reason, final String text) {
final State previous = this.state; protected void sendSessionTerminate(final Reason reason, final String text) {
final State target = reasonToState(reason); sendSessionTerminate(reason,text, this::writeLogMessage);
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();
} }
private void sendTransportInfo( private void sendTransportInfo(
final String contentName, IceUdpTransportInfo.Candidate candidate) { final String contentName, IceUdpTransportInfo.Candidate candidate) {
final RtpContentMap transportInfo; final RtpContentMap transportInfo;
@ -2099,110 +1972,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
send(jinglePacket); 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() { public RtpEndUserState getEndUserState() {
switch (this.state) { switch (this.state) {
case NULL, PROPOSED, SESSION_INITIALIZED -> { 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"); + ": received endCall() when session has already been terminated. nothing to do");
return; return;
} }
if (isInState(State.PROPOSED) && !isInitiator()) { if (isInState(State.PROPOSED) && isResponder()) {
rejectCallFromProposed(); rejectCallFromProposed();
return; return;
} }
@ -2527,22 +2296,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
sendSessionAccept(); sendSessionAccept();
} }
private synchronized boolean isInState(State... state) {
return Arrays.asList(state).contains(this.state);
}
private boolean transition(final State target) { @Override
return transition(target, null); protected synchronized boolean transition(final State target, final Runnable runnable) {
} if (super.transition(target, runnable)) {
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);
updateEndUserState(); updateEndUserState();
updateOngoingCallNotification(); updateOngoingCallNotification();
return true; 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 @Override
public void onIceCandidate(final IceCandidate iceCandidate) { public void onIceCandidate(final IceCandidate iceCandidate) {
final RtpContentMap rtpContentMap = final RtpContentMap rtpContentMap =
@ -2893,98 +2643,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
id.account, id.account,
request, request,
(account, response) -> { (account, response) -> {
ImmutableList.Builder<PeerConnection.IceServer> listBuilder = final var iceServers = IceServers.parse(response);
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();
if (iceServers.size() == 0) { if (iceServers.size() == 0) {
Log.w( Log.w(
Config.LOGTAG, Config.LOGTAG,
@ -3001,13 +2660,19 @@ public class JingleRtpConnection extends AbstractJingleConnection
onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList()); onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList());
} }
} }
@Override
protected void terminateTransport() {
this.webRTCWrapper.close();
}
private void finish() { @Override
protected void finish() {
if (isTerminated()) { if (isTerminated()) {
this.cancelRingingTimeout(); this.cancelRingingTimeout();
this.webRTCWrapper.verifyClosed(); this.webRTCWrapper.verifyClosed();
this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia()); this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
this.jingleConnectionManager.finishConnectionOrThrow(this); super.finish();
} else { } else {
throw new IllegalStateException( throw new IllegalStateException(
String.format("Unable to call finish from %s", this.state)); 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() { public Optional<VideoTrack> getLocalVideoTrack() {
return webRTCWrapper.getLocalVideoTrack(); return webRTCWrapper.getLocalVideoTrack();
} }
@ -3091,17 +2748,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
return remoteHasFeature(Namespace.SDP_OFFER_ANSWER); 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 { private interface OnIceServersDiscovered {
void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers); void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
} }

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package eu.siacs.conversations.xmpp.jingle; 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; import java.util.List;
@ -8,9 +9,9 @@ public class MediaBuilder {
private String media; private String media;
private int port; private int port;
private String protocol; private String protocol;
private List<Integer> formats; private String format;
private String connectionData; private String connectionData;
private ArrayListMultimap<String,String> attributes; private Multimap<String, String> attributes;
public MediaBuilder setMedia(String media) { public MediaBuilder setMedia(String media) {
this.media = media; this.media = media;
@ -27,8 +28,13 @@ public class MediaBuilder {
return this; return this;
} }
public MediaBuilder setFormats(List<Integer> formats) { public MediaBuilder setFormats(final List<Integer> formats) {
this.formats = formats; this.format = Joiner.on(' ').join(formats);
return this;
}
public MediaBuilder setFormat(final String format) {
this.format = format;
return this; return this;
} }
@ -37,12 +43,13 @@ public class MediaBuilder {
return this; return this;
} }
public MediaBuilder setAttributes(ArrayListMultimap<String,String> attributes) { public MediaBuilder setAttributes(Multimap<String, String> attributes) {
this.attributes = attributes; this.attributes = attributes;
return this; return this;
} }
public SessionDescription.Media createMedia() { public SessionDescription.Media createMedia() {
return new SessionDescription.Media(media, port, protocol, formats, connectionData, attributes); return new SessionDescription.Media(
media, port, protocol, format, connectionData, attributes);
} }
} }

View file

@ -3,12 +3,14 @@ package eu.siacs.conversations.xmpp.jingle;
import java.util.Map; import java.util.Map;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group; 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.OmemoVerifiedIceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
public class OmemoVerifiedRtpContentMap extends RtpContentMap { 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); super(group, contents);
for(final DescriptionTransport descriptionTransport : contents.values()) { for(final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport : contents.values()) {
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) { if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint(); ((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint();
continue; continue;

View file

@ -1,5 +0,0 @@
package eu.siacs.conversations.xmpp.jingle;
public interface OnPrimaryCandidateFound {
void onPrimaryCandidateFound(boolean success, JingleCandidate canditate);
}

View file

@ -6,7 +6,6 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
@ -31,19 +30,17 @@ import java.util.Set;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
public class RtpContentMap { public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTransportInfo> {
public final Group group; public RtpContentMap(
public final Map<String, DescriptionTransport> contents; Group group,
Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
public RtpContentMap(Group group, Map<String, DescriptionTransport> contents) { super(group, contents);
this.group = group;
this.contents = contents;
} }
public static RtpContentMap of(final JinglePacket jinglePacket) { public static RtpContentMap of(final JinglePacket jinglePacket) {
final Map<String, DescriptionTransport> contents = final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents =
DescriptionTransport.of(jinglePacket.getJingleContents()); of(jinglePacket.getJingleContents());
if (isOmemoVerified(contents)) { if (isOmemoVerified(contents)) {
return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents); return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
} else { } else {
@ -51,12 +48,15 @@ public class RtpContentMap {
} }
} }
private static boolean isOmemoVerified(Map<String, DescriptionTransport> contents) { private static boolean isOmemoVerified(
final Collection<DescriptionTransport> values = contents.values(); Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
final Collection<DescriptionTransport<RtpDescription, IceUdpTransportInfo>> values =
contents.values();
if (values.size() == 0) { if (values.size() == 0) {
return false; return false;
} }
for (final DescriptionTransport descriptionTransport : values) { for (final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport :
values) {
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) { if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
continue; continue;
} }
@ -67,13 +67,13 @@ public class RtpContentMap {
public static RtpContentMap of( public static RtpContentMap of(
final SessionDescription sessionDescription, final boolean isInitiator) { final SessionDescription sessionDescription, final boolean isInitiator) {
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = final ImmutableMap.Builder<
new ImmutableMap.Builder<>(); String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
contentMapBuilder = new ImmutableMap.Builder<>();
for (SessionDescription.Media media : sessionDescription.media) { for (SessionDescription.Media media : sessionDescription.media) {
final String id = Iterables.getFirst(media.attributes.get("mid"), null); final String id = Iterables.getFirst(media.attributes.get("mid"), null);
Preconditions.checkNotNull(id, "media has no mid"); Preconditions.checkNotNull(id, "media has no mid");
contentMapBuilder.put( contentMapBuilder.put(id, of(sessionDescription, isInitiator, media));
id, DescriptionTransport.of(sessionDescription, isInitiator, media));
} }
final String groupAttribute = final String groupAttribute =
Iterables.getFirst(sessionDescription.attributes.get("group"), null); 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() { void requireDTLSFingerprint() {
requireDTLSFingerprint(false); requireDTLSFingerprint(false);
} }
@ -122,7 +102,8 @@ public class RtpContentMap {
if (this.contents.size() == 0) { if (this.contents.size() == 0) {
throw new IllegalStateException("No contents available"); 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 transport = entry.getValue().transport;
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
if (fingerprint == null 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( RtpContentMap transportInfo(
final String contentName, final IceUdpTransportInfo.Candidate candidate) { 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 = final IceUdpTransportInfo transportInfo =
descriptionTransport == null ? null : descriptionTransport.transport; descriptionTransport == null ? null : descriptionTransport.transport;
if (transportInfo == null) { if (transportInfo == null) {
@ -183,7 +143,7 @@ public class RtpContentMap {
null, null,
ImmutableMap.of( ImmutableMap.of(
contentName, contentName,
new DescriptionTransport( new DescriptionTransport<>(
descriptionTransport.senders, null, newTransportInfo))); descriptionTransport.senders, null, newTransportInfo)));
} }
@ -193,21 +153,24 @@ public class RtpContentMap {
Maps.transformValues( Maps.transformValues(
contents, contents,
dt -> dt ->
new DescriptionTransport( new DescriptionTransport<>(
dt.senders, null, dt.transport.cloneWrapper()))); dt.senders, null, dt.transport.cloneWrapper())));
} }
RtpContentMap withCandidates( RtpContentMap withCandidates(
ImmutableMultimap<String, IceUdpTransportInfo.Candidate> candidates) { ImmutableMultimap<String, IceUdpTransportInfo.Candidate> candidates) {
final ImmutableMap.Builder<String, DescriptionTransport> contentBuilder = final ImmutableMap.Builder<
new ImmutableMap.Builder<>(); String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
for (final Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) { contentBuilder = new ImmutableMap.Builder<>();
for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
entry : this.contents.entrySet()) {
final String name = entry.getKey(); final String name = entry.getKey();
final DescriptionTransport descriptionTransport = entry.getValue(); final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
entry.getValue();
final var transport = descriptionTransport.transport; final var transport = descriptionTransport.transport;
contentBuilder.put( contentBuilder.put(
name, name,
new DescriptionTransport( new DescriptionTransport<>(
descriptionTransport.senders, descriptionTransport.senders,
descriptionTransport.description, descriptionTransport.description,
transport.withCandidates(candidates.get(name)))); transport.withCandidates(candidates.get(name))));
@ -247,7 +210,7 @@ public class RtpContentMap {
} }
public IceUdpTransportInfo.Credentials getCredentials(final String contentName) { public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
final DescriptionTransport descriptionTransport = this.contents.get(contentName); final var descriptionTransport = this.contents.get(contentName);
if (descriptionTransport == null) { if (descriptionTransport == null) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
String.format( String.format(
@ -287,7 +250,7 @@ public class RtpContentMap {
public boolean emptyCandidates() { public boolean emptyCandidates() {
int count = 0; int count = 0;
for (DescriptionTransport descriptionTransport : contents.values()) { for (final var descriptionTransport : contents.values()) {
count += descriptionTransport.transport.getCandidates().size(); count += descriptionTransport.transport.getCandidates().size();
} }
return count == 0; return count == 0;
@ -300,17 +263,19 @@ public class RtpContentMap {
public RtpContentMap modifiedCredentials( public RtpContentMap modifiedCredentials(
IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) { IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = final ImmutableMap.Builder<
new ImmutableMap.Builder<>(); String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) { contentMapBuilder = new ImmutableMap.Builder<>();
final DescriptionTransport descriptionTransport = content.getValue(); for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
content : contents.entrySet()) {
final var descriptionTransport = content.getValue();
final RtpDescription rtpDescription = descriptionTransport.description; final RtpDescription rtpDescription = descriptionTransport.description;
final IceUdpTransportInfo transportInfo = descriptionTransport.transport; final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
final IceUdpTransportInfo modifiedTransportInfo = final IceUdpTransportInfo modifiedTransportInfo =
transportInfo.modifyCredentials(credentials, setup); transportInfo.modifyCredentials(credentials, setup);
contentMapBuilder.put( contentMapBuilder.put(
content.getKey(), content.getKey(),
new DescriptionTransport( new DescriptionTransport<>(
descriptionTransport.senders, rtpDescription, modifiedTransportInfo)); descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
} }
return new RtpContentMap(this.group, contentMapBuilder.build()); return new RtpContentMap(this.group, contentMapBuilder.build());
@ -321,16 +286,18 @@ public class RtpContentMap {
this.group, this.group,
Maps.transformValues( Maps.transformValues(
contents, contents,
dt -> new DescriptionTransport(senders, dt.description, dt.transport))); dt -> new DescriptionTransport<>(senders, dt.description, dt.transport)));
} }
public RtpContentMap modifiedSendersChecked( public RtpContentMap modifiedSendersChecked(
final boolean isInitiator, final Map<String, Content.Senders> modification) { final boolean isInitiator, final Map<String, Content.Senders> modification) {
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = final ImmutableMap.Builder<
new ImmutableMap.Builder<>(); String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) { contentMapBuilder = new ImmutableMap.Builder<>();
for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
content : contents.entrySet()) {
final String id = content.getKey(); final String id = content.getKey();
final DescriptionTransport descriptionTransport = content.getValue(); final var descriptionTransport = content.getValue();
final Content.Senders currentSenders = descriptionTransport.senders; final Content.Senders currentSenders = descriptionTransport.senders;
final Content.Senders targetSenders = modification.get(id); final Content.Senders targetSenders = modification.get(id);
if (targetSenders == null || currentSenders == targetSenders) { if (targetSenders == null || currentSenders == targetSenders) {
@ -339,7 +306,7 @@ public class RtpContentMap {
checkSenderModification(isInitiator, currentSenders, targetSenders); checkSenderModification(isInitiator, currentSenders, targetSenders);
contentMapBuilder.put( contentMapBuilder.put(
id, id,
new DescriptionTransport( new DescriptionTransport<>(
targetSenders, targetSenders,
descriptionTransport.description, descriptionTransport.description,
descriptionTransport.transport)); descriptionTransport.transport));
@ -386,7 +353,7 @@ public class RtpContentMap {
Maps.transformValues( Maps.transformValues(
this.contents, this.contents,
dt -> dt ->
new DescriptionTransport( new DescriptionTransport<>(
dt.senders, dt.senders,
RtpDescription.stub(dt.description.getMedia()), RtpDescription.stub(dt.description.getMedia()),
IceUdpTransportInfo.STUB))); IceUdpTransportInfo.STUB)));
@ -415,120 +382,96 @@ public class RtpContentMap {
public RtpContentMap addContent( public RtpContentMap addContent(
final RtpContentMap modification, final IceUdpTransportInfo.Setup setupOverwrite) { final RtpContentMap modification, final IceUdpTransportInfo.Setup setupOverwrite) {
final Map<String, DescriptionTransport> combined = merge(contents, modification.contents); final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> combined =
final Map<String, DescriptionTransport> combinedFixedTransport = merge(contents, modification.contents);
Maps.transformValues( final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
combined, combinedFixedTransport =
dt -> { Maps.transformValues(
final IceUdpTransportInfo iceUdpTransportInfo; combined,
if (dt.transport.isStub()) { dt -> {
final IceUdpTransportInfo.Credentials credentials = final IceUdpTransportInfo iceUdpTransportInfo;
getDistinctCredentials(); if (dt.transport.isStub()) {
final Collection<String> iceOptions = getCombinedIceOptions(); final IceUdpTransportInfo.Credentials credentials =
final DTLS dtls = getDistinctDtls(); getDistinctCredentials();
iceUdpTransportInfo = final Collection<String> iceOptions =
IceUdpTransportInfo.of( getCombinedIceOptions();
credentials, final DTLS dtls = getDistinctDtls();
iceOptions, iceUdpTransportInfo =
setupOverwrite, IceUdpTransportInfo.of(
dtls.hash, credentials,
dtls.fingerprint); iceOptions,
} else { setupOverwrite,
final IceUdpTransportInfo.Fingerprint fp = dtls.hash,
dt.transport.getFingerprint(); dtls.fingerprint);
final IceUdpTransportInfo.Setup setup = fp.getSetup(); } else {
iceUdpTransportInfo = final IceUdpTransportInfo.Fingerprint fp =
IceUdpTransportInfo.of( dt.transport.getFingerprint();
dt.transport.getCredentials(), final IceUdpTransportInfo.Setup setup = fp.getSetup();
dt.transport.getIceOptions(), iceUdpTransportInfo =
setup == IceUdpTransportInfo.Setup.ACTPASS IceUdpTransportInfo.of(
? setupOverwrite dt.transport.getCredentials(),
: setup, dt.transport.getIceOptions(),
fp.getHash(), setup == IceUdpTransportInfo.Setup.ACTPASS
fp.getContent()); ? setupOverwrite
} : setup,
return new DescriptionTransport( fp.getHash(),
dt.senders, dt.description, iceUdpTransportInfo); fp.getContent());
}); }
return new DescriptionTransport<>(
dt.senders, dt.description, iceUdpTransportInfo);
});
return new RtpContentMap(modification.group, ImmutableMap.copyOf(combinedFixedTransport)); return new RtpContentMap(modification.group, ImmutableMap.copyOf(combinedFixedTransport));
} }
private static Map<String, DescriptionTransport> merge( private static Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> merge(
final Map<String, DescriptionTransport> a, final Map<String, DescriptionTransport> b) { final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> a,
final Map<String, DescriptionTransport> combined = new LinkedHashMap<>(); final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> b) {
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> combined =
new LinkedHashMap<>();
combined.putAll(a); combined.putAll(a);
combined.putAll(b); combined.putAll(b);
return ImmutableMap.copyOf(combined); return ImmutableMap.copyOf(combined);
} }
public static class DescriptionTransport { public static DescriptionTransport<RtpDescription, IceUdpTransportInfo> of(
public final Content.Senders senders; final Content content) {
public final RtpDescription description; final GenericDescription description = content.getDescription();
public final IceUdpTransportInfo transport; final GenericTransportInfo transportInfo = content.getTransport();
final Content.Senders senders = content.getSenders();
public DescriptionTransport( final RtpDescription rtpDescription;
final Content.Senders senders, final IceUdpTransportInfo iceUdpTransportInfo;
final RtpDescription description, if (description == null) {
final IceUdpTransportInfo transport) { rtpDescription = null;
this.senders = senders; } else if (description instanceof RtpDescription) {
this.description = description; rtpDescription = (RtpDescription) description;
this.transport = transport; } else {
throw new UnsupportedApplicationException("Content does not contain rtp description");
} }
if (transportInfo instanceof IceUdpTransportInfo) {
public static DescriptionTransport of(final Content content) { iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
final GenericDescription description = content.getDescription(); } else {
final GenericTransportInfo transportInfo = content.getTransport(); throw new UnsupportedTransportException("Content does not contain ICE-UDP transport");
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 Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
return ImmutableMap.copyOf(
Maps.transformValues(
contents, content -> content == null ? null : of(content)));
} }
return new DescriptionTransport<>(
senders,
rtpDescription,
OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
} }
public static class UnsupportedApplicationException extends IllegalArgumentException { private static DescriptionTransport<RtpDescription, IceUdpTransportInfo> of(
UnsupportedApplicationException(String message) { final SessionDescription sessionDescription,
super(message); 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 { private static Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> of(
UnsupportedTransportException(String message) { final Map<String, Content> contents) {
super(message); return ImmutableMap.copyOf(
} Maps.transformValues(contents, content -> content == null ? null : of(content)));
} }
public static final class Diff { public static final class Diff {

View file

@ -10,12 +10,17 @@ import com.google.common.base.Joiner;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList; 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.Config;
import eu.siacs.conversations.xml.Namespace; 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.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -28,6 +33,8 @@ public class SessionDescription {
public static final String LINE_DIVIDER = "\r\n"; public static final String LINE_DIVIDER = "\r\n";
private static final String HARDCODED_MEDIA_PROTOCOL = private static final String HARDCODED_MEDIA_PROTOCOL =
"UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint "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 int HARDCODED_MEDIA_PORT = 9;
private static final Collection<String> HARDCODED_ICE_OPTIONS = private static final Collection<String> HARDCODED_ICE_OPTIONS =
Collections.singleton("trickle"); Collections.singleton("trickle");
@ -52,9 +59,8 @@ public class SessionDescription {
this.media = media; this.media = media;
} }
private static void appendAttributes( private static void appendAttributes(StringBuilder s, Multimap<String, String> attributes) {
StringBuilder s, ArrayListMultimap<String, String> attributes) { for (final Map.Entry<String, String> attribute : attributes.entries()) {
for (Map.Entry<String, String> attribute : attributes.entries()) {
final String key = attribute.getKey(); final String key = attribute.getKey();
final String value = attribute.getValue(); final String value = attribute.getValue();
s.append("a=").append(key); s.append("a=").append(key);
@ -79,24 +85,20 @@ public class SessionDescription {
final char key = pair[0].charAt(0); final char key = pair[0].charAt(0);
final String value = pair[1]; final String value = pair[1];
switch (key) { switch (key) {
case 'v': case 'v' -> sessionDescriptionBuilder.setVersion(ignorantIntParser(value));
sessionDescriptionBuilder.setVersion(ignorantIntParser(value)); case 'c' -> {
break;
case 'c':
if (currentMediaBuilder != null) { if (currentMediaBuilder != null) {
currentMediaBuilder.setConnectionData(value); currentMediaBuilder.setConnectionData(value);
} else { } else {
sessionDescriptionBuilder.setConnectionData(value); sessionDescriptionBuilder.setConnectionData(value);
} }
break; }
case 's': case 's' -> sessionDescriptionBuilder.setName(value);
sessionDescriptionBuilder.setName(value); case 'a' -> {
break;
case 'a':
final Pair<String, String> attribute = parseAttribute(value); final Pair<String, String> attribute = parseAttribute(value);
attributeMap.put(attribute.first, attribute.second); attributeMap.put(attribute.first, attribute.second);
break; }
case 'm': case 'm' -> {
if (currentMediaBuilder == null) { if (currentMediaBuilder == null) {
sessionDescriptionBuilder.setAttributes(attributeMap); sessionDescriptionBuilder.setAttributes(attributeMap);
} else { } else {
@ -118,7 +120,7 @@ public class SessionDescription {
} else { } else {
Log.d(Config.LOGTAG, "skipping media line " + line); Log.d(Config.LOGTAG, "skipping media line " + line);
} }
break; }
} }
} }
if (currentMediaBuilder != null) { if (currentMediaBuilder != null) {
@ -131,6 +133,56 @@ public class SessionDescription {
return sessionDescriptionBuilder.createSessionDescription(); 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( public static SessionDescription of(
final RtpContentMap contentMap, final boolean isInitiatorContentMap) { final RtpContentMap contentMap, final boolean isInitiatorContentMap) {
final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
@ -140,58 +192,27 @@ public class SessionDescription {
if (group != null) { if (group != null) {
final String semantics = group.getSemantics(); final String semantics = group.getSemantics();
checkNoWhitespace(semantics, "group semantics value must not contain any whitespace"); checkNoWhitespace(semantics, "group semantics value must not contain any whitespace");
attributeMap.put( final var idTags = group.getIdentificationTags();
"group", for (final String content : idTags) {
group.getSemantics() checkNoWhitespace(content, "group content names must not contain any whitespace");
+ " " }
+ Joiner.on(' ').join(group.getIdentificationTags())); 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"); attributeMap.put("msid-semantic", " WMS my-media-stream");
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> entry : for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
contentMap.contents.entrySet()) { entry : contentMap.contents.entrySet()) {
final String name = entry.getKey(); final String name = entry.getKey();
RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue(); checkNoWhitespace(name, "content name must not contain any whitespace");
RtpDescription description = descriptionTransport.description; final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
IceUdpTransportInfo transport = descriptionTransport.transport; entry.getValue();
final RtpDescription description = descriptionTransport.description;
final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create(); final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
final String ufrag = transport.getAttribute("ufrag"); mediaAttributes.putAll(transportInfoMediaAttributes(descriptionTransport.transport));
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));
}
}
final ImmutableList.Builder<Integer> formatBuilder = new ImmutableList.Builder<>(); 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(); final String id = payloadType.getId();
if (Strings.isNullOrEmpty(id)) { if (Strings.isNullOrEmpty(id)) {
throw new IllegalArgumentException("Payload type is missing id"); throw new IllegalArgumentException("Payload type is missing id");
@ -353,6 +374,69 @@ public class SessionDescription {
return sessionDescriptionBuilder.createSessionDescription(); 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) { public static String checkNoWhitespace(final String input, final String message) {
if (CharMatcher.whitespace().matchesAnyOf(input)) { if (CharMatcher.whitespace().matchesAnyOf(input)) {
throw new IllegalArgumentException(message); throw new IllegalArgumentException(message);
@ -421,7 +505,7 @@ public class SessionDescription {
.append(' ') .append(' ')
.append(media.protocol) .append(media.protocol)
.append(' ') .append(' ')
.append(Joiner.on(' ').join(media.formats)) .append(media.format)
.append(LINE_DIVIDER); .append(LINE_DIVIDER);
s.append("c=").append(media.connectionData).append(LINE_DIVIDER); s.append("c=").append(media.connectionData).append(LINE_DIVIDER);
appendAttributes(s, media.attributes); appendAttributes(s, media.attributes);
@ -433,21 +517,21 @@ public class SessionDescription {
public final String media; public final String media;
public final int port; public final int port;
public final String protocol; public final String protocol;
public final List<Integer> formats; public final String format;
public final String connectionData; public final String connectionData;
public final ArrayListMultimap<String, String> attributes; public final Multimap<String, String> attributes;
public Media( public Media(
String media, String media,
int port, int port,
String protocol, String protocol,
List<Integer> formats, String format,
String connectionData, String connectionData,
ArrayListMultimap<String, String> attributes) { Multimap<String, String> attributes) {
this.media = media; this.media = media;
this.port = port; this.port = port;
this.protocol = protocol; this.protocol = protocol;
this.formats = formats; this.format = format;
this.connectionData = connectionData; this.connectionData = connectionData;
this.attributes = attributes; this.attributes = attributes;
} }

View file

@ -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 List<PeerConnection.IceServer> iceServers, final boolean trickle) {
final PeerConnection.RTCConfiguration rtcConfig = final PeerConnection.RTCConfiguration rtcConfig =
new PeerConnection.RTCConfiguration(iceServers); new PeerConnection.RTCConfiguration(iceServers);
@ -774,7 +774,7 @@ public class WebRTCWrapper {
void onRenegotiationNeeded(); void onRenegotiationNeeded();
} }
private abstract static class SetSdpObserver implements SdpObserver { public abstract static class SetSdpObserver implements SdpObserver {
@Override @Override
public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) { public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
@ -800,12 +800,12 @@ public class WebRTCWrapper {
public static class PeerConnectionNotInitialized extends IllegalStateException { public static class PeerConnectionNotInitialized extends IllegalStateException {
private PeerConnectionNotInitialized() { public PeerConnectionNotInitialized() {
super("initialize PeerConnection first"); super("initialize PeerConnection first");
} }
} }
private static class FailureToSetDescriptionException extends IllegalArgumentException { public static class FailureToSetDescriptionException extends IllegalArgumentException {
public FailureToSetDescriptionException(String message) { public FailureToSetDescriptionException(String message) {
super(message); super(message);
} }

View file

@ -8,14 +8,14 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import java.util.Locale;
import java.util.Set;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.SessionDescription; import eu.siacs.conversations.xmpp.jingle.SessionDescription;
import java.util.Locale;
import java.util.Set;
public class Content extends Element { public class Content extends Element {
public Content(final Creator creator, final Senders senders, final String name) { public Content(final Creator creator, final Senders senders, final String name) {
@ -65,7 +65,7 @@ public class Content extends Element {
return null; return null;
} }
final String namespace = description.getNamespace(); final String namespace = description.getNamespace();
if (FileTransferDescription.NAMESPACES.contains(namespace)) { if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) {
return FileTransferDescription.upgrade(description); return FileTransferDescription.upgrade(description);
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
return RtpDescription.upgrade(description); return RtpDescription.upgrade(description);
@ -90,9 +90,11 @@ public class Content extends Element {
if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) { if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) {
return IbbTransportInfo.upgrade(transport); return IbbTransportInfo.upgrade(transport);
} else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) { } 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)) { } else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
return IceUdpTransportInfo.upgrade(transport); return IceUdpTransportInfo.upgrade(transport);
} else if (Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL.equals(namespace)) {
return WebRTCDataChannelTransportInfo.upgrade(transport);
} else if (transport != null) { } else if (transport != null) {
return GenericTransportInfo.upgrade(transport); return GenericTransportInfo.upgrade(transport);
} else { } else {
@ -100,7 +102,6 @@ public class Content extends Element {
} }
} }
public void setTransport(GenericTransportInfo transportInfo) { public void setTransport(GenericTransportInfo transportInfo) {
this.addChild(transportInfo); this.addChild(transportInfo);
} }
@ -141,7 +142,7 @@ public class Content extends Element {
} else if (attributes.contains("recvonly")) { } else if (attributes.contains("recvonly")) {
return initiator ? RESPONDER : INITIATOR; 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 // If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is
// present, "sendrecv" SHOULD be assumed as the default // present, "sendrecv" SHOULD be assumed as the default
// https://www.rfc-editor.org/rfc/rfc4566 // https://www.rfc-editor.org/rfc/rfc4566

View file

@ -1,89 +1,233 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.common.base.CaseFormat;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions; 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;
import java.util.Arrays; import eu.siacs.conversations.Config;
import java.util.List;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import java.util.List;
public class FileTransferDescription extends GenericDescription { public class FileTransferDescription extends GenericDescription {
public static List<String> NAMESPACES = Arrays.asList( private FileTransferDescription() {
Version.FT_3.namespace, super("description", Namespace.JINGLE_APPS_FILE_TRANSFER);
Version.FT_4.namespace,
Version.FT_5.namespace
);
private FileTransferDescription(String name, String namespace) {
super(name, namespace);
} }
public Version getVersion() { public static FileTransferDescription of(final File fileDescription) {
final String namespace = getNamespace(); final var description = new FileTransferDescription();
if (namespace.equals(Version.FT_3.namespace)) { final var file = description.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
return Version.FT_3; file.addChild("name").setContent(fileDescription.name);
} else if (namespace.equals(Version.FT_4.namespace)) { file.addChild("size").setContent(Long.toString(fileDescription.size));
return Version.FT_4; if (fileDescription.mediaType != null) {
} else if (namespace.equals(Version.FT_5.namespace)) { file.addChild("mediaType").setContent(fileDescription.mediaType);
return Version.FT_5;
} else {
throw new IllegalStateException("Unknown namespace");
}
}
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 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");
}
fileElement.addChild("size").setContent(Long.toString(file.getExpectedSize()));
fileElement.addChild("name").setContent(file.getName());
if (axolotlMessage != null) {
fileElement.addChild(axolotlMessage.toElement());
} }
return description; return description;
} }
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 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;
}
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()));
}
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) { public static FileTransferDescription upgrade(final Element element) {
Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); Preconditions.checkArgument(
Preconditions.checkArgument(NAMESPACES.contains(element.getNamespace()), "Element does not match a file transfer namespace"); "description".equals(element.getName()),
final FileTransferDescription description = new FileTransferDescription("description", element.getNamespace()); "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.setAttributes(element.getAttributes());
description.setChildren(element.getChildren()); description.setChildren(element.getChildren());
return description; return description;
} }
public enum Version { public static final class Checksum extends SessionInfo {
FT_3("urn:xmpp:jingle:apps:file-transfer:3"), public final List<Hash> hashes;
FT_4("urn:xmpp:jingle:apps:file-transfer:4"),
FT_5("urn:xmpp:jingle:apps:file-transfer:5");
private final String namespace; public Checksum(final String name, List<Hash> hashes) {
super(name);
Version(String namespace) { this.hashes = hashes;
this.namespace = namespace;
} }
public String getNamespace() { @Override
return namespace; @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 {
public final String name;
protected SessionInfo(final String name) {
this.name = name;
}
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));
} }
} }
} }

View file

@ -8,6 +8,7 @@ public class GenericDescription extends Element {
GenericDescription(String name, final String namespace) { GenericDescription(String name, final String namespace) {
super(name, namespace); super(name, namespace);
Preconditions.checkArgument("description".equals(name));
} }
public static GenericDescription upgrade(final Element element) { public static GenericDescription upgrade(final Element element) {

View file

@ -41,7 +41,7 @@ public class Group extends Element {
} }
public static Group ofSdpString(final String input) { 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(" "); final String[] parts = input.split(" ");
if (parts.length >= 2) { if (parts.length >= 2) {
final String semantics = parts[0]; final String semantics = parts[0];

View file

@ -1,6 +1,8 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import com.google.common.base.Preconditions; 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.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
@ -23,16 +25,9 @@ public class IbbTransportInfo extends GenericTransportInfo {
return this.getAttribute("sid"); return this.getAttribute("sid");
} }
public int getBlockSize() { public Long getBlockSize() {
final String blockSize = this.getAttribute("block-size"); final String blockSize = this.getAttribute("block-size");
if (blockSize == null) { return Strings.isNullOrEmpty(blockSize) ? null : Longs.tryParse(blockSize);
return 0;
}
try {
return Integer.parseInt(blockSize);
} catch (NumberFormatException e) {
return 0;
}
} }
public static IbbTransportInfo upgrade(final Element element) { public static IbbTransportInfo upgrade(final Element element) {

View file

@ -15,11 +15,13 @@ import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.SessionDescription; import eu.siacs.conversations.xmpp.jingle.SessionDescription;
import eu.siacs.conversations.xmpp.jingle.transports.Transport;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; 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() { private Candidate() {
super("candidate"); super("candidate");
@ -396,7 +398,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
return fingerprint; 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 fingerprint = Iterables.getFirst(attributes.get("fingerprint"), null);
final String setup = Iterables.getFirst(attributes.get("setup"), null); final String setup = Iterables.getFirst(attributes.get("setup"), null);
if (setup != null && fingerprint != null) { if (setup != null && fingerprint != null) {

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.common.base.CaseFormat; 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.base.Strings;
import com.google.common.collect.ImmutableMap; 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.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import java.util.Map;
public class JinglePacket extends IqPacket { public class JinglePacket extends IqPacket {
private JinglePacket() { private JinglePacket() {
@ -36,7 +41,7 @@ public class JinglePacket extends IqPacket {
return jinglePacket; 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() { public Content getJingleContent() {
final Element content = getJingleChild("content"); final Element content = getJingleChild("content");
return content == null ? null : Content.upgrade(content); return content == null ? null : Content.upgrade(content);
@ -64,7 +69,7 @@ public class JinglePacket extends IqPacket {
return builder.build(); return builder.build();
} }
public void addJingleContent(final Content content) { //take content interface public void addJingleContent(final Content content) { // take content interface
addJingleChild(content); 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) { public void setInitiator(final Jid initiator) {
Preconditions.checkArgument(initiator.isFullJid(), "initiator should be a full JID"); Preconditions.checkArgument(initiator.isFullJid(), "initiator should be a full JID");
findChild("jingle", Namespace.JINGLE).setAttribute("initiator", initiator); 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) { public void setResponder(Jid responder) {
Preconditions.checkArgument(responder.isFullJid(), "responder should be a full JID"); Preconditions.checkArgument(responder.isFullJid(), "responder should be a full JID");
findChild("jingle", Namespace.JINGLE).setAttribute("responder", responder); findChild("jingle", Namespace.JINGLE).setAttribute("responder", responder);
@ -116,6 +121,39 @@ public class JinglePacket extends IqPacket {
jingle.addChild(child); 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() { public String getSessionId() {
return findChild("jingle", Namespace.JINGLE).getAttribute("sid"); return findChild("jingle", Namespace.JINGLE).getAttribute("sid");
} }
@ -142,7 +180,7 @@ public class JinglePacket extends IqPacket {
TRANSPORT_REPLACE; TRANSPORT_REPLACE;
public static Action of(final String value) { public static Action of(final String value) {
//TODO handle invalid // TODO handle invalid
return Action.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value)); 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 static class ReasonWrapper {
public final Reason reason; public final Reason reason;
public final String text; public final String text;

View file

@ -18,7 +18,7 @@ public class Propose extends Element {
for (final Element child : this.children) { for (final Element child : this.children) {
if ("description".equals(child.getName())) { if ("description".equals(child.getName())) {
final String namespace = child.getNamespace(); final String namespace = child.getNamespace();
if (FileTransferDescription.NAMESPACES.contains(namespace)) { if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) {
builder.add(FileTransferDescription.upgrade(child)); builder.add(FileTransferDescription.upgrade(child));
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
builder.add(RtpDescription.upgrade(child)); builder.add(RtpDescription.upgrade(child));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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