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