Captcha support.

Armin Novak created

Change summary

src/main/java/eu/siacs/conversations/generator/IqGenerator.java          |  10 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  57 
src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java         |  74 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                |   6 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java            | 112 
src/main/res/values/strings.xml                                          |   3 
6 files changed, 238 insertions(+), 24 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -266,4 +266,14 @@ public class IqGenerator extends AbstractGenerator {
 		}
 		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;
+	}
 }

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -29,6 +29,7 @@ import android.security.KeyChain;
 import android.security.KeyChainException;
 import android.util.Log;
 import android.util.LruCache;
+import android.util.DisplayMetrics;
 
 import net.java.otr4j.OtrException;
 import net.java.otr4j.session.Session;
@@ -257,6 +258,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 	private int showErrorToastListenerCount = 0;
 	private int unreadCount = -1;
 	private OnAccountUpdate mOnAccountUpdate = null;
+	private OnCaptchaRequested mOnCaptchaRequested = null;
 	private OnStatusChanged statusListener = new OnStatusChanged() {
 
 		@Override
@@ -315,6 +317,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 		}
 	};
 	private int accountChangedListenerCount = 0;
+	private int captchaRequestedListenerCount = 0;
 	private OnRosterUpdate mOnRosterUpdate = null;
 	private OnUpdateBlocklist mOnUpdateBlocklist = null;
 	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) {
 		synchronized (this) {
 			if (checkListeners()) {
@@ -1563,6 +1591,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 		return (this.mOnAccountUpdate == null
 				&& this.mOnConversationUpdate == null
 				&& this.mOnRosterUpdate == null
+				&& this.mOnCaptchaRequested == null
 				&& this.mOnUpdateBlocklist == null
 				&& this.mOnShowErrorToast == 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) {
 		if (mOnUpdateBlocklist != null) {
 			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) {
 		final XmppConnection connection = account.getXmppConnection();
 		if (connection != null) {
@@ -2786,6 +2836,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 		void onAccountUpdate();
 	}
 
+	public interface OnCaptchaRequested {
+		void onCaptchaRequested(Account account,
+					String id,
+					Data data,
+					Bitmap captcha);
+	}
+
 	public interface OnRosterUpdate {
 		void onRosterUpdate();
 	}

src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java 🔗

@@ -1,9 +1,11 @@
 package eu.siacs.conversations.ui;
 
+import android.app.AlertDialog;
 import android.app.AlertDialog.Builder;
 import android.app.PendingIntent;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.text.Editable;
 import android.text.TextWatcher;
@@ -31,17 +33,20 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested;
 import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
 import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 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.Jid;
 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 EditText mPassword;
@@ -72,6 +77,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 	private ImageButton mRegenerateAxolotlKeyButton;
 	private LinearLayout keys;
 	private LinearLayout keysCard;
+	private AlertDialog mCaptchaDialog = null;
 
 	private Jid jidToEdit;
 	private boolean mInitMode = false;
@@ -681,4 +687,70 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 	public void onKeyStatusUpdated() {
 		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();
+			}
+		});
+	}
 }

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

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

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

@@ -1,10 +1,15 @@
 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.Parcelable;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
 import android.os.SystemClock;
+import android.util.Base64;
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
@@ -14,6 +19,7 @@ import org.json.JSONException;
 import org.json.JSONObject;
 import org.xmlpull.v1.XmlPullParserException;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -24,6 +30,8 @@ import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.UnknownHostException;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
 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.TagWriter;
 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.Jid;
 import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
@@ -116,6 +126,29 @@ public class XmppConnection implements Runnable {
 
 	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) {
 		this.account = account;
 		this.wakeLock = service.getPowerManager().newWakeLock(
@@ -643,6 +676,15 @@ public class XmppConnection implements Runnable {
 		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() {
 		final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
 		register.query("jabber:iq:register");
@@ -651,6 +693,7 @@ public class XmppConnection implements Runnable {
 
 			@Override
 			public void onIqPacketReceived(final Account account, final IqPacket packet) {
+				boolean failed = false;
 				if (packet.getType() == IqPacket.TYPE.RESULT
 						&& packet.query().hasChild("username")
 						&& (packet.query().hasChild("password"))) {
@@ -659,37 +702,60 @@ public class XmppConnection implements Runnable {
 					final Element password = new Element("password").setContent(account.getPassword());
 					register.query("jabber:iq:register").addChild(username);
 					register.query().addChild(password);
-					sendIqPacket(register, new OnIqPacketReceived() {
-
-						@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);
+					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) {
+
+						}
+					} 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");
-					changeStatus(Account.State.REGISTRATION_FAILED);
-					disconnect(true);
-					Log.d(Config.LOGTAG, account.getJid().toBareJid()
-							+ ": could not register. instructions are"
-							+ (instructions != null ? instructions.getContent() : ""));
+					setAccountCreationFailed((instructions != null) ? instructions.getContent() : "");
 				}
 			}
 		});
 	}
 
+	private void setAccountCreationFailed(String instructions) {
+		changeStatus(Account.State.REGISTRATION_FAILED);
+		disconnect(true);
+		Log.d(Config.LOGTAG, account.getJid().toBareJid()
+				+ ": could not register. instructions are"
+				+ instructions);
+	}
+
 	private void sendBindRequest() {
 		while(!mXmppConnectionService.areMessagesInitialized()) {
 			try {

src/main/res/values/strings.xml 🔗

@@ -527,4 +527,7 @@
 	<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="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>