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