1package eu.siacs.conversations.xmpp.jingle.stanzas;
2
3import android.util.Log;
4
5import androidx.annotation.NonNull;
6
7import com.google.common.base.Preconditions;
8import com.google.common.base.Strings;
9import com.google.common.collect.ImmutableSet;
10
11import eu.siacs.conversations.Config;
12import eu.siacs.conversations.crypto.axolotl.AxolotlService;
13import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
14import eu.siacs.conversations.xml.Element;
15import eu.siacs.conversations.xml.Namespace;
16import eu.siacs.conversations.xmpp.Jid;
17import eu.siacs.conversations.xmpp.jingle.SessionDescription;
18
19import java.util.Locale;
20import java.util.Set;
21
22public class Content extends Element {
23
24 public Content(final Creator creator, final Senders senders, final String name) {
25 super("content", Namespace.JINGLE);
26 this.setAttribute("creator", creator.toString());
27 this.setAttribute("name", name);
28 this.setSenders(senders);
29 }
30
31 private Content() {
32 super("content", Namespace.JINGLE);
33 }
34
35 public static Content upgrade(final Element element) {
36 Preconditions.checkArgument("content".equals(element.getName()));
37 final Content content = new Content();
38 content.setAttributes(element.getAttributes());
39 content.setChildren(element.getChildren());
40 return content;
41 }
42
43 public String getContentName() {
44 return this.getAttribute("name");
45 }
46
47 public Creator getCreator() {
48 return Creator.of(getAttribute("creator"));
49 }
50
51 public Senders getSenders() {
52 final String attribute = getAttribute("senders");
53 if (Strings.isNullOrEmpty(attribute)) {
54 return Senders.BOTH;
55 }
56 return Senders.of(getAttribute("senders"));
57 }
58
59 public void setSenders(final Senders senders) {
60 if (senders != null && senders != Senders.BOTH) {
61 this.setAttribute("senders", senders.toString());
62 }
63 }
64
65 public GenericDescription getDescription() {
66 final Element description = this.findChild("description");
67 if (description == null) {
68 return null;
69 }
70 final String namespace = description.getNamespace();
71 if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) {
72 return FileTransferDescription.upgrade(description);
73 } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
74 return RtpDescription.upgrade(description);
75 } else {
76 return GenericDescription.upgrade(description);
77 }
78 }
79
80 public void setDescription(final GenericDescription description) {
81 Preconditions.checkNotNull(description);
82 this.addChild(description);
83 }
84
85 public String getDescriptionNamespace() {
86 final Element description = this.findChild("description");
87 return description == null ? null : description.getNamespace();
88 }
89
90 public GenericTransportInfo getTransport() {
91 final Element transport = this.findChild("transport");
92 final String namespace = transport == null ? null : transport.getNamespace();
93 if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) {
94 return IbbTransportInfo.upgrade(transport);
95 } else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) {
96 return SocksByteStreamsTransportInfo.upgrade(transport);
97 } else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
98 return IceUdpTransportInfo.upgrade(transport);
99 } else if (Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL.equals(namespace)) {
100 return WebRTCDataChannelTransportInfo.upgrade(transport);
101 } else if (transport != null) {
102 return GenericTransportInfo.upgrade(transport);
103 } else {
104 return null;
105 }
106 }
107
108 public void setSecurity(final XmppAxolotlMessage xmppAxolotlMessage) {
109 final String contentName = this.getContentName();
110 final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
111 security.setAttribute("name", contentName);
112 security.setAttribute("cipher", "urn:xmpp:ciphers:aes-128-gcm-nopadding");
113 security.setAttribute("type", AxolotlService.PEP_PREFIX);
114 security.addChild(xmppAxolotlMessage.toElement());
115 this.addChild(security);
116 }
117
118 public XmppAxolotlMessage getSecurity(final Jid from) {
119 final String contentName = this.getContentName();
120 for (final Element child : getChildren()) {
121 if ("security".equals(child.getName())
122 && Namespace.JINGLE_ENCRYPTED_TRANSPORT.equals(child.getNamespace())) {
123 final String name = child.getAttribute("name");
124 final String type = child.getAttribute("type");
125 final String cipher = child.getAttribute("cipher");
126 if (contentName.equals(name)
127 && AxolotlService.PEP_PREFIX.equals(type)
128 && "urn:xmpp:ciphers:aes-128-gcm-nopadding".equals(cipher)) {
129 final var encrypted = child.findChild("encrypted", AxolotlService.PEP_PREFIX);
130 if (encrypted != null) {
131 return XmppAxolotlMessage.fromElement(encrypted, from.asBareJid());
132 }
133 }
134 }
135 }
136 return null;
137 }
138
139 public void setTransport(GenericTransportInfo transportInfo) {
140 this.addChild(transportInfo);
141 }
142
143 public enum Creator {
144 INITIATOR,
145 RESPONDER;
146
147 public static Creator of(final String value) {
148 return Creator.valueOf(value.toUpperCase(Locale.ROOT));
149 }
150
151 @Override
152 @NonNull
153 public String toString() {
154 return super.toString().toLowerCase(Locale.ROOT);
155 }
156 }
157
158 public enum Senders {
159 BOTH,
160 INITIATOR,
161 NONE,
162 RESPONDER;
163
164 public static Senders of(final String value) {
165 return Senders.valueOf(value.toUpperCase(Locale.ROOT));
166 }
167
168 public static Senders of(final SessionDescription.Media media, final boolean initiator) {
169 final Set<String> attributes = media.attributes.keySet();
170 if (attributes.contains("sendrecv")) {
171 return BOTH;
172 } else if (attributes.contains("inactive")) {
173 return NONE;
174 } else if (attributes.contains("sendonly")) {
175 return initiator ? INITIATOR : RESPONDER;
176 } else if (attributes.contains("recvonly")) {
177 return initiator ? RESPONDER : INITIATOR;
178 }
179 Log.w(Config.LOGTAG, "assuming default value for senders");
180 // If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is
181 // present, "sendrecv" SHOULD be assumed as the default
182 // https://www.rfc-editor.org/rfc/rfc4566
183 return BOTH;
184 }
185
186 public static Set<Senders> receiveOnly(final boolean initiator) {
187 return ImmutableSet.of(initiator ? RESPONDER : INITIATOR);
188 }
189
190 @Override
191 @NonNull
192 public String toString() {
193 return super.toString().toLowerCase(Locale.ROOT);
194 }
195
196 public String asMediaAttribute(final boolean initiator) {
197 final boolean responder = !initiator;
198 if (this == Content.Senders.BOTH) {
199 return "sendrecv";
200 } else if (this == Content.Senders.NONE) {
201 return "inactive";
202 } else if ((initiator && this == Content.Senders.INITIATOR)
203 || (responder && this == Content.Senders.RESPONDER)) {
204 return "sendonly";
205 } else if ((initiator && this == Content.Senders.RESPONDER)
206 || (responder && this == Content.Senders.INITIATOR)) {
207 return "recvonly";
208 } else {
209 throw new IllegalStateException(
210 String.format(
211 "illegal combination of initiator=%s and %s", initiator, this));
212 }
213 }
214 }
215}