XmppUri.java

  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}