1package eu.siacs.conversations.utils;
2
3import android.net.Uri;
4import android.support.annotation.NonNull;
5
6import com.google.common.collect.ImmutableList;
7import com.google.common.collect.ImmutableMap;
8
9import java.io.UnsupportedEncodingException;
10import java.net.URLDecoder;
11import java.util.ArrayList;
12import java.util.Collections;
13import java.util.List;
14import java.util.Locale;
15import java.util.Map;
16
17import eu.siacs.conversations.xmpp.Jid;
18
19public class XmppUri {
20
21 public static final String ACTION_JOIN = "join";
22 public static final String ACTION_MESSAGE = "message";
23 public static final String ACTION_REGISTER = "register";
24 public static final String ACTION_ROSTER = "roster";
25 public static final String PARAMETER_PRE_AUTH = "preauth";
26 public static final String PARAMETER_IBR = "ibr";
27 private static final String OMEMO_URI_PARAM = "omemo-sid-";
28 protected Uri uri;
29 protected String jid;
30 private List<Fingerprint> fingerprints = new ArrayList<>();
31 private Map<String, String> parameters = Collections.emptyMap();
32 private boolean safeSource = true;
33
34 public XmppUri(String uri) {
35 try {
36 parse(Uri.parse(uri));
37 } catch (IllegalArgumentException e) {
38 try {
39 jid = Jid.ofEscaped(uri).asBareJid().toEscapedString();
40 } catch (IllegalArgumentException e2) {
41 jid = null;
42 }
43 }
44 }
45
46 public XmppUri(Uri uri) {
47 parse(uri);
48 }
49
50 public XmppUri(Uri uri, boolean safeSource) {
51 this.safeSource = safeSource;
52 parse(uri);
53 }
54
55 private static Map<String, String> parseParameters(final String query, final char seperator) {
56 final ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
57 final String[] pairs = query == null ? new String[0] : query.split(String.valueOf(seperator));
58 for (String pair : pairs) {
59 final String[] parts = pair.split("=", 2);
60 if (parts.length == 0) {
61 continue;
62 }
63 final String key = parts[0].toLowerCase(Locale.US);
64 final String value;
65 if (parts.length == 2) {
66 String decoded;
67 try {
68 decoded = URLDecoder.decode(parts[1], "UTF-8");
69 } catch (UnsupportedEncodingException e) {
70 decoded = "";
71 }
72 value = decoded;
73 } else {
74 value = "";
75 }
76 builder.put(key, value);
77 }
78 return builder.build();
79 }
80
81 private static List<Fingerprint> parseFingerprints(Map<String, String> parameters) {
82 ImmutableList.Builder<Fingerprint> builder = new ImmutableList.Builder<>();
83 for (Map.Entry<String, String> parameter : parameters.entrySet()) {
84 final String key = parameter.getKey();
85 final String value = parameter.getValue().toLowerCase(Locale.US);
86 if (key.startsWith(OMEMO_URI_PARAM)) {
87 try {
88 final int id = Integer.parseInt(key.substring(OMEMO_URI_PARAM.length()));
89 builder.add(new Fingerprint(FingerprintType.OMEMO, value, id));
90 } catch (Exception e) {
91 //ignoring invalid device id
92 }
93 } else if ("omemo".equals(key)) {
94 builder.add(new Fingerprint(FingerprintType.OMEMO, value, 0));
95 }
96 }
97 return builder.build();
98 }
99
100 public static String getFingerprintUri(final String base, final List<XmppUri.Fingerprint> fingerprints, char separator) {
101 final StringBuilder builder = new StringBuilder(base);
102 builder.append('?');
103 for (int i = 0; i < fingerprints.size(); ++i) {
104 XmppUri.FingerprintType type = fingerprints.get(i).type;
105 if (type == XmppUri.FingerprintType.OMEMO) {
106 builder.append(XmppUri.OMEMO_URI_PARAM);
107 builder.append(fingerprints.get(i).deviceId);
108 }
109 builder.append('=');
110 builder.append(fingerprints.get(i).fingerprint);
111 if (i != fingerprints.size() - 1) {
112 builder.append(separator);
113 }
114 }
115 return builder.toString();
116 }
117
118 private static String lameUrlDecode(String url) {
119 return url.replace("%23", "#").replace("%25", "%");
120 }
121
122 public static String lameUrlEncode(String url) {
123 return url.replace("%", "%25").replace("#", "%23");
124 }
125
126 public boolean isSafeSource() {
127 return safeSource;
128 }
129
130 protected void parse(final Uri uri) {
131 if (uri == null) {
132 return;
133 }
134 this.uri = uri;
135 String scheme = uri.getScheme();
136 String host = uri.getHost();
137 List<String> segments = uri.getPathSegments();
138 if ("https".equalsIgnoreCase(scheme) && "conversations.im".equalsIgnoreCase(host)) {
139 if (segments.size() >= 2 && segments.get(1).contains("@")) {
140 // sample : https://conversations.im/i/foo@bar.com
141 try {
142 jid = Jid.ofEscaped(lameUrlDecode(segments.get(1))).toEscapedString();
143 } catch (Exception e) {
144 jid = null;
145 }
146 } else if (segments.size() >= 3) {
147 // sample : https://conversations.im/i/foo/bar.com
148 jid = segments.get(1) + "@" + segments.get(2);
149 }
150 if (segments.size() > 1 && "j".equalsIgnoreCase(segments.get(0))) {
151 this.parameters = ImmutableMap.of(ACTION_JOIN, "");
152 }
153 final Map<String, String> parameters = parseParameters(uri.getQuery(), '&');
154 this.fingerprints = parseFingerprints(parameters);
155 } else if ("xmpp".equalsIgnoreCase(scheme)) {
156 // sample: xmpp:foo@bar.com
157 this.parameters = parseParameters(uri.getQuery(), ';');
158 if (uri.getAuthority() != null) {
159 jid = uri.getAuthority();
160 } else {
161 final String[] parts = uri.getSchemeSpecificPart().split("\\?");
162 if (parts.length > 0) {
163 jid = parts[0];
164 } else {
165 return;
166 }
167 }
168 this.fingerprints = parseFingerprints(parameters);
169 } else if ("imto".equalsIgnoreCase(scheme)) {
170 // sample: imto://xmpp/foo@bar.com
171 try {
172 jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1].trim();
173 } catch (final UnsupportedEncodingException ignored) {
174 jid = null;
175 }
176 } else {
177 try {
178 jid = Jid.ofEscaped(uri.toString()).asBareJid().toEscapedString();
179 } catch (final IllegalArgumentException ignored) {
180 jid = null;
181 }
182 }
183 }
184
185 @Override
186 @NonNull
187 public String toString() {
188 if (uri != null) {
189 return uri.toString();
190 }
191 return "";
192 }
193
194 public boolean isAction(final String action) {
195 return parameters.containsKey(action);
196 }
197
198 public Jid getJid() {
199 try {
200 return this.jid == null ? null : Jid.ofEscaped(this.jid);
201 } catch (IllegalArgumentException e) {
202 return null;
203 }
204 }
205
206 public boolean isValidJid() {
207 if (jid == null) {
208 return false;
209 }
210 try {
211 Jid.ofEscaped(jid);
212 return true;
213 } catch (IllegalArgumentException e) {
214 return false;
215 }
216 }
217
218 public String getBody() {
219 return parameters.get("body");
220 }
221
222 public String getName() {
223 return parameters.get("name");
224 }
225
226 public String getParameter(String key) {
227 return this.parameters.get(key);
228 }
229
230 public List<Fingerprint> getFingerprints() {
231 return this.fingerprints;
232 }
233
234 public boolean hasFingerprints() {
235 return fingerprints.size() > 0;
236 }
237
238 public enum FingerprintType {
239 OMEMO
240 }
241
242 public static class Fingerprint {
243 public final FingerprintType type;
244 public final String fingerprint;
245 final int deviceId;
246
247 public Fingerprint(FingerprintType type, String fingerprint, int deviceId) {
248 this.type = type;
249 this.fingerprint = fingerprint;
250 this.deviceId = deviceId;
251 }
252
253 @NonNull
254 @Override
255 public String toString() {
256 return type.toString() + ": " + fingerprint + (deviceId != 0 ? " " + deviceId : "");
257 }
258 }
259}