Detailed changes
@@ -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";
@@ -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();
@@ -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;
}
@@ -181,7 +181,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
}
@Override
- public void onFailure(@NonNull Throwable throwable) {}
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(Config.LOGTAG, "could not prepare transport info", throwable);
+ }
},
MoreExecutors.directExecutor());
}
@@ -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;
@@ -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");
+ }
+ }
+}
@@ -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());
+ }
+}
@@ -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) {
@@ -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());
+ }
+}
@@ -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);
+ }
+}
@@ -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();
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.BYTE_STREAMS)
+package im.conversations.android.xmpp.model.socks5;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;