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