Merge pull request #1461 from akallabeth/captcha_support

Implemented account registration with captcha
This commit is contained in:
Daniel Gultsch 2015-10-11 15:56:42 +02:00
commit da31582911
6 changed files with 237 additions and 23 deletions

View file

@ -266,4 +266,14 @@ public class IqGenerator extends AbstractGenerator {
} }
return packet; return packet;
} }
public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) {
final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
register.setTo(account.getServer());
register.setId(id);
register.query("jabber:iq:register").addChild(data);
return register;
}
} }

View file

@ -29,6 +29,7 @@ import android.security.KeyChain;
import android.security.KeyChainException; import android.security.KeyChainException;
import android.util.Log; import android.util.Log;
import android.util.LruCache; import android.util.LruCache;
import android.util.DisplayMetrics;
import net.java.otr4j.OtrException; import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session; import net.java.otr4j.session.Session;
@ -257,6 +258,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private int showErrorToastListenerCount = 0; private int showErrorToastListenerCount = 0;
private int unreadCount = -1; private int unreadCount = -1;
private OnAccountUpdate mOnAccountUpdate = null; private OnAccountUpdate mOnAccountUpdate = null;
private OnCaptchaRequested mOnCaptchaRequested = null;
private OnStatusChanged statusListener = new OnStatusChanged() { private OnStatusChanged statusListener = new OnStatusChanged() {
@Override @Override
@ -315,6 +317,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
} }
}; };
private int accountChangedListenerCount = 0; private int accountChangedListenerCount = 0;
private int captchaRequestedListenerCount = 0;
private OnRosterUpdate mOnRosterUpdate = null; private OnRosterUpdate mOnRosterUpdate = null;
private OnUpdateBlocklist mOnUpdateBlocklist = null; private OnUpdateBlocklist mOnUpdateBlocklist = null;
private int updateBlocklistListenerCount = 0; private int updateBlocklistListenerCount = 0;
@ -1459,6 +1462,31 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
} }
} }
public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
synchronized (this) {
if (checkListeners()) {
switchToForeground();
}
this.mOnCaptchaRequested = listener;
if (this.captchaRequestedListenerCount < 2) {
this.captchaRequestedListenerCount++;
}
}
}
public void removeOnCaptchaRequestedListener() {
synchronized (this) {
this.captchaRequestedListenerCount--;
if (this.captchaRequestedListenerCount <= 0) {
this.mOnCaptchaRequested = null;
this.captchaRequestedListenerCount = 0;
if (checkListeners()) {
switchToBackground();
}
}
}
}
public void setOnRosterUpdateListener(final OnRosterUpdate listener) { public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
synchronized (this) { synchronized (this) {
if (checkListeners()) { if (checkListeners()) {
@ -1563,6 +1591,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
return (this.mOnAccountUpdate == null return (this.mOnAccountUpdate == null
&& this.mOnConversationUpdate == null && this.mOnConversationUpdate == null
&& this.mOnRosterUpdate == null && this.mOnRosterUpdate == null
&& this.mOnCaptchaRequested == null
&& this.mOnUpdateBlocklist == null && this.mOnUpdateBlocklist == null
&& this.mOnShowErrorToast == null && this.mOnShowErrorToast == null
&& this.mOnKeyStatusUpdated == null); && this.mOnKeyStatusUpdated == null);
@ -2464,6 +2493,20 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
} }
} }
public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
boolean rc = false;
if (mOnCaptchaRequested != null) {
DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int)(captcha.getWidth() * metrics.scaledDensity),
(int)(captcha.getHeight() * metrics.scaledDensity), false);
mOnCaptchaRequested.onCaptchaRequested(account, id, data, scaled);
rc = true;
}
return rc;
}
public void updateBlocklistUi(final OnUpdateBlocklist.Status status) { public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
if (mOnUpdateBlocklist != null) { if (mOnUpdateBlocklist != null) {
mOnUpdateBlocklist.OnUpdateBlocklist(status); mOnUpdateBlocklist.OnUpdateBlocklist(status);
@ -2620,6 +2663,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
} }
} }
public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
XmppConnection connection = account.getXmppConnection();
if (connection != null) {
connection.sendCaptchaRegistryRequest(id, data);
}
}
public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) { public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
final XmppConnection connection = account.getXmppConnection(); final XmppConnection connection = account.getXmppConnection();
if (connection != null) { if (connection != null) {
@ -2786,6 +2836,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
void onAccountUpdate(); void onAccountUpdate();
} }
public interface OnCaptchaRequested {
void onCaptchaRequested(Account account,
String id,
Data data,
Bitmap captcha);
}
public interface OnRosterUpdate { public interface OnRosterUpdate {
void onRosterUpdate(); void onRosterUpdate();
} }

View file

@ -1,9 +1,11 @@
package eu.siacs.conversations.ui; package eu.siacs.conversations.ui;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder; import android.app.AlertDialog.Builder;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
@ -31,17 +33,20 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.XmppConnection.Features; import eu.siacs.conversations.xmpp.XmppConnection.Features;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.Avatar;
public class EditAccountActivity extends XmppActivity implements OnAccountUpdate, OnKeyStatusUpdated { public class EditAccountActivity extends XmppActivity implements OnAccountUpdate,
OnKeyStatusUpdated, OnCaptchaRequested {
private AutoCompleteTextView mAccountJid; private AutoCompleteTextView mAccountJid;
private EditText mPassword; private EditText mPassword;
@ -72,6 +77,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
private ImageButton mRegenerateAxolotlKeyButton; private ImageButton mRegenerateAxolotlKeyButton;
private LinearLayout keys; private LinearLayout keys;
private LinearLayout keysCard; private LinearLayout keysCard;
private AlertDialog mCaptchaDialog = null;
private Jid jidToEdit; private Jid jidToEdit;
private boolean mInitMode = false; private boolean mInitMode = false;
@ -681,4 +687,70 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
public void onKeyStatusUpdated() { public void onKeyStatusUpdated() {
refreshUi(); refreshUi();
} }
@Override
public void onCaptchaRequested(final Account account, final String id, final Data data,
final Bitmap captcha) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
final ImageView view = new ImageView(this);
final LinearLayout layout = new LinearLayout(this);
final EditText input = new EditText(this);
view.setImageBitmap(captcha);
view.setScaleType(ImageView.ScaleType.FIT_CENTER);
input.setHint(getString(R.string.captcha_hint));
layout.setOrientation(LinearLayout.VERTICAL);
layout.addView(view);
layout.addView(input);
builder.setTitle(getString(R.string.captcha_required));
builder.setView(layout);
builder.setPositiveButton(getString(R.string.ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String rc = input.getText().toString();
data.put("username", account.getUsername());
data.put("password", account.getPassword());
data.put("ocr", rc);
data.submit();
if (xmppConnectionServiceBound) {
xmppConnectionService.sendCreateAccountWithCaptchaPacket(
account, id, data);
}
}
});
builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (xmppConnectionService != null) {
xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null);
}
}
});
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
if (xmppConnectionService != null) {
xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null);
}
}
});
runOnUiThread(new Runnable() {
@Override
public void run() {
if ((mCaptchaDialog != null) && mCaptchaDialog.isShowing()) {
mCaptchaDialog.dismiss();
}
mCaptchaDialog = builder.create();
mCaptchaDialog.show();
}
});
}
} }

View file

@ -281,6 +281,9 @@ public abstract class XmppActivity extends Activity {
if (this instanceof XmppConnectionService.OnAccountUpdate) { if (this instanceof XmppConnectionService.OnAccountUpdate) {
this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
} }
if (this instanceof XmppConnectionService.OnCaptchaRequested) {
this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
}
if (this instanceof XmppConnectionService.OnRosterUpdate) { if (this instanceof XmppConnectionService.OnRosterUpdate) {
this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
} }
@ -305,6 +308,9 @@ public abstract class XmppActivity extends Activity {
if (this instanceof XmppConnectionService.OnAccountUpdate) { if (this instanceof XmppConnectionService.OnAccountUpdate) {
this.xmppConnectionService.removeOnAccountListChangedListener(); this.xmppConnectionService.removeOnAccountListChangedListener();
} }
if (this instanceof XmppConnectionService.OnCaptchaRequested) {
this.xmppConnectionService.removeOnCaptchaRequestedListener();
}
if (this instanceof XmppConnectionService.OnRosterUpdate) { if (this instanceof XmppConnectionService.OnRosterUpdate) {
this.xmppConnectionService.removeOnRosterUpdateListener(); this.xmppConnectionService.removeOnRosterUpdateListener();
} }

View file

@ -1,10 +1,15 @@
package eu.siacs.conversations.xmpp; package eu.siacs.conversations.xmpp;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.wifi.WifiConfiguration;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.os.PowerManager; import android.os.PowerManager;
import android.os.PowerManager.WakeLock; import android.os.PowerManager.WakeLock;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Base64;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.util.SparseArray; import android.util.SparseArray;
@ -14,6 +19,7 @@ import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -24,6 +30,8 @@ import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket; import java.net.Socket;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
@ -59,6 +67,8 @@ import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.Tag;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
import eu.siacs.conversations.xml.XmlReader; import eu.siacs.conversations.xml.XmlReader;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.forms.Field;
import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
@ -116,6 +126,29 @@ public class XmppConnection implements Runnable {
private SaslMechanism saslMechanism; private SaslMechanism saslMechanism;
private OnIqPacketReceived createPacketReceiveHandler() {
OnIqPacketReceived receiver = new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() == IqPacket.TYPE.RESULT) {
account.setOption(Account.OPTION_REGISTER,
false);
changeStatus(Account.State.REGISTRATION_SUCCESSFUL);
} else if (packet.hasChild("error")
&& (packet.findChild("error")
.hasChild("conflict"))) {
changeStatus(Account.State.REGISTRATION_CONFLICT);
} else {
changeStatus(Account.State.REGISTRATION_FAILED);
Log.d(Config.LOGTAG, packet.toString());
}
disconnect(true);
}
};
return receiver;
}
public XmppConnection(final Account account, final XmppConnectionService service) { public XmppConnection(final Account account, final XmppConnectionService service) {
this.account = account; this.account = account;
this.wakeLock = service.getPowerManager().newWakeLock( this.wakeLock = service.getPowerManager().newWakeLock(
@ -643,6 +676,15 @@ public class XmppConnection implements Runnable {
return mechanisms; return mechanisms;
} }
public void sendCaptchaRegistryRequest(String id, Data data) {
if (data == null) {
setAccountCreationFailed("");
} else {
IqPacket request = getIqGenerator().generateCreateAccountWithCaptcha(account, id, data);
sendIqPacket(request, createPacketReceiveHandler());
}
}
private void sendRegistryRequest() { private void sendRegistryRequest() {
final IqPacket register = new IqPacket(IqPacket.TYPE.GET); final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
register.query("jabber:iq:register"); register.query("jabber:iq:register");
@ -651,6 +693,7 @@ public class XmppConnection implements Runnable {
@Override @Override
public void onIqPacketReceived(final Account account, final IqPacket packet) { public void onIqPacketReceived(final Account account, final IqPacket packet) {
boolean failed = false;
if (packet.getType() == IqPacket.TYPE.RESULT if (packet.getType() == IqPacket.TYPE.RESULT
&& packet.query().hasChild("username") && packet.query().hasChild("username")
&& (packet.query().hasChild("password"))) { && (packet.query().hasChild("password"))) {
@ -659,35 +702,58 @@ public class XmppConnection implements Runnable {
final Element password = new Element("password").setContent(account.getPassword()); final Element password = new Element("password").setContent(account.getPassword());
register.query("jabber:iq:register").addChild(username); register.query("jabber:iq:register").addChild(username);
register.query().addChild(password); register.query().addChild(password);
sendIqPacket(register, new OnIqPacketReceived() { sendIqPacket(register, createPacketReceiveHandler());
} else if (packet.getType() == IqPacket.TYPE.RESULT
&& (packet.query().hasChild("x", "jabber:x:data"))) {
final Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
final Element blob = packet.query().findChild("data", "urn:xmpp:bob");
final String id = packet.getId();
Bitmap captcha = null;
if (blob != null) {
try {
final String base64Blob = blob.getContent();
final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
InputStream stream = new ByteArrayInputStream(strBlob);
captcha = BitmapFactory.decodeStream(stream);
} catch (Exception e) {
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
if (packet.getType() == IqPacket.TYPE.RESULT) {
account.setOption(Account.OPTION_REGISTER,
false);
changeStatus(Account.State.REGISTRATION_SUCCESSFUL);
} else if (packet.hasChild("error")
&& (packet.findChild("error")
.hasChild("conflict"))) {
changeStatus(Account.State.REGISTRATION_CONFLICT);
} else {
changeStatus(Account.State.REGISTRATION_FAILED);
Log.d(Config.LOGTAG, packet.toString());
} }
disconnect(true); } else {
try {
Field url = data.getFieldByName("url");
String urlString = url.findChildContent("value");
URL uri = new URL(urlString);
captcha = BitmapFactory.decodeStream(uri.openConnection().getInputStream());
} catch(MalformedURLException e) {
Log.e(Config.LOGTAG, e.toString());
} catch(IOException e) {
Log.e(Config.LOGTAG, e.toString());
}
}
if (captcha != null) {
failed = !mXmppConnectionService.displayCaptchaRequest(account, id, data, captcha);
}
} else {
failed = true;
}
if (failed) {
final Element instructions = packet.query().findChild("instructions");
setAccountCreationFailed((instructions != null) ? instructions.getContent() : "");
}
} }
}); });
} else { }
final Element instructions = packet.query().findChild("instructions");
private void setAccountCreationFailed(String instructions) {
changeStatus(Account.State.REGISTRATION_FAILED); changeStatus(Account.State.REGISTRATION_FAILED);
disconnect(true); disconnect(true);
Log.d(Config.LOGTAG, account.getJid().toBareJid() Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ ": could not register. instructions are" + ": could not register. instructions are"
+ (instructions != null ? instructions.getContent() : "")); + instructions);
}
}
});
} }
private void sendBindRequest() { private void sendBindRequest() {

View file

@ -527,4 +527,7 @@
<string name="action_add_account_from_key">Add account from key</string> <string name="action_add_account_from_key">Add account from key</string>
<string name="unable_to_parse_certificate">Unable to parse certificate</string> <string name="unable_to_parse_certificate">Unable to parse certificate</string>
<string name="authenticate_with_certificate">Leave empty to authenticate w/ certificate</string> <string name="authenticate_with_certificate">Leave empty to authenticate w/ certificate</string>
<string name="captcha_ocr">Captcha text</string>
<string name="captcha_required">Captcha required</string>
<string name="captcha_hint">enter the text from the image</string>
</resources> </resources>