2020-04-05 08:20:34 +00:00
|
|
|
package eu.siacs.conversations.xmpp.jingle;
|
|
|
|
|
|
|
|
import com.google.common.base.Function;
|
|
|
|
import com.google.common.base.Preconditions;
|
2020-04-10 05:45:23 +00:00
|
|
|
import com.google.common.base.Strings;
|
2020-04-15 08:49:38 +00:00
|
|
|
import com.google.common.collect.Collections2;
|
2020-06-22 16:07:27 +00:00
|
|
|
import com.google.common.collect.ImmutableList;
|
2020-04-05 08:20:34 +00:00
|
|
|
import com.google.common.collect.ImmutableMap;
|
2021-11-15 20:49:31 +00:00
|
|
|
import com.google.common.collect.ImmutableSet;
|
2020-04-05 08:20:34 +00:00
|
|
|
import com.google.common.collect.Iterables;
|
|
|
|
import com.google.common.collect.Maps;
|
2020-04-15 08:49:38 +00:00
|
|
|
import com.google.common.collect.Sets;
|
2020-04-05 08:20:34 +00:00
|
|
|
|
|
|
|
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
|
|
|
|
|
2021-03-02 20:13:49 +00:00
|
|
|
import java.util.Collection;
|
2020-06-22 16:07:27 +00:00
|
|
|
import java.util.List;
|
2020-04-05 08:20:34 +00:00
|
|
|
import java.util.Map;
|
2020-04-15 08:49:38 +00:00
|
|
|
import java.util.Set;
|
2020-04-05 08:20:34 +00:00
|
|
|
|
|
|
|
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.IceUdpTransportInfo;
|
|
|
|
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
|
2021-03-02 20:13:49 +00:00
|
|
|
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
|
2020-04-05 08:20:34 +00:00
|
|
|
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
|
|
|
|
|
|
|
public class RtpContentMap {
|
|
|
|
|
|
|
|
public final Group group;
|
|
|
|
public final Map<String, DescriptionTransport> contents;
|
|
|
|
|
2021-03-02 20:13:49 +00:00
|
|
|
public RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
|
2020-04-05 08:20:34 +00:00
|
|
|
this.group = group;
|
|
|
|
this.contents = contents;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static RtpContentMap of(final JinglePacket jinglePacket) {
|
2022-02-25 16:26:36 +00:00
|
|
|
final Map<String, DescriptionTransport> contents =
|
|
|
|
DescriptionTransport.of(jinglePacket.getJingleContents());
|
2021-03-02 20:13:49 +00:00
|
|
|
if (isOmemoVerified(contents)) {
|
|
|
|
return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
|
|
|
|
} else {
|
|
|
|
return new RtpContentMap(jinglePacket.getGroup(), contents);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static boolean isOmemoVerified(Map<String, DescriptionTransport> contents) {
|
|
|
|
final Collection<DescriptionTransport> values = contents.values();
|
|
|
|
if (values.size() == 0) {
|
|
|
|
return false;
|
|
|
|
}
|
2021-03-16 14:52:51 +00:00
|
|
|
for (final DescriptionTransport descriptionTransport : values) {
|
2021-03-02 20:13:49 +00:00
|
|
|
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
2020-04-05 08:20:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public static RtpContentMap of(final SessionDescription sessionDescription) {
|
2022-02-25 16:26:36 +00:00
|
|
|
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
|
|
|
|
new ImmutableMap.Builder<>();
|
2020-04-05 08:20:34 +00:00
|
|
|
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));
|
|
|
|
}
|
2022-02-25 16:26:36 +00:00
|
|
|
final String groupAttribute =
|
|
|
|
Iterables.getFirst(sessionDescription.attributes.get("group"), null);
|
2020-04-05 08:20:34 +00:00
|
|
|
final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
|
|
|
|
return new RtpContentMap(group, contentMapBuilder.build());
|
|
|
|
}
|
|
|
|
|
2020-04-15 08:49:38 +00:00
|
|
|
public Set<Media> getMedia() {
|
2022-02-25 16:26:36 +00:00
|
|
|
return Sets.newHashSet(
|
|
|
|
Collections2.transform(
|
|
|
|
contents.values(),
|
|
|
|
input -> {
|
|
|
|
final RtpDescription rtpDescription =
|
|
|
|
input == null ? null : input.description;
|
|
|
|
return rtpDescription == null
|
|
|
|
? Media.UNKNOWN
|
|
|
|
: input.description.getMedia();
|
|
|
|
}));
|
2020-04-15 08:49:38 +00:00
|
|
|
}
|
|
|
|
|
2020-06-22 16:07:27 +00:00
|
|
|
public List<String> getNames() {
|
|
|
|
return ImmutableList.copyOf(contents.keySet());
|
|
|
|
}
|
|
|
|
|
2020-04-29 13:54:02 +00:00
|
|
|
void requireContentDescriptions() {
|
2020-04-06 08:26:29 +00:00
|
|
|
if (this.contents.size() == 0) {
|
|
|
|
throw new IllegalStateException("No contents available");
|
|
|
|
}
|
2020-04-10 05:45:23 +00:00
|
|
|
for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
|
2020-04-06 08:26:29 +00:00
|
|
|
if (entry.getValue().description == null) {
|
2022-02-25 16:26:36 +00:00
|
|
|
throw new IllegalStateException(
|
|
|
|
String.format("%s is lacking content description", entry.getKey()));
|
2020-04-06 08:26:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-29 13:54:02 +00:00
|
|
|
void requireDTLSFingerprint() {
|
2021-11-17 09:49:16 +00:00
|
|
|
requireDTLSFingerprint(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void requireDTLSFingerprint(final boolean requireActPass) {
|
2020-04-10 05:45:23 +00:00
|
|
|
if (this.contents.size() == 0) {
|
|
|
|
throw new IllegalStateException("No contents available");
|
|
|
|
}
|
|
|
|
for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
|
|
|
|
final IceUdpTransportInfo transport = entry.getValue().transport;
|
|
|
|
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
|
2022-02-25 16:26:36 +00:00
|
|
|
if (fingerprint == null
|
|
|
|
|| Strings.isNullOrEmpty(fingerprint.getContent())
|
|
|
|
|| Strings.isNullOrEmpty(fingerprint.getHash())) {
|
|
|
|
throw new SecurityException(
|
|
|
|
String.format(
|
|
|
|
"Use of DTLS-SRTP (XEP-0320) is required for content %s",
|
|
|
|
entry.getKey()));
|
2020-04-10 05:45:23 +00:00
|
|
|
}
|
2021-11-15 20:49:31 +00:00
|
|
|
final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
|
|
|
|
if (setup == null) {
|
2022-02-25 16:26:36 +00:00
|
|
|
throw new SecurityException(
|
|
|
|
String.format(
|
|
|
|
"Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute",
|
|
|
|
entry.getKey()));
|
2020-04-29 13:54:02 +00:00
|
|
|
}
|
2021-11-17 09:49:16 +00:00
|
|
|
if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
|
2022-02-25 16:26:36 +00:00
|
|
|
throw new SecurityException(
|
|
|
|
"Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
|
2021-11-17 09:49:16 +00:00
|
|
|
}
|
2020-04-10 05:45:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-29 13:54:02 +00:00
|
|
|
JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
|
2020-04-05 08:20:34 +00:00
|
|
|
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 Content content = new Content(Content.Creator.INITIATOR, entry.getKey());
|
2020-04-05 11:58:05 +00:00
|
|
|
if (entry.getValue().description != null) {
|
|
|
|
content.addChild(entry.getValue().description);
|
|
|
|
}
|
2020-04-05 08:20:34 +00:00
|
|
|
content.addChild(entry.getValue().transport);
|
|
|
|
jinglePacket.addJingleContent(content);
|
|
|
|
}
|
|
|
|
return jinglePacket;
|
|
|
|
}
|
|
|
|
|
2022-02-25 16:26:36 +00:00
|
|
|
RtpContentMap transportInfo(
|
|
|
|
final String contentName, final IceUdpTransportInfo.Candidate candidate) {
|
2020-04-10 05:45:23 +00:00
|
|
|
final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
|
2022-02-25 16:26:36 +00:00
|
|
|
final IceUdpTransportInfo transportInfo =
|
|
|
|
descriptionTransport == null ? null : descriptionTransport.transport;
|
2020-04-05 11:58:05 +00:00
|
|
|
if (transportInfo == null) {
|
2022-02-25 16:26:36 +00:00
|
|
|
throw new IllegalArgumentException(
|
|
|
|
"Unable to find transport info for content name " + contentName);
|
2020-04-05 11:58:05 +00:00
|
|
|
}
|
|
|
|
final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
|
|
|
|
newTransportInfo.addChild(candidate);
|
2022-02-25 16:26:36 +00:00
|
|
|
return new RtpContentMap(
|
|
|
|
null,
|
|
|
|
ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
|
2021-11-14 17:22:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
RtpContentMap transportInfo() {
|
|
|
|
return new RtpContentMap(
|
|
|
|
null,
|
2022-02-25 16:26:36 +00:00
|
|
|
Maps.transformValues(
|
|
|
|
contents,
|
|
|
|
dt -> new DescriptionTransport(null, dt.transport.cloneWrapper())));
|
2021-11-14 17:22:18 +00:00
|
|
|
}
|
|
|
|
|
2022-02-25 16:26:36 +00:00
|
|
|
public IceUdpTransportInfo.Credentials getDistinctCredentials() {
|
|
|
|
final Set<IceUdpTransportInfo.Credentials> allCredentials = getCredentials();
|
|
|
|
final IceUdpTransportInfo.Credentials credentials =
|
|
|
|
Iterables.getFirst(allCredentials, null);
|
2021-11-16 16:08:34 +00:00
|
|
|
if (allCredentials.size() == 1 && credentials != null) {
|
|
|
|
return credentials;
|
|
|
|
}
|
|
|
|
throw new IllegalStateException("Content map does not have distinct credentials");
|
2021-11-14 17:22:18 +00:00
|
|
|
}
|
2020-04-05 11:58:05 +00:00
|
|
|
|
2022-02-25 16:26:36 +00:00
|
|
|
public Set<IceUdpTransportInfo.Credentials> getCredentials() {
|
|
|
|
final Set<IceUdpTransportInfo.Credentials> credentials =
|
|
|
|
ImmutableSet.copyOf(
|
|
|
|
Collections2.transform(
|
|
|
|
contents.values(), dt -> dt.transport.getCredentials()));
|
|
|
|
if (credentials.isEmpty()) {
|
|
|
|
throw new IllegalStateException("Content map does not have any credentials");
|
|
|
|
}
|
|
|
|
return credentials;
|
|
|
|
}
|
|
|
|
|
|
|
|
public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
|
|
|
|
final DescriptionTransport descriptionTransport = this.contents.get(contentName);
|
|
|
|
if (descriptionTransport == null) {
|
|
|
|
throw new IllegalArgumentException(
|
|
|
|
String.format(
|
|
|
|
"Unable to find transport info for content name %s", contentName));
|
|
|
|
}
|
|
|
|
return descriptionTransport.transport.getCredentials();
|
|
|
|
}
|
|
|
|
|
2021-11-15 20:49:31 +00:00
|
|
|
public IceUdpTransportInfo.Setup getDtlsSetup() {
|
2022-02-25 16:26:36 +00:00
|
|
|
final Set<IceUdpTransportInfo.Setup> setups =
|
|
|
|
ImmutableSet.copyOf(
|
|
|
|
Collections2.transform(
|
|
|
|
contents.values(), dt -> dt.transport.getFingerprint().getSetup()));
|
2021-11-16 10:21:11 +00:00
|
|
|
final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
|
|
|
|
if (setups.size() == 1 && setup != null) {
|
|
|
|
return setup;
|
|
|
|
}
|
|
|
|
throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
|
2021-11-15 20:49:31 +00:00
|
|
|
}
|
|
|
|
|
2021-11-14 17:22:18 +00:00
|
|
|
public boolean emptyCandidates() {
|
|
|
|
int count = 0;
|
|
|
|
for (DescriptionTransport descriptionTransport : contents.values()) {
|
|
|
|
count += descriptionTransport.transport.getCandidates().size();
|
|
|
|
}
|
|
|
|
return count == 0;
|
|
|
|
}
|
|
|
|
|
2022-02-25 16:26:36 +00:00
|
|
|
public RtpContentMap modifiedCredentials(
|
|
|
|
IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
|
|
|
|
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
|
|
|
|
new ImmutableMap.Builder<>();
|
2021-11-14 17:22:18 +00:00
|
|
|
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
|
|
|
|
final RtpDescription rtpDescription = content.getValue().description;
|
|
|
|
IceUdpTransportInfo transportInfo = content.getValue().transport;
|
2022-02-25 16:26:36 +00:00
|
|
|
final IceUdpTransportInfo modifiedTransportInfo =
|
|
|
|
transportInfo.modifyCredentials(credentials, setup);
|
|
|
|
contentMapBuilder.put(
|
|
|
|
content.getKey(),
|
|
|
|
new DescriptionTransport(rtpDescription, modifiedTransportInfo));
|
2021-11-14 17:22:18 +00:00
|
|
|
}
|
|
|
|
return new RtpContentMap(this.group, contentMapBuilder.build());
|
2020-04-05 11:58:05 +00:00
|
|
|
}
|
|
|
|
|
2020-04-05 08:20:34 +00:00
|
|
|
public static class DescriptionTransport {
|
|
|
|
public final RtpDescription description;
|
|
|
|
public final IceUdpTransportInfo transport;
|
|
|
|
|
2022-02-25 16:26:36 +00:00
|
|
|
public DescriptionTransport(
|
|
|
|
final RtpDescription description, final IceUdpTransportInfo transport) {
|
2020-04-05 08:20:34 +00:00
|
|
|
this.description = description;
|
|
|
|
this.transport = transport;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static DescriptionTransport of(final Content content) {
|
|
|
|
final GenericDescription description = content.getDescription();
|
|
|
|
final GenericTransportInfo transportInfo = content.getTransport();
|
|
|
|
final RtpDescription rtpDescription;
|
|
|
|
final IceUdpTransportInfo iceUdpTransportInfo;
|
2020-04-06 08:26:29 +00:00
|
|
|
if (description == null) {
|
|
|
|
rtpDescription = null;
|
|
|
|
} else if (description instanceof RtpDescription) {
|
2020-04-05 08:20:34 +00:00
|
|
|
rtpDescription = (RtpDescription) description;
|
|
|
|
} else {
|
2022-02-25 16:26:36 +00:00
|
|
|
throw new UnsupportedApplicationException(
|
|
|
|
"Content does not contain rtp description");
|
2020-04-05 08:20:34 +00:00
|
|
|
}
|
|
|
|
if (transportInfo instanceof IceUdpTransportInfo) {
|
|
|
|
iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
|
|
|
|
} else {
|
2022-02-25 16:26:36 +00:00
|
|
|
throw new UnsupportedTransportException(
|
|
|
|
"Content does not contain ICE-UDP transport");
|
2020-04-05 08:20:34 +00:00
|
|
|
}
|
2021-03-02 20:13:49 +00:00
|
|
|
return new DescriptionTransport(
|
2022-02-25 16:26:36 +00:00
|
|
|
rtpDescription, OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
|
2020-04-05 08:20:34 +00:00
|
|
|
}
|
|
|
|
|
2022-02-25 16:26:36 +00:00
|
|
|
public static DescriptionTransport of(
|
|
|
|
final SessionDescription sessionDescription, final SessionDescription.Media media) {
|
2021-03-16 14:52:51 +00:00
|
|
|
final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
|
2022-02-25 16:26:36 +00:00
|
|
|
final IceUdpTransportInfo transportInfo =
|
|
|
|
IceUdpTransportInfo.of(sessionDescription, media);
|
2020-04-05 08:20:34 +00:00
|
|
|
return new DescriptionTransport(rtpDescription, transportInfo);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
|
2022-02-25 16:26:36 +00:00
|
|
|
return ImmutableMap.copyOf(
|
|
|
|
Maps.transformValues(
|
|
|
|
contents,
|
|
|
|
new Function<Content, DescriptionTransport>() {
|
|
|
|
@NullableDecl
|
|
|
|
@Override
|
|
|
|
public DescriptionTransport apply(@NullableDecl Content content) {
|
|
|
|
return content == null ? null : of(content);
|
|
|
|
}
|
|
|
|
}));
|
2020-04-05 08:20:34 +00:00
|
|
|
}
|
|
|
|
}
|
2020-04-16 06:20:13 +00:00
|
|
|
|
|
|
|
public static class UnsupportedApplicationException extends IllegalArgumentException {
|
|
|
|
UnsupportedApplicationException(String message) {
|
|
|
|
super(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static class UnsupportedTransportException extends IllegalArgumentException {
|
|
|
|
UnsupportedTransportException(String message) {
|
|
|
|
super(message);
|
|
|
|
}
|
|
|
|
}
|
2020-04-05 08:20:34 +00:00
|
|
|
}
|