1package eu.siacs.conversations.xmpp.jingle.stanzas;
2
3import androidx.annotation.NonNull;
4
5import com.google.common.base.Joiner;
6import com.google.common.base.MoreObjects;
7import com.google.common.base.Objects;
8import com.google.common.base.Preconditions;
9import com.google.common.base.Strings;
10import com.google.common.collect.ArrayListMultimap;
11import com.google.common.collect.Collections2;
12import com.google.common.collect.ImmutableList;
13import com.google.common.collect.Iterables;
14
15import java.util.ArrayList;
16import java.util.Collection;
17import java.util.HashMap;
18import java.util.Hashtable;
19import java.util.LinkedHashMap;
20import java.util.List;
21import java.util.Locale;
22import java.util.Map;
23import java.util.UUID;
24
25import eu.siacs.conversations.xml.Element;
26import eu.siacs.conversations.xml.Namespace;
27import eu.siacs.conversations.xmpp.jingle.SessionDescription;
28
29public class IceUdpTransportInfo extends GenericTransportInfo {
30
31 public IceUdpTransportInfo() {
32 super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP);
33 }
34
35 public static IceUdpTransportInfo upgrade(final Element element) {
36 Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport");
37 Preconditions.checkArgument(Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()), "Element does not match ice-udp transport namespace");
38 final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
39 transportInfo.setAttributes(element.getAttributes());
40 transportInfo.setChildren(element.getChildren());
41 return transportInfo;
42 }
43
44 public static IceUdpTransportInfo of(SessionDescription sessionDescription, SessionDescription.Media media) {
45 final String ufrag = Iterables.getFirst(media.attributes.get("ice-ufrag"), null);
46 final String pwd = Iterables.getFirst(media.attributes.get("ice-pwd"), null);
47 IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo();
48 if (ufrag != null) {
49 iceUdpTransportInfo.setAttribute("ufrag", ufrag);
50 }
51 if (pwd != null) {
52 iceUdpTransportInfo.setAttribute("pwd", pwd);
53 }
54 final Fingerprint fingerprint = Fingerprint.of(sessionDescription, media);
55 if (fingerprint != null) {
56 iceUdpTransportInfo.addChild(fingerprint);
57 }
58 return iceUdpTransportInfo;
59
60 }
61
62 public Fingerprint getFingerprint() {
63 final Element fingerprint = this.findChild("fingerprint", Namespace.JINGLE_APPS_DTLS);
64 return fingerprint == null ? null : Fingerprint.upgrade(fingerprint);
65 }
66
67 public Credentials getCredentials() {
68 final String ufrag = this.getAttribute("ufrag");
69 final String password = this.getAttribute("pwd");
70 return new Credentials(ufrag, password);
71 }
72
73 public List<Candidate> getCandidates() {
74 final ImmutableList.Builder<Candidate> builder = new ImmutableList.Builder<>();
75 for (final Element child : getChildren()) {
76 if ("candidate".equals(child.getName())) {
77 builder.add(Candidate.upgrade(child));
78 }
79 }
80 return builder.build();
81 }
82
83 public IceUdpTransportInfo cloneWrapper() {
84 final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
85 transportInfo.setAttributes(new Hashtable<>(getAttributes()));
86 return transportInfo;
87 }
88
89 public IceUdpTransportInfo modifyCredentials(final Credentials credentials, final Setup setup) {
90 final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
91 transportInfo.setAttribute("ufrag", credentials.ufrag);
92 transportInfo.setAttribute("pwd", credentials.password);
93 for (final Element child : getChildren()) {
94 if (child.getName().equals("fingerprint") && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) {
95 final Fingerprint fingerprint = new Fingerprint();
96 fingerprint.setAttributes(new Hashtable<>(child.getAttributes()));
97 fingerprint.setContent(child.getContent());
98 fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT));
99 transportInfo.addChild(fingerprint);
100 }
101 }
102 return transportInfo;
103 }
104
105 public static class Credentials {
106 public final String ufrag;
107 public final String password;
108
109 public Credentials(String ufrag, String password) {
110 this.ufrag = ufrag;
111 this.password = password;
112 }
113
114 @Override
115 public boolean equals(Object o) {
116 if (this == o) return true;
117 if (o == null || getClass() != o.getClass()) return false;
118 Credentials that = (Credentials) o;
119 return Objects.equal(ufrag, that.ufrag) && Objects.equal(password, that.password);
120 }
121
122 @Override
123 public int hashCode() {
124 return Objects.hashCode(ufrag, password);
125 }
126
127 @Override
128 @NonNull
129 public String toString() {
130 return MoreObjects.toStringHelper(this)
131 .add("ufrag", ufrag)
132 .add("password", password)
133 .toString();
134 }
135 }
136
137 public static class Candidate extends Element {
138
139 private Candidate() {
140 super("candidate");
141 }
142
143 public static Candidate upgrade(final Element element) {
144 Preconditions.checkArgument("candidate".equals(element.getName()));
145 final Candidate candidate = new Candidate();
146 candidate.setAttributes(element.getAttributes());
147 candidate.setChildren(element.getChildren());
148 return candidate;
149 }
150
151 // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1
152 public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) {
153 final String[] pair = attribute.split(":", 2);
154 if (pair.length == 2 && "candidate".equals(pair[0])) {
155 final String[] segments = pair[1].split(" ");
156 if (segments.length >= 6) {
157 final String id = UUID.randomUUID().toString();
158 final String foundation = segments[0];
159 final String component = segments[1];
160 final String transport = segments[2].toLowerCase(Locale.ROOT);
161 final String priority = segments[3];
162 final String connectionAddress = segments[4];
163 final String port = segments[5];
164 final HashMap<String, String> additional = new HashMap<>();
165 for (int i = 6; i < segments.length - 1; i = i + 2) {
166 additional.put(segments[i], segments[i + 1]);
167 }
168 final String ufrag = additional.get("ufrag");
169 if (ufrag != null && !ufrag.equals(currentUfrag)) {
170 return null;
171 }
172 final Candidate candidate = new Candidate();
173 candidate.setAttribute("component", component);
174 candidate.setAttribute("foundation", foundation);
175 candidate.setAttribute("generation", additional.get("generation"));
176 candidate.setAttribute("rel-addr", additional.get("raddr"));
177 candidate.setAttribute("rel-port", additional.get("rport"));
178 candidate.setAttribute("id", id);
179 candidate.setAttribute("ip", connectionAddress);
180 candidate.setAttribute("port", port);
181 candidate.setAttribute("priority", priority);
182 candidate.setAttribute("protocol", transport);
183 candidate.setAttribute("type", additional.get("typ"));
184 return candidate;
185 }
186 }
187 return null;
188 }
189
190 public int getComponent() {
191 return getAttributeAsInt("component");
192 }
193
194 public int getFoundation() {
195 return getAttributeAsInt("foundation");
196 }
197
198 public int getGeneration() {
199 return getAttributeAsInt("generation");
200 }
201
202 public String getId() {
203 return getAttribute("id");
204 }
205
206 public String getIp() {
207 return getAttribute("ip");
208 }
209
210 public int getNetwork() {
211 return getAttributeAsInt("network");
212 }
213
214 public int getPort() {
215 return getAttributeAsInt("port");
216 }
217
218 public int getPriority() {
219 return getAttributeAsInt("priority");
220 }
221
222 public String getProtocol() {
223 return getAttribute("protocol");
224 }
225
226 public String getRelAddr() {
227 return getAttribute("rel-addr");
228 }
229
230 public int getRelPort() {
231 return getAttributeAsInt("rel-port");
232 }
233
234 public String getType() { //TODO might be converted to enum
235 return getAttribute("type");
236 }
237
238 private int getAttributeAsInt(final String name) {
239 final String value = this.getAttribute(name);
240 if (value == null) {
241 return 0;
242 }
243 try {
244 return Integer.parseInt(value);
245 } catch (NumberFormatException e) {
246 return 0;
247 }
248 }
249
250 public String toSdpAttribute(final String ufrag) {
251 final String foundation = this.getAttribute("foundation");
252 checkNotNullNoWhitespace(foundation, "foundation");
253 final String component = this.getAttribute("component");
254 checkNotNullNoWhitespace(component, "component");
255 final String protocol = this.getAttribute("protocol");
256 checkNotNullNoWhitespace(protocol, "protocol");
257 final String transport = protocol.toLowerCase(Locale.ROOT);
258 if (!"udp".equals(transport)) {
259 throw new IllegalArgumentException(String.format("'%s' is not a supported protocol", transport));
260 }
261 final String priority = this.getAttribute("priority");
262 checkNotNullNoWhitespace(priority, "priority");
263 final String connectionAddress = this.getAttribute("ip");
264 checkNotNullNoWhitespace(connectionAddress, "ip");
265 final String port = this.getAttribute("port");
266 checkNotNullNoWhitespace(port, "port");
267 final Map<String, String> additionalParameter = new LinkedHashMap<>();
268 final String relAddr = this.getAttribute("rel-addr");
269 final String type = this.getAttribute("type");
270 if (type != null) {
271 additionalParameter.put("typ", type);
272 }
273 if (relAddr != null) {
274 additionalParameter.put("raddr", relAddr);
275 }
276 final String relPort = this.getAttribute("rel-port");
277 if (relPort != null) {
278 additionalParameter.put("rport", relPort);
279 }
280 final String generation = this.getAttribute("generation");
281 if (generation != null) {
282 additionalParameter.put("generation", generation);
283 }
284 if (ufrag != null) {
285 additionalParameter.put("ufrag", ufrag);
286 }
287 final String parametersString = Joiner.on(' ').join(Collections2.transform(additionalParameter.entrySet(), input -> String.format("%s %s", input.getKey(), input.getValue())));
288 return String.format(
289 "candidate:%s %s %s %s %s %s %s",
290 foundation,
291 component,
292 transport,
293 priority,
294 connectionAddress,
295 port,
296 parametersString
297
298 );
299 }
300 }
301
302 private static void checkNotNullNoWhitespace(final String value, final String name) {
303 if (Strings.isNullOrEmpty(value)) {
304 throw new IllegalArgumentException(String.format("Parameter %s is missing or empty", name));
305 }
306 SessionDescription.checkNoWhitespace(value, String.format("Parameter %s contains white spaces", name));
307 }
308
309
310 public static class Fingerprint extends Element {
311
312 private Fingerprint() {
313 super("fingerprint", Namespace.JINGLE_APPS_DTLS);
314 }
315
316 public static Fingerprint upgrade(final Element element) {
317 Preconditions.checkArgument("fingerprint".equals(element.getName()));
318 Preconditions.checkArgument(Namespace.JINGLE_APPS_DTLS.equals(element.getNamespace()));
319 final Fingerprint fingerprint = new Fingerprint();
320 fingerprint.setAttributes(element.getAttributes());
321 fingerprint.setContent(element.getContent());
322 return fingerprint;
323 }
324
325 private static Fingerprint of(ArrayListMultimap<String, String> attributes) {
326 final String fingerprint = Iterables.getFirst(attributes.get("fingerprint"), null);
327 final String setup = Iterables.getFirst(attributes.get("setup"), null);
328 if (setup != null && fingerprint != null) {
329 final String[] fingerprintParts = fingerprint.split(" ", 2);
330 if (fingerprintParts.length == 2) {
331 final String hash = fingerprintParts[0];
332 final String actualFingerprint = fingerprintParts[1];
333 final Fingerprint element = new Fingerprint();
334 element.setAttribute("hash", hash);
335 element.setAttribute("setup", setup);
336 element.setContent(actualFingerprint);
337 return element;
338 }
339 }
340 return null;
341 }
342
343 public static Fingerprint of(final SessionDescription sessionDescription, final SessionDescription.Media media) {
344 final Fingerprint fingerprint = of(media.attributes);
345 return fingerprint == null ? of(sessionDescription.attributes) : fingerprint;
346 }
347
348 public String getHash() {
349 return this.getAttribute("hash");
350 }
351
352 public Setup getSetup() {
353 final String setup = this.getAttribute("setup");
354 return setup == null ? null : Setup.of(setup);
355 }
356 }
357
358 public enum Setup {
359 ACTPASS, PASSIVE, ACTIVE;
360
361 public static Setup of(String setup) {
362 try {
363 return valueOf(setup.toUpperCase(Locale.ROOT));
364 } catch (IllegalArgumentException e) {
365 return null;
366 }
367 }
368
369 public Setup flip() {
370 if (this == PASSIVE) {
371 return ACTIVE;
372 }
373 if (this == ACTIVE) {
374 return PASSIVE;
375 }
376 throw new IllegalStateException(this.name()+" can not be flipped");
377 }
378 }
379}