put socks5 proxy discovery into manager

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/xml/Namespace.java                                    |   1 
src/main/java/eu/siacs/conversations/xmpp/Managers.java                                    |   4 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                              |   9 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java         |   4 
src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java |  76 
src/main/java/eu/siacs/conversations/xmpp/manager/RegistrationManager.java                 | 147 
src/main/java/eu/siacs/conversations/xmpp/manager/StreamHostManager.java                   |  68 
src/main/java/im/conversations/android/xmpp/model/register/Register.java                   |   5 
src/main/java/im/conversations/android/xmpp/model/socks5/Activate.java                     |  18 
src/main/java/im/conversations/android/xmpp/model/socks5/Query.java                        |  20 
src/main/java/im/conversations/android/xmpp/model/socks5/StreamHost.java                   |  25 
src/main/java/im/conversations/android/xmpp/model/socks5/package-info.java                 |   5 
12 files changed, 308 insertions(+), 74 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/xml/Namespace.java 🔗

@@ -14,6 +14,7 @@ public final class Namespace {
     public static final String RESULT_SET_MANAGEMENT = "http://jabber.org/protocol/rsm";
     public static final String CHAT_MARKERS = "urn:xmpp:chat-markers:0";
     public static final String CHAT_STATES = "http://jabber.org/protocol/chatstates";
+    public static final String CAPTCHA = "urn:xmpp:captcha";
     public static final String DELIVERY_RECEIPTS = "urn:xmpp:receipts";
     public static final String REACTIONS = "urn:xmpp:reactions:0";
     public static final String VCARD_TEMP = "vcard-temp";

src/main/java/eu/siacs/conversations/xmpp/Managers.java 🔗

@@ -20,7 +20,9 @@ import eu.siacs.conversations.xmpp.manager.PingManager;
 import eu.siacs.conversations.xmpp.manager.PresenceManager;
 import eu.siacs.conversations.xmpp.manager.PrivateStorageManager;
 import eu.siacs.conversations.xmpp.manager.PubSubManager;
+import eu.siacs.conversations.xmpp.manager.RegistrationManager;
 import eu.siacs.conversations.xmpp.manager.RosterManager;
+import eu.siacs.conversations.xmpp.manager.StreamHostManager;
 import eu.siacs.conversations.xmpp.manager.UnifiedPushManager;
 import eu.siacs.conversations.xmpp.manager.VCardManager;
 
@@ -51,7 +53,9 @@ public class Managers {
                 .put(PresenceManager.class, new PresenceManager(context, connection))
                 .put(PrivateStorageManager.class, new PrivateStorageManager(context, connection))
                 .put(PubSubManager.class, new PubSubManager(context, connection))
+                .put(RegistrationManager.class, new RegistrationManager(context, connection))
                 .put(RosterManager.class, new RosterManager(context, connection))
+                .put(StreamHostManager.class, new StreamHostManager(context, connection))
                 .put(UnifiedPushManager.class, new UnifiedPushManager(context, connection))
                 .put(VCardManager.class, new VCardManager(context, connection))
                 .build();

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -2520,8 +2520,12 @@ public class XmppConnection implements Runnable {
     }
 
     public ListenableFuture<Iq> sendIqPacket(final Iq request) {
+        return sendIqPacket(request, false);
+    }
+
+    public ListenableFuture<Iq> sendIqPacket(final Iq request, final boolean allowUnbound) {
         final SettableFuture<Iq> settable = SettableFuture.create();
-        this.sendIqPacket(
+        this.sendUnmodifiedIqPacket(
                 request,
                 response -> {
                     final var type = response.getType();
@@ -2530,7 +2534,8 @@ public class XmppConnection implements Runnable {
                         case TIMEOUT -> settable.setException(new TimeoutException());
                         default -> settable.setException(new IqErrorException(response));
                     }
-                });
+                },
+                allowUnbound);
         return settable;
     }
 

src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java 🔗

@@ -14,7 +14,6 @@ import com.google.common.collect.Iterables;
 import com.google.common.collect.Ordering;
 import com.google.common.hash.Hashing;
 import com.google.common.io.ByteStreams;
-import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -29,7 +28,9 @@ import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
 import eu.siacs.conversations.xmpp.jingle.DirectConnectionUtils;
 import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
-import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.StreamHostManager;
+import im.conversations.android.xmpp.model.socks5.Activate;
+import im.conversations.android.xmpp.model.socks5.Query;
 import im.conversations.android.xmpp.model.stanza.Iq;
 import java.io.IOException;
 import java.io.InputStream;
@@ -249,10 +250,9 @@ public class SocksByteStreamsTransport implements Transport {
         final SettableFuture<String> iqFuture = SettableFuture.create();
         final Iq proxyActivation = new Iq(Iq.Type.SET);
         proxyActivation.setTo(candidate.jid);
-        final Element query = proxyActivation.addChild("query", Namespace.BYTE_STREAMS);
-        query.setAttribute("sid", this.streamId);
-        final Element activate = query.addChild("activate");
-        activate.setContent(id.with.toString());
+        final var query = proxyActivation.addExtension(new Query());
+        query.setSid(this.streamId);
+        query.addExtension(new Activate(id.with));
         xmppConnection.sendIqPacket(
                 proxyActivation,
                 (response) -> {
@@ -276,7 +276,8 @@ public class SocksByteStreamsTransport implements Transport {
     }
 
     private ListenableFuture<Connection> getOurProxyConnection(final String ourDestination) {
-        final var proxyFuture = getProxyCandidate();
+        final var proxyFuture =
+                xmppConnection.getManager(StreamHostManager.class).getProxyCandidate(initiator);
         return Futures.transformAsync(
                 proxyFuture,
                 proxy -> {
@@ -302,67 +303,6 @@ public class SocksByteStreamsTransport implements Transport {
                 MoreExecutors.directExecutor());
     }
 
-    private ListenableFuture<Candidate> getProxyCandidate() {
-        if (Config.DISABLE_PROXY_LOOKUP) {
-            return Futures.immediateFailedFuture(
-                    new IllegalStateException("Proxy look up is disabled"));
-        }
-        final var streamer =
-                xmppConnection
-                        .getManager(DiscoManager.class)
-                        .findDiscoItemByFeature(Namespace.BYTE_STREAMS);
-        if (streamer == null) {
-            return Futures.immediateFailedFuture(
-                    new IllegalStateException("No proxy/streamer found"));
-        }
-        final Iq iqRequest = new Iq(Iq.Type.GET);
-        iqRequest.setTo(streamer.getKey());
-        // TODO urgent refactor to extension
-        // TODO and maybe move to Manager
-        iqRequest.query(Namespace.BYTE_STREAMS);
-        final SettableFuture<Candidate> candidateFuture = SettableFuture.create();
-        xmppConnection.sendIqPacket(
-                iqRequest,
-                (response) -> {
-                    if (response.getType() == Iq.Type.RESULT) {
-                        final Element query = response.findChild("query", Namespace.BYTE_STREAMS);
-                        final Element streamHost =
-                                query == null
-                                        ? null
-                                        : query.findChild("streamhost", Namespace.BYTE_STREAMS);
-                        final String host =
-                                streamHost == null ? null : streamHost.getAttribute("host");
-                        final Integer port =
-                                Ints.tryParse(
-                                        Strings.nullToEmpty(
-                                                streamHost == null
-                                                        ? null
-                                                        : streamHost.getAttribute("port")));
-                        if (Strings.isNullOrEmpty(host) || port == null) {
-                            candidateFuture.setException(
-                                    new IOException("Proxy response is missing attributes"));
-                            return;
-                        }
-                        candidateFuture.set(
-                                new Candidate(
-                                        UUID.randomUUID().toString(),
-                                        host,
-                                        streamer.getKey(),
-                                        port,
-                                        655360 + (initiator ? 0 : 15),
-                                        CandidateType.PROXY));
-
-                    } else if (response.getType() == Iq.Type.TIMEOUT) {
-                        candidateFuture.setException(new TimeoutException());
-                    } else {
-                        candidateFuture.setException(
-                                new IOException(
-                                        "received iq error in response to proxy discovery"));
-                    }
-                });
-        return candidateFuture;
-    }
-
     @Override
     public OutputStream getOutputStream() throws IOException {
         final var connection = this.connection;

src/main/java/eu/siacs/conversations/xmpp/manager/RegistrationManager.java 🔗

@@ -0,0 +1,147 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import android.util.Patterns;
+import androidx.annotation.NonNull;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.data.Data;
+import im.conversations.android.xmpp.model.oob.OutOfBandData;
+import im.conversations.android.xmpp.model.pars.PreAuth;
+import im.conversations.android.xmpp.model.register.Instructions;
+import im.conversations.android.xmpp.model.register.Password;
+import im.conversations.android.xmpp.model.register.Register;
+import im.conversations.android.xmpp.model.register.Remove;
+import im.conversations.android.xmpp.model.register.Username;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import okhttp3.HttpUrl;
+
+public class RegistrationManager extends AbstractManager {
+
+    public RegistrationManager(Context context, XmppConnection connection) {
+        super(context, connection);
+    }
+
+    public ListenableFuture<Void> setPassword(final String password) {
+        final var account = getAccount();
+        final var iq = new Iq(Iq.Type.SET);
+        iq.setTo(account.getJid().getDomain());
+        final var register = iq.addExtension(new Register());
+        register.addUsername(account.getJid().getLocal());
+        register.addPassword(password);
+        return Futures.transform(
+                connection.sendIqPacket(iq), r -> null, MoreExecutors.directExecutor());
+    }
+
+    public ListenableFuture<Void> unregister() {
+        final var account = getAccount();
+        final var iq = new Iq(Iq.Type.SET);
+        iq.setTo(account.getJid().getDomain());
+        final var register = iq.addExtension(new Register());
+        register.addExtension(new Remove());
+        return Futures.transform(
+                connection.sendIqPacket(iq), r -> null, MoreExecutors.directExecutor());
+    }
+
+    public ListenableFuture<Registration> getRegistration() {
+        final var account = getAccount();
+        final var iq = new Iq(Iq.Type.GET);
+        iq.setTo(account.getDomain());
+        iq.addExtension(new Register());
+        final var future = connection.sendIqPacket(iq, true);
+        return Futures.transform(
+                future,
+                result -> {
+                    final var register = result.getExtension(Register.class);
+                    if (register == null) {
+                        throw new IllegalStateException(
+                                "Server did not include register in response");
+                    }
+                    if (register.hasExtension(Username.class)
+                            && register.hasExtension(Password.class)) {
+                        return new SimpleRegistration();
+                    }
+                    final var data = register.getExtension(Data.class);
+                    // note that the captcha namespace is incorrect here. That namespace is only
+                    // used in message challenges. ejabberd uses the incorrect namespace though
+                    if (data != null
+                            && Arrays.asList(Namespace.REGISTER, Namespace.CAPTCHA)
+                                    .contains(data.getFormType())) {
+                        return new ExtendedRegistration(data);
+                    }
+                    final var oob = register.getExtension(OutOfBandData.class);
+                    final var instructions = register.getExtension(Instructions.class);
+                    final String instructionsText =
+                            instructions == null ? null : instructions.getContent();
+                    final String redirectUrl = oob == null ? null : oob.getURL();
+                    if (redirectUrl != null) {
+                        return RedirectRegistration.ifValid(redirectUrl);
+                    }
+                    if (instructionsText != null) {
+                        final Matcher matcher = Patterns.WEB_URL.matcher(instructionsText);
+                        if (matcher.find()) {
+                            final String instructionsUrl =
+                                    instructionsText.substring(matcher.start(), matcher.end());
+                            return RedirectRegistration.ifValid(instructionsUrl);
+                        }
+                    }
+                    throw new IllegalStateException("No supported registration method found");
+                },
+                MoreExecutors.directExecutor());
+    }
+
+    public ListenableFuture<Void> sendPreAuthentication(final String token) {
+        final var account = getAccount();
+        final var iq = new Iq(Iq.Type.GET);
+        iq.setTo(account.getJid().getDomain());
+        final var preAuthentication = iq.addExtension(new PreAuth());
+        preAuthentication.setToken(token);
+        final var future = connection.sendIqPacket(iq, true);
+        return Futures.transform(future, result -> null, MoreExecutors.directExecutor());
+    }
+
+    public abstract static class Registration {}
+
+    // only requires Username + Password
+    public static class SimpleRegistration extends Registration {}
+
+    // Captcha as shown here: https://xmpp.org/extensions/xep-0158.html#register
+    public static class ExtendedRegistration extends Registration {
+        private final Data data;
+
+        public ExtendedRegistration(Data data) {
+            this.data = data;
+        }
+
+        public Data getData() {
+            return this.data;
+        }
+    }
+
+    // Redirection as show here: https://xmpp.org/extensions/xep-0077.html#redirect
+    public static class RedirectRegistration extends Registration {
+        private final HttpUrl url;
+
+        private RedirectRegistration(@NonNull HttpUrl url) {
+            this.url = url;
+        }
+
+        public @NonNull HttpUrl getURL() {
+            return this.url;
+        }
+
+        public static RedirectRegistration ifValid(final String url) {
+            final HttpUrl httpUrl = HttpUrl.parse(url);
+            if (httpUrl != null && httpUrl.isHttps()) {
+                return new RedirectRegistration(httpUrl);
+            }
+            throw new IllegalStateException(
+                    "A URL found the registration instructions is not valid");
+        }
+    }
+}

src/main/java/eu/siacs/conversations/xmpp/manager/StreamHostManager.java 🔗

@@ -0,0 +1,68 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport;
+import im.conversations.android.xmpp.model.socks5.Query;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import java.util.UUID;
+
+public class StreamHostManager extends AbstractManager {
+
+    public StreamHostManager(final Context context, final XmppConnection connection) {
+        super(context, connection);
+    }
+
+    public ListenableFuture<SocksByteStreamsTransport.Candidate> getProxyCandidate(
+            final boolean asInitiator) {
+        if (Config.DISABLE_PROXY_LOOKUP) {
+            return Futures.immediateFailedFuture(
+                    new IllegalStateException("Proxy look up is disabled"));
+        }
+        final var streamer =
+                getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.BYTE_STREAMS);
+        if (streamer == null) {
+            return Futures.immediateFailedFuture(
+                    new IllegalStateException("No proxy/streamer found"));
+        }
+        return getProxyCandidate(asInitiator, streamer.getKey());
+    }
+
+    private ListenableFuture<SocksByteStreamsTransport.Candidate> getProxyCandidate(
+            final boolean asInitiator, final Jid streamer) {
+        final var iq = new Iq(Iq.Type.GET, new Query());
+        iq.setTo(streamer);
+        return Futures.transform(
+                connection.sendIqPacket(iq),
+                response -> {
+                    final var query = response.getExtension(Query.class);
+                    if (query == null) {
+                        throw new IllegalStateException("No stream host query found in response");
+                    }
+                    final var streamHost = query.getStreamHost();
+                    if (streamHost == null) {
+                        throw new IllegalStateException("no stream host found in query");
+                    }
+                    final var jid = streamHost.getJid();
+                    final var host = streamHost.getHost();
+                    final var port = streamHost.getPort();
+                    if (jid == null || host == null || port == null) {
+                        throw new IllegalStateException("StreamHost had incomplete information");
+                    }
+                    return new SocksByteStreamsTransport.Candidate(
+                            UUID.randomUUID().toString(),
+                            host,
+                            streamer,
+                            port,
+                            655360 + (asInitiator ? 0 : 15),
+                            SocksByteStreamsTransport.CandidateType.PROXY);
+                },
+                MoreExecutors.directExecutor());
+    }
+}

src/main/java/im/conversations/android/xmpp/model/register/Register.java 🔗

@@ -2,7 +2,6 @@ package im.conversations.android.xmpp.model.register;
 
 import im.conversations.android.annotation.XmlElement;
 import im.conversations.android.xmpp.model.Extension;
-import org.jxmpp.jid.parts.Localpart;
 
 @XmlElement(name = "query")
 public class Register extends Extension {
@@ -11,8 +10,8 @@ public class Register extends Extension {
         super(Register.class);
     }
 
-    public void addUsername(final Localpart username) {
-        this.addExtension(new Username()).setContent(username.toString());
+    public void addUsername(final String username) {
+        this.addExtension(new Username()).setContent(username);
     }
 
     public void addPassword(final String password) {

src/main/java/im/conversations/android/xmpp/model/socks5/Activate.java 🔗

@@ -0,0 +1,18 @@
+package im.conversations.android.xmpp.model.socks5;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Activate extends Extension {
+
+    public Activate() {
+        super(Activate.class);
+    }
+
+    public Activate(final Jid jid) {
+        this();
+        this.setContent(jid.toString());
+    }
+}

src/main/java/im/conversations/android/xmpp/model/socks5/Query.java 🔗

@@ -0,0 +1,20 @@
+package im.conversations.android.xmpp.model.socks5;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Query extends Extension {
+
+    public Query() {
+        super(Query.class);
+    }
+
+    public StreamHost getStreamHost() {
+        return this.getExtension(StreamHost.class);
+    }
+
+    public void setSid(final String streamId) {
+        this.setAttribute("sid", streamId);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/socks5/StreamHost.java 🔗

@@ -0,0 +1,25 @@
+package im.conversations.android.xmpp.model.socks5;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "streamhost")
+public class StreamHost extends Extension {
+
+    public StreamHost() {
+        super(StreamHost.class);
+    }
+
+    public Jid getJid() {
+        return this.getAttributeAsJid("jid");
+    }
+
+    public String getHost() {
+        return this.getAttribute("host");
+    }
+
+    public Integer getPort() {
+        return this.getOptionalIntAttribute("port").orNull();
+    }
+}