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