From 304205b2e344ae1c1b6b17e230589109a230b121 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 20 Nov 2022 17:00:40 +0100 Subject: [PATCH] take senders attr into account when converting to and from sdp --- .../crypto/axolotl/AxolotlService.java | 4 +- .../jingle/JingleFileTransferConnection.java | 27 +-- .../xmpp/jingle/JingleRtpConnection.java | 14 +- .../xmpp/jingle/RtpContentMap.java | 104 +++++++++--- .../xmpp/jingle/SessionDescription.java | 158 ++++++++++++------ .../xmpp/jingle/stanzas/Content.java | 74 ++++++-- 6 files changed, 274 insertions(+), 107 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 3d4f23360..05ffdbdca 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1272,7 +1272,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } descriptionTransportBuilder.put( content.getKey(), - new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo) + new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo) ); } return Futures.immediateFuture( @@ -1306,7 +1306,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { omemoVerification.setOrEnsureEqual(decryptedTransport); descriptionTransportBuilder.put( content.getKey(), - new RtpContentMap.DescriptionTransport(descriptionTransport.description, decryptedTransport.payload) + new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload) ); } processPostponed(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 43aaa54b5..c4ed04bd0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -577,8 +577,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendInitRequest() { final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET"); final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); @@ -656,8 +655,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple gatherAndConnectDirectCandidates(); this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); - final Content content = new Content(contentCreator, contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(contentCreator, contentSenders, contentName); content.setDescription(this.description); if (success && candidate != null && !equalCandidateExists(candidate)) { final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate); @@ -696,8 +694,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendAcceptIbb() { this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); - final Content content = new Content(contentCreator, contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(contentCreator, contentSenders, contentName); content.setDescription(this.description); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); packet.addJingleContent(content); @@ -910,8 +907,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendFallbackToIbb() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb"); final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); this.transportId = JingleConnectionManager.nextRandomId(); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); packet.addJingleContent(content); @@ -944,8 +940,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT); - final Content content = new Content(contentCreator, contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(contentCreator, contentSenders, contentName); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); answer.addJingleContent(content); @@ -1124,8 +1119,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendProxyActivated(String cid) { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid))); packet.addJingleContent(content); this.sendJinglePacket(packet); @@ -1133,8 +1127,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendProxyError() { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error"))); packet.addJingleContent(content); this.sendJinglePacket(packet); @@ -1142,8 +1135,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendCandidateUsed(final String cid) { JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid))); packet.addJingleContent(content); this.sentCandidate = true; @@ -1156,8 +1148,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendCandidateError() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error"); JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error"))); packet.addJingleContent(content); this.sentCandidate = true; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e2832355d..5cccafa5a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -425,7 +425,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { - final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); + final SessionDescription sessionDescription = SessionDescription.of(restartContentMap, !isInitiator()); final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER @@ -444,7 +444,7 @@ public class JingleRtpConnection extends AbstractJingleConnection if (isOffer) { webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription localSessionDescription = setLocalSessionDescription(); - setLocalContentMap(RtpContentMap.of(localSessionDescription)); + setLocalContentMap(RtpContentMap.of(localSessionDescription, isInitiator())); // We need to respond OK before sending any candidates respondOk(jinglePacket); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); @@ -726,7 +726,7 @@ public class JingleRtpConnection extends AbstractJingleConnection this.storePeerDtlsSetup(contentMap.getDtlsSetup()); final SessionDescription sessionDescription; try { - sessionDescription = SessionDescription.of(contentMap); + sessionDescription = SessionDescription.of(contentMap, false); } catch (final IllegalArgumentException | NullPointerException e) { Log.d( Config.LOGTAG, @@ -763,7 +763,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } final SessionDescription offer; try { - offer = SessionDescription.of(rtpContentMap); + offer = SessionDescription.of(rtpContentMap, true); } catch (final IllegalArgumentException | NullPointerException e) { Log.d( Config.LOGTAG, @@ -838,7 +838,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final org.webrtc.SessionDescription webRTCSessionDescription) { final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false); this.responderRtpContentMap = respondingRtpContentMap; storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); @@ -1289,7 +1289,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true); this.initiatorRtpContentMap = rtpContentMap; //TODO delay ready to receive ice until after session-init this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); @@ -1922,7 +1922,7 @@ public class JingleRtpConnection extends AbstractJingleConnection sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); return; } - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, isInitiator()); final RtpContentMap transportInfo = rtpContentMap.transportInfo(); final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index bba44f963..f9c245b0e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; +import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Collections2; @@ -15,6 +16,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; + import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; @@ -58,13 +61,15 @@ public class RtpContentMap { return true; } - public static RtpContentMap of(final SessionDescription sessionDescription) { + public static RtpContentMap of( + final SessionDescription sessionDescription, final boolean isInitiator) { final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); for (SessionDescription.Media media : sessionDescription.media) { final String id = Iterables.getFirst(media.attributes.get("mid"), null); Preconditions.checkNotNull(id, "media has no mid"); - contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media)); + contentMapBuilder.put( + id, DescriptionTransport.of(sessionDescription, isInitiator, media)); } final String groupAttribute = Iterables.getFirst(sessionDescription.attributes.get("group"), null); @@ -140,11 +145,16 @@ public class RtpContentMap { jinglePacket.addGroup(this.group); } for (Map.Entry entry : this.contents.entrySet()) { - final Content content = new Content(Content.Creator.INITIATOR, entry.getKey()); - if (entry.getValue().description != null) { - content.addChild(entry.getValue().description); + 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(entry.getValue().transport); + content.addChild(descriptionTransport.transport); jinglePacket.addJingleContent(content); } return jinglePacket; @@ -163,7 +173,10 @@ public class RtpContentMap { newTransportInfo.addChild(candidate); return new RtpContentMap( null, - ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); + ImmutableMap.of( + contentName, + new DescriptionTransport( + descriptionTransport.senders, null, newTransportInfo))); } RtpContentMap transportInfo() { @@ -171,7 +184,9 @@ public class RtpContentMap { null, Maps.transformValues( contents, - dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))); + dt -> + new DescriptionTransport( + dt.senders, null, dt.transport.cloneWrapper()))); } public IceUdpTransportInfo.Credentials getDistinctCredentials() { @@ -179,7 +194,8 @@ public class RtpContentMap { final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null); if (allCredentials.size() == 1 && credentials != null) { - if (Strings.isNullOrEmpty(credentials.password) || Strings.isNullOrEmpty(credentials.ufrag)) { + if (Strings.isNullOrEmpty(credentials.password) + || Strings.isNullOrEmpty(credentials.ufrag)) { throw new IllegalStateException("Credentials are missing password or ufrag"); } return credentials; @@ -233,23 +249,45 @@ public class RtpContentMap { final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); for (final Map.Entry content : contents.entrySet()) { - final RtpDescription rtpDescription = content.getValue().description; - IceUdpTransportInfo transportInfo = content.getValue().transport; + final DescriptionTransport descriptionTransport = content.getValue(); + final RtpDescription rtpDescription = descriptionTransport.description; + final IceUdpTransportInfo transportInfo = descriptionTransport.transport; final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup); contentMapBuilder.put( content.getKey(), - new DescriptionTransport(rtpDescription, modifiedTransportInfo)); + new DescriptionTransport( + descriptionTransport.senders, rtpDescription, modifiedTransportInfo)); } return new RtpContentMap(this.group, contentMapBuilder.build()); } + public Diff diff(final RtpContentMap rtpContentMap) { + final Set existingContentIds = this.contents.keySet(); + final Set newContentIds = rtpContentMap.contents.keySet(); + return new Diff( + Sets.difference(newContentIds, existingContentIds), + Sets.difference(existingContentIds, newContentIds)); + } + + public boolean iceRestart(final RtpContentMap rtpContentMap) { + try { + return !getDistinctCredentials().equals(rtpContentMap.getDistinctCredentials()); + } catch (final IllegalStateException e) { + return false; + } + } + public static class DescriptionTransport { + public final Content.Senders senders; public final RtpDescription description; public final IceUdpTransportInfo transport; public DescriptionTransport( - final RtpDescription description, final IceUdpTransportInfo transport) { + final Content.Senders senders, + final RtpDescription description, + final IceUdpTransportInfo transport) { + this.senders = senders; this.description = description; this.transport = transport; } @@ -257,6 +295,7 @@ public class RtpContentMap { public static DescriptionTransport of(final Content content) { final GenericDescription description = content.getDescription(); final GenericTransportInfo transportInfo = content.getTransport(); + final Content.Senders senders = content.getSenders(); final RtpDescription rtpDescription; final IceUdpTransportInfo iceUdpTransportInfo; if (description == null) { @@ -274,22 +313,26 @@ public class RtpContentMap { "Content does not contain ICE-UDP transport"); } return new DescriptionTransport( - rtpDescription, OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)); + senders, + rtpDescription, + OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)); } - public static DescriptionTransport of( - final SessionDescription sessionDescription, final SessionDescription.Media media) { + 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(rtpDescription, transportInfo); + return new DescriptionTransport(senders, rtpDescription, transportInfo); } public static Map of(final Map contents) { return ImmutableMap.copyOf( Maps.transformValues( - contents, - content -> content == null ? null : of(content))); + contents, content -> content == null ? null : of(content))); } } @@ -304,4 +347,27 @@ public class RtpContentMap { super(message); } } + + public static final class Diff { + public final Set added; + public final Set removed; + + private Diff(final Set added, final Set removed) { + this.added = added; + this.removed = removed; + } + + public boolean hasModifications() { + return !this.added.isEmpty() || !this.removed.isEmpty(); + } + + @Override + @Nonnull + public String toString() { + return MoreObjects.toStringHelper(this) + .add("added", added) + .add("removed", removed) + .toString(); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index e113146b1..eef7ae0da 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -3,6 +3,8 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; import android.util.Pair; +import androidx.annotation.NonNull; + import com.google.common.base.CharMatcher; import com.google.common.base.Joiner; import com.google.common.base.Strings; @@ -21,11 +23,12 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; public class SessionDescription { - public final static String LINE_DIVIDER = "\r\n"; - private final static String HARDCODED_MEDIA_PROTOCOL = "UDP/TLS/RTP/SAVPF"; //probably only true for DTLS-SRTP aka when we have a fingerprint - private final static int HARDCODED_MEDIA_PORT = 9; - private final static String HARDCODED_ICE_OPTIONS = "trickle"; - private final static String HARDCODED_CONNECTION = "IN IP4 0.0.0.0"; + public static final String LINE_DIVIDER = "\r\n"; + private static final String HARDCODED_MEDIA_PROTOCOL = + "UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint + private static final int HARDCODED_MEDIA_PORT = 9; + private static final String HARDCODED_ICE_OPTIONS = "trickle"; + private static final String HARDCODED_CONNECTION = "IN IP4 0.0.0.0"; public final int version; public final String name; @@ -33,8 +36,12 @@ public class SessionDescription { public final ArrayListMultimap attributes; public final List media; - - public SessionDescription(int version, String name, String connectionData, ArrayListMultimap attributes, List media) { + public SessionDescription( + int version, + String name, + String connectionData, + ArrayListMultimap attributes, + List media) { this.version = version; this.name = name; this.connectionData = connectionData; @@ -42,7 +49,8 @@ public class SessionDescription { this.media = media; } - private static void appendAttributes(StringBuilder s, ArrayListMultimap attributes) { + private static void appendAttributes( + StringBuilder s, ArrayListMultimap attributes) { for (Map.Entry attribute : attributes.entries()) { final String key = attribute.getKey(); final String value = attribute.getValue(); @@ -109,7 +117,6 @@ public class SessionDescription { } break; } - } if (currentMediaBuilder != null) { currentMediaBuilder.setAttributes(attributeMap); @@ -121,7 +128,7 @@ public class SessionDescription { return sessionDescriptionBuilder.createSessionDescription(); } - public static SessionDescription of(final RtpContentMap contentMap) { + public static SessionDescription of(final RtpContentMap contentMap, final boolean isInitiatorContentMap) { final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); final ArrayListMultimap attributeMap = ArrayListMultimap.create(); final ImmutableList.Builder mediaListBuilder = new ImmutableList.Builder<>(); @@ -129,12 +136,17 @@ public class SessionDescription { if (group != null) { final String semantics = group.getSemantics(); checkNoWhitespace(semantics, "group semantics value must not contain any whitespace"); - attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(group.getIdentificationTags())); + attributeMap.put( + "group", + group.getSemantics() + + " " + + Joiner.on(' ').join(group.getIdentificationTags())); } attributeMap.put("msid-semantic", " WMS my-media-stream"); - for (final Map.Entry entry : contentMap.contents.entrySet()) { + for (final Map.Entry entry : + contentMap.contents.entrySet()) { final String name = entry.getKey(); RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue(); RtpDescription description = descriptionTransport.description; @@ -143,19 +155,22 @@ public class SessionDescription { 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"); + 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"); + throw new IllegalArgumentException( + "Transport element is missing required pwd attribute"); } checkNoWhitespace(pwd, "pwd value must not contain any whitespaces"); mediaAttributes.put("ice-pwd", pwd); mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS); final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); if (fingerprint != null) { - mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); + mediaAttributes.put( + "fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); if (setup != null) { mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT)); @@ -174,37 +189,56 @@ public class SessionDescription { mediaAttributes.put("rtpmap", payloadType.toSdpAttribute()); final List parameters = payloadType.getParameters(); if (parameters.size() == 1) { - mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0))); + mediaAttributes.put( + "fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0))); } else if (parameters.size() > 0) { - mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters)); + mediaAttributes.put( + "fmtp", RtpDescription.Parameter.toSdpString(id, parameters)); } - for (RtpDescription.FeedbackNegotiation feedbackNegotiation : payloadType.getFeedbackNegotiations()) { + for (RtpDescription.FeedbackNegotiation feedbackNegotiation : + payloadType.getFeedbackNegotiations()) { final String type = feedbackNegotiation.getType(); final String subtype = feedbackNegotiation.getSubType(); if (Strings.isNullOrEmpty(type)) { - throw new IllegalArgumentException("a feedback for payload-type " + id + " negotiation is missing type"); + throw new IllegalArgumentException( + "a feedback for payload-type " + + id + + " negotiation is missing type"); } - checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); - mediaAttributes.put("rtcp-fb", id + " " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); + checkNoWhitespace( + type, "feedback negotiation type must not contain whitespace"); + mediaAttributes.put( + "rtcp-fb", + id + + " " + + type + + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); } - for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : payloadType.feedbackNegotiationTrrInts()) { - mediaAttributes.put("rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue()); + for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : + payloadType.feedbackNegotiationTrrInts()) { + mediaAttributes.put( + "rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue()); } } - for (RtpDescription.FeedbackNegotiation feedbackNegotiation : description.getFeedbackNegotiations()) { + for (RtpDescription.FeedbackNegotiation feedbackNegotiation : + description.getFeedbackNegotiations()) { final String type = feedbackNegotiation.getType(); final String subtype = feedbackNegotiation.getSubType(); if (Strings.isNullOrEmpty(type)) { throw new IllegalArgumentException("a feedback negotiation is missing type"); } checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); - mediaAttributes.put("rtcp-fb", "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); + mediaAttributes.put( + "rtcp-fb", + "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); } - for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) { + for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : + description.feedbackNegotiationTrrInts()) { mediaAttributes.put("rtcp-fb", "* trr-int " + feedbackNegotiationTrrInt.getValue()); } - for (final RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) { + for (final RtpDescription.RtpHeaderExtension extension : + description.getHeaderExtensions()) { final String id = extension.getId(); final String uri = extension.getUri(); if (Strings.isNullOrEmpty(id)) { @@ -218,7 +252,8 @@ public class SessionDescription { mediaAttributes.put("extmap", id + " " + uri); } - if (description.hasChild("extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) { + if (description.hasChild( + "extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) { mediaAttributes.put("extmap-allow-mixed", ""); } @@ -226,13 +261,16 @@ public class SessionDescription { final String semantics = sourceGroup.getSemantics(); final List groups = sourceGroup.getSsrcs(); if (Strings.isNullOrEmpty(semantics)) { - throw new IllegalArgumentException("A SSRC group is missing semantics attribute"); + throw new IllegalArgumentException( + "A SSRC group is missing semantics attribute"); } checkNoWhitespace(semantics, "source group semantics must not contain whitespace"); if (groups.size() == 0) { throw new IllegalArgumentException("A SSRC group is missing SSRC ids"); } - mediaAttributes.put("ssrc-group", String.format("%s %s", semantics, Joiner.on(' ').join(groups))); + mediaAttributes.put( + "ssrc-group", + String.format("%s %s", semantics, Joiner.on(' ').join(groups))); } for (final RtpDescription.Source source : description.getSources()) { for (final RtpDescription.Source.Parameter parameter : source.getParameters()) { @@ -240,14 +278,18 @@ public class SessionDescription { final String parameterName = parameter.getParameterName(); final String parameterValue = parameter.getParameterValue(); if (Strings.isNullOrEmpty(id)) { - throw new IllegalArgumentException("A source specific media attribute is missing the id"); + throw new IllegalArgumentException( + "A source specific media attribute is missing the id"); } - checkNoWhitespace(id, "A source specific media attributes must not contain whitespaces"); + checkNoWhitespace( + id, "A source specific media attributes must not contain whitespaces"); if (Strings.isNullOrEmpty(parameterName)) { - throw new IllegalArgumentException("A source specific media attribute is missing its name"); + throw new IllegalArgumentException( + "A source specific media attribute is missing its name"); } if (Strings.isNullOrEmpty(parameterValue)) { - throw new IllegalArgumentException("A source specific media attribute is missing its value"); + throw new IllegalArgumentException( + "A source specific media attribute is missing its value"); } mediaAttributes.put("ssrc", id + " " + parameterName + ":" + parameterValue); } @@ -255,14 +297,14 @@ public class SessionDescription { mediaAttributes.put("mid", name); - //random additional attributes - mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0"); - mediaAttributes.put("sendrecv", ""); - + mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), ""); if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP)) { mediaAttributes.put("rtcp-mux", ""); } + // random additional attributes + mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0"); + final MediaBuilder mediaBuilder = new MediaBuilder(); mediaBuilder.setMedia(description.getMedia().toString().toLowerCase(Locale.ROOT)); mediaBuilder.setConnectionData(HARDCODED_CONNECTION); @@ -271,7 +313,6 @@ public class SessionDescription { mediaBuilder.setAttributes(mediaAttributes); mediaBuilder.setFormats(formatBuilder.build()); mediaListBuilder.add(mediaBuilder.createMedia()); - } sessionDescriptionBuilder.setVersion(0); sessionDescriptionBuilder.setName("-"); @@ -317,17 +358,33 @@ public class SessionDescription { } } + @NonNull @Override public String toString() { - final StringBuilder s = new StringBuilder() - .append("v=").append(version).append(LINE_DIVIDER) - //TODO randomize or static - .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1").append(LINE_DIVIDER) //what ever that means - .append("s=").append(name).append(LINE_DIVIDER) - .append("t=0 0").append(LINE_DIVIDER); + final StringBuilder s = + new StringBuilder() + .append("v=") + .append(version) + .append(LINE_DIVIDER) + // TODO randomize or static + .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1") + .append(LINE_DIVIDER) // what ever that means + .append("s=") + .append(name) + .append(LINE_DIVIDER) + .append("t=0 0") + .append(LINE_DIVIDER); appendAttributes(s, attributes); for (Media media : this.media) { - s.append("m=").append(media.media).append(' ').append(media.port).append(' ').append(media.protocol).append(' ').append(Joiner.on(' ').join(media.formats)).append(LINE_DIVIDER); + s.append("m=") + .append(media.media) + .append(' ') + .append(media.port) + .append(' ') + .append(media.protocol) + .append(' ') + .append(Joiner.on(' ').join(media.formats)) + .append(LINE_DIVIDER); s.append("c=").append(media.connectionData).append(LINE_DIVIDER); appendAttributes(s, media.attributes); } @@ -342,7 +399,13 @@ public class SessionDescription { public final String connectionData; public final ArrayListMultimap attributes; - public Media(String media, int port, String protocol, List formats, String connectionData, ArrayListMultimap attributes) { + public Media( + String media, + int port, + String protocol, + List formats, + String connectionData, + ArrayListMultimap attributes) { this.media = media; this.port = port; this.protocol = protocol; @@ -351,5 +414,4 @@ public class SessionDescription { this.attributes = attributes; } } - } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index e21c38968..962515293 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -1,20 +1,27 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import android.util.Log; + import androidx.annotation.NonNull; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import java.util.Locale; +import java.util.Set; +import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.SessionDescription; public class Content extends Element { - public Content(final Creator creator, final String name) { + public Content(final Creator creator, final Senders senders, final String name) { super("content", Namespace.JINGLE); this.setAttribute("creator", creator.toString()); this.setAttribute("name", name); + this.setSenders(senders); } private Content() { @@ -38,11 +45,17 @@ public class Content extends Element { } public Senders getSenders() { + final String attribute = getAttribute("senders"); + if (Strings.isNullOrEmpty(attribute)) { + return Senders.BOTH; + } return Senders.of(getAttribute("senders")); } - public void setSenders(Senders senders) { - this.setAttribute("senders", senders.toString()); + public void setSenders(final Senders senders) { + if (senders != null && senders != Senders.BOTH) { + this.setAttribute("senders", senders.toString()); + } } public GenericDescription getDescription() { @@ -51,9 +64,7 @@ public class Content extends Element { return null; } final String namespace = description.getNamespace(); - if (FileTransferDescription.NAMESPACES.contains(namespace)) { - return FileTransferDescription.upgrade(description); - } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { return RtpDescription.upgrade(description); } else { return GenericDescription.upgrade(description); @@ -73,11 +84,7 @@ public class Content extends Element { public GenericTransportInfo getTransport() { final Element transport = this.findChild("transport"); final String namespace = transport == null ? null : transport.getNamespace(); - if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) { - return IbbTransportInfo.upgrade(transport); - } else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) { - return S5BTransportInfo.upgrade(transport); - } else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) { + if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) { return IceUdpTransportInfo.upgrade(transport); } else if (transport != null) { return GenericTransportInfo.upgrade(transport); @@ -91,7 +98,8 @@ public class Content extends Element { } public enum Creator { - INITIATOR, RESPONDER; + INITIATOR, + RESPONDER; public static Creator of(final String value) { return Creator.valueOf(value.toUpperCase(Locale.ROOT)); @@ -105,16 +113,56 @@ public class Content extends Element { } public enum Senders { - BOTH, INITIATOR, NONE, RESPONDER; + BOTH, + INITIATOR, + NONE, + RESPONDER; public static Senders of(final String value) { return Senders.valueOf(value.toUpperCase(Locale.ROOT)); } + public static Senders of(final SessionDescription.Media media, final boolean initiator) { + final Set attributes = media.attributes.keySet(); + if (attributes.contains("sendrecv")) { + return BOTH; + } else if (attributes.contains("inactive")) { + return NONE; + } else if (attributes.contains("sendonly")) { + return initiator ? INITIATOR : RESPONDER; + } else if (attributes.contains("recvonly")) { + return initiator ? RESPONDER : INITIATOR; + } + Log.w(Config.LOGTAG,"assuming default value for senders"); + // If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is + // present, "sendrecv" SHOULD be assumed as the default + // https://www.rfc-editor.org/rfc/rfc4566 + return BOTH; + } + @Override @NonNull public String toString() { return super.toString().toLowerCase(Locale.ROOT); } + + public String asMediaAttribute(final boolean initiator) { + final boolean responder = !initiator; + if (this == Content.Senders.BOTH) { + return "sendrecv"; + } else if (this == Content.Senders.NONE) { + return "inactive"; + } else if ((initiator && this == Content.Senders.INITIATOR) + || (responder && this == Content.Senders.RESPONDER)) { + return "sendonly"; + } else if ((initiator && this == Content.Senders.RESPONDER) + || (responder && this == Content.Senders.INITIATOR)) { + return "recvonly"; + } else { + throw new IllegalStateException( + String.format( + "illegal combination of initiator=%s and %s", initiator, this)); + } + } } }