diff --git a/schemas/im.conversations.android.database.ConversationsDatabase/1.json b/schemas/im.conversations.android.database.ConversationsDatabase/1.json index bb6820596..f1ccd490a 100644 --- a/schemas/im.conversations.android.database.ConversationsDatabase/1.json +++ b/schemas/im.conversations.android.database.ConversationsDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "219a451e9a1889222b7549c8b3c0a5b3", + "identityHash": "1952101c2c0d439fcd6c9d417f126a54", "entities": [ { "tableName": "account", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `address` TEXT NOT NULL, `resource` TEXT, `randomSeed` BLOB, `enabled` INTEGER NOT NULL, `quickStartAvailable` INTEGER NOT NULL, `pendingRegistration` INTEGER NOT NULL, `loggedInSuccessfully` INTEGER NOT NULL, `showErrorNotification` INTEGER NOT NULL, `rosterVersion` TEXT, `hostname` TEXT, `port` INTEGER, `directTls` INTEGER, `proxytype` TEXT, `proxyhostname` TEXT, `proxyport` INTEGER)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `address` TEXT NOT NULL, `resource` TEXT, `randomSeed` BLOB, `enabled` INTEGER NOT NULL, `quickStartAvailable` INTEGER NOT NULL, `loginAndBind` INTEGER NOT NULL, `showErrorNotification` INTEGER NOT NULL, `rosterVersion` TEXT, `hostname` TEXT, `port` INTEGER, `directTls` INTEGER, `proxytype` TEXT, `proxyhostname` TEXT, `proxyport` INTEGER)", "fields": [ { "fieldPath": "id", @@ -45,14 +45,8 @@ "notNull": true }, { - "fieldPath": "pendingRegistration", - "columnName": "pendingRegistration", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "loggedInSuccessfully", - "columnName": "loggedInSuccessfully", + "fieldPath": "loginAndBind", + "columnName": "loginAndBind", "affinity": "INTEGER", "notNull": true }, @@ -2266,7 +2260,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '219a451e9a1889222b7549c8b3c0a5b3')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1952101c2c0d439fcd6c9d417f126a54')" ] } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 82f94d77f..4c05ec958 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -16,6 +16,7 @@ public final class Namespace { public static final String BOOKMARKS2_COMPAT = BOOKMARKS2 + "#compat"; public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams"; + public static final String CAPTCHA = "urn:xmpp:captcha"; public static final String CARBONS = "urn:xmpp:carbons:2"; public static final String CHANNEL_BINDING = "urn:xmpp:sasl-cb:0"; public static final String CHAT_MARKERS = "urn:xmpp:chat-markers:0"; diff --git a/src/main/java/im/conversations/android/database/dao/AccountDao.java b/src/main/java/im/conversations/android/database/dao/AccountDao.java index 1e6b76daf..b1da1cc26 100644 --- a/src/main/java/im/conversations/android/database/dao/AccountDao.java +++ b/src/main/java/im/conversations/android/database/dao/AccountDao.java @@ -2,6 +2,7 @@ package im.conversations.android.database.dao; import androidx.room.Dao; import androidx.room.Insert; +import androidx.room.OnConflictStrategy; import androidx.room.Query; import com.google.common.util.concurrent.ListenableFuture; import eu.siacs.conversations.xmpp.Jid; @@ -13,7 +14,7 @@ import java.util.List; @Dao public interface AccountDao { - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) long insert(final AccountEntity account); @Query("SELECT id,address,randomSeed FROM account WHERE enabled = 1") @@ -37,11 +38,8 @@ public interface AccountDao { @Query("SELECT quickStartAvailable FROM account where id=:id") boolean quickStartAvailable(long id); - @Query("SELECT pendingRegistration FROM account where id=:id") - boolean pendingRegistration(long id); - - @Query("SELECT loggedInSuccessfully == 0 FROM account where id=:id") - boolean isInitialLogin(long id); + @Query("SELECT loginAndBind FROM account where id=:id") + boolean loginAndBind(long id); @Query( "UPDATE account set quickStartAvailable=:available WHERE id=:id AND" @@ -49,14 +47,9 @@ public interface AccountDao { void setQuickStartAvailable(long id, boolean available); @Query( - "UPDATE account set pendingRegistration=:pendingRegistration WHERE id=:id AND" - + " pendingRegistration != :pendingRegistration") - void setPendingRegistration(long id, boolean pendingRegistration); - - @Query( - "UPDATE account set loggedInSuccessfully=:loggedInSuccessfully WHERE id=:id AND" - + " loggedInSuccessfully != :loggedInSuccessfully") - int setLoggedInSuccessfully(long id, boolean loggedInSuccessfully); + "UPDATE account set loginAndBind=:loginAndBind WHERE id=:id AND" + + " loginAndBind != :loginAndBind") + void setLoginAndBind(long id, boolean loginAndBind); @Query( "UPDATE account set showErrorNotification=:showErrorNotification WHERE id=:id AND" diff --git a/src/main/java/im/conversations/android/database/entity/AccountEntity.java b/src/main/java/im/conversations/android/database/entity/AccountEntity.java index f23a183d2..e4d197510 100644 --- a/src/main/java/im/conversations/android/database/entity/AccountEntity.java +++ b/src/main/java/im/conversations/android/database/entity/AccountEntity.java @@ -28,11 +28,7 @@ public class AccountEntity { public boolean enabled; public boolean quickStartAvailable = false; - public boolean pendingRegistration = false; - - // TODO this is only used during setup; depending on how the setup procedure will look in the - // future we might get rid of this property - public boolean loggedInSuccessfully = false; + public boolean loginAndBind = true; public boolean showErrorNotification = true; diff --git a/src/main/java/im/conversations/android/repository/AccountRepository.java b/src/main/java/im/conversations/android/repository/AccountRepository.java index b38e7786a..eb03b97f8 100644 --- a/src/main/java/im/conversations/android/repository/AccountRepository.java +++ b/src/main/java/im/conversations/android/repository/AccountRepository.java @@ -5,12 +5,15 @@ import androidx.annotation.NonNull; import com.google.common.base.Preconditions; 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.xmpp.Jid; import im.conversations.android.IDs; import im.conversations.android.database.CredentialStore; import im.conversations.android.database.entity.AccountEntity; import im.conversations.android.database.model.Account; import im.conversations.android.xmpp.ConnectionPool; +import im.conversations.android.xmpp.XmppConnection; +import im.conversations.android.xmpp.manager.RegistrationManager; public class AccountRepository extends AbstractRepository { @@ -18,7 +21,8 @@ public class AccountRepository extends AbstractRepository { super(context); } - private Account createAccount(@NonNull final Jid address, final String password) { + private Account createAccount( + @NonNull final Jid address, final String password, final boolean loginAndBind) { Preconditions.checkArgument( address.isBareJid(), "Account should be specified without resource"); Preconditions.checkArgument(password != null, "Missing password"); @@ -26,6 +30,7 @@ public class AccountRepository extends AbstractRepository { final var entity = new AccountEntity(); entity.address = address; entity.enabled = true; + entity.loginAndBind = loginAndBind; entity.randomSeed = randomSeed; final long id = database.accountDao().insert(entity); final var account = new Account(id, address, entity.randomSeed); @@ -38,8 +43,23 @@ public class AccountRepository extends AbstractRepository { return account; } + public ListenableFuture createAccountAsync( + final @NonNull Jid address, final String password, final boolean loginAndBind) { + return Futures.submit(() -> createAccount(address, password, loginAndBind), IO_EXECUTOR); + } + public ListenableFuture createAccountAsync( final @NonNull Jid address, final String password) { - return Futures.submit(() -> createAccount(address, password), IO_EXECUTOR); + return createAccountAsync(address, password, true); + } + + public ListenableFuture getRegistration( + final Account account) { + final ListenableFuture connectedFuture = + ConnectionPool.getInstance(context).reconfigure(account).asConnectedFuture(); + return Futures.transformAsync( + connectedFuture, + xc -> xc.getManager(RegistrationManager.class).getRegistration(), + MoreExecutors.directExecutor()); } } diff --git a/src/main/java/im/conversations/android/xmpp/XmppConnection.java b/src/main/java/im/conversations/android/xmpp/XmppConnection.java index da9145811..5259c47ea 100644 --- a/src/main/java/im/conversations/android/xmpp/XmppConnection.java +++ b/src/main/java/im/conversations/android/xmpp/XmppConnection.java @@ -118,10 +118,14 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; import okhttp3.HttpUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.xmlpull.v1.XmlPullParserException; public class XmppConnection implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(XmppConnection.class); + protected final Account account; private final SparseArray mStanzaQueue = new SparseArray<>(); private final Hashtable>> packetCallbacks = new Hashtable<>(); @@ -289,9 +293,9 @@ public class XmppConnection implements Runnable { try { Socket localSocket; shouldAuthenticate = - !ConversationsDatabase.getInstance(context) + ConversationsDatabase.getInstance(context) .accountDao() - .pendingRegistration(account.id); + .loginAndBind(account.id); this.changeStatus(ConnectionState.CONNECTING); // TODO introduce proxy check final boolean useTor = /*fcontext.useTorToConnect() ||*/ account.isOnion(); @@ -1177,7 +1181,7 @@ public class XmppConnection implements Runnable { final SSLSocket sslSocket = upgradeSocketToTls(socket); tagReader.setInputStream(sslSocket.getInputStream()); tagWriter.setOutputStream(sslSocket.getOutputStream()); - Log.d(Config.LOGTAG, account.address + ": TLS connection established"); + LOGGER.info("TLS connection established"); final boolean quickStart; try { quickStart = establishStream(SSLSockets.version(sslSocket)); @@ -1234,13 +1238,10 @@ public class XmppConnection implements Runnable { } private void processStreamFeatures(final Tag currentTag) throws IOException { - final boolean pendingRegistration = - ConversationsDatabase.getInstance(context) - .accountDao() - .pendingRegistration(account.id); + final boolean loginAndBind = + ConversationsDatabase.getInstance(context).accountDao().loginAndBind(account.id); this.streamFeatures = tagReader.readElement(currentTag, Features.class); - final boolean isSecure = isSecure(); - final boolean needsBinding = !isBound && !pendingRegistration; + final boolean needsBinding = !isBound && loginAndBind; if (this.quickStartInProgress) { if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) { Log.d( @@ -1271,29 +1272,23 @@ public class XmppConnection implements Runnable { throw new StateChangingException(ConnectionState.INCOMPATIBLE_SERVER); } if (this.streamFeatures.hasChild("starttls", Namespace.TLS) && !this.encryptionEnabled) { + LOGGER.info("Negotiating TLS (STARTTLS)"); sendStartTLS(); - } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) - && pendingRegistration) { - if (isSecure) { - register(); - } else { - Log.d( - Config.LOGTAG, - account.address - + ": unable to find STARTTLS for registration process " - + XmlHelper.printElementNames(this.streamFeatures)); - throw new StateChangingException(ConnectionState.INCOMPATIBLE_SERVER); - } - } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) - && pendingRegistration) { - throw new StateChangingException(ConnectionState.REGISTRATION_NOT_SUPPORTED); + return; + } else if (!isSecure()) { + LOGGER.error("Server does not support STARTTLS"); + throw new StateChangingException(ConnectionState.INCOMPATIBLE_SERVER); + } + + if (Boolean.FALSE.equals(loginAndBind)) { + LOGGER.info("No login and bind required. Connection is considered online"); + this.lastPacketReceived = SystemClock.elapsedRealtime(); + this.changeStatus(ConnectionState.ONLINE); } else if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2) - && shouldAuthenticate - && isSecure) { + && shouldAuthenticate) { authenticate(SaslMechanism.Version.SASL_2); } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL) - && shouldAuthenticate - && isSecure) { + && shouldAuthenticate) { authenticate(SaslMechanism.Version.SASL); } else if (this.streamFeatures.streamManagement() && streamId != null && !inSmacksSession) { if (Config.EXTENDED_SM_LOGGING) { @@ -1306,7 +1301,7 @@ public class XmppConnection implements Runnable { this.mWaitingForSmCatchup.set(true); this.tagWriter.writeStanzaAsync(resume); } else if (needsBinding) { - if (this.streamFeatures.hasChild("bind", Namespace.BIND) && isSecure) { + if (this.streamFeatures.hasChild("bind", Namespace.BIND)) { sendBindRequest(); } else { Log.d( @@ -1516,7 +1511,7 @@ public class XmppConnection implements Runnable { } final Iq preAuthRequest = new Iq(Iq.Type.SET); preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth); - sendUnmodifiedIqPacket( + sendIqPacketUnbound( preAuthRequest, (response) -> { if (response.getType() == Iq.Type.RESULT) { @@ -1526,15 +1521,14 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.address + ": failed to pre auth. " + error); throw new StateChangingError(ConnectionState.REGISTRATION_INVALID_TOKEN); } - }, - true); + }); } private void sendRegistryRequest() { final Iq retrieveRegistration = new Iq(Iq.Type.GET); retrieveRegistration.addExtension(new Register()); retrieveRegistration.setTo(account.address.getDomain()); - sendUnmodifiedIqPacket( + sendIqPacketUnbound( retrieveRegistration, (packet) -> { if (packet.getType() == Iq.Type.TIMEOUT) { @@ -1562,8 +1556,7 @@ public class XmppConnection implements Runnable { register.addChild(username); register.addChild(password); registrationRequest.setFrom(account.address); - sendUnmodifiedIqPacket( - registrationRequest, this::handleRegistrationResponse, true); + sendIqPacketUnbound(registrationRequest, this::handleRegistrationResponse); } else if (query.hasChild("x", Namespace.DATA)) { final Data data = Data.parse(query.findChild("x", Namespace.DATA)); final Element blob = query.findChild("data", "urn:xmpp:bob"); @@ -1621,15 +1614,14 @@ public class XmppConnection implements Runnable { } throw new StateChangingError(ConnectionState.REGISTRATION_FAILED); } - }, - true); + }); } private void handleRegistrationResponse(final Iq packet) { if (packet.getType() == Iq.Type.RESULT) { ConversationsDatabase.getInstance(context) .accountDao() - .setPendingRegistration(account.id, false); + .setLoginAndBind(account.id, true); Log.d( Config.LOGTAG, account.address + ": successfully registered new account on server"); @@ -1693,7 +1685,7 @@ public class XmppConnection implements Runnable { } final Iq iq = new Iq(Iq.Type.SET); iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource); - this.sendUnmodifiedIqPacket( + this.sendIqPacketUnbound( iq, (packet) -> { if (packet.getType() == Iq.Type.TIMEOUT) { @@ -1759,8 +1751,7 @@ public class XmppConnection implements Runnable { + ")"); } throw new StateChangingError(ConnectionState.BIND_FAILURE); - }, - true); + }); } private void setConnectionAddress(final Jid jid) { @@ -1813,7 +1804,7 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.address + ": sending legacy session to outdated server"); final Iq startSession = new Iq(Iq.Type.SET); startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session"); - this.sendUnmodifiedIqPacket( + this.sendIqPacketUnbound( startSession, (packet) -> { if (packet.getType() == Iq.Type.RESULT) { @@ -1822,8 +1813,7 @@ public class XmppConnection implements Runnable { } else if (packet.getType() != Iq.Type.TIMEOUT) { throw new StateChangingError(ConnectionState.SESSION_FAILURE); } - }, - true); + }); } // TODO the return value is not used any more @@ -2009,6 +1999,14 @@ public class XmppConnection implements Runnable { } public ListenableFuture sendIqPacket(final Iq packet) { + return sendIqPacket(packet, false); + } + + public ListenableFuture sendIqPacketUnbound(final Iq packet) { + return sendIqPacket(packet, true); + } + + private ListenableFuture sendIqPacket(final Iq packet, final boolean sendToUnboundStream) { final SettableFuture future = SettableFuture.create(); sendIqPacket( packet, @@ -2021,17 +2019,21 @@ public class XmppConnection implements Runnable { } else { future.setException(new IqErrorException(result)); } - }); + }, + sendToUnboundStream); return future; } - public String sendIqPacket(final Iq packet, final Consumer callback) { - packet.setFrom(account.address); - return this.sendUnmodifiedIqPacket(packet, callback, false); + public void sendIqPacket(final Iq packet, final Consumer callback) { + this.sendIqPacket(packet, callback, false); } - public synchronized String sendUnmodifiedIqPacket( - final Iq packet, final Consumer callback, boolean force) { + public void sendIqPacketUnbound(final Iq packet, final Consumer callback) { + this.sendIqPacket(packet, callback, true); + } + + private synchronized void sendIqPacket( + final Iq packet, final Consumer callback, final boolean sendToUnboundStream) { if (Strings.isNullOrEmpty(packet.getId())) { packet.setId(IDs.medium()); } @@ -2040,8 +2042,7 @@ public class XmppConnection implements Runnable { packetCallbacks.put(packet.getId(), new Pair<>(packet, callback)); } } - this.sendPacket(packet, force); - return packet.getId(); + this.sendPacket(packet, sendToUnboundStream); } public void sendResultFor(final Iq request, final Extension... extensions) { @@ -2079,14 +2080,15 @@ public class XmppConnection implements Runnable { sendPacket(packet, false); } - private synchronized void sendPacket(final StreamElement packet, final boolean force) { + private synchronized void sendPacket( + final StreamElement packet, final boolean sendToUnboundStream) { if (stanzasSent == Integer.MAX_VALUE) { resetStreamId(); disconnect(true); return; } synchronized (this.mStanzaQueue) { - if (force || isBound) { + if (sendToUnboundStream || isBound) { tagWriter.writeStanzaAsync(packet); } else { Log.d( @@ -2131,11 +2133,17 @@ public class XmppConnection implements Runnable { } public void sendPing() { - if (!r()) { + if (this.inSmacksSession) { + this.tagWriter.writeStanzaAsync(new Request()); + } else { final Iq iq = new Iq(Iq.Type.GET); iq.setFrom(account.address); iq.addExtension(new Ping()); - this.sendIqPacket(iq, null); + this.sendIqPacket( + iq, + response -> { + LOGGER.info("Server responded to ping"); + }); } this.lastPingSent = SystemClock.elapsedRealtime(); } @@ -2213,15 +2221,6 @@ public class XmppConnection implements Runnable { this.streamId = null; } - public boolean r() { - if (this.inSmacksSession) { - this.tagWriter.writeStanzaAsync(new Request()); - return true; - } else { - return false; - } - } - public int getTimeToNextAttempt() { final int additionalTime = recentErrorConnectionState == ConnectionState.POLICY_VIOLATION ? 3 : 0; diff --git a/src/main/java/im/conversations/android/xmpp/manager/RegistrationManager.java b/src/main/java/im/conversations/android/xmpp/manager/RegistrationManager.java index 8231c82d0..73317d117 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/RegistrationManager.java +++ b/src/main/java/im/conversations/android/xmpp/manager/RegistrationManager.java @@ -10,16 +10,23 @@ import eu.siacs.conversations.xml.Namespace; import im.conversations.android.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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class RegistrationManager extends AbstractManager { + private static final Logger LOGGER = LoggerFactory.getLogger(RegistrationManager.class); + public RegistrationManager(Context context, XmppConnection connection) { super(context, connection); } @@ -27,6 +34,7 @@ public class RegistrationManager extends AbstractManager { public ListenableFuture setPassword(final String password) { final var account = getAccount(); final var iq = new Iq(Iq.Type.SET); + iq.setTo(account.address.getDomain()); final var register = iq.addExtension(new Register()); register.addUsername(account.address.getEscapedLocal()); register.addPassword(password); @@ -36,6 +44,7 @@ public class RegistrationManager extends AbstractManager { public ListenableFuture unregister() { final var iq = new Iq(Iq.Type.SET); + iq.setTo(getAccount().address.getDomain()); final var register = iq.addExtension(new Register()); register.addExtension(new Remove()); return Futures.transform( @@ -43,10 +52,11 @@ public class RegistrationManager extends AbstractManager { } public ListenableFuture getRegistration() { - final var iq = new Iq(Iq.Type.SET); + final var iq = new Iq(Iq.Type.GET); + iq.setTo(getAccount().address.getDomain()); iq.addExtension(new Register()); return Futures.transform( - connection.sendIqPacket(iq), + connection.sendIqPacketUnbound(iq), result -> { final var register = result.getExtension(Register.class); if (register == null) { @@ -58,7 +68,11 @@ public class RegistrationManager extends AbstractManager { return new SimpleRegistration(); } final var data = register.getExtension(Data.class); - if (data != null && Namespace.REGISTER.equals(data.getFormType())) { + // 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); @@ -67,14 +81,14 @@ public class RegistrationManager extends AbstractManager { instructions == null ? null : instructions.getContent(); final String redirectUrl = oob == null ? null : oob.getURL(); if (redirectUrl != null) { - return new RedirectRegistration(redirectUrl); + 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 new RedirectRegistration(instructionsUrl); + return RedirectRegistration.ifValid(instructionsUrl); } } throw new IllegalStateException("No supported registration method found"); @@ -82,7 +96,16 @@ public class RegistrationManager extends AbstractManager { MoreExecutors.directExecutor()); } - private abstract static class Registration {} + public ListenableFuture sendPreAuthentication(final String token) { + final var iq = new Iq(Iq.Type.GET); + iq.setTo(getAccount().address.getDomain()); + final var preAuthentication = iq.addExtension(new PreAuth()); + preAuthentication.setToken(token); + return Futures.transform( + connection.sendIqPacketUnbound(iq), result -> null, MoreExecutors.directExecutor()); + } + + public abstract static class Registration {} // only requires Username + Password public static class SimpleRegistration extends Registration {} @@ -102,14 +125,23 @@ public class RegistrationManager extends AbstractManager { // Redirection as show here: https://xmpp.org/extensions/xep-0077.html#redirect public static class RedirectRegistration extends Registration { - private final String url; + private final HttpUrl url; - public RedirectRegistration(@NonNull final String url) { + private RedirectRegistration(@NonNull HttpUrl url) { this.url = url; } - public @NonNull String getURL() { + 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"); + } } } diff --git a/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java b/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java new file mode 100644 index 000000000..6841313b8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.pars; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.PARS) +public class PreAuth extends Extension { + + public PreAuth() { + super(PreAuth.class); + } + + public void setToken(final String token) { + this.setAttribute("token", token); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index 71f474f80..0c0a519ef 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -24,14 +24,6 @@ public class BindProcessor extends XmppConnection.Delegate implements Consumer 0; - - if (firstLogin) { - // TODO publish display name if this is the first attempt - // IIRC this is used when the display name is set from a certificate or something - } - database.presenceDao().deletePresences(account.id); getManager(RosterManager.class).fetch();