From 71102c0160a590e144bd97465f0f79ee51472204 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Thu, 15 Aug 2013 14:09:29 +0200 Subject: [PATCH 01/33] Starting to add WhatsApp protocol stuff. --- .gitignore | 1 + res/values/account_editor.xml | 10 +++- res/values/connections.xml | 15 ++++++ res/xml/account_editor_wapp.xml | 51 +++++++++++++++++++ .../android/data/account/AccountProtocol.java | 9 ++++ src/com/xabber/android/ui/AccountEditor.java | 2 + 6 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 res/xml/account_editor_wapp.xml diff --git a/.gitignore b/.gitignore index 3e0e2af634..c9cdf333ac 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ gen/* *.orig .pydevproject .settings +*~ diff --git a/res/values/account_editor.xml b/res/values/account_editor.xml index ff4b40cbbe..bf67ed1d0c 100644 --- a/res/values/account_editor.xml +++ b/res/values/account_editor.xml @@ -25,6 +25,8 @@ username for gmail.com or Google Apps domain + Telephone number (with prefix) + username for livejournal.com username for qip.ru @@ -39,6 +41,8 @@ If you don\'t have Google account you may create one at http://mail.google.com\nAlso you can use your_user_name@your_google_domain + You need an already existing WhatsApp account. TODO: Add references on how to create/get password + If you don\'t have Livejournal account you may create one at http://livejournal.com If you don\'t have QIP account you may create one at http://qip.ru @@ -53,6 +57,8 @@ Google Talk + WhatsApp + LiveJournal QIP @@ -101,6 +107,8 @@ Google Talk + WhatsApp + WLM XMPP @@ -159,4 +167,4 @@ Are you sure you want to discard all the changes? Incorrect user name. Check help text below for details. - \ No newline at end of file + diff --git a/res/values/connections.xml b/res/values/connections.xml index 2e88520645..4e4042f6c1 100644 --- a/res/values/connections.xml +++ b/res/values/connections.xml @@ -16,6 +16,7 @@ xmpp gtalk wlm + wapp + + + + + + + + diff --git a/src/com/xabber/android/data/account/AccountProtocol.java b/src/com/xabber/android/data/account/AccountProtocol.java index caee5ba0d6..661af1d148 100644 --- a/src/com/xabber/android/data/account/AccountProtocol.java +++ b/src/com/xabber/android/data/account/AccountProtocol.java @@ -33,6 +33,11 @@ public enum AccountProtocol { * GTalk. */ gtalk, + + /** + * WhatsApp. + */ + wapp, /** * Windows Live Messenger. @@ -54,6 +59,8 @@ public int getNameResource() { return R.string.account_type_names_xmpp; else if (this == gtalk) return R.string.account_type_names_gtalk; + else if (this == wapp) + return R.string.account_type_names_wapp; else if (this == wlm) return R.string.account_type_names_wlm; else @@ -68,6 +75,8 @@ public int getShortResource() { return R.string.account_protocol_xmpp_title; else if (this == gtalk) return R.string.account_protocol_gtalk_title; + else if (this == wapp) + return R.string.account_protocol_wapp_title; else if (this == wlm) return R.string.account_protocol_wlm_title; else diff --git a/src/com/xabber/android/ui/AccountEditor.java b/src/com/xabber/android/ui/AccountEditor.java index 22b73c5314..fe6dc0be86 100644 --- a/src/com/xabber/android/ui/AccountEditor.java +++ b/src/com/xabber/android/ui/AccountEditor.java @@ -75,6 +75,8 @@ protected void onInflate(Bundle savedInstanceState) { addPreferencesFromResource(R.xml.account_editor_xmpp); else if (protocol == AccountProtocol.gtalk) addPreferencesFromResource(R.xml.account_editor_xmpp); + else if (protocol == AccountProtocol.wapp) + addPreferencesFromResource(R.xml.account_editor_wapp); else if (protocol == AccountProtocol.wlm) addPreferencesFromResource(R.xml.account_editor_oauth); else From 5b52c61aabd19b4c6d87d999c04ed6c4dbda2675 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Fri, 16 Aug 2013 00:09:44 +0200 Subject: [PATCH 02/33] Adding whatsapp protocol internals. Translated from libpurple implementation. --- src/net/davidgf/android/DataBuffer.java | 247 ++++++++++++++++++++++ src/net/davidgf/android/KeyGenerator.java | 115 ++++++++++ src/net/davidgf/android/MiscUtil.java | 119 +++++++++++ src/net/davidgf/android/RC4Decoder.java | 50 +++++ src/net/davidgf/android/Tree.java | 139 ++++++++++++ 5 files changed, 670 insertions(+) create mode 100644 src/net/davidgf/android/DataBuffer.java create mode 100644 src/net/davidgf/android/KeyGenerator.java create mode 100644 src/net/davidgf/android/MiscUtil.java create mode 100644 src/net/davidgf/android/RC4Decoder.java create mode 100644 src/net/davidgf/android/Tree.java diff --git a/src/net/davidgf/android/DataBuffer.java b/src/net/davidgf/android/DataBuffer.java new file mode 100644 index 0000000000..5993925f2b --- /dev/null +++ b/src/net/davidgf/android/DataBuffer.java @@ -0,0 +1,247 @@ + +/* + * JAVA WhatsApp API implementation + * Written by David Guillen Fandos (david@davidgf.net) based + * on the sources of WhatsAPI PHP implementation and whatsapp + * for libpurple. + * + * Share and enjoy! + * + */ + +import java.util.*; + +public class DataBuffer { + private byte [] buffer; + + public DataBuffer (byte [] buf) { + buffer = new byte[buf.length]; + for (int c = 0; c < buf.length; c++) + buffer[c] = buf[c]; + } + public DataBuffer () { + buffer = new byte[0]; + } + public DataBuffer (DataBuffer other) { + buffer = new byte[other.buffer.length]; + for (int c = 0; c < other.buffer.length; c++) + buffer[c] = other.buffer[c]; + } + + DataBuffer addBuf(DataBuffer other) { + DataBuffer ret = new DataBuffer(); + ret.buffer = new byte[this.buffer.length + other.buffer.length]; + + for (int c = 0; c < this.buffer.length; c++) + ret.buffer[c] = this.buffer[c]; + for (int c = 0; c < other.buffer.length; c++) + ret.buffer[c+this.buffer.length] = other.buffer[c]; + + return ret; + } + DataBuffer decodedBuffer(RC4Decoder decoder, int clength, boolean dout) { + DataBuffer deco = new DataBuffer(copyOfRange(this.buffer,0,clength)); + if (dout) decoder.cipher(&deco->buffer[0],clength-4); + else decoder.cipher(&deco->buffer[4],clength-4); + return deco; + } + DataBuffer encodedBuffer(RC4Decoder decoder, byte [] key, boolean dout) { + DataBuffer deco = *this; + decoder->cipher(&deco.buffer[0],blen); + unsigned char hmacint[4]; DataBuffer hmac; + KeyGenerator::calc_hmac(deco.buffer,blen,key,(unsigned char*)&hmacint); + hmac.addData(hmacint,4); + + if (dout) deco = deco + hmac; + else deco = hmac + deco; + + return deco; + } + byte [] getPtr() { + return buffer; + } + void addData(byte [] dta) { + byte [] newbuf = new byte[buffer.length + dta.length]; + for (int c = 0; c < buffer.length; c++) + newbuf[c] = buffer[c]; + for (int c = 0; c < dta.length; c++) + newbuf[c+buffer.length] = dta[c]; + + this.buffer = newbuf; + } + void popData(int size) { + if (buffer.length >= size) { + byte [] newbuf = new byte[buffer.length - size]; + for (int c = 0; c < newbuf.length; c++) + newbuf[c] = buffer[c+size]; + + this.buffer = newbuf; + } + } + void crunchData(int size) { + if (buffer.length >= size) { + byte [] newbuf = new byte[buffer.length - size]; + for (int c = 0; c < newbuf.length; c++) + newbuf[c] = buffer[c]; + + this.buffer = newbuf; + } + } + int getInt(int nbytes, int offset) { + //if (nbytes > blen) + // throw 0; + int ret = 0; + for (int i = 0; i < nbytes; i++) { + ret <<= 8; + ret |= (int)(buffer[i+offset]0xFF); + } + return ret; + } + void putInt(int value, int nbytes) { + //assert(nbytes > 0); + + byte [] out = new byte[nbytes]; + for (int i = 0; i < nbytes; i++) { + out[nbytes-i-1] = (byte)(value>>(i<<3))&0xFF; + } + this.addData(out); + } + int readInt(int nbytes) { + //if (nbytes > blen) + // throw 0; + int ret = getInt(nbytes,0); + popData(nbytes); + return ret; + } + int readListSize() { + //if (blen == 0) + // throw 0; + int ret; + if (buffer[0] == 0xf8 || buffer[0] == 0xf3) { + ret = (int)buffer[1]; + popData(2); + } + else if (buffer[0] == 0xf9) { + ret = getInt(2,1); + popData(3); + } + else { + // FIXME throw 0 error + ret = -1; + //printf("Parse error!!\n"); + } + return ret; + } + void writeListSize(int size) { + if (size == 0) { + putInt(0,1); + } + else if (size < 256) { + putInt(0xf8,1); + putInt(size,1); + } + else { + putInt(0xf9,1); + putInt(size,2); + } + } + String readRawString(int size) { + //if (size < 0 or size > blen) + // throw 0; + String s = new String(buffer,0,size); + popData(size); + return s; + } + String readString() { + //if (blen == 0) + // throw 0; + int type = readInt(1); + if (type > 4 && type < 0xf5) { + return getDecoded(type); + } + else if (type == 0) { + return ""; + } + else if (type == 0xfc) { + int slen = readInt(1); + return readRawString(slen); + } + else if (type == 0xfd) { + int slen = readInt(3); + return readRawString(slen); + } + else if (type == 0xfe) { + return getDecoded(readInt(1)+0xf5); + } + else if (type == 0xfa) { + String u = readString(); + String s = readString(); + + if (u.length() > 0 && s.length() > 0) + return u + "@" + s; + else if (s.length() > 0) + return s; + return ""; + } + return ""; + } + void putRawString(byte [] s) { + if (s.length < 256) { + putInt(0xfc,1); + putInt(s.length,1); + addData(s); + } + else { + putInt(0xfd,1); + putInt(s.length,3); + addData(s); + } + } + void putString(String s) { + int lu = lookupDecoded(s); + if (lu > 4 && lu < 0xf5) { + putInt(lu,1); + } + else if (s.indexOf('@') >= 0) { + String p1 = s.substring(0,s.indexOf('@')); + String p2 = s.substring(s.indexOf('@')+1); + putInt(0xfa,1); + putString(p1); + putString(p2); + } + else if (s.length() < 256) { + putInt(0xfc,1); + putInt(s.length(),1); + addData(s.getBytes()); + } + else { + putInt(0xfd,1); + putInt(s.length(),3); + addData(s.getBytes()); + } + } + boolean isList() { + //if (blen == 0) + // throw 0; + return (buffer[0] == 248 || buffer[0] == 0 || buffer[0] == 249); + } + Vector readList(WhatsappConnection c) { + Vector l; + int size = readListSize(); + while (size-- > 0) { + l.add(c.read_tree(this)); + } + return l; + } + int size() { + return buffer.length; + } + byte [] ToString() { + byte [] bb = new byte[buffer.length]; + for (int c = 0; c < buffer.length; c++) + bb[c] = buffer[c]; + return bb; + } +}; + + diff --git a/src/net/davidgf/android/KeyGenerator.java b/src/net/davidgf/android/KeyGenerator.java new file mode 100644 index 0000000000..d1335797b0 --- /dev/null +++ b/src/net/davidgf/android/KeyGenerator.java @@ -0,0 +1,115 @@ + +import java.security.*; +import java.util.*; + +public class KeyGenerator { + + private static byte [] hexmap = new byte[]{ '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; + + public static byte [] generateKeyImei(String imei, byte [] salt) { + try { + String imeir = (new StringBuilder(imei)).reverse().toString(); + + byte [] hash = MessageDigest.getInstance("MD5").digest(imeir.getBytes()); + + byte [] hashhex = new byte[32]; + for (int i = 0; i < 16; i++) { + hashhex[2*i] = hexmap[(hash[i]>>4)&0xF]; + hashhex[2*i+1] = hexmap[hash[i]&0xF]; + } + + //PKCS5_PBKDF2_HMAC_SHA1 (hashhex,32,(unsigned char*)salt,saltlen,16,20,(unsigned char*)out); + } + catch (Exception e) { + return new byte[0]; + } + } + public static byte[] generateKeyV2(String pw, byte [] salt) { + try { + byte [] decpass = MiscUtil.base64_decode(pw.getBytes()); + + //PKCS5_PBKDF2_HMAC_SHA1 (dec.c_str(),20,(unsigned char*)salt,saltlen,16,20,(unsigned char*)out); + } + catch (Exception e) { + return new byte[0]; + } + } + public static byte [] generateKeyMAC(String macaddr, byte [] salt) { + try { + macaddr = macaddr+macaddr; + + byte [] hash = MessageDigest.getInstance("MD5").digest(macaddr.getBytes()); + + byte [] hashhex = new byte[32]; + for (int i = 0; i < 16; i++) { + hashhex[2*i] = hexmap[(hash[i]>>4)&0xF]; + hashhex[2*i+1] = hexmap[hash[i]&0xF]; + } + + //PKCS5_PBKDF2_HMAC_SHA1 (hashhex,32,(unsigned char*)salt,saltlen,16,20,(unsigned char*)out); + } + catch (Exception e) { + return new byte[0]; + } + } + public static byte [] calc_hmac(byte [] data, byte [] key) { + byte [] hash = HMAC_SHA1 (data,key); + byte [] ret = new byte[4]; + for (int i = 0; i < 4; i++) + ret[i] = hash[i]; + return ret; + } + + private static byte [] HMAC_SHA1(byte [] text, byte [] key) { + try { + byte [] AppendBuf1 = new byte [text.length+64]; + + byte [] SHA1_Key = new byte[4096]; + for (int i = 0; i < 4096; i++) + SHA1_Key[i] = 0; + + byte [] m_ipad = new byte[64]; + byte [] m_opad = new byte[64]; + for (int i = 0; i < 64; i++) { + m_ipad[i] = 0x36; + m_opad[i] = 0x5c; + } + + if (key.length > 64) { + byte [] t = MessageDigest.getInstance("SHA").digest(key); + for (int i = 0; i < t.length; i++) + SHA1_Key[i] = t[i]; + } + else { + for (int i = 0; i < key.length; i++) + SHA1_Key[i] = key[i]; + } + + for (int i = 0; i < 64; i++) + m_ipad[i] ^= SHA1_Key[i]; + + for (int i = 0; i < 64; i++) + AppendBuf1[i] = m_ipad[i]; + for (int i = 0; i < text.length; i++) + AppendBuf1[i+64] = text[i]; + + byte [] szReport = MessageDigest.getInstance("SHA").digest(AppendBuf1); + + for (int j = 0; j < 64; j++) + m_opad[j] ^= SHA1_Key[j]; + + byte [] AppendBuf2 = new byte [4096]; + for (int i = 0; i < 64; i++) + AppendBuf2[i] = m_opad[i]; + for (int i = 0; i < szReport.length; i++) + AppendBuf2[i+64] = szReport[i]; + + return MessageDigest.getInstance("SHA").digest(AppendBuf2); + } + catch (Exception e) { + return new byte[0]; + } + } +}; + + diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java new file mode 100644 index 0000000000..287f541b06 --- /dev/null +++ b/src/net/davidgf/android/MiscUtil.java @@ -0,0 +1,119 @@ + +public class MiscUtil { + + private static byte [] base64_chars = new byte []{'A','B','C','D','E','F','G','H','I','J','K','L', + 'M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k', + 'l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'}; + + private static boolean is_base64(byte cc) { + for (int c = 0; c < base64_chars.length; c++) + if (base64_chars[c] == cc) + return true; + return false; + } + + private static byte findarray(byte what) { + for (int c = 0; c < base64_chars.length; c++) + if (base64_chars[c] == what) + return (byte)c; + return 0; + } + + private static byte [] concat(byte [] array, byte c) { + byte [] bb = new byte[array.length+1]; + for (int i = 0; i < array.length; i++) + bb[i] = array[i]; + + bb[array.length] = c; + + return bb; + } + + public static byte [] base64_decode(byte [] encoded_string) { + int in_len = encoded_string.length; + int i = 0; + int j = 0; + int in_ = 0; + byte [] char_array_4 = new byte [4]; + byte [] char_array_3 = new byte [3]; + byte [] ret = new byte[0]; + + while (in_len-- > 0 && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { + char_array_4[i++] = encoded_string[in_]; in_++; + if (i ==4) { + for (i = 0; i <4; i++) + char_array_4[i] = findarray(char_array_4[i]); + + char_array_3[0] = (byte)((char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >>> 4)); + char_array_3[1] = (byte)(((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >>> 2)); + char_array_3[2] = (byte)(((char_array_4[2] & 0x3) << 6) + char_array_4[3]); + + for (i = 0; (i < 3); i++) + ret = concat(ret,char_array_3[i]); + i = 0; + } + } + + if (i != 0) { + for (j = i; j <4; j++) + char_array_4[j] = 0; + + for (j = 0; j <4; j++) + char_array_4[j] = findarray(char_array_4[j]); + + char_array_3[0] = (byte)((char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >>> 4)); + char_array_3[1] = (byte)(((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >>> 2)); + char_array_3[2] = (byte)(((char_array_4[2] & 0x3) << 6) + char_array_4[3]); + + for (j = 0; (j < i - 1); j++) + ret = concat(ret,char_array_3[j]); + } + + return ret; + } + + + private static String [] dictionary = new String[] + { "","","","","", "account","ack","action","active","add","after", + "ib","all","allow","apple","audio","auth","author","available","bad-protocol","bad-request", + "before","Bell.caf","body","Boing.caf","cancel","category","challenge","chat","clean","code", + "composing","config","conflict","contacts","count","create","creation","default","delay", + "delete","delivered","deny","digest","DIGEST-MD5-1","DIGEST-MD5-2","dirty","elapsed","broadcast", + "enable","encoding","duplicate","error","event","expiration","expired","fail","failure","false", + "favorites","feature","features","field","first","free","from","g.us","get","Glass.caf","google", + "group","groups","g_notify","g_sound","Harp.caf","http://etherx.jabber.org/streams", + "http://jabber.org/protocol/chatstates","id","image","img","inactive","index","internal-server-error", + "invalid-mechanism","ip","iq","item","item-not-found","user-not-found","jabber:iq:last","jabber:iq:privacy", + "jabber:x:delay","jabber:x:event","jid","jid-malformed","kind","last","latitude","lc","leave","leave-all", + "lg","list","location","longitude","max","max_groups","max_participants","max_subject","mechanism", + "media","message","message_acks","method","microsoft","missing","modify","mute","name","nokia","none", + "not-acceptable","not-allowed","not-authorized","notification","notify","off","offline","order","owner", + "owning","paid","participant","participants","participating","password","paused","picture","pin","ping", + "platform","pop_mean_time","pop_plus_minus","port","presence","preview","probe","proceed","prop","props", + "p_o","p_t","query","raw","reason","receipt","receipt_acks","received","registration","relay", + "remote-server-timeout","remove","Replaced by new connection","request","required","resource", + "resource-constraint","response","result","retry","rim","s.whatsapp.net","s.us","seconds","server", + "server-error","service-unavailable","set","show","sid","silent","sound","stamp","unsubscribe","stat", + "status","stream:error","stream:features","subject","subscribe","success","sync","system-shutdown", + "s_o","s_t","t","text","timeout","TimePassing.caf","timestamp","to","Tri-tone.caf","true","type", + "unavailable","uri","url","urn:ietf:params:xml:ns:xmpp-sasl","urn:ietf:params:xml:ns:xmpp-stanzas", + "urn:ietf:params:xml:ns:xmpp-streams","urn:xmpp:delay","urn:xmpp:ping","urn:xmpp:receipts", + "urn:xmpp:whatsapp","urn:xmpp:whatsapp:account","urn:xmpp:whatsapp:dirty","urn:xmpp:whatsapp:mms", + "urn:xmpp:whatsapp:push","user","username","value","vcard","version","video","w","w:g","w:p","w:p:r", + "w:profile:picture","wait","x","xml-not-well-formed","xmlns","xmlns:stream","Xylophone.caf","1","WAUTH-1", + "","","","","","","","","","","","XXX","","","","","","","" }; + + public static String getDecoded(int n) { + return new String(dictionary[n & 255]); + } + int lookupDecoded(String value) { + for (int i = 0; i < 256; i++) { + if (dictionary[i].equals(value)) + return i; + } + return 0; + } + +} + + diff --git a/src/net/davidgf/android/RC4Decoder.java b/src/net/davidgf/android/RC4Decoder.java new file mode 100644 index 0000000000..fcec23ca44 --- /dev/null +++ b/src/net/davidgf/android/RC4Decoder.java @@ -0,0 +1,50 @@ + +/* + * Java WhatsApp API implementation + * Written by David Guillen Fandos (david@davidgf.net) based + * on the sources of WhatsAPI PHP implementation and whatsapp + * for libpurple. + * + * Share and enjoy! + * + */ + +public class RC4Decoder { + public int [] s; + public int i,j; + public void swap (int i, int j) { + int t = s[i]; + s[i] = s[j]; + s[j] = t; + } + public RC4Decoder(byte [] key, int drop) { + s = new int[256]; + for (int k = 0; k < 256; k++) s[k] = k; + i = j = 0; + do { + int k = key[i % key.length] & 0xFF; + j = (j + k + s[i]) & 0xFF; + swap(i,j); + i = (i+1) & 0xFF; + } while (i != 0); + i = j = 0; + + byte [] temp = new byte[drop]; + for (int k = 0; k < drop; k++) temp[k] = (byte)(k & 0xFF); + cipher(temp); + } + + public byte [] cipher (byte [] data) { + byte [] out = new byte[data.length]; + for (int c = 0; c < data.length; c++) { + i = (i+1) & 0xFF; + j = (j + s[i]) & 0xFF; + swap(i,j); + int idx = (s[i]+s[j]) & 0xFF; + out[c] = (byte)((data[c] ^ s[idx]) & 0xFF); + } + return out; + } +}; + + diff --git a/src/net/davidgf/android/Tree.java b/src/net/davidgf/android/Tree.java new file mode 100644 index 0000000000..f39f5c758a --- /dev/null +++ b/src/net/davidgf/android/Tree.java @@ -0,0 +1,139 @@ + +/* + * Java WhatsApp API implementation. + * Written by David Guillen Fandos (david@davidgf.net) based + * on the sources of WhatsAPI PHP implementation and whatsapp + * for libpurple. + * + * Share and enjoy! + * + */ + +import java.util.*; + +public class Tree { + private Map < String, String > attributes; + private Vector < Tree > children; + private String tag; + private byte [] data; + private boolean forcedata; + + public Tree(String tag) { + this.tag = tag; + this.forcedata=false; + this.data = new byte[0]; + } + public Tree(String tag, Map < String,String > attributes) { + this.tag = tag; + this.attributes = attributes; + this.forcedata = false; + this.data = new byte[0]; + } + public void forceDataWrite() { + forcedata=true; + } + public boolean forcedData() { + return forcedata; + } + public void addChild(Tree t) { + children.add(t); + } + public void setTag(String tag) { + this.tag = tag; + } + public void setAttributes(Map < String, String > attributes) { + this.attributes = attributes; + } + public void readAttributes(DataBuffer data, int size) { + int count = (size - 2 + (size % 2)) / 2; + while (count-- > 0) { + String key = data.readString(); + String value = data.readString(); + attributes.put(key,value); + } + } + public void writeAttributes(DataBuffer data) { + for (String key: attributes.keySet()) { + data.putString(key); + data.putString(attributes.get(key)); + } + } + public void setData(byte [] d) { + data = new byte[d.length]; + for (int c = 0; c < d.length; c++) + data[c] = d[c]; + } + public byte [] getData() { + byte [] b = new byte[data.length]; + for (int c = 0; c < data.length; c++) + b[c] = data[c]; + return b; + } + public String getTag() { + return tag; + } + public void setChildren(Vector < Tree > c) { + children = c; + } + public Vector < Tree > getChildren() { + return children; + } + public Map < String, String > getAttributes() { + return attributes; + } + public boolean hasAttributeValue(String at, String val) { + if (hasAttribute(at)) { + return (attributes.get(at) == val); + } + return false; + } + public boolean hasAttribute(String at) { + return (attributes.containsKey(at)); + } + public String getAttribute(String at) { + if (attributes.containsKey(at)) + return (attributes.get(at)); + return ""; + } + + public Tree getChild(String tag) { + for (int i = 0; i < children.size(); i++) { + if (children.get(i).getTag() == tag) + return children.get(i); + Tree t = children.get(i).getChild(tag); + if (t == null) + return t; + } + return null; + } + public boolean hasChild(String tag) { + for (int i = 0; i < children.size(); i++) { + if (children.get(i).getTag() == tag) + return true; + if (children.get(i).hasChild(tag)) + return true; + } + return false; + } + + String toString(int sp) { + String ret = ""; + String spacing = ""; + for (int i = 0; i < sp; i++) + spacing += " "; + ret += spacing+"Tag: "+tag+"\n"; + for (String key: attributes.keySet()) { + ret += spacing+"at["+key+"]="+attributes.get(key)+"\n"; + } + ret += spacing+"Data: "+data+"\n"; + + for (int i = 0; i < children.size(); i++) { + ret += children.get(i).toString(sp+1); + } + return ret; + } +}; + + + + From cf5e0feba28c15664ee89d08814a663e63c2b01c Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Fri, 16 Aug 2013 02:20:25 +0200 Subject: [PATCH 03/33] Putting aside some files for some time. Seems to build ok and the WhatsApp protocol appears as a valid protocol with valid options. --- project.properties | 2 +- src/net/davidgf/android/{DataBuffer.java => DataBuffer.java_} | 0 .../davidgf/android/{KeyGenerator.java => KeyGenerator.java_} | 0 src/net/davidgf/android/{Tree.java => Tree.java_} | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename src/net/davidgf/android/{DataBuffer.java => DataBuffer.java_} (100%) rename src/net/davidgf/android/{KeyGenerator.java => KeyGenerator.java_} (100%) rename src/net/davidgf/android/{Tree.java => Tree.java_} (100%) diff --git a/project.properties b/project.properties index 4d7ea1e41e..ddd0fc4187 100644 --- a/project.properties +++ b/project.properties @@ -10,4 +10,4 @@ # Indicates whether an apk should be generated for each density. split.density=false # Project target. -target=android-9 +target=android-10 diff --git a/src/net/davidgf/android/DataBuffer.java b/src/net/davidgf/android/DataBuffer.java_ similarity index 100% rename from src/net/davidgf/android/DataBuffer.java rename to src/net/davidgf/android/DataBuffer.java_ diff --git a/src/net/davidgf/android/KeyGenerator.java b/src/net/davidgf/android/KeyGenerator.java_ similarity index 100% rename from src/net/davidgf/android/KeyGenerator.java rename to src/net/davidgf/android/KeyGenerator.java_ diff --git a/src/net/davidgf/android/Tree.java b/src/net/davidgf/android/Tree.java_ similarity index 100% rename from src/net/davidgf/android/Tree.java rename to src/net/davidgf/android/Tree.java_ From a7593759836de97ba5f8247b9ec265ce45c22780 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Fri, 16 Aug 2013 17:55:49 +0200 Subject: [PATCH 04/33] More WA protocol classes. Modified XMPP connection wrapping so we can implement a WhatsappConnection class under the Connection object. --- .../data/connection/ConnectionItem.java | 8 ++-- .../data/connection/ConnectionManager.java | 4 +- .../data/connection/ConnectionThread.java | 5 ++- .../extension/attention/AttentionManager.java | 4 +- .../data/extension/muc/MUCManager.java | 3 +- .../{DataBuffer.java_ => DataBuffer.java} | 40 ++++++++++-------- .../{KeyGenerator.java_ => KeyGenerator.java} | 18 ++++++-- src/net/davidgf/android/MiscUtil.java | 2 +- .../davidgf/android/{Tree.java_ => Tree.java} | 0 .../davidgf/android/WhatsappConnection.java | 41 +++++++++++++++++++ src/org/jivesoftware/smack/Connection.java | 4 ++ .../jivesoftware/smack/XMPPConnection.java | 1 + 12 files changed, 98 insertions(+), 32 deletions(-) rename src/net/davidgf/android/{DataBuffer.java_ => DataBuffer.java} (84%) rename src/net/davidgf/android/{KeyGenerator.java_ => KeyGenerator.java} (81%) rename src/net/davidgf/android/{Tree.java_ => Tree.java} (100%) create mode 100644 src/net/davidgf/android/WhatsappConnection.java diff --git a/src/com/xabber/android/data/connection/ConnectionItem.java b/src/com/xabber/android/data/connection/ConnectionItem.java index 4fbf4008b1..825b018452 100644 --- a/src/com/xabber/android/data/connection/ConnectionItem.java +++ b/src/com/xabber/android/data/connection/ConnectionItem.java @@ -14,7 +14,7 @@ */ package com.xabber.android.data.connection; -import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.Connection; import com.xabber.android.data.Application; import com.xabber.android.data.LogManager; @@ -99,7 +99,7 @@ public String getRealJid() { ConnectionThread connectionThread = getConnectionThread(); if (connectionThread == null) return null; - XMPPConnection xmppConnection = connectionThread.getXMPPConnection(); + Connection xmppConnection = connectionThread.getXMPPConnection(); if (xmppConnection == null) return null; String user = xmppConnection.getUser(); @@ -188,7 +188,7 @@ protected void disconnect(final ConnectionThread connectionThread) { Thread thread = new Thread("Disconnection thread for " + this) { @Override public void run() { - XMPPConnection xmppConnection = connectionThread + Connection xmppConnection = connectionThread .getXMPPConnection(); if (xmppConnection != null) try { @@ -261,7 +261,7 @@ protected void onAuthorized(ConnectionThread connectionThread) { * @return true if connection thread was managed. */ private boolean onDisconnect(ConnectionThread connectionThread) { - XMPPConnection xmppConnection = connectionThread.getXMPPConnection(); + Connection xmppConnection = connectionThread.getXMPPConnection(); boolean acceptable = isManaged(connectionThread); if (xmppConnection == null) LogManager.i(this, "onClose " + acceptable); diff --git a/src/com/xabber/android/data/connection/ConnectionManager.java b/src/com/xabber/android/data/connection/ConnectionManager.java index 0d42d0179f..f74bf183b8 100644 --- a/src/com/xabber/android/data/connection/ConnectionManager.java +++ b/src/com/xabber/android/data/connection/ConnectionManager.java @@ -25,7 +25,7 @@ import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.SASLAuthentication; import org.jivesoftware.smack.SmackConfiguration; -import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ.Type; import org.jivesoftware.smack.packet.Packet; @@ -181,7 +181,7 @@ public void sendPacket(String account, Packet packet) || !connectionThread.getConnectionItem().getState() .isConnected()) throw new NetworkException(R.string.NOT_CONNECTED); - XMPPConnection xmppConnection = connectionThread.getXMPPConnection(); + Connection xmppConnection = connectionThread.getXMPPConnection(); try { xmppConnection.sendPacket(packet); } catch (IllegalStateException e) { diff --git a/src/com/xabber/android/data/connection/ConnectionThread.java b/src/com/xabber/android/data/connection/ConnectionThread.java index a57ce86d14..d037fb34a6 100644 --- a/src/com/xabber/android/data/connection/ConnectionThread.java +++ b/src/com/xabber/android/data/connection/ConnectionThread.java @@ -25,6 +25,7 @@ import javax.net.ssl.SSLException; import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.filter.PacketFilter; @@ -67,7 +68,7 @@ public class ConnectionThread implements /** * SMACK connection. */ - private XMPPConnection xmppConnection; + private Connection xmppConnection; /** * Thread holder for this connection. @@ -144,7 +145,7 @@ public Thread newThread(Runnable runnable) { started = false; } - public XMPPConnection getXMPPConnection() { + public Connection getXMPPConnection() { return xmppConnection; } diff --git a/src/com/xabber/android/data/extension/attention/AttentionManager.java b/src/com/xabber/android/data/extension/attention/AttentionManager.java index f995ba0c2e..2721759e36 100644 --- a/src/com/xabber/android/data/extension/attention/AttentionManager.java +++ b/src/com/xabber/android/data/extension/attention/AttentionManager.java @@ -18,7 +18,7 @@ import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.ConnectionCreationListener; -import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.PacketExtension; @@ -112,7 +112,7 @@ public void onSettingsChanged() { .getConnectionThread(); if (connectionThread == null) continue; - XMPPConnection xmppConnection = connectionThread + Connection xmppConnection = connectionThread .getXMPPConnection(); if (xmppConnection == null) continue; diff --git a/src/com/xabber/android/data/extension/muc/MUCManager.java b/src/com/xabber/android/data/extension/muc/MUCManager.java index 83065482fd..a6a35c6388 100644 --- a/src/com/xabber/android/data/extension/muc/MUCManager.java +++ b/src/com/xabber/android/data/extension/muc/MUCManager.java @@ -19,6 +19,7 @@ import java.util.Collections; import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Packet; @@ -279,7 +280,7 @@ public boolean inUse(final String account, final String room) { */ public void joinRoom(final String account, final String room, boolean requested) { - final XMPPConnection xmppConnection; + final Connection xmppConnection; final RoomChat roomChat; final String nickname; final String password; diff --git a/src/net/davidgf/android/DataBuffer.java_ b/src/net/davidgf/android/DataBuffer.java similarity index 84% rename from src/net/davidgf/android/DataBuffer.java_ rename to src/net/davidgf/android/DataBuffer.java index 5993925f2b..805402735c 100644 --- a/src/net/davidgf/android/DataBuffer.java_ +++ b/src/net/davidgf/android/DataBuffer.java @@ -40,22 +40,28 @@ DataBuffer addBuf(DataBuffer other) { return ret; } DataBuffer decodedBuffer(RC4Decoder decoder, int clength, boolean dout) { - DataBuffer deco = new DataBuffer(copyOfRange(this.buffer,0,clength)); - if (dout) decoder.cipher(&deco->buffer[0],clength-4); - else decoder.cipher(&deco->buffer[4],clength-4); + DataBuffer deco = new DataBuffer(Arrays.copyOfRange(this.buffer,0,clength)); + if (dout) decoder.cipher(Arrays.copyOfRange(deco.buffer,0,clength-4)); + else decoder.cipher(Arrays.copyOfRange(deco.buffer,4,clength-4)); return deco; } DataBuffer encodedBuffer(RC4Decoder decoder, byte [] key, boolean dout) { - DataBuffer deco = *this; - decoder->cipher(&deco.buffer[0],blen); - unsigned char hmacint[4]; DataBuffer hmac; - KeyGenerator::calc_hmac(deco.buffer,blen,key,(unsigned char*)&hmacint); - hmac.addData(hmacint,4); + DataBuffer deco = new DataBuffer(Arrays.copyOfRange(this.buffer,0,this.buffer.length)); - if (dout) deco = deco + hmac; - else deco = hmac + deco; + decoder.cipher(deco.buffer); + byte [] hmacint = KeyGenerator.calc_hmac(deco.buffer,key); + DataBuffer hmac = new DataBuffer(hmacint); - return deco; + DataBuffer res = new DataBuffer(); + if (dout) { + res.addBuf(deco); + res.addBuf(hmac); + }else{ + res.addBuf(hmac); + res.addBuf(deco); + } + + return res; } byte [] getPtr() { return buffer; @@ -93,7 +99,7 @@ int getInt(int nbytes, int offset) { int ret = 0; for (int i = 0; i < nbytes; i++) { ret <<= 8; - ret |= (int)(buffer[i+offset]0xFF); + ret |= (int)(buffer[i+offset] & 0xFF); } return ret; } @@ -102,7 +108,7 @@ void putInt(int value, int nbytes) { byte [] out = new byte[nbytes]; for (int i = 0; i < nbytes; i++) { - out[nbytes-i-1] = (byte)(value>>(i<<3))&0xFF; + out[nbytes-i-1] = (byte)((value>>(i<<3)) & 0xFF); } this.addData(out); } @@ -157,7 +163,7 @@ String readString() { // throw 0; int type = readInt(1); if (type > 4 && type < 0xf5) { - return getDecoded(type); + return MiscUtil.getDecoded(type); } else if (type == 0) { return ""; @@ -171,7 +177,7 @@ else if (type == 0xfd) { return readRawString(slen); } else if (type == 0xfe) { - return getDecoded(readInt(1)+0xf5); + return MiscUtil.getDecoded(readInt(1)+0xf5); } else if (type == 0xfa) { String u = readString(); @@ -198,7 +204,7 @@ void putRawString(byte [] s) { } } void putString(String s) { - int lu = lookupDecoded(s); + int lu = MiscUtil.lookupDecoded(s); if (lu > 4 && lu < 0xf5) { putInt(lu,1); } @@ -226,7 +232,7 @@ boolean isList() { return (buffer[0] == 248 || buffer[0] == 0 || buffer[0] == 249); } Vector readList(WhatsappConnection c) { - Vector l; + Vector l = new Vector(); int size = readListSize(); while (size-- > 0) { l.add(c.read_tree(this)); diff --git a/src/net/davidgf/android/KeyGenerator.java_ b/src/net/davidgf/android/KeyGenerator.java similarity index 81% rename from src/net/davidgf/android/KeyGenerator.java_ rename to src/net/davidgf/android/KeyGenerator.java index d1335797b0..fb705f9a41 100644 --- a/src/net/davidgf/android/KeyGenerator.java_ +++ b/src/net/davidgf/android/KeyGenerator.java @@ -1,9 +1,21 @@ import java.security.*; import java.util.*; +import javax.crypto.*; +import javax.crypto.spec.*; public class KeyGenerator { + public static byte [] PKCS5_PBKDF2_HMAC_SHA1(byte [] pass, byte [] salt) throws Exception { + char [] password = new char[pass.length]; + for (int i = 0; i < pass.length; i++) + password[i] = (char)(pass[i]&0xFF); + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + PBEKeySpec ks = new PBEKeySpec(password,salt,16,20); + SecretKey s = f.generateSecret(ks); + return s.getEncoded(); + } + private static byte [] hexmap = new byte[]{ '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; public static byte [] generateKeyImei(String imei, byte [] salt) { @@ -18,7 +30,7 @@ public class KeyGenerator { hashhex[2*i+1] = hexmap[hash[i]&0xF]; } - //PKCS5_PBKDF2_HMAC_SHA1 (hashhex,32,(unsigned char*)salt,saltlen,16,20,(unsigned char*)out); + return PKCS5_PBKDF2_HMAC_SHA1 (hashhex,salt); } catch (Exception e) { return new byte[0]; @@ -28,7 +40,7 @@ public static byte[] generateKeyV2(String pw, byte [] salt) { try { byte [] decpass = MiscUtil.base64_decode(pw.getBytes()); - //PKCS5_PBKDF2_HMAC_SHA1 (dec.c_str(),20,(unsigned char*)salt,saltlen,16,20,(unsigned char*)out); + return PKCS5_PBKDF2_HMAC_SHA1 (decpass,salt); } catch (Exception e) { return new byte[0]; @@ -46,7 +58,7 @@ public static byte[] generateKeyV2(String pw, byte [] salt) { hashhex[2*i+1] = hexmap[hash[i]&0xF]; } - //PKCS5_PBKDF2_HMAC_SHA1 (hashhex,32,(unsigned char*)salt,saltlen,16,20,(unsigned char*)out); + return PKCS5_PBKDF2_HMAC_SHA1 (hashhex,salt); } catch (Exception e) { return new byte[0]; diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java index 287f541b06..032b7cfc3f 100644 --- a/src/net/davidgf/android/MiscUtil.java +++ b/src/net/davidgf/android/MiscUtil.java @@ -106,7 +106,7 @@ private static byte findarray(byte what) { public static String getDecoded(int n) { return new String(dictionary[n & 255]); } - int lookupDecoded(String value) { + public static int lookupDecoded(String value) { for (int i = 0; i < 256; i++) { if (dictionary[i].equals(value)) return i; diff --git a/src/net/davidgf/android/Tree.java_ b/src/net/davidgf/android/Tree.java similarity index 100% rename from src/net/davidgf/android/Tree.java_ rename to src/net/davidgf/android/Tree.java diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java new file mode 100644 index 0000000000..961222de9b --- /dev/null +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -0,0 +1,41 @@ + + +public class WhatsappConnection { + + public Tree read_tree(DataBuffer data) { + Tree t = new Tree(""); + return t; + } + + /*int lsize = data->readListSize(); + int type = data->getInt(1); + if (type == 1) { + data->popData(1); + Tree t; + t.readAttributes(data,lsize); + t.setTag("start"); + return t; + }else if (type == 2) { + data->popData(1); + return Tree("treeerr"); // No data in this tree... + } + + Tree t; + t.setTag(data->readString()); + t.readAttributes(data,lsize); + + if ((lsize & 1) == 1) { + return t; + } + + if (data->isList()) { + t.setChildren(data->readList(this)); + }else{ + t.setData(data->readString()); + } + + return t;*/ + +} + + diff --git a/src/org/jivesoftware/smack/Connection.java b/src/org/jivesoftware/smack/Connection.java index a96567f2d1..4478072200 100644 --- a/src/org/jivesoftware/smack/Connection.java +++ b/src/org/jivesoftware/smack/Connection.java @@ -226,6 +226,10 @@ protected ConnectionConfiguration getConfiguration() { public String getServiceName() { return config.getServiceName(); } + + public boolean isAlive() { + return true; + } /** * Returns the host name of the server where the XMPP server is running. This would be the diff --git a/src/org/jivesoftware/smack/XMPPConnection.java b/src/org/jivesoftware/smack/XMPPConnection.java index 809955207b..aadd182793 100644 --- a/src/org/jivesoftware/smack/XMPPConnection.java +++ b/src/org/jivesoftware/smack/XMPPConnection.java @@ -1095,6 +1095,7 @@ public void setRosterStorage(RosterStorage storage) * * @return false if timeout occur. */ + @Override public boolean isAlive() { PacketWriter packetWriter = this.packetWriter; return packetWriter == null || packetWriter.isAlive(); From c58e5d0633b5cca34556b71e04a6fb4cd3a3b8c2 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sat, 17 Aug 2013 02:46:54 +0200 Subject: [PATCH 05/33] Fixing some bugs and adding more functions for WhatsApp protocol functionality --- src/net/davidgf/android/DataBuffer.java | 34 ++-- .../davidgf/android/WhatsappConnection.java | 169 +++++++++++++++--- 2 files changed, 167 insertions(+), 36 deletions(-) diff --git a/src/net/davidgf/android/DataBuffer.java b/src/net/davidgf/android/DataBuffer.java index 805402735c..107c01d134 100644 --- a/src/net/davidgf/android/DataBuffer.java +++ b/src/net/davidgf/android/DataBuffer.java @@ -40,10 +40,21 @@ DataBuffer addBuf(DataBuffer other) { return ret; } DataBuffer decodedBuffer(RC4Decoder decoder, int clength, boolean dout) { - DataBuffer deco = new DataBuffer(Arrays.copyOfRange(this.buffer,0,clength)); - if (dout) decoder.cipher(Arrays.copyOfRange(deco.buffer,0,clength-4)); - else decoder.cipher(Arrays.copyOfRange(deco.buffer,4,clength-4)); - return deco; + byte [] carray, array4; + if (dout) { + carray = decoder.cipher(Arrays.copyOfRange(this.buffer,0,clength-4)); + array4 = Arrays.copyOfRange(this.buffer,clength-4,clength); + DataBuffer deco = new DataBuffer(carray); + DataBuffer extra = new DataBuffer(array4); + return deco.addBuf(extra); + } + else { + carray = decoder.cipher(Arrays.copyOfRange(this.buffer,4,clength)); + array4 = Arrays.copyOfRange(this.buffer,0,4); + DataBuffer deco = new DataBuffer(carray); + DataBuffer extra = new DataBuffer(array4); + return extra.addBuf(deco); + } } DataBuffer encodedBuffer(RC4Decoder decoder, byte [] key, boolean dout) { DataBuffer deco = new DataBuffer(Arrays.copyOfRange(this.buffer,0,this.buffer.length)); @@ -52,16 +63,11 @@ DataBuffer encodedBuffer(RC4Decoder decoder, byte [] key, boolean dout) { byte [] hmacint = KeyGenerator.calc_hmac(deco.buffer,key); DataBuffer hmac = new DataBuffer(hmacint); - DataBuffer res = new DataBuffer(); - if (dout) { - res.addBuf(deco); - res.addBuf(hmac); - }else{ - res.addBuf(hmac); - res.addBuf(deco); - } - - return res; + DataBuffer res; + if (dout) + return deco.addBuf(hmac); + else + return hmac.addBuf(deco); } byte [] getPtr() { return buffer; diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 961222de9b..b7b90a4dca 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -1,41 +1,166 @@ +import java.util.*; public class WhatsappConnection { - public Tree read_tree(DataBuffer data) { - Tree t = new Tree(""); - return t; + + private RC4Decoder in, out; + private byte session_key[]; + private DataBuffer outbuffer; + + private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChallenge, SessionWaitingAuthOK, SessionConnected }; + private SessionStatus conn_status; + + private String phone, password; + private String whatsappserver; + private String whatsappservergroup; + + + public WhatsappConnection(String phone, String password, String nick) { + session_key = new byte[20]; + + this.phone = phone; + this.password = password.trim(); + this.conn_status = SessionStatus.SessionNone; + this.whatsappserver = "s.whatsapp.net"; + this.whatsappservergroup = "g.us"; } + + public Tree read_tree(DataBuffer data) { + int lsize = data.readListSize(); + int type = data.getInt(1,0); + if (type == 1) { + data.popData(1); + Tree t = new Tree("start"); + t.readAttributes(data,lsize); + return t; + }else if (type == 2) { + data.popData(1); + return new Tree("treeerr"); // No data in this tree... + } - /*int lsize = data->readListSize(); - int type = data->getInt(1); - if (type == 1) { - data->popData(1); - Tree t; + Tree t = new Tree(data.readString()); t.readAttributes(data,lsize); - t.setTag("start"); + + if ((lsize & 1) == 1) { + return t; + } + + if (data.isList()) { + t.setChildren(data.readList(this)); + }else{ + t.setData(data.readString().getBytes()); + } + return t; - }else if (type == 2) { - data->popData(1); - return Tree("treeerr"); // No data in this tree... } - Tree t; - t.setTag(data->readString()); - t.readAttributes(data,lsize); + Tree parse_tree(DataBuffer data) { + int bflag = (data.getInt(1,0) & 0xF0)>>4; + int bsize = data.getInt(2,1); + if (bsize > data.size()-3) { + return new Tree("treeerr"); // Next message incomplete, return consumed data + } + data.popData(3); + + if ((bflag & 0x8) != 0) { + // Decode data, buffer conversion + if (this.in != null) { + DataBuffer decoded_data = data.decodedBuffer(this.in,bsize,false); + + // Remove hash + decoded_data.popData(4); + + // Call recursive + data.popData(bsize); // Pop data unencrypted for next parsing! + return read_tree(decoded_data); + }else{ + data.popData(bsize); + return new Tree("treeerr"); + } + }else{ + return read_tree(data); + } + } + + public DataBuffer write_tree(Tree tree) { + DataBuffer bout = new DataBuffer(); + int len = 1; - if ((lsize & 1) == 1) { - return t; + if (tree.getAttributes().size() != 0) len += tree.getAttributes().size()*2; + if (tree.getChildren().size() != 0) len++; + if (tree.getData().length != 0 || tree.forcedData()) len++; + + bout.writeListSize(len); + if (tree.getTag() == "start") bout.putInt(1,1); + else bout.putString(tree.getTag()); + tree.writeAttributes(bout); + + if (tree.getData().length > 0 || tree.forcedData()) + bout.putRawString(tree.getData()); + if (tree.getChildren().size() > 0) { + bout.writeListSize(tree.getChildren().size()); + + for (int i = 0; i < tree.getChildren().size(); i++) { + DataBuffer tt = write_tree(tree.getChildren().get(i)); + bout = bout.addBuf(tt); + } + } + return bout; } - if (data->isList()) { - t.setChildren(data->readList(this)); - }else{ - t.setData(data->readString()); + public DataBuffer serialize_tree(Tree tree, boolean crypt) { + DataBuffer data = write_tree(tree); + int flag = 0; + if (crypt) { + data = data.encodedBuffer(this.out,this.session_key,true); + flag = 0x10; + } + + DataBuffer ret = new DataBuffer(); + ret.putInt(flag,1); + ret.putInt(data.size(),2); + return ret.addBuf(data); } - return t;*/ + + public void doLogin(String useragent) { + DataBuffer first = new DataBuffer(); + + { + Map < String,String > auth = new HashMap (); + auth.put("resource", useragent); + auth.put("to", whatsappserver); + Tree t = new Tree("start",auth); + first.addData( new byte [] {'W','A',1,2} ); + first = first.addBuf(serialize_tree(t,false)); + } + // Send features + { + Tree p = new Tree("stream:features"); + p.addChild(new Tree("receipt_acks")); + p.addChild(new Tree("w:profile:picture",new HashMap < String,String >() {{ put("type","all"); }} )); + p.addChild(new Tree("w:profile:picture",new HashMap < String,String >() {{ put("type","group"); }} )); + p.addChild(new Tree("notification",new HashMap < String,String >() {{ put("type","participant"); }} )); + p.addChild(new Tree("status")); + first = first.addBuf(serialize_tree(p,false)); + } + + // Send auth request + { + Map < String,String > auth = new HashMap (); + auth.put("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + auth.put("mechanism", "WAUTH-1"); + auth.put("user", phone); + Tree t = new Tree("auth",auth); + t.forceDataWrite(); + first = first.addBuf(serialize_tree(t,false)); + } + + conn_status = SessionStatus.SessionWaitingChallenge; + outbuffer = first; + } } From 037bd319b5cc8abdbe32e555229b7de2c836e767 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sun, 18 Aug 2013 00:32:41 +0200 Subject: [PATCH 06/33] Adding WA connection class wrapper. Need to work with Roster class to properly handle all the functionality. --- src/net/davidgf/android/WAConnection.java | 621 ++++++++++++++++++ .../davidgf/android/WhatsappConnection.java | 11 + 2 files changed, 632 insertions(+) create mode 100644 src/net/davidgf/android/WAConnection.java diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java new file mode 100644 index 0000000000..51705bd6ac --- /dev/null +++ b/src/net/davidgf/android/WAConnection.java @@ -0,0 +1,621 @@ + +/* + * WhatsApp API extension for smack-xabber + * Written by David Guillen Fandos (david@davidgf.net) based + * on the sources of WhatsAPI PHP implementation and whatsapp + * for libpurple. + * + * Share and enjoy! + * + */ + +package org.jivesoftware.smack; + +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.StringUtils; + +import org.apache.harmony.javax.security.auth.callback.Callback; +import org.apache.harmony.javax.security.auth.callback.CallbackHandler; +import org.apache.harmony.javax.security.auth.callback.PasswordCallback; +import java.io.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyStore; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Security; +import java.util.*; + +/** + * Creates a socket connection to a WA server. + * + * @see Connection + * @author David Guillen Fandos + */ +public class WAConnection extends Connection { + + /** + * The socket which is used for this connection. + */ + protected Socket socket; + + String connectionID = null; + private String user = null; + private boolean connected = false; + /** + * Flag that indicates if the user is currently authenticated with the server. + */ + private boolean authenticated = false; + /** + * Flag that indicates if the user was authenticated with the server when the connection + * to the server was closed (abruptly or not). + */ + private boolean wasAuthenticated = false; + + private OutputStream ostream; + private InputStream istream; + + private Thread writerThread, readerThread; + + byte [] inbuffer, outbuffer; + + Roster roster = null; + + /** + * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be + * performed to determine the IP address and port corresponding to the + * service name; if that lookup fails, it's assumed that server resides at + * serviceName with the default port of 443. + * This is the simplest constructor for connecting to an WA server. Alternatively, + * you can get fine-grained control over connection settings using the + * {@link #WAConnection(ConnectionConfiguration)} constructor.

+ *

+ * Note that WAConnection constructors do not establish a connection to the server + * and you must call {@link #connect()}.

+ *

+ * The CallbackHandler is ignored. + * + * @param serviceName the name of the WA server to connect to; e.g. example.com. + * @param callbackHandler ignored and should be set to null + */ + public WAConnection(String serviceName, CallbackHandler callbackHandler) { + // Create the configuration for this new connection + super(new ConnectionConfiguration(serviceName)); + config.setCompressionEnabled(false); + config.setSASLAuthenticationEnabled(true); + config.setDebuggerEnabled(DEBUG_ENABLED); + } + + /** + * Creates a new WA connection in the same way {@link #WAConnection(String,CallbackHandler)} does, but + * with no callback handler for password prompting of the keystore. + * + * @param serviceName the name of the WA server to connect to; e.g. example.com. + */ + public WAConnection(String serviceName) { + // Create the configuration for this new connection + super(new ConnectionConfiguration(serviceName)); + config.setCompressionEnabled(false); + config.setSASLAuthenticationEnabled(true); + config.setDebuggerEnabled(DEBUG_ENABLED); + } + + /** + * Creates a new WA connection in the same way {@link #WAConnection(ConnectionConfiguration,CallbackHandler)} does, but + * with no callback handler for password prompting of the keystore. + * + * @param config the connection configuration. + */ + public WAConnection(ConnectionConfiguration config) { + super(config); + } + + public WAConnection(ConnectionConfiguration config, CallbackHandler callbackHandler) { + super(config); + config.setCallbackHandler(callbackHandler); + } + + public String getConnectionID() { + if (!isConnected()) { + return null; + } + return connectionID; + } + + public String getUser() { + if (!isAuthenticated()) { + return null; + } + return user; + } + + @Override + public synchronized void login(String username, String password, String resource) throws XMPPException { + if (!isConnected()) { + throw new IllegalStateException("Not connected to server."); + } + if (authenticated) { + throw new IllegalStateException("Already logged in to server."); + } + // Do partial version of nameprep on the username. + username = username.toLowerCase().trim(); + + String response; + if (config.isSASLAuthenticationEnabled() && + saslAuthentication.hasNonAnonymousAuthentication()) { + // Authenticate using SASL + if (password != null) { + response = saslAuthentication.authenticate(username, password, resource); + } + else { + response = saslAuthentication + .authenticate(username, resource, config.getCallbackHandler()); + } + } + else { + // Authenticate using Non-SASL + response = new NonSASLAuthentication(this).authenticate(username, password, resource); + } + + // Set the user. + if (response != null) { + this.user = response; + // Update the serviceName with the one returned by the server + config.setServiceName(StringUtils.parseServer(response)); + } + else { + this.user = username + "@" + getServiceName(); + if (resource != null) { + this.user += "/" + resource; + } + } + + // Indicate that we're now authenticated. + authenticated = true; + + if (config.isRosterLoadedAtLogin()) { + // Create the roster if it is not a reconnection or roster already + // created by getRoster() + if (this.roster == null) { + if (rosterStorage == null) { + this.roster = new Roster(this); + } else { + this.roster = new Roster(this, rosterStorage); + } + } + this.roster.reload(); + } + + // Stores the authentication for future reconnection + config.setLoginInfo(username, password, resource); + + // If debugging is enabled, change the the debug window title to include the + // name we are now logged-in as. + // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger + // will be null + if (config.isDebuggerEnabled() && debugger != null) { + debugger.userHasLogged(user); + } + } + + @Override + public synchronized void loginAnonymously() throws XMPPException { + // No anonymous login supported by WA + } + + public Roster getRoster() { + // synchronize against login() + synchronized(this) { + // if connection is authenticated the roster is already set by login() + // or a previous call to getRoster() + if (!isAuthenticated()) { + if (roster == null) { + roster = new Roster(this); + } + return roster; + } + } + + if (!config.isRosterLoadedAtLogin()) { + if (roster == null) { + roster = new Roster(this); + } + roster.reload(); + } + // If this is the first time the user has asked for the roster after calling + // login, we want to wait for the server to send back the user's roster. This + // behavior shields API users from having to worry about the fact that roster + // operations are asynchronous, although they'll still have to listen for + // changes to the roster. Note: because of this waiting logic, internal + // Smack code should be wary about calling the getRoster method, and may need to + // access the roster object directly. + if (!roster.rosterInitialized) { + try { + synchronized (roster) { + long waitTime = SmackConfiguration.getPacketReplyTimeout(); + long start = System.currentTimeMillis(); + while (!roster.rosterInitialized) { + if (waitTime <= 0) { + break; + } + roster.wait(waitTime); + long now = System.currentTimeMillis(); + waitTime -= now - start; + start = now; + } + } + } + catch (InterruptedException ie) { + // Ignore. + } + } + return roster; + } + + /** + * Returns roster immediately without waiting for initialization. + * + * @return the user's roster, or null if the user has not logged in + * or has not received roster yet. + */ + public Roster getRosterImmediately() { + return roster; + } + + public boolean isConnected() { + return connected; + } + + /** + * + * Returns true if the connection to the server is using legacy SSL or has successfully + * negotiated TLS. Once TLS has been negotiatied the connection has been secured. @see #isUsingTLS. @see #isUsingSSL. + * + * @return true if a secure connection to the server. + */ + public boolean isSecureConnection() { + return false; + } + + public boolean isAuthenticated() { + return authenticated; + } + + public boolean isAnonymous() { + return false; + } + + /** + * Closes the connection by setting presence to unavailable then closing the stream to + * the XMPP server. The shutdown logic will be used during a planned disconnection or when + * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's + * packet reader, packet writer, and {@link Roster} will not be removed; thus + * connection's state is kept. + * + * @param unavailablePresence the presence packet to send during shutdown. + */ + protected void shutdown(Presence unavailablePresence) { + // Close connection + istream = null; + ostream = null; + writerThread = null; + readerThread = null; + + // Set status + authenticated = false; + connected = false; + + // Socket close + try { + if (socket != null) + socket.close(); + } + catch (Exception e) { + // Ignore. + } + socket = null; + } + + public void disconnect(Presence unavailablePresence) { + shutdown(unavailablePresence); + + if (roster != null) { + roster.cleanup(); + roster = null; + } + wasAuthenticated = false; + } + + public void sendPacket(Packet packet) { + // + } + + /** + * Registers a packet interceptor with this connection. The interceptor will be + * invoked every time a packet is about to be sent by this connection. Interceptors + * may modify the packet to be sent. A packet filter determines which packets + * will be delivered to the interceptor. + * + * @param packetInterceptor the packet interceptor to notify of packets about to be sent. + * @param packetFilter the packet filter to use. + * @deprecated replaced by {@link Connection#addPacketInterceptor(PacketInterceptor, PacketFilter)}. + */ + public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor, + PacketFilter packetFilter) { + addPacketInterceptor(packetInterceptor, packetFilter); + } + + /** + * Removes a packet interceptor. + * + * @param packetInterceptor the packet interceptor to remove. + * @deprecated replaced by {@link Connection#removePacketInterceptor(PacketInterceptor)}. + */ + public void removePacketWriterInterceptor(PacketInterceptor packetInterceptor) { + removePacketInterceptor(packetInterceptor); + } + + /** + * Registers a packet listener with this connection. The listener will be + * notified of every packet that this connection sends. A packet filter determines + * which packets will be delivered to the listener. Note that the thread + * that writes packets will be used to invoke the listeners. Therefore, each + * packet listener should complete all operations quickly or use a different + * thread for processing. + * + * @param packetListener the packet listener to notify of sent packets. + * @param packetFilter the packet filter to use. + * @deprecated replaced by {@link #addPacketSendingListener(PacketListener, PacketFilter)}. + */ + public void addPacketWriterListener(PacketListener packetListener, PacketFilter packetFilter) { + addPacketSendingListener(packetListener, packetFilter); + } + + /** + * Removes a packet listener for sending packets from this connection. + * + * @param packetListener the packet listener to remove. + * @deprecated replaced by {@link #removePacketSendingListener(PacketListener)}. + */ + public void removePacketWriterListener(PacketListener packetListener) { + removePacketSendingListener(packetListener); + } + + private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException { + String host = config.getHost(); + int port = config.getPort(); + try { + if (config.getSocketFactory() == null) { + this.socket = new Socket(host, port); + } + else { + this.socket = config.getSocketFactory().createSocket(host, port); + } + } + catch (UnknownHostException uhe) { + String errorMessage = "Could not connect to " + host + ":" + port + "."; + throw new XMPPException(errorMessage, new XMPPError( + XMPPError.Condition.remote_server_timeout, errorMessage), + uhe); + } + catch (IOException ioe) { + String errorMessage = "XMPPError connecting to " + host + ":" + + port + "."; + throw new XMPPException(errorMessage, new XMPPError( + XMPPError.Condition.remote_server_error, errorMessage), ioe); + } + initConnection(); + } + + /** + * Initializes the connection by creating a packet reader and writer and opening a + * XMPP stream to the server. + * + * @throws XMPPException if establishing a connection to the server fails. + */ + private void initConnection() throws XMPPException { + // Set the stream reader/writer + initReaderAndWriter(); + + // Spawn reader and writer threads + writerThread = new Thread() { + public void run() { + writePackets(this,ostream); + } + }; + writerThread.setName("Socket data writer"); + writerThread.setDaemon(true); + + readerThread = new Thread() { + public void run() { + readPackets(this,istream); + } + }; + readerThread.setName("Socket data reader"); + readerThread.setDaemon(true); + + // Make note of the fact that we're now connected. + connected = true; + + for (ConnectionCreationListener listener : getConnectionCreationListeners()) { + listener.connectionCreated(this); + } + } + + private void initReaderAndWriter() throws XMPPException { + try { + istream = socket.getInputStream(); + ostream = socket.getOutputStream(); + } + catch (IOException ioe) { + throw new XMPPException( + "XMPPError establishing connection with server.", + new XMPPError(XMPPError.Condition.remote_server_error, + "XMPPError establishing connection with server."), + ioe); + } + + // If debugging is enabled, we open a window and write out all network traffic. + initDebugger(); + + reader = new AliveReader(reader); + } + + public boolean isUsingCompression() { + return false; + } + + private void writePackets(Thread thisThread, OutputStream ostream) { + + } + + private void readPackets(Thread thisThread, InputStream istream) { + try { + int r; + do { + byte [] buf = new byte[1024]; + r = istream.read(buf,0,buf.length); + if (r > 0) { + synchronized (inbuffer) { + // Extend the array size and add the new bytes + inbuffer = Arrays.copyOf(inbuffer, inbuffer.length + r); + Arrays.copyOfRange(inbuffer,inbuffer.length-r,r); + System.arraycopy(buf,0, inbuffer,inbuffer.length-r, r); + } + } + } while (r >= 0); + }catch (IOException e) { + // + } + } + + /** + * Establishes a connection to the XMPP server and performs an automatic login + * only if the previous connection state was logged (authenticated). It basically + * creates and maintains a socket connection to the server.

+ *

+ * Listeners will be preserved from a previous connection if the reconnection + * occurs after an abrupt termination. + * + * @throws XMPPException if an error occurs while trying to establish the connection. + * Two possible errors can occur which will be wrapped by an XMPPException -- + * UnknownHostException (XMPP error code 504), and IOException (XMPP error code + * 502). The error codes and wrapped exceptions can be used to present more + * appropiate error messages to end-users. + */ + public void connect() throws XMPPException { + // Stablishes the connection, readers and writers + connectUsingConfiguration(config); + // Automatically makes the login if the user was previouslly connected successfully + // to the server and the connection was terminated abruptly + if (connected && wasAuthenticated) { + // Make the login + try { + login(config.getUsername(), config.getPassword(), config.getResource()); + } + catch (XMPPException e) { + e.printStackTrace(); + } + } + } + + /** + * Sets whether the connection has already logged in the server. + * + * @param wasAuthenticated true if the connection has already been authenticated. + */ + private void setWasAuthenticated(boolean wasAuthenticated) { + if (!this.wasAuthenticated) { + this.wasAuthenticated = wasAuthenticated; + } + } + + @Override + public void setRosterStorage(RosterStorage storage) + throws IllegalStateException { + if(roster!=null){ + throw new IllegalStateException("Roster is already initialized"); + } + this.rosterStorage = storage; + } + + /** + * Returns whether connection with server is alive. + * + * @return false if timeout occur. + */ + @Override + public boolean isAlive() { + //PacketWriter packetWriter = this.packetWriter; + //return packetWriter == null || packetWriter.isAlive(); + // FIXME + return true; + } + + /** + * Wrapper for server keep alive. + * + * @author alexander.ivanov + * + */ + private class AliveReader extends Reader { + final Reader wrappedReader; + + public AliveReader(Reader wrappedReader) { + this.wrappedReader = wrappedReader; + } + + private void onRead() { + //packetWriter.responseReceived(); + } + + @Override + public int read(char[] buf, int offset, int count) throws IOException { + final int result = wrappedReader.read(buf, offset, count); + onRead(); + return result; + } + + public void close() throws IOException { + wrappedReader.close(); + } + + public int read() throws IOException { + final int result = wrappedReader.read(); + onRead(); + return result; + } + + public int read(char buf[]) throws IOException { + final int result = wrappedReader.read(buf); + onRead(); + return result; + } + + public long skip(long n) throws IOException { + return wrappedReader.skip(n); + } + + public boolean ready() throws IOException { + return wrappedReader.ready(); + } + + public boolean markSupported() { + return wrappedReader.markSupported(); + } + + public void mark(int readAheadLimit) throws IOException { + wrappedReader.mark(readAheadLimit); + } + + public void reset() throws IOException { + wrappedReader.reset(); + } + } +} + diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index b7b90a4dca..20146a4782 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -1,4 +1,15 @@ +/* + * Java WhatsApp API implementation. + * Written by David Guillen Fandos (david@davidgf.net) based + * on the sources of WhatsAPI PHP implementation and whatsapp + * for libpurple. + * + * Share and enjoy! + * + */ + + import java.util.*; public class WhatsappConnection { From 288f9f32c49c63fc5bad8c95c4adc82ba19a3556 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sun, 18 Aug 2013 01:54:22 +0200 Subject: [PATCH 07/33] Adding message serialization and socket read/write stuff. --- src/net/davidgf/android/DataBuffer.java | 2 + src/net/davidgf/android/KeyGenerator.java | 2 + src/net/davidgf/android/MiscUtil.java | 2 + src/net/davidgf/android/RC4Decoder.java | 2 + src/net/davidgf/android/Tree.java | 2 + src/net/davidgf/android/WAConnection.java | 50 +++++++++++++++++-- .../davidgf/android/WhatsappConnection.java | 37 +++++++++++--- .../jivesoftware/smack/packet/Message.java | 1 - 8 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/net/davidgf/android/DataBuffer.java b/src/net/davidgf/android/DataBuffer.java index 107c01d134..e54b14e71f 100644 --- a/src/net/davidgf/android/DataBuffer.java +++ b/src/net/davidgf/android/DataBuffer.java @@ -9,6 +9,8 @@ * */ +package net.davidgf.android; + import java.util.*; public class DataBuffer { diff --git a/src/net/davidgf/android/KeyGenerator.java b/src/net/davidgf/android/KeyGenerator.java index fb705f9a41..9547b26387 100644 --- a/src/net/davidgf/android/KeyGenerator.java +++ b/src/net/davidgf/android/KeyGenerator.java @@ -1,4 +1,6 @@ +package net.davidgf.android; + import java.security.*; import java.util.*; import javax.crypto.*; diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java index 032b7cfc3f..cf9bcce1ce 100644 --- a/src/net/davidgf/android/MiscUtil.java +++ b/src/net/davidgf/android/MiscUtil.java @@ -1,4 +1,6 @@ +package net.davidgf.android; + public class MiscUtil { private static byte [] base64_chars = new byte []{'A','B','C','D','E','F','G','H','I','J','K','L', diff --git a/src/net/davidgf/android/RC4Decoder.java b/src/net/davidgf/android/RC4Decoder.java index fcec23ca44..c33acd375d 100644 --- a/src/net/davidgf/android/RC4Decoder.java +++ b/src/net/davidgf/android/RC4Decoder.java @@ -8,6 +8,8 @@ * Share and enjoy! * */ + +package net.davidgf.android; public class RC4Decoder { public int [] s; diff --git a/src/net/davidgf/android/Tree.java b/src/net/davidgf/android/Tree.java index f39f5c758a..8ea339fe72 100644 --- a/src/net/davidgf/android/Tree.java +++ b/src/net/davidgf/android/Tree.java @@ -9,6 +9,8 @@ * */ +package net.davidgf.android; + import java.util.*; public class Tree { diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index 51705bd6ac..dfb2db529a 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -16,6 +16,9 @@ import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.packet.Message; + +import net.davidgf.android.WhatsappConnection; import org.apache.harmony.javax.security.auth.callback.Callback; import org.apache.harmony.javax.security.auth.callback.CallbackHandler; @@ -63,6 +66,9 @@ public class WAConnection extends Connection { private Thread writerThread, readerThread; byte [] inbuffer, outbuffer; + WhatsappConnection waconnection; + + int msgid; Roster roster = null; @@ -201,6 +207,11 @@ public synchronized void login(String username, String password, String resource if (config.isDebuggerEnabled() && debugger != null) { debugger.userHasLogged(user); } + + // Create WA connection API object + // FIXME: Set proper nickname + msgid = 0; + waconnection = new WhatsappConnection(config.getUsername(), config.getPassword(), config.getUsername()); } @Override @@ -305,6 +316,8 @@ protected void shutdown(Presence unavailablePresence) { ostream = null; writerThread = null; readerThread = null; + outbuffer = null; + inbuffer = null; // Set status authenticated = false; @@ -332,7 +345,20 @@ public void disconnect(Presence unavailablePresence) { } public void sendPacket(Packet packet) { - // + // If the packet if a Message, serialize and send it! + //return WhatsappConnection.serializeMessage(getTo(),getMessageBody().message); + if (packet instanceof Message) { + Message m = (Message)packet; + synchronized (outbuffer) { + msgid++; + byte [] msg = waconnection.serializeMessage(m.getTo(), m.getBody(null), msgid); + + // Put data in the output buffer + outbuffer = Arrays.copyOf(outbuffer, outbuffer.length + msg.length); + System.arraycopy(msg,0, outbuffer,outbuffer.length-msg.length, msg.length); + } + outbuffer.notify(); + } } /** @@ -421,6 +447,8 @@ private void connectUsingConfiguration(ConnectionConfiguration config) throws XM private void initConnection() throws XMPPException { // Set the stream reader/writer initReaderAndWriter(); + outbuffer = new byte[0]; + inbuffer = new byte[0]; // Spawn reader and writer threads writerThread = new Thread() { @@ -471,7 +499,24 @@ public boolean isUsingCompression() { } private void writePackets(Thread thisThread, OutputStream ostream) { - + try { + while (outbuffer != null) { + outbuffer.wait(); + if (outbuffer.length > 0) { + // Try to write the whole buffer + byte [] t; + synchronized (outbuffer) { + t = Arrays.copyOf(outbuffer,outbuffer.length); + } + ostream.write(t,0,t.length); + synchronized (outbuffer) { + // Pop the written data (the outbuffer may grow while writing t + outbuffer = Arrays.copyOfRange(outbuffer,t.length,outbuffer.length); + } + } + } + }catch (Exception e) { + } } private void readPackets(Thread thisThread, InputStream istream) { @@ -484,7 +529,6 @@ private void readPackets(Thread thisThread, InputStream istream) { synchronized (inbuffer) { // Extend the array size and add the new bytes inbuffer = Arrays.copyOf(inbuffer, inbuffer.length + r); - Arrays.copyOfRange(inbuffer,inbuffer.length-r,r); System.arraycopy(buf,0, inbuffer,inbuffer.length-r, r); } } diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 20146a4782..952a3e25d7 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -9,12 +9,11 @@ * */ +package net.davidgf.android; import java.util.*; public class WhatsappConnection { - - private RC4Decoder in, out; private byte session_key[]; private DataBuffer outbuffer; @@ -23,8 +22,8 @@ private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChall private SessionStatus conn_status; private String phone, password; - private String whatsappserver; - private String whatsappservergroup; + private static String whatsappserver = "s.whatsapp.net"; + private static String whatsappservergroup = "g.us"; public WhatsappConnection(String phone, String password, String nick) { @@ -33,8 +32,6 @@ public WhatsappConnection(String phone, String password, String nick) { this.phone = phone; this.password = password.trim(); this.conn_status = SessionStatus.SessionNone; - this.whatsappserver = "s.whatsapp.net"; - this.whatsappservergroup = "g.us"; } public Tree read_tree(DataBuffer data) { @@ -172,6 +169,34 @@ public void doLogin(String useragent) { conn_status = SessionStatus.SessionWaitingChallenge; outbuffer = first; } + + // Helper for Message class + public byte [] serializeMessage(final String to, String message, int id) { + try { + Tree request = new Tree ("request",new HashMap < String,String >() {{ put("xmlns","urn:xmpp:receipts"); }} ); + Tree notify = new Tree ("notify", new HashMap < String,String >() {{ put("xmlns","urn:xmpp:whatsapp"); put("name",to); }} ); + Tree xhash = new Tree ("x", new HashMap < String,String >() {{ put("xmlns","jabber:x:event"); }} ); + xhash.addChild(new Tree("server")); + Tree tbody = new Tree("body"); + tbody.setData(message.getBytes("UTF-8")); + + long epoch = System.currentTimeMillis()/1000; + String stime = String.valueOf(epoch); + Map < String,String > attrs = new HashMap (); + attrs.put("to",to+"@"+whatsappserver); + attrs.put("type","chat"); + attrs.put("id",stime+"-"+String.valueOf(id)); + attrs.put("t",stime); + + Tree mes = new Tree("message",attrs); + mes.addChild(xhash); mes.addChild(notify); + mes.addChild(request); mes.addChild(tbody); + + return serialize_tree(mes,true).getPtr(); + }catch (Exception e) { + return new byte[0]; + } + } } diff --git a/src/org/jivesoftware/smack/packet/Message.java b/src/org/jivesoftware/smack/packet/Message.java index d28a9f4848..0ca9037c16 100644 --- a/src/org/jivesoftware/smack/packet/Message.java +++ b/src/org/jivesoftware/smack/packet/Message.java @@ -470,7 +470,6 @@ public String toXML() { return buf.toString(); } - public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; From 16ef9e18cb011a56d725a0d71d01f7cb6ab132ed Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Wed, 21 Aug 2013 03:13:57 +0200 Subject: [PATCH 08/33] Fixed many bugs and added integration in the system. It seems that the packet parsing is not working properly :( Need to fix those errors --- .../data/connection/ConnectionThread.java | 16 +- src/net/davidgf/android/Tree.java | 3 + src/net/davidgf/android/WAConnection.java | 146 ++++++++++-------- .../davidgf/android/WhatsappConnection.java | 30 +++- src/org/jivesoftware/smack/Connection.java | 4 + 5 files changed, 130 insertions(+), 69 deletions(-) diff --git a/src/com/xabber/android/data/connection/ConnectionThread.java b/src/com/xabber/android/data/connection/ConnectionThread.java index d037fb34a6..1351c6459b 100644 --- a/src/com/xabber/android/data/connection/ConnectionThread.java +++ b/src/com/xabber/android/data/connection/ConnectionThread.java @@ -22,6 +22,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jivesoftware.smack.WAConnection; + import javax.net.ssl.SSLException; import org.jivesoftware.smack.ConnectionConfiguration; @@ -319,13 +321,22 @@ private void onReady(final InetAddress address, final int port) { connectionConfiguration.setSecurityMode(tlsMode.getSecurityMode()); connectionConfiguration.setCompressionEnabled(compression); - xmppConnection = new XMPPConnection(connectionConfiguration); + // Create different underlying classes depending on the protocol + AccountProtocol proto = connectionItem.getConnectionSettings().getProtocol(); + if (proto == AccountProtocol.wapp) { + System.out.println("Creating WA connection...\n"); + xmppConnection = new WAConnection(connectionConfiguration); + }else{ + xmppConnection = new XMPPConnection(connectionConfiguration); + System.out.println("Creating WA connection...\n"); + } xmppConnection.addPacketListener(this, ACCEPT_ALL); xmppConnection.forceAddConnectionListener(this); connectionItem.onSRVResolved(this); final String password = OAuthManager.getInstance().getPassword( protocol, token); if (password != null) { + System.out.println("Connection pass "+password+"\n"); runOnConnectionThread(new Runnable() { @Override public void run() { @@ -417,7 +428,8 @@ public void run() { */ private void connect(final String password) { try { - xmppConnection.connect(); + ConnectionSettings connectionSettings = connectionItem.getConnectionSettings(); + xmppConnection.connect(connectionSettings.getUserName(), password, connectionSettings.getResource()); } catch (XMPPException e) { checkForCertificateError(e); if (!checkForSeeOtherHost(e)) { diff --git a/src/net/davidgf/android/Tree.java b/src/net/davidgf/android/Tree.java index 8ea339fe72..de679b8a1c 100644 --- a/src/net/davidgf/android/Tree.java +++ b/src/net/davidgf/android/Tree.java @@ -24,12 +24,15 @@ public Tree(String tag) { this.tag = tag; this.forcedata=false; this.data = new byte[0]; + children = new Vector < Tree > (); + attributes = new HashMap < String, String > (); } public Tree(String tag, Map < String,String > attributes) { this.tag = tag; this.attributes = attributes; this.forcedata = false; this.data = new byte[0]; + children = new Vector < Tree > (); } public void forceDataWrite() { forcedata=true; diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index dfb2db529a..bee0853d33 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -33,6 +33,7 @@ import java.security.SecureRandom; import java.security.Security; import java.util.*; +import java.util.concurrent.Semaphore; /** * Creates a socket connection to a WA server. @@ -54,21 +55,22 @@ public class WAConnection extends Connection { * Flag that indicates if the user is currently authenticated with the server. */ private boolean authenticated = false; - /** - * Flag that indicates if the user was authenticated with the server when the connection - * to the server was closed (abruptly or not). - */ - private boolean wasAuthenticated = false; private OutputStream ostream; private InputStream istream; private Thread writerThread, readerThread; - byte [] inbuffer, outbuffer; + byte [] inbuffer; + byte [] outbuffer; + byte [] outbuffer_mutex; WhatsappConnection waconnection; + Semaphore readwait,writewait; + int msgid; + + private static final String waUA = "WhatsApp/2.10.750 Android/4.2.1 Device/GalaxyS3"; Roster roster = null; @@ -95,6 +97,7 @@ public WAConnection(String serviceName, CallbackHandler callbackHandler) { config.setCompressionEnabled(false); config.setSASLAuthenticationEnabled(true); config.setDebuggerEnabled(DEBUG_ENABLED); + outbuffer_mutex = new byte[1]; } /** @@ -109,6 +112,9 @@ public WAConnection(String serviceName) { config.setCompressionEnabled(false); config.setSASLAuthenticationEnabled(true); config.setDebuggerEnabled(DEBUG_ENABLED); + outbuffer_mutex = new byte[1]; + readwait = new Semaphore(0); + writewait = new Semaphore(0); } /** @@ -119,11 +125,17 @@ public WAConnection(String serviceName) { */ public WAConnection(ConnectionConfiguration config) { super(config); + outbuffer_mutex = new byte[1]; + readwait = new Semaphore(0); + writewait = new Semaphore(0); } public WAConnection(ConnectionConfiguration config, CallbackHandler callbackHandler) { super(config); config.setCallbackHandler(callbackHandler); + outbuffer_mutex = new byte[1]; + readwait = new Semaphore(0); + writewait = new Semaphore(0); } public String getConnectionID() { @@ -151,35 +163,8 @@ public synchronized void login(String username, String password, String resource // Do partial version of nameprep on the username. username = username.toLowerCase().trim(); - String response; - if (config.isSASLAuthenticationEnabled() && - saslAuthentication.hasNonAnonymousAuthentication()) { - // Authenticate using SASL - if (password != null) { - response = saslAuthentication.authenticate(username, password, resource); - } - else { - response = saslAuthentication - .authenticate(username, resource, config.getCallbackHandler()); - } - } - else { - // Authenticate using Non-SASL - response = new NonSASLAuthentication(this).authenticate(username, password, resource); - } - - // Set the user. - if (response != null) { - this.user = response; - // Update the serviceName with the one returned by the server - config.setServiceName(StringUtils.parseServer(response)); - } - else { - this.user = username + "@" + getServiceName(); - if (resource != null) { - this.user += "/" + resource; - } - } + this.user = username + "@" + getServiceName(); + this.user += "/" + resource; // Indicate that we're now authenticated. authenticated = true; @@ -197,9 +182,6 @@ public synchronized void login(String username, String password, String resource this.roster.reload(); } - // Stores the authentication for future reconnection - config.setLoginInfo(username, password, resource); - // If debugging is enabled, change the the debug window title to include the // name we are now logged-in as. // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger @@ -208,10 +190,11 @@ public synchronized void login(String username, String password, String resource debugger.userHasLogged(user); } - // Create WA connection API object - // FIXME: Set proper nickname - msgid = 0; - waconnection = new WhatsappConnection(config.getUsername(), config.getPassword(), config.getUsername()); + // Start login! + synchronized (waconnection) { + waconnection.doLogin(waUA); + } + popWriteData(); } @Override @@ -341,7 +324,6 @@ public void disconnect(Presence unavailablePresence) { roster.cleanup(); roster = null; } - wasAuthenticated = false; } public void sendPacket(Packet packet) { @@ -349,15 +331,28 @@ public void sendPacket(Packet packet) { //return WhatsappConnection.serializeMessage(getTo(),getMessageBody().message); if (packet instanceof Message) { Message m = (Message)packet; - synchronized (outbuffer) { + synchronized (outbuffer_mutex) { msgid++; byte [] msg = waconnection.serializeMessage(m.getTo(), m.getBody(null), msgid); // Put data in the output buffer outbuffer = Arrays.copyOf(outbuffer, outbuffer.length + msg.length); System.arraycopy(msg,0, outbuffer,outbuffer.length-msg.length, msg.length); + writewait.release(); } - outbuffer.notify(); + } + } + + private void popWriteData() { + // Fill outbuffer with fresh data ready to be written + byte [] data; + synchronized (waconnection) { + data = waconnection.getWriteData(); + } + synchronized (outbuffer_mutex) { + outbuffer = Arrays.copyOf(outbuffer, outbuffer.length + data.length); + System.arraycopy(data,0, outbuffer,outbuffer.length-data.length, data.length); + writewait.release(); } } @@ -415,6 +410,14 @@ public void removePacketWriterListener(PacketListener packetListener) { private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException { String host = config.getHost(); int port = config.getPort(); + + System.out.println("Connecting! User: "+config.getUsername() + " pass: "+config.getPassword()+"\n"); + System.out.println("Connecting! Host: "+host + " Port: "+String.valueOf(port)+"\n"); + // Create WA connection API object + // FIXME: Set proper nickname + msgid = 0; + waconnection = new WhatsappConnection(config.getUsername(), config.getPassword(), config.getUsername()); + try { if (config.getSocketFactory() == null) { this.socket = new Socket(host, port); @@ -436,6 +439,9 @@ private void connectUsingConfiguration(ConnectionConfiguration config) throws XM XMPPError.Condition.remote_server_error, errorMessage), ioe); } initConnection(); + connected = true; + + System.out.println("Connected!\n"); } /** @@ -458,6 +464,7 @@ public void run() { }; writerThread.setName("Socket data writer"); writerThread.setDaemon(true); + writerThread.start(); readerThread = new Thread() { public void run() { @@ -466,6 +473,7 @@ public void run() { }; readerThread.setName("Socket data reader"); readerThread.setDaemon(true); + readerThread.start(); // Make note of the fact that we're now connected. connected = true; @@ -488,10 +496,7 @@ private void initReaderAndWriter() throws XMPPException { ioe); } - // If debugging is enabled, we open a window and write out all network traffic. - initDebugger(); - - reader = new AliveReader(reader); + //reader = new AliveReader(reader); } public boolean isUsingCompression() { @@ -501,28 +506,33 @@ public boolean isUsingCompression() { private void writePackets(Thread thisThread, OutputStream ostream) { try { while (outbuffer != null) { - outbuffer.wait(); if (outbuffer.length > 0) { // Try to write the whole buffer + System.out.println("Writing packets...\n"); byte [] t; - synchronized (outbuffer) { + synchronized (outbuffer_mutex) { t = Arrays.copyOf(outbuffer,outbuffer.length); } ostream.write(t,0,t.length); - synchronized (outbuffer) { + synchronized (outbuffer_mutex) { // Pop the written data (the outbuffer may grow while writing t outbuffer = Arrays.copyOfRange(outbuffer,t.length,outbuffer.length); } } + System.out.println("Sleeping while no packets are availbles...\n"); + writewait.acquire(); } }catch (Exception e) { + System.out.println("Error!\n" + e.toString()); } + System.out.println("Exiting writepackets thread (WA)\n"); } private void readPackets(Thread thisThread, InputStream istream) { try { int r; do { + System.out.println("Reading packets...\n"); byte [] buf = new byte[1024]; r = istream.read(buf,0,buf.length); if (r > 0) { @@ -532,10 +542,19 @@ private void readPackets(Thread thisThread, InputStream istream) { System.arraycopy(buf,0, inbuffer,inbuffer.length-r, r); } } + + // Proceed to push data to underlying connection class + synchronized ( waconnection ) { + synchronized (inbuffer) { + int used = waconnection.pushIncomingData(inbuffer); + inbuffer = Arrays.copyOf(inbuffer, inbuffer.length - used); + } + } } while (r >= 0); }catch (IOException e) { - // + System.out.println("Error!\n" + e.toString()); } + System.out.println("Exiting readpackets thread (WA)\n"); } /** @@ -552,12 +571,20 @@ private void readPackets(Thread thisThread, InputStream istream) { * 502). The error codes and wrapped exceptions can be used to present more * appropiate error messages to end-users. */ + + // Specify pass at connect time + @Override + public void connect(final String user, final String pass, final String res) throws XMPPException { + config.setLoginInfo(user, pass, res); + connect(); + } + public void connect() throws XMPPException { // Stablishes the connection, readers and writers connectUsingConfiguration(config); // Automatically makes the login if the user was previouslly connected successfully // to the server and the connection was terminated abruptly - if (connected && wasAuthenticated) { + if (connected) { // Make the login try { login(config.getUsername(), config.getPassword(), config.getResource()); @@ -568,17 +595,6 @@ public void connect() throws XMPPException { } } - /** - * Sets whether the connection has already logged in the server. - * - * @param wasAuthenticated true if the connection has already been authenticated. - */ - private void setWasAuthenticated(boolean wasAuthenticated) { - if (!this.wasAuthenticated) { - this.wasAuthenticated = wasAuthenticated; - } - } - @Override public void setRosterStorage(RosterStorage storage) throws IllegalStateException { diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 952a3e25d7..5238cef456 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -26,12 +26,13 @@ private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChall private static String whatsappservergroup = "g.us"; - public WhatsappConnection(String phone, String password, String nick) { + public WhatsappConnection(String phone, String pass, String nick) { session_key = new byte[20]; this.phone = phone; - this.password = password.trim(); + this.password = pass.trim(); this.conn_status = SessionStatus.SessionNone; + outbuffer = new DataBuffer(); } public Tree read_tree(DataBuffer data) { @@ -91,6 +92,25 @@ Tree parse_tree(DataBuffer data) { } } + public int pushIncomingData(byte [] data) { + Vector treelist = new Vector (); + if (data.length < 3) return 0; + + DataBuffer db = new DataBuffer(data); + + Tree t; + do { + t = this.parse_tree(db); + if (t.getTag() != "treeerr") + treelist.add(t); + + System.out.println("Received tree!\n"); + System.out.println(t.toString(0)); + } while (t.getTag() != "treeerr" && db.size() >= 3); + + return data.length - db.size(); + } + public DataBuffer write_tree(Tree tree) { DataBuffer bout = new DataBuffer(); int len = 1; @@ -170,6 +190,12 @@ public void doLogin(String useragent) { outbuffer = first; } + public byte [] getWriteData() { + byte [] r = outbuffer.getPtr(); + outbuffer = new DataBuffer(); + return r; + } + // Helper for Message class public byte [] serializeMessage(final String to, String message, int id) { try { diff --git a/src/org/jivesoftware/smack/Connection.java b/src/org/jivesoftware/smack/Connection.java index 4478072200..f3fd899388 100644 --- a/src/org/jivesoftware/smack/Connection.java +++ b/src/org/jivesoftware/smack/Connection.java @@ -333,6 +333,10 @@ protected boolean isReconnectionAllowed() { * @throws XMPPException if an error occurs while trying to establish the connection. */ public abstract void connect() throws XMPPException; + + public void connect(final String user, final String pass, final String res) throws XMPPException { + connect(); + } /** * Logs in to the server using the strongest authentication mode supported by From b4a2ea94b1f704ad3f21a6b5cce27a7593af821d Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Wed, 21 Aug 2013 16:32:36 +0200 Subject: [PATCH 09/33] Fixed many bugs. Now we are able to communicate with the servers and send the auth packets. Need to add disconnect/auth failed callbacks to notify UI in case of wrong auth. Also need to signal the UI when the auth has finished and we are connected. --- .../data/connection/ConnectionThread.java | 2 +- src/net/davidgf/android/DataBuffer.java | 14 ++-- src/net/davidgf/android/WAConnection.java | 23 ++++-- .../davidgf/android/WhatsappConnection.java | 81 ++++++++++++++++++- 4 files changed, 103 insertions(+), 17 deletions(-) diff --git a/src/com/xabber/android/data/connection/ConnectionThread.java b/src/com/xabber/android/data/connection/ConnectionThread.java index 1351c6459b..8733dca2bc 100644 --- a/src/com/xabber/android/data/connection/ConnectionThread.java +++ b/src/com/xabber/android/data/connection/ConnectionThread.java @@ -325,7 +325,7 @@ private void onReady(final InetAddress address, final int port) { AccountProtocol proto = connectionItem.getConnectionSettings().getProtocol(); if (proto == AccountProtocol.wapp) { System.out.println("Creating WA connection...\n"); - xmppConnection = new WAConnection(connectionConfiguration); + xmppConnection = new WAConnection(this, connectionConfiguration); }else{ xmppConnection = new XMPPConnection(connectionConfiguration); System.out.println("Creating WA connection...\n"); diff --git a/src/net/davidgf/android/DataBuffer.java b/src/net/davidgf/android/DataBuffer.java index e54b14e71f..0f41574d18 100644 --- a/src/net/davidgf/android/DataBuffer.java +++ b/src/net/davidgf/android/DataBuffer.java @@ -85,11 +85,7 @@ void addData(byte [] dta) { } void popData(int size) { if (buffer.length >= size) { - byte [] newbuf = new byte[buffer.length - size]; - for (int c = 0; c < newbuf.length; c++) - newbuf[c] = buffer[c+size]; - - this.buffer = newbuf; + buffer = Arrays.copyOfRange(buffer,size,buffer.length); } } void crunchData(int size) { @@ -131,11 +127,12 @@ int readListSize() { //if (blen == 0) // throw 0; int ret; - if (buffer[0] == 0xf8 || buffer[0] == 0xf3) { + System.out.println("byte " + String.valueOf((int)buffer[0])+ "\n"); + if (buffer[0] == (byte)0xf8 || buffer[0] == (byte)0xf3) { ret = (int)buffer[1]; popData(2); } - else if (buffer[0] == 0xf9) { + else if (buffer[0] == (byte)0xf9) { ret = getInt(2,1); popData(3); } @@ -170,6 +167,7 @@ String readString() { //if (blen == 0) // throw 0; int type = readInt(1); + System.out.println("readstr " + String.valueOf(type) + "\n"); if (type > 4 && type < 0xf5) { return MiscUtil.getDecoded(type); } @@ -237,7 +235,7 @@ else if (s.length() < 256) { boolean isList() { //if (blen == 0) // throw 0; - return (buffer[0] == 248 || buffer[0] == 0 || buffer[0] == 249); + return (buffer[0] == (byte)248 || buffer[0] == (byte)0 || buffer[0] == (byte)249); } Vector readList(WhatsappConnection c) { Vector l = new Vector(); diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index bee0853d33..f9cb15660e 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -17,6 +17,7 @@ import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.packet.Message; +import com.xabber.android.data.connection.ConnectionThread; import net.davidgf.android.WhatsappConnection; @@ -43,6 +44,7 @@ */ public class WAConnection extends Connection { + private ConnectionThread cthread; /** * The socket which is used for this connection. */ @@ -91,13 +93,16 @@ public class WAConnection extends Connection { * @param serviceName the name of the WA server to connect to; e.g. example.com. * @param callbackHandler ignored and should be set to null */ - public WAConnection(String serviceName, CallbackHandler callbackHandler) { + public WAConnection(ConnectionThread ct, String serviceName, CallbackHandler callbackHandler) { // Create the configuration for this new connection super(new ConnectionConfiguration(serviceName)); config.setCompressionEnabled(false); config.setSASLAuthenticationEnabled(true); config.setDebuggerEnabled(DEBUG_ENABLED); outbuffer_mutex = new byte[1]; + readwait = new Semaphore(0); + writewait = new Semaphore(0); + this.cthread = ct; } /** @@ -106,7 +111,7 @@ public WAConnection(String serviceName, CallbackHandler callbackHandler) { * * @param serviceName the name of the WA server to connect to; e.g. example.com. */ - public WAConnection(String serviceName) { + public WAConnection(ConnectionThread ct, String serviceName) { // Create the configuration for this new connection super(new ConnectionConfiguration(serviceName)); config.setCompressionEnabled(false); @@ -115,6 +120,7 @@ public WAConnection(String serviceName) { outbuffer_mutex = new byte[1]; readwait = new Semaphore(0); writewait = new Semaphore(0); + this.cthread = ct; } /** @@ -123,19 +129,21 @@ public WAConnection(String serviceName) { * * @param config the connection configuration. */ - public WAConnection(ConnectionConfiguration config) { + public WAConnection(ConnectionThread ct, ConnectionConfiguration config) { super(config); outbuffer_mutex = new byte[1]; readwait = new Semaphore(0); writewait = new Semaphore(0); + this.cthread = ct; } - public WAConnection(ConnectionConfiguration config, CallbackHandler callbackHandler) { + public WAConnection(ConnectionThread ct, ConnectionConfiguration config, CallbackHandler callbackHandler) { super(config); config.setCallbackHandler(callbackHandler); outbuffer_mutex = new byte[1]; readwait = new Semaphore(0); writewait = new Semaphore(0); + this.cthread = ct; } public String getConnectionID() { @@ -505,7 +513,7 @@ public boolean isUsingCompression() { private void writePackets(Thread thisThread, OutputStream ostream) { try { - while (outbuffer != null) { + while (outbuffer != null && ostream == this.ostream) { if (outbuffer.length > 0) { // Try to write the whole buffer System.out.println("Writing packets...\n"); @@ -550,11 +558,16 @@ private void readPackets(Thread thisThread, InputStream istream) { inbuffer = Arrays.copyOf(inbuffer, inbuffer.length - used); } } + + this.popWriteData(); // Ready data might be waiting ... } while (r >= 0); }catch (IOException e) { System.out.println("Error!\n" + e.toString()); } System.out.println("Exiting readpackets thread (WA)\n"); + disconnect(new Presence(Presence.Type.unavailable)); + // Signal the writer thread so it can also end + writewait.release(); } /** diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 5238cef456..594f9e635a 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -17,6 +17,7 @@ public class WhatsappConnection { private RC4Decoder in, out; private byte session_key[]; private DataBuffer outbuffer; + private byte []challenge_data; private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChallenge, SessionWaitingAuthOK, SessionConnected }; private SessionStatus conn_status; @@ -38,6 +39,7 @@ public WhatsappConnection(String phone, String pass, String nick) { public Tree read_tree(DataBuffer data) { int lsize = data.readListSize(); int type = data.getInt(1,0); + System.out.println("lsize: " + String.valueOf(lsize) + " type: " + String.valueOf(type) + "\n"); if (type == 1) { data.popData(1); Tree t = new Tree("start"); @@ -71,6 +73,7 @@ Tree parse_tree(DataBuffer data) { return new Tree("treeerr"); // Next message incomplete, return consumed data } data.popData(3); + System.out.println("Reading tree size " + String.valueOf(bsize) + " flag " + String.valueOf(bflag) + "\n"); if ((bflag & 0x8) != 0) { // Decode data, buffer conversion @@ -93,16 +96,17 @@ Tree parse_tree(DataBuffer data) { } public int pushIncomingData(byte [] data) { - Vector treelist = new Vector (); if (data.length < 3) return 0; DataBuffer db = new DataBuffer(data); + System.out.println("We have " + String.valueOf(db.size()) + " bytes of data\n"); Tree t; do { t = this.parse_tree(db); if (t.getTag() != "treeerr") - treelist.add(t); + this.processPacket(t); + System.out.println("We have " + String.valueOf(db.size()) + " bytes of data\n"); System.out.println("Received tree!\n"); System.out.println(t.toString(0)); @@ -111,6 +115,58 @@ public int pushIncomingData(byte [] data) { return data.length - db.size(); } + private void processPacket(Tree t) { + // Now process the tree list! + if (t.getTag().equals("challenge")) { + // Generate a session key using the challege & the password + assert(conn_status == SessionStatus.SessionWaitingChallenge); + + if (password.length() == 15) { + KeyGenerator.generateKeyImei(password,t.getData()); + } + else if (password.contains(":")) { + KeyGenerator.generateKeyMAC(password,t.getData()); + } + else { + KeyGenerator.generateKeyV2(password,t.getData()); + } + + this.in = new RC4Decoder(session_key, 256); + this.out = new RC4Decoder(session_key, 256); + + conn_status = SessionStatus.SessionWaitingAuthOK; + challenge_data = t.getData(); + + this.sendAuthResponse(); + } +/* else if (treelist[i].getTag() == "success") { + // Notifies the success of the auth + conn_status = SessionConnected; + if (treelist[i].hasAttribute("status")) + this->account_status = treelist[i].getAttributes()["status"]; + if (treelist[i].hasAttribute("kind")) + this->account_type = treelist[i].getAttributes()["kind"]; + if (treelist[i].hasAttribute("expiration")) + this->account_expiration = treelist[i].getAttributes()["expiration"]; + if (treelist[i].hasAttribute("creation")) + this->account_creation = treelist[i].getAttributes()["creation"]; + + this->notifyMyPresence(); + this->sendInitial(); + this->updateGroups(); + + //std::cout << "Logged in!!!" << std::endl; + //std::cout << "Account " << phone << " status: " << account_status << " kind: " << account_type << + // " expires: " << account_expiration << " creation: " << account_creation << std::endl; + } + else if (treelist[i].getTag() == "failure") { + if (conn_status == SessionWaitingAuthOK) + this->notifyError(errorAuth); + else + this->notifyError(errorUnknown); + }*/ + } + public DataBuffer write_tree(Tree tree) { DataBuffer bout = new DataBuffer(); int len = 1; @@ -120,7 +176,7 @@ public DataBuffer write_tree(Tree tree) { if (tree.getData().length != 0 || tree.forcedData()) len++; bout.writeListSize(len); - if (tree.getTag() == "start") bout.putInt(1,1); + if (tree.getTag().equals("start")) bout.putInt(1,1); else bout.putString(tree.getTag()); tree.writeAttributes(bout); @@ -190,8 +246,27 @@ public void doLogin(String useragent) { outbuffer = first; } + void sendAuthResponse() { + Tree t = new Tree("response",new HashMap < String,String >() {{ put("xmlns","urn:ietf:params:xml:ns:xmpp-sasl"); }}); + + long epoch = System.currentTimeMillis()/1000; + String stime = String.valueOf(epoch); + DataBuffer eresponse = new DataBuffer(); + eresponse.addData(phone.getBytes()); + eresponse.addData(challenge_data); + eresponse.addData(stime.getBytes()); + + eresponse = eresponse.encodedBuffer(this.out,this.session_key,false); + t.setData(eresponse.getPtr()); + + System.out.println("Challenge reply\n"); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(t,false))); + } + + public byte [] getWriteData() { byte [] r = outbuffer.getPtr(); + System.out.println("retrieveing data to send " + String.valueOf(r.length)); outbuffer = new DataBuffer(); return r; } From ad1aa7c9caadf861d9666a3105568c900731b5c8 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Thu, 22 Aug 2013 02:55:11 +0200 Subject: [PATCH 10/33] Fixed many bugs with auth. Now auth works! When auth fails it tries to reconnect (maybe I should fix this behavior). When auth is OK the UI does not get notified, which I don't know why. --- src/net/davidgf/android/DataBuffer.java | 35 ++++++++++++- src/net/davidgf/android/KeyGenerator.java | 35 +++++++++++-- src/net/davidgf/android/WAConnection.java | 21 ++++++++ .../davidgf/android/WhatsappConnection.java | 49 ++++++++++++------- 4 files changed, 117 insertions(+), 23 deletions(-) diff --git a/src/net/davidgf/android/DataBuffer.java b/src/net/davidgf/android/DataBuffer.java index 0f41574d18..4c3311e8cb 100644 --- a/src/net/davidgf/android/DataBuffer.java +++ b/src/net/davidgf/android/DataBuffer.java @@ -61,7 +61,7 @@ DataBuffer decodedBuffer(RC4Decoder decoder, int clength, boolean dout) { DataBuffer encodedBuffer(RC4Decoder decoder, byte [] key, boolean dout) { DataBuffer deco = new DataBuffer(Arrays.copyOfRange(this.buffer,0,this.buffer.length)); - decoder.cipher(deco.buffer); + deco.buffer = decoder.cipher(deco.buffer); byte [] hmacint = KeyGenerator.calc_hmac(deco.buffer,key); DataBuffer hmac = new DataBuffer(hmacint); @@ -163,11 +163,42 @@ String readRawString(int size) { popData(size); return s; } + byte [] readRawByteString(int size) { + byte [] r = Arrays.copyOf(buffer,size); + popData(size); + return r; + } + byte [] readByteString() { + int type = readInt(1); + if (type > 4 && type < 0xf5) { + return MiscUtil.getDecoded(type).getBytes(); + } + else if (type == 0xfc) { + int slen = readInt(1); + return readRawByteString(slen); + } + else if (type == 0xfd) { + int slen = readInt(3); + return readRawByteString(slen); + } + else if (type == 0xfe) { + return MiscUtil.getDecoded(readInt(1)+0xf5).getBytes(); + } + else if (type == 0xfa) { + String u = readString(); + String s = readString(); + + if (u.length() > 0 && s.length() > 0) + return (u + "@" + s).getBytes(); + else if (s.length() > 0) + return s.getBytes(); + } + return new byte [0]; + } String readString() { //if (blen == 0) // throw 0; int type = readInt(1); - System.out.println("readstr " + String.valueOf(type) + "\n"); if (type > 4 && type < 0xf5) { return MiscUtil.getDecoded(type); } diff --git a/src/net/davidgf/android/KeyGenerator.java b/src/net/davidgf/android/KeyGenerator.java index 9547b26387..6e17c0de5a 100644 --- a/src/net/davidgf/android/KeyGenerator.java +++ b/src/net/davidgf/android/KeyGenerator.java @@ -12,9 +12,25 @@ public class KeyGenerator { char [] password = new char[pass.length]; for (int i = 0; i < pass.length; i++) password[i] = (char)(pass[i]&0xFF); + + System.out.println("KEY V2 -- \n"); + for (int i = 0; i < password.length; i++) { + System.out.print((int)password[i]); + System.out.print(" "); + } + System.out.println("SALT V2 -- \n"); + for (int i = 0; i < salt.length; i++) { + System.out.print((int)salt[i]); + System.out.print(" "); + } + + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - PBEKeySpec ks = new PBEKeySpec(password,salt,16,20); + PBEKeySpec ks = new PBEKeySpec(password,salt,16,20*8); SecretKey s = f.generateSecret(ks); + + System.out.println("Result from PKCS5 key len: " + String.valueOf(s.getEncoded().length) + "\n"); + return s.getEncoded(); } @@ -41,7 +57,7 @@ public class KeyGenerator { public static byte[] generateKeyV2(String pw, byte [] salt) { try { byte [] decpass = MiscUtil.base64_decode(pw.getBytes()); - + return PKCS5_PBKDF2_HMAC_SHA1 (decpass,salt); } catch (Exception e) { @@ -74,7 +90,7 @@ public static byte[] generateKeyV2(String pw, byte [] salt) { return ret; } - private static byte [] HMAC_SHA1(byte [] text, byte [] key) { + /*private static byte [] HMAC_SHA1(byte [] text, byte [] key) { try { byte [] AppendBuf1 = new byte [text.length+64]; @@ -123,7 +139,20 @@ public static byte[] generateKeyV2(String pw, byte [] salt) { catch (Exception e) { return new byte[0]; } + }*/ + + private static byte [] HMAC_SHA1(byte [] text, byte [] key) { + try { + SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA1"); + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(signingKey); + // Compute the hmac on input data bytes + return mac.doFinal(text); + } catch (Exception e) { + return new byte[0]; + } } + }; diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index f9cb15660e..fa4964a2e9 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -53,6 +53,7 @@ public class WAConnection extends Connection { String connectionID = null; private String user = null; private boolean connected = false; + private boolean waconnected = false; /** * Flag that indicates if the user is currently authenticated with the server. */ @@ -313,6 +314,7 @@ protected void shutdown(Presence unavailablePresence) { // Set status authenticated = false; connected = false; + waconnected = false; // Socket close try { @@ -560,12 +562,17 @@ private void readPackets(Thread thisThread, InputStream istream) { } this.popWriteData(); // Ready data might be waiting ... + if (waconnection.isConnected() && !waconnected) { + connectionOK(); // Notify the connection status + waconnected = true; + } } while (r >= 0); }catch (IOException e) { System.out.println("Error!\n" + e.toString()); } System.out.println("Exiting readpackets thread (WA)\n"); disconnect(new Presence(Presence.Type.unavailable)); + cthread.connectionClosed(); // Signal the writer thread so it can also end writewait.release(); } @@ -607,6 +614,20 @@ public void connect() throws XMPPException { } } } + + public void connectionOK() { + System.out.println("Connected OK!\n"); + for (ConnectionListener listener : getConnectionListeners()) { + try { + listener.reconnectionSuccessful(); + } + catch (Exception e) { + // Catch and print any exception so we can recover + // from a faulty listener + e.printStackTrace(); + } + } + } @Override public void setRosterStorage(RosterStorage storage) diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 594f9e635a..5714baa773 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -25,6 +25,8 @@ private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChall private String phone, password; private static String whatsappserver = "s.whatsapp.net"; private static String whatsappservergroup = "g.us"; + + private String account_type, account_status, account_expiration, account_creation; public WhatsappConnection(String phone, String pass, String nick) { @@ -60,7 +62,7 @@ public Tree read_tree(DataBuffer data) { if (data.isList()) { t.setChildren(data.readList(this)); }else{ - t.setData(data.readString().getBytes()); + t.setData(data.readByteString()); } return t; @@ -115,6 +117,10 @@ public int pushIncomingData(byte [] data) { return data.length - db.size(); } + public boolean isConnected() { + return conn_status == SessionStatus.SessionConnected; + } + private void processPacket(Tree t) { // Now process the tree list! if (t.getTag().equals("challenge")) { @@ -122,14 +128,21 @@ private void processPacket(Tree t) { assert(conn_status == SessionStatus.SessionWaitingChallenge); if (password.length() == 15) { - KeyGenerator.generateKeyImei(password,t.getData()); + session_key = KeyGenerator.generateKeyImei(password,t.getData()); } else if (password.contains(":")) { - KeyGenerator.generateKeyMAC(password,t.getData()); + session_key = KeyGenerator.generateKeyMAC(password,t.getData()); } else { - KeyGenerator.generateKeyV2(password,t.getData()); + session_key = KeyGenerator.generateKeyV2(password,t.getData()); + } + + System.out.print("SESSION KEY:\n"); + for (int i = 0; i < session_key.length; i++) { + System.out.print(session_key[i]); + System.out.print(" "); } + System.out.print("\n"); this.in = new RC4Decoder(session_key, 256); this.out = new RC4Decoder(session_key, 256); @@ -139,27 +152,27 @@ else if (password.contains(":")) { this.sendAuthResponse(); } -/* else if (treelist[i].getTag() == "success") { + else if (t.getTag().equals("success")) { // Notifies the success of the auth - conn_status = SessionConnected; - if (treelist[i].hasAttribute("status")) - this->account_status = treelist[i].getAttributes()["status"]; - if (treelist[i].hasAttribute("kind")) - this->account_type = treelist[i].getAttributes()["kind"]; - if (treelist[i].hasAttribute("expiration")) - this->account_expiration = treelist[i].getAttributes()["expiration"]; - if (treelist[i].hasAttribute("creation")) - this->account_creation = treelist[i].getAttributes()["creation"]; + conn_status = SessionStatus.SessionConnected; + if (t.hasAttribute("status")) + this.account_status = t.getAttributes().get("status"); + if (t.hasAttribute("kind")) + this.account_type = t.getAttributes().get("kind"); + if (t.hasAttribute("expiration")) + this.account_expiration = t.getAttributes().get("expiration"); + if (t.hasAttribute("creation")) + this.account_creation = t.getAttributes().get("creation"); - this->notifyMyPresence(); - this->sendInitial(); - this->updateGroups(); + //this->notifyMyPresence(); + //this->sendInitial(); + //this->updateGroups(); //std::cout << "Logged in!!!" << std::endl; //std::cout << "Account " << phone << " status: " << account_status << " kind: " << account_type << // " expires: " << account_expiration << " creation: " << account_creation << std::endl; } - else if (treelist[i].getTag() == "failure") { + /*else if (treelist[i].getTag() == "failure") { if (conn_status == SessionWaitingAuthOK) this->notifyError(errorAuth); else From 518e6b9dc7a42b2fd7bd9b7bf6967fa04d20d717 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Thu, 22 Aug 2013 03:22:46 +0200 Subject: [PATCH 11/33] Bit of cleanup. Adding presence sending at startup. Adding ping/pong keepalive, seems that it is NOT working :( --- src/net/davidgf/android/DataBuffer.java | 1 - src/net/davidgf/android/KeyGenerator.java | 65 ------------------- .../davidgf/android/WhatsappConnection.java | 37 ++++++----- 3 files changed, 22 insertions(+), 81 deletions(-) diff --git a/src/net/davidgf/android/DataBuffer.java b/src/net/davidgf/android/DataBuffer.java index 4c3311e8cb..3b24f6ffcf 100644 --- a/src/net/davidgf/android/DataBuffer.java +++ b/src/net/davidgf/android/DataBuffer.java @@ -127,7 +127,6 @@ int readListSize() { //if (blen == 0) // throw 0; int ret; - System.out.println("byte " + String.valueOf((int)buffer[0])+ "\n"); if (buffer[0] == (byte)0xf8 || buffer[0] == (byte)0xf3) { ret = (int)buffer[1]; popData(2); diff --git a/src/net/davidgf/android/KeyGenerator.java b/src/net/davidgf/android/KeyGenerator.java index 6e17c0de5a..bf58df3260 100644 --- a/src/net/davidgf/android/KeyGenerator.java +++ b/src/net/davidgf/android/KeyGenerator.java @@ -12,25 +12,11 @@ public class KeyGenerator { char [] password = new char[pass.length]; for (int i = 0; i < pass.length; i++) password[i] = (char)(pass[i]&0xFF); - - System.out.println("KEY V2 -- \n"); - for (int i = 0; i < password.length; i++) { - System.out.print((int)password[i]); - System.out.print(" "); - } - System.out.println("SALT V2 -- \n"); - for (int i = 0; i < salt.length; i++) { - System.out.print((int)salt[i]); - System.out.print(" "); - } - SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); PBEKeySpec ks = new PBEKeySpec(password,salt,16,20*8); SecretKey s = f.generateSecret(ks); - System.out.println("Result from PKCS5 key len: " + String.valueOf(s.getEncoded().length) + "\n"); - return s.getEncoded(); } @@ -90,57 +76,6 @@ public static byte[] generateKeyV2(String pw, byte [] salt) { return ret; } - /*private static byte [] HMAC_SHA1(byte [] text, byte [] key) { - try { - byte [] AppendBuf1 = new byte [text.length+64]; - - byte [] SHA1_Key = new byte[4096]; - for (int i = 0; i < 4096; i++) - SHA1_Key[i] = 0; - - byte [] m_ipad = new byte[64]; - byte [] m_opad = new byte[64]; - for (int i = 0; i < 64; i++) { - m_ipad[i] = 0x36; - m_opad[i] = 0x5c; - } - - if (key.length > 64) { - byte [] t = MessageDigest.getInstance("SHA").digest(key); - for (int i = 0; i < t.length; i++) - SHA1_Key[i] = t[i]; - } - else { - for (int i = 0; i < key.length; i++) - SHA1_Key[i] = key[i]; - } - - for (int i = 0; i < 64; i++) - m_ipad[i] ^= SHA1_Key[i]; - - for (int i = 0; i < 64; i++) - AppendBuf1[i] = m_ipad[i]; - for (int i = 0; i < text.length; i++) - AppendBuf1[i+64] = text[i]; - - byte [] szReport = MessageDigest.getInstance("SHA").digest(AppendBuf1); - - for (int j = 0; j < 64; j++) - m_opad[j] ^= SHA1_Key[j]; - - byte [] AppendBuf2 = new byte [4096]; - for (int i = 0; i < 64; i++) - AppendBuf2[i] = m_opad[i]; - for (int i = 0; i < szReport.length; i++) - AppendBuf2[i+64] = szReport[i]; - - return MessageDigest.getInstance("SHA").digest(AppendBuf2); - } - catch (Exception e) { - return new byte[0]; - } - }*/ - private static byte [] HMAC_SHA1(byte [] text, byte [] key) { try { SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA1"); diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 5714baa773..3337a1e8b9 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -22,12 +22,12 @@ public class WhatsappConnection { private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChallenge, SessionWaitingAuthOK, SessionConnected }; private SessionStatus conn_status; - private String phone, password; + private String phone, password, nickname; private static String whatsappserver = "s.whatsapp.net"; private static String whatsappservergroup = "g.us"; private String account_type, account_status, account_expiration, account_creation; - + private String mypresence; public WhatsappConnection(String phone, String pass, String nick) { session_key = new byte[20]; @@ -35,13 +35,14 @@ public WhatsappConnection(String phone, String pass, String nick) { this.phone = phone; this.password = pass.trim(); this.conn_status = SessionStatus.SessionNone; + this.nickname = nick; + this.mypresence = "available"; outbuffer = new DataBuffer(); } public Tree read_tree(DataBuffer data) { int lsize = data.readListSize(); int type = data.getInt(1,0); - System.out.println("lsize: " + String.valueOf(lsize) + " type: " + String.valueOf(type) + "\n"); if (type == 1) { data.popData(1); Tree t = new Tree("start"); @@ -75,7 +76,6 @@ Tree parse_tree(DataBuffer data) { return new Tree("treeerr"); // Next message incomplete, return consumed data } data.popData(3); - System.out.println("Reading tree size " + String.valueOf(bsize) + " flag " + String.valueOf(bflag) + "\n"); if ((bflag & 0x8) != 0) { // Decode data, buffer conversion @@ -101,14 +101,12 @@ public int pushIncomingData(byte [] data) { if (data.length < 3) return 0; DataBuffer db = new DataBuffer(data); - System.out.println("We have " + String.valueOf(db.size()) + " bytes of data\n"); Tree t; do { t = this.parse_tree(db); if (t.getTag() != "treeerr") this.processPacket(t); - System.out.println("We have " + String.valueOf(db.size()) + " bytes of data\n"); System.out.println("Received tree!\n"); System.out.println(t.toString(0)); @@ -137,13 +135,6 @@ else if (password.contains(":")) { session_key = KeyGenerator.generateKeyV2(password,t.getData()); } - System.out.print("SESSION KEY:\n"); - for (int i = 0; i < session_key.length; i++) { - System.out.print(session_key[i]); - System.out.print(" "); - } - System.out.print("\n"); - this.in = new RC4Decoder(session_key, 256); this.out = new RC4Decoder(session_key, 256); @@ -164,7 +155,7 @@ else if (t.getTag().equals("success")) { if (t.hasAttribute("creation")) this.account_creation = t.getAttributes().get("creation"); - //this->notifyMyPresence(); + this.notifyMyPresence(); //this->sendInitial(); //this->updateGroups(); @@ -172,6 +163,11 @@ else if (t.getTag().equals("success")) { //std::cout << "Account " << phone << " status: " << account_status << " kind: " << account_type << // " expires: " << account_expiration << " creation: " << account_creation << std::endl; } + else if (t.getTag().equals("iq")) { + if (t.hasAttribute("from") && t.hasAttribute("id") && t.hasChild("ping")) { + this.doPong(t.getAttribute("id"),t.getAttribute("from")); + } + } /*else if (treelist[i].getTag() == "failure") { if (conn_status == SessionWaitingAuthOK) this->notifyError(errorAuth); @@ -180,6 +176,18 @@ else if (t.getTag().equals("success")) { }*/ } + private void notifyMyPresence() { + // Send the nickname and the current status + Tree pres = new Tree("presence", new HashMap < String,String >() {{ put("name",nickname); put("type",mypresence); }} ); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(pres,true))); + } + + void doPong(final String id, final String from) { + Tree t = new Tree("iq",new HashMap < String,String >() {{ put("to",from); put("id",id); put("type","result"); }} ); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(t,true))); + } + public DataBuffer write_tree(Tree tree) { DataBuffer bout = new DataBuffer(); int len = 1; @@ -272,7 +280,6 @@ void sendAuthResponse() { eresponse = eresponse.encodedBuffer(this.out,this.session_key,false); t.setData(eresponse.getPtr()); - System.out.println("Challenge reply\n"); outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(t,false))); } From db86318c3c5807c83aeb84982a2842e815fb048a Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Thu, 22 Aug 2013 13:27:35 +0200 Subject: [PATCH 12/33] Connect & login working! Now the work must focus on adding contacts and sending/receiving messages properly. --- .../data/connection/ConnectionThread.java | 7 ++++- src/net/davidgf/android/Tree.java | 6 ++-- src/net/davidgf/android/WAConnection.java | 30 +++++++++---------- .../davidgf/android/WhatsappConnection.java | 1 + 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/com/xabber/android/data/connection/ConnectionThread.java b/src/com/xabber/android/data/connection/ConnectionThread.java index 8733dca2bc..1086b0722b 100644 --- a/src/com/xabber/android/data/connection/ConnectionThread.java +++ b/src/com/xabber/android/data/connection/ConnectionThread.java @@ -328,7 +328,7 @@ private void onReady(final InetAddress address, final int port) { xmppConnection = new WAConnection(this, connectionConfiguration); }else{ xmppConnection = new XMPPConnection(connectionConfiguration); - System.out.println("Creating WA connection...\n"); + System.out.println("Creating XMPP connection...\n"); } xmppConnection.addPacketListener(this, ACCEPT_ALL); xmppConnection.forceAddConnectionListener(this); @@ -499,6 +499,7 @@ private boolean checkForSeeOtherHost(Exception e) { * @param password */ private void onConnected(final String password) { + System.out.println("onConnected!\n"); connectionItem.onConnected(this); ConnectionManager.getInstance().onConnected(this); runOnConnectionThread(new Runnable() { @@ -515,6 +516,7 @@ public void run() { * @param password */ private void authorization(String password) { + System.out.println("authoriation!\n"); try { xmppConnection.login(login, password, resource); } catch (IllegalStateException e) { @@ -550,6 +552,7 @@ public void run() { xmppConnection.disconnect(); return; } + System.out.println("Now run on UI\n"); runOnUiThread(new Runnable() { @Override public void run() { @@ -562,6 +565,8 @@ public void run() { * Authorization passed. */ private void onAuthorized() { + (new Exception()).printStackTrace(); + System.out.println("onAuthorized!\n"); connectionItem.onAuthorized(this); ConnectionManager.getInstance().onAuthorized(this); if (connectionItem instanceof AccountItem) diff --git a/src/net/davidgf/android/Tree.java b/src/net/davidgf/android/Tree.java index de679b8a1c..1b719cf2de 100644 --- a/src/net/davidgf/android/Tree.java +++ b/src/net/davidgf/android/Tree.java @@ -88,7 +88,7 @@ public Map < String, String > getAttributes() { } public boolean hasAttributeValue(String at, String val) { if (hasAttribute(at)) { - return (attributes.get(at) == val); + return (attributes.get(at).equals(val)); } return false; } @@ -103,7 +103,7 @@ public String getAttribute(String at) { public Tree getChild(String tag) { for (int i = 0; i < children.size(); i++) { - if (children.get(i).getTag() == tag) + if (children.get(i).getTag().equals(tag)) return children.get(i); Tree t = children.get(i).getChild(tag); if (t == null) @@ -113,7 +113,7 @@ public Tree getChild(String tag) { } public boolean hasChild(String tag) { for (int i = 0; i < children.size(); i++) { - if (children.get(i).getTag() == tag) + if (children.get(i).getTag().equals(tag)) return true; if (children.get(i).hasChild(tag)) return true; diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index fa4964a2e9..67714e7b43 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -17,6 +17,8 @@ import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Registration; +import org.jivesoftware.smack.packet.RosterPacket; import com.xabber.android.data.connection.ConnectionThread; import net.davidgf.android.WhatsappConnection; @@ -337,8 +339,9 @@ public void disconnect(Presence unavailablePresence) { } public void sendPacket(Packet packet) { + this.firePacketInterceptors(packet); + // If the packet if a Message, serialize and send it! - //return WhatsappConnection.serializeMessage(getTo(),getMessageBody().message); if (packet instanceof Message) { Message m = (Message)packet; synchronized (outbuffer_mutex) { @@ -351,6 +354,17 @@ public void sendPacket(Packet packet) { writewait.release(); } } + if (packet instanceof Registration) { + Registration r = (Registration)packet; + System.out.println(r.getChildElementXML()); + } + if (packet instanceof RosterPacket) { + RosterPacket r = (RosterPacket)packet; + System.out.println(r.toXML()); + } + + // Notify others + this.firePacketSendingListeners(packet); } private void popWriteData() { @@ -421,8 +435,6 @@ private void connectUsingConfiguration(ConnectionConfiguration config) throws XM String host = config.getHost(); int port = config.getPort(); - System.out.println("Connecting! User: "+config.getUsername() + " pass: "+config.getPassword()+"\n"); - System.out.println("Connecting! Host: "+host + " Port: "+String.valueOf(port)+"\n"); // Create WA connection API object // FIXME: Set proper nickname msgid = 0; @@ -450,8 +462,6 @@ private void connectUsingConfiguration(ConnectionConfiguration config) throws XM } initConnection(); connected = true; - - System.out.println("Connected!\n"); } /** @@ -604,19 +614,9 @@ public void connect() throws XMPPException { connectUsingConfiguration(config); // Automatically makes the login if the user was previouslly connected successfully // to the server and the connection was terminated abruptly - if (connected) { - // Make the login - try { - login(config.getUsername(), config.getPassword(), config.getResource()); - } - catch (XMPPException e) { - e.printStackTrace(); - } - } } public void connectionOK() { - System.out.println("Connected OK!\n"); for (ConnectionListener listener : getConnectionListeners()) { try { listener.reconnectionSuccessful(); diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 3337a1e8b9..638a2f40d5 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -165,6 +165,7 @@ else if (t.getTag().equals("success")) { } else if (t.getTag().equals("iq")) { if (t.hasAttribute("from") && t.hasAttribute("id") && t.hasChild("ping")) { + System.out.println("Received PING!\n"); this.doPong(t.getAttribute("id"),t.getAttribute("from")); } } From 2785d076071c2299355b758a2d457397c257e4e6 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Thu, 22 Aug 2013 18:16:23 +0200 Subject: [PATCH 13/33] Message sending works. Contacts are not retained across sessions and connection drops frequently. Receiving messages does not work yet :( --- src/net/davidgf/android/MiscUtil.java | 9 ++ src/net/davidgf/android/WAConnection.java | 67 ++++++++++++++- .../davidgf/android/WhatsappConnection.java | 84 +++++++++++++++++++ 3 files changed, 158 insertions(+), 2 deletions(-) diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java index cf9bcce1ce..475d7fb30a 100644 --- a/src/net/davidgf/android/MiscUtil.java +++ b/src/net/davidgf/android/MiscUtil.java @@ -115,6 +115,15 @@ public static int lookupDecoded(String value) { } return 0; } + + public static String bytesToUTF8(byte [] ba) { + try { + return new String(ba, "UTF-8"); + } + catch (Exception e) { + return new String(); + } + } } diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index 67714e7b43..35e873abc7 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -19,6 +19,7 @@ import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Registration; import org.jivesoftware.smack.packet.RosterPacket; +import org.jivesoftware.smack.packet.IQ; import com.xabber.android.data.connection.ConnectionThread; import net.davidgf.android.WhatsappConnection; @@ -36,7 +37,7 @@ import java.security.SecureRandom; import java.security.Security; import java.util.*; -import java.util.concurrent.Semaphore; +import java.util.concurrent.*; /** * Creates a socket connection to a WA server. @@ -70,9 +71,10 @@ public class WAConnection extends Connection { byte [] outbuffer; byte [] outbuffer_mutex; WhatsappConnection waconnection; - Semaphore readwait,writewait; + private ExecutorService listenerExecutor; + int msgid; private static final String waUA = "WhatsApp/2.10.750 Android/4.2.1 Device/GalaxyS3"; @@ -106,6 +108,16 @@ public WAConnection(ConnectionThread ct, String serviceName, CallbackHandler cal readwait = new Semaphore(0); writewait = new Semaphore(0); this.cthread = ct; + + listenerExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, + "WA Listener Processor"); + thread.setDaemon(true); + return thread; + } + }); + } /** @@ -360,6 +372,21 @@ public void sendPacket(Packet packet) { } if (packet instanceof RosterPacket) { RosterPacket r = (RosterPacket)packet; + // Check Add/Remove Contact/Group: + if (r.getType() == IQ.Type.SET) { + Collection items = r.getRosterItems(); + if (items.size() == 1) { + RosterPacket.Item it = ((RosterPacket.Item)(items.toArray()[0])); + if (it.getItemType() == RosterPacket.ItemType.remove) { + System.out.println("Remove contact!\n"); + }else{ + // Adding contact, notify underlying connection for status query + //waconnection.addContact(it.getUser()); + System.out.println("Add contact!\n"); + } + } + } + System.out.println(r.toXML()); } @@ -571,11 +598,22 @@ private void readPackets(Thread thisThread, InputStream istream) { } } + // Process stuff this.popWriteData(); // Ready data might be waiting ... + if (waconnection.isConnected() && !waconnected) { connectionOK(); // Notify the connection status waconnected = true; } + + Packet p = waconnection.getNextPacket(); + while (p != null) { + for (PacketCollector collector: getPacketCollectors()) { + collector.processPacket(p); + } + listenerExecutor.submit(new ListenerNotification(p)); + p = waconnection.getNextPacket(); + } } while (r >= 0); }catch (IOException e) { System.out.println("Error!\n" + e.toString()); @@ -711,5 +749,30 @@ public void reset() throws IOException { wrappedReader.reset(); } } + + + + /** + * A runnable to notify all listeners of a packet. + */ + private class ListenerNotification implements Runnable { + + private Packet packet; + + public ListenerNotification(Packet packet) { + this.packet = packet; + } + + public void run() { + for (ListenerWrapper listenerWrapper : recvListeners.values()) { + try { + listenerWrapper.notifyListener(packet); + } catch (RuntimeException e) { + e.printStackTrace(); + } + } + } + } + } diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 638a2f40d5..7880503e8e 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -10,6 +10,8 @@ */ package net.davidgf.android; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; import java.util.*; @@ -28,6 +30,8 @@ private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChall private String account_type, account_status, account_expiration, account_creation; private String mypresence; + + private Vector received_packets; public WhatsappConnection(String phone, String pass, String nick) { session_key = new byte[20]; @@ -38,6 +42,7 @@ public WhatsappConnection(String phone, String pass, String nick) { this.nickname = nick; this.mypresence = "available"; outbuffer = new DataBuffer(); + received_packets = new Vector (); } public Tree read_tree(DataBuffer data) { @@ -169,6 +174,31 @@ else if (t.getTag().equals("iq")) { this.doPong(t.getAttribute("id"),t.getAttribute("from")); } } + else if (t.getTag().equals("message")) { + if (t.hasAttributeValue("type","chat") && t.hasAttribute("from")) { + long time = 0; + if (t.hasAttribute("t")) + time = Integer.parseInt(t.getAttribute("t")); + String from = t.getAttribute("from"); + String id = t.getAttribute("id"); + String author = t.getAttribute("author"); + + Tree tb = t.getChild("body"); + if (tb != null) { + this.receiveMessage( + new ChatMessage(from,time,id,MiscUtil.bytesToUTF8(tb.getData()),author)); + } + } + + // Received ACK + if (t.hasAttribute("type") && t.hasAttribute("from") && !t.hasChild("received")) { + DataBuffer reply = generateResponse(t.getAttribute("from"), + t.getAttribute("type"), + t.getAttribute("id")); + outbuffer = outbuffer.addBuf(reply); + + } + } /*else if (treelist[i].getTag() == "failure") { if (conn_status == SessionWaitingAuthOK) this->notifyError(errorAuth); @@ -177,6 +207,28 @@ else if (t.getTag().equals("iq")) { }*/ } + private DataBuffer generateResponse(final String from, final String type, final String id) { + Tree received = new Tree("received",new HashMap < String,String >() {{ put("xmlns","urn:xmpp:receipts"); }} ); + Tree mes = new Tree("message",new HashMap < String,String >() {{ + put("to",from); put("type",type); put("id",id); }} ); + mes.addChild(received); + return serialize_tree(mes,true); + } + + + private void receiveMessage(AbstractMessage msg) { + received_packets.add(msg.serializePacket()); + } + + public Packet getNextPacket() { + if (received_packets.size() == 0) + return null; + Packet r = received_packets.get(0); + received_packets.remove(0); + + return r; + } + private void notifyMyPresence() { // Send the nickname and the current status Tree pres = new Tree("presence", new HashMap < String,String >() {{ put("name",nickname); put("type",mypresence); }} ); @@ -319,6 +371,38 @@ void sendAuthResponse() { return new byte[0]; } } + + public abstract class AbstractMessage { + protected String from, id, author; + protected long time; + + public AbstractMessage(String from, long time, String id, String author) { + this.from = from; + this.id = id; + this.author = author; + this.time = time; + } + + public abstract Packet serializePacket(); + } + + public class ChatMessage extends AbstractMessage { + private String message; + public ChatMessage(String from, long time, String id, String message, String author) { + super(from, time, id, author); + this.message = message; + } + + public Packet serializePacket() { + Message message = new Message(); + message.setTo(this.from); + message.setFrom(""); + message.setType(Message.Type.chat); + message.setBody(this.message); + + return message; + } + } } From 574917191634a39c1263af30f079f687e8024b7d Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Fri, 23 Aug 2013 17:15:48 +0200 Subject: [PATCH 14/33] Fixed many java related bugs. Now it's possible to send and receive messages using whatsapp. The stream keeps disconnecting though (probably due to not answering to some packets). Contact status (available / away) shown in the contact list. Next steps should be: Typing status + Permanently saving contacts. --- src/net/davidgf/android/MiscUtil.java | 4 ++ src/net/davidgf/android/Tree.java | 6 +- src/net/davidgf/android/WAConnection.java | 49 ++++++------- .../davidgf/android/WhatsappConnection.java | 68 +++++++++++++++++-- 4 files changed, 96 insertions(+), 31 deletions(-) diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java index 475d7fb30a..6757ad4d3b 100644 --- a/src/net/davidgf/android/MiscUtil.java +++ b/src/net/davidgf/android/MiscUtil.java @@ -124,6 +124,10 @@ public static String bytesToUTF8(byte [] ba) { return new String(); } } + + public static String getUser(String user) { + return user.split("@")[0]; + } } diff --git a/src/net/davidgf/android/Tree.java b/src/net/davidgf/android/Tree.java index 1b719cf2de..faddcb179e 100644 --- a/src/net/davidgf/android/Tree.java +++ b/src/net/davidgf/android/Tree.java @@ -106,7 +106,7 @@ public Tree getChild(String tag) { if (children.get(i).getTag().equals(tag)) return children.get(i); Tree t = children.get(i).getChild(tag); - if (t == null) + if (t != null) return t; } return null; @@ -124,13 +124,13 @@ public boolean hasChild(String tag) { String toString(int sp) { String ret = ""; String spacing = ""; - for (int i = 0; i < sp; i++) + for (int i = 0; i < sp*3; i++) spacing += " "; ret += spacing+"Tag: "+tag+"\n"; for (String key: attributes.keySet()) { ret += spacing+"at["+key+"]="+attributes.get(key)+"\n"; } - ret += spacing+"Data: "+data+"\n"; + ret += spacing+"Data: "+MiscUtil.bytesToUTF8(data)+"\n"; for (int i = 0; i < children.size(); i++) { ret += children.get(i).toString(sp+1); diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index 35e873abc7..2a2f94942f 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -74,7 +74,6 @@ public class WAConnection extends Connection { Semaphore readwait,writewait; private ExecutorService listenerExecutor; - int msgid; private static final String waUA = "WhatsApp/2.10.750 Android/4.2.1 Device/GalaxyS3"; @@ -104,20 +103,8 @@ public WAConnection(ConnectionThread ct, String serviceName, CallbackHandler cal config.setCompressionEnabled(false); config.setSASLAuthenticationEnabled(true); config.setDebuggerEnabled(DEBUG_ENABLED); - outbuffer_mutex = new byte[1]; - readwait = new Semaphore(0); - writewait = new Semaphore(0); this.cthread = ct; - - listenerExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(runnable, - "WA Listener Processor"); - thread.setDaemon(true); - return thread; - } - }); - + commonInit(); } /** @@ -132,10 +119,8 @@ public WAConnection(ConnectionThread ct, String serviceName) { config.setCompressionEnabled(false); config.setSASLAuthenticationEnabled(true); config.setDebuggerEnabled(DEBUG_ENABLED); - outbuffer_mutex = new byte[1]; - readwait = new Semaphore(0); - writewait = new Semaphore(0); this.cthread = ct; + commonInit(); } /** @@ -146,19 +131,30 @@ public WAConnection(ConnectionThread ct, String serviceName) { */ public WAConnection(ConnectionThread ct, ConnectionConfiguration config) { super(config); - outbuffer_mutex = new byte[1]; - readwait = new Semaphore(0); - writewait = new Semaphore(0); this.cthread = ct; + commonInit(); } public WAConnection(ConnectionThread ct, ConnectionConfiguration config, CallbackHandler callbackHandler) { super(config); config.setCallbackHandler(callbackHandler); + this.cthread = ct; + commonInit(); + } + + private void commonInit() { outbuffer_mutex = new byte[1]; readwait = new Semaphore(0); writewait = new Semaphore(0); - this.cthread = ct; + + this.listenerExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, + "WA Listener Processor"); + thread.setDaemon(true); + return thread; + } + }); } public String getConnectionID() { @@ -381,7 +377,7 @@ public void sendPacket(Packet packet) { System.out.println("Remove contact!\n"); }else{ // Adding contact, notify underlying connection for status query - //waconnection.addContact(it.getUser()); + waconnection.addContact(it.getUser(),true); System.out.println("Add contact!\n"); } } @@ -512,7 +508,7 @@ public void run() { writerThread.setName("Socket data writer"); writerThread.setDaemon(true); writerThread.start(); - + readerThread = new Thread() { public void run() { readPackets(this,istream); @@ -594,7 +590,7 @@ private void readPackets(Thread thisThread, InputStream istream) { synchronized ( waconnection ) { synchronized (inbuffer) { int used = waconnection.pushIncomingData(inbuffer); - inbuffer = Arrays.copyOf(inbuffer, inbuffer.length - used); + inbuffer = Arrays.copyOfRange(inbuffer, used, inbuffer.length); } } @@ -606,8 +602,13 @@ private void readPackets(Thread thisThread, InputStream istream) { waconnected = true; } + if (listenerExecutor == null) { + System.out.println("Null!!! Shit\n"); + } + Packet p = waconnection.getNextPacket(); while (p != null) { + System.out.println("Received message!\n"); for (PacketCollector collector: getPacketCollectors()) { collector.processPacket(p); } diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 7880503e8e..6ad697712e 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -12,6 +12,7 @@ package net.davidgf.android; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.Presence; import java.util.*; @@ -32,6 +33,7 @@ private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChall private String mypresence; private Vector received_packets; + private Vector contacts; public WhatsappConnection(String phone, String pass, String nick) { session_key = new byte[20]; @@ -43,6 +45,7 @@ public WhatsappConnection(String phone, String pass, String nick) { this.mypresence = "available"; outbuffer = new DataBuffer(); received_packets = new Vector (); + contacts = new Vector (); } public Tree read_tree(DataBuffer data) { @@ -55,6 +58,7 @@ public Tree read_tree(DataBuffer data) { return t; }else if (type == 2) { data.popData(1); + System.out.println("NO data in this tree\n"); return new Tree("treeerr"); // No data in this tree... } @@ -78,6 +82,7 @@ Tree parse_tree(DataBuffer data) { int bflag = (data.getInt(1,0) & 0xF0)>>4; int bsize = data.getInt(2,1); if (bsize > data.size()-3) { + System.out.println("NO data enough\n"); return new Tree("treeerr"); // Next message incomplete, return consumed data } data.popData(3); @@ -110,12 +115,12 @@ public int pushIncomingData(byte [] data) { Tree t; do { t = this.parse_tree(db); - if (t.getTag() != "treeerr") + if (!t.getTag().equals("treeerr")) this.processPacket(t); System.out.println("Received tree!\n"); System.out.println(t.toString(0)); - } while (t.getTag() != "treeerr" && db.size() >= 3); + } while (!t.getTag().equals("treeerr") && db.size() >= 3); return data.length - db.size(); } @@ -168,6 +173,20 @@ else if (t.getTag().equals("success")) { //std::cout << "Account " << phone << " status: " << account_status << " kind: " << account_type << // " expires: " << account_expiration << " creation: " << account_creation << std::endl; } + else if (t.getTag().equals("presence")) { + // Receives the presence of the user + if ( t.hasAttribute("from") && t.hasAttribute("type") ) { + Presence.Mode mode = Presence.Mode.away; + if (t.getAttribute("type").equals("available")) + mode = Presence.Mode.available; + + Presence presp = new Presence(Presence.Type.available); + presp.setMode(mode); + presp.setFrom(MiscUtil.getUser(t.getAttribute("from"))); + presp.setTo(MiscUtil.getUser(phone)); + received_packets.add(presp); + } + } else if (t.getTag().equals("iq")) { if (t.hasAttribute("from") && t.hasAttribute("id") && t.hasChild("ping")) { System.out.println("Received PING!\n"); @@ -336,6 +355,27 @@ void sendAuthResponse() { outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(t,false))); } + public void addContact(String user, boolean user_request) { + user = MiscUtil.getUser(user); + + for (int i = 0; i < contacts.size(); i++) + if (contacts.get(i).phone.equals(user)) + return; + + Contact c = new Contact(user, user_request); + contacts.add(c); + + subscribePresence(user); + } + + public void subscribePresence(String user) { + final String username = MiscUtil.getUser(user); + Tree request = new Tree("presence", + new HashMap < String,String >() {{ put("type","subscribe"); put("to",username); }} ); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(request,false))); + } + public byte [] getWriteData() { byte [] r = outbuffer.getPtr(); @@ -395,14 +435,34 @@ public ChatMessage(String from, long time, String id, String message, String aut public Packet serializePacket() { Message message = new Message(); - message.setTo(this.from); - message.setFrom(""); + message.setTo(MiscUtil.getUser(phone)); + message.setFrom(MiscUtil.getUser(this.from)); message.setType(Message.Type.chat); message.setBody(this.message); return message; } } + + + public class Contact { + String phone, name; + String presence, typing; + String status; + long last_seen, last_status; + boolean mycontact; + String ppprev, pppicture; + boolean subscribed; + + Contact(String phone, boolean myc) { + this.phone = phone; + this.mycontact = myc; + this.last_seen = 0; + this.subscribed = false; + this.typing = "paused"; + this.status = ""; + } + }; } From 28fdc14bae66d131fdb6db05bb8e0175bbb57764 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Fri, 23 Aug 2013 18:02:43 +0200 Subject: [PATCH 15/33] Chat working more or less. Typing has issues. Fixed a bug with presence subscription. --- .../data/connection/ConnectionThread.java | 8 ------- .../davidgf/android/WhatsappConnection.java | 24 ++++++++++++++++++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/com/xabber/android/data/connection/ConnectionThread.java b/src/com/xabber/android/data/connection/ConnectionThread.java index 1086b0722b..6cf16742bf 100644 --- a/src/com/xabber/android/data/connection/ConnectionThread.java +++ b/src/com/xabber/android/data/connection/ConnectionThread.java @@ -324,11 +324,9 @@ private void onReady(final InetAddress address, final int port) { // Create different underlying classes depending on the protocol AccountProtocol proto = connectionItem.getConnectionSettings().getProtocol(); if (proto == AccountProtocol.wapp) { - System.out.println("Creating WA connection...\n"); xmppConnection = new WAConnection(this, connectionConfiguration); }else{ xmppConnection = new XMPPConnection(connectionConfiguration); - System.out.println("Creating XMPP connection...\n"); } xmppConnection.addPacketListener(this, ACCEPT_ALL); xmppConnection.forceAddConnectionListener(this); @@ -336,7 +334,6 @@ private void onReady(final InetAddress address, final int port) { final String password = OAuthManager.getInstance().getPassword( protocol, token); if (password != null) { - System.out.println("Connection pass "+password+"\n"); runOnConnectionThread(new Runnable() { @Override public void run() { @@ -499,7 +496,6 @@ private boolean checkForSeeOtherHost(Exception e) { * @param password */ private void onConnected(final String password) { - System.out.println("onConnected!\n"); connectionItem.onConnected(this); ConnectionManager.getInstance().onConnected(this); runOnConnectionThread(new Runnable() { @@ -516,7 +512,6 @@ public void run() { * @param password */ private void authorization(String password) { - System.out.println("authoriation!\n"); try { xmppConnection.login(login, password, resource); } catch (IllegalStateException e) { @@ -552,7 +547,6 @@ public void run() { xmppConnection.disconnect(); return; } - System.out.println("Now run on UI\n"); runOnUiThread(new Runnable() { @Override public void run() { @@ -565,8 +559,6 @@ public void run() { * Authorization passed. */ private void onAuthorized() { - (new Exception()).printStackTrace(); - System.out.println("onAuthorized!\n"); connectionItem.onAuthorized(this); ConnectionManager.getInstance().onAuthorized(this); if (connectionItem instanceof AccountItem) diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 6ad697712e..1fe703b0af 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -13,6 +13,7 @@ import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smackx.packet.MessageEvent; import java.util.*; @@ -204,10 +205,18 @@ else if (t.getTag().equals("message")) { Tree tb = t.getChild("body"); if (tb != null) { + // TODO: Add user here and at UI in case we don't have it this.receiveMessage( new ChatMessage(from,time,id,MiscUtil.bytesToUTF8(tb.getData()),author)); + addContact(from,false); } } + if (t.hasChild("composing")) { + gotTyping(t.getAttribute("from"),true); + } + if (t.hasChild("paused")) { + gotTyping(t.getAttribute("from"),false); + } // Received ACK if (t.hasAttribute("type") && t.hasAttribute("from") && !t.hasChild("received")) { @@ -239,6 +248,19 @@ private void receiveMessage(AbstractMessage msg) { received_packets.add(msg.serializePacket()); } + private void gotTyping(final String user, boolean typing) { + Message msg = new Message(); + msg.setTo(MiscUtil.getUser(phone)); + msg.setFrom(MiscUtil.getUser(user)); + + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setComposing(true); + msg.addExtension(messageEvent); + // Send the packet + received_packets.add(msg); + } + public Packet getNextPacket() { if (received_packets.size() == 0) return null; @@ -373,7 +395,7 @@ public void subscribePresence(String user) { Tree request = new Tree("presence", new HashMap < String,String >() {{ put("type","subscribe"); put("to",username); }} ); - outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(request,false))); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(request,true))); } From 4cc20d6ebec0f328dc5d0515562998c4cc1027ce Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Fri, 23 Aug 2013 18:18:16 +0200 Subject: [PATCH 16/33] Add makefile to remember build commands --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..884e116cf5 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ + +android update project --path . --target android-10 --name Xabber + +ant release + From 9cd5e658197632b382610cbb9d6802fc712c6131 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sat, 24 Aug 2013 16:55:03 +0200 Subject: [PATCH 17/33] Added rather simple support for image chats. Fixed a bug with the ACKing of received messages (same fix as in libpurple's whatsapp). Added Roster simulated reception, need to add something to add the contacts to the account. --- src/net/davidgf/android/WAConnection.java | 19 ++++++--- .../davidgf/android/WhatsappConnection.java | 41 ++++++++++++++++--- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index 2a2f94942f..502b2eaa77 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -382,6 +382,11 @@ public void sendPacket(Packet packet) { } } } + // Query contacts! + if (r.getType() == IQ.Type.GET) { + RosterPacket rr = new RosterPacket(); + submitPacket(rr); + } System.out.println(r.toXML()); } @@ -571,6 +576,14 @@ private void writePackets(Thread thisThread, OutputStream ostream) { System.out.println("Exiting writepackets thread (WA)\n"); } + private void submitPacket(Packet p) { + System.out.println("Received message!\n"); + for (PacketCollector collector: getPacketCollectors()) { + collector.processPacket(p); + } + listenerExecutor.submit(new ListenerNotification(p)); + } + private void readPackets(Thread thisThread, InputStream istream) { try { int r; @@ -608,11 +621,7 @@ private void readPackets(Thread thisThread, InputStream istream) { Packet p = waconnection.getNextPacket(); while (p != null) { - System.out.println("Received message!\n"); - for (PacketCollector collector: getPacketCollectors()) { - collector.processPacket(p); - } - listenerExecutor.submit(new ListenerNotification(p)); + submitPacket(p); p = waconnection.getNextPacket(); } } while (r >= 0); diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 1fe703b0af..130044cbac 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -210,6 +210,15 @@ else if (t.getTag().equals("message")) { new ChatMessage(from,time,id,MiscUtil.bytesToUTF8(tb.getData()),author)); addContact(from,false); } + + tb = t.getChild("media"); + if (tb != null) { + // Photo/audio + if (tb.hasAttributeValue("type","image")) { + this.receiveMessage( + new ImageMessage(from,time,id,tb.getAttribute("url"),tb.getData(),author)); + } + } } if (t.hasChild("composing")) { gotTyping(t.getAttribute("from"),true); @@ -219,10 +228,14 @@ else if (t.getTag().equals("message")) { } // Received ACK - if (t.hasAttribute("type") && t.hasAttribute("from") && !t.hasChild("received")) { + if (t.hasAttribute("type") && t.hasAttribute("from")) { + String answer = "received"; + if (t.hasChild("received")) + answer = "ack"; DataBuffer reply = generateResponse(t.getAttribute("from"), t.getAttribute("type"), - t.getAttribute("id")); + t.getAttribute("id"), + answer); outbuffer = outbuffer.addBuf(reply); } @@ -235,14 +248,13 @@ else if (t.getTag().equals("message")) { }*/ } - private DataBuffer generateResponse(final String from, final String type, final String id) { - Tree received = new Tree("received",new HashMap < String,String >() {{ put("xmlns","urn:xmpp:receipts"); }} ); + private DataBuffer generateResponse(final String from, final String type, final String id, final String ans) { + Tree received = new Tree(ans,new HashMap < String,String >() {{ put("xmlns","urn:xmpp:receipts"); }} ); Tree mes = new Tree("message",new HashMap < String,String >() {{ put("to",from); put("type",type); put("id",id); }} ); mes.addChild(received); return serialize_tree(mes,true); } - private void receiveMessage(AbstractMessage msg) { received_packets.add(msg.serializePacket()); @@ -465,6 +477,25 @@ public Packet serializePacket() { return message; } } + public class ImageMessage extends AbstractMessage { + private String url; + private byte [] preview; + public ImageMessage(String from, long time, String id, String url, byte [] prev, String author) { + super(from, time, id, author); + this.url = url; + this.preview = prev; + } + + public Packet serializePacket() { + Message message = new Message(); + message.setTo(MiscUtil.getUser(phone)); + message.setFrom(MiscUtil.getUser(this.from)); + message.setType(Message.Type.chat); + message.setBody(url); + + return message; + } + } public class Contact { From f57034e887febce2eb110d38b42490af597c6075 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sat, 24 Aug 2013 20:18:06 +0200 Subject: [PATCH 18/33] Adding permanent contacts using a SQLite table. Fixed some other bugs (still some bugs in the queue...) I'd consider this a beta version of the App. --- src/net/davidgf/android/WAConnection.java | 25 ++- src/net/davidgf/android/WAContacts.java | 146 ++++++++++++++++++ .../davidgf/android/WhatsappConnection.java | 15 +- 3 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 src/net/davidgf/android/WAContacts.java diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index 502b2eaa77..df27187682 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -23,6 +23,7 @@ import com.xabber.android.data.connection.ConnectionThread; import net.davidgf.android.WhatsappConnection; +import net.davidgf.android.WAContacts; import org.apache.harmony.javax.security.auth.callback.Callback; import org.apache.harmony.javax.security.auth.callback.CallbackHandler; @@ -374,17 +375,37 @@ public void sendPacket(Packet packet) { if (items.size() == 1) { RosterPacket.Item it = ((RosterPacket.Item)(items.toArray()[0])); if (it.getItemType() == RosterPacket.ItemType.remove) { - System.out.println("Remove contact!\n"); + // Bypasss roster send/rcv for WA, as we do not store them in the server + submitPacket(r); + // Remove fromm storage + WAContacts.getInstance().removeContact(config.getUsername(), it.getUser()); }else{ // Adding contact, notify underlying connection for status query waconnection.addContact(it.getUser(),true); - System.out.println("Add contact!\n"); + // Bypasss roster send/rcv for WA, as we do not store them in the server + submitPacket(r); + // Add the contact to the storage + WAContacts.getInstance().addContact(config.getUsername(), it.getUser(), it.getName()); } } } // Query contacts! if (r.getType() == IQ.Type.GET) { RosterPacket rr = new RosterPacket(); + rr.setType(IQ.Type.SET); + + // Add saved contacts! + Vector < Vector < String > > saved_contacts = WAContacts.getInstance().getContacts(config.getUsername()); + for (int i = 0; i < saved_contacts.size(); i++) { + String user = saved_contacts.get(i).get(0); + String name = saved_contacts.get(i).get(1); + rr.addRosterItem(new RosterPacket.Item(user, name)); + // Subscribe presence + // FIXME: addContact fails when not connected, should + // add a fallback to a queue or something... + //waconnection.addContact(user,true); + } + submitPacket(rr); } diff --git a/src/net/davidgf/android/WAContacts.java b/src/net/davidgf/android/WAContacts.java new file mode 100644 index 0000000000..aa4349b8a8 --- /dev/null +++ b/src/net/davidgf/android/WAContacts.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2013, Redsolution LTD. All rights reserved. + * + * This file is part of Xabber project; you can redistribute it and/or + * modify it under the terms of the GNU General Public License, Version 3. + * + * Xabber is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License, + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package net.davidgf.android; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; + +import com.xabber.android.data.DatabaseManager; +import com.xabber.android.data.entity.AbstractAccountTable; +import java.util.*; + +/** + * Storage for whatsapp contacts + * + * @author david.guillen + */ +public class WAContacts extends AbstractAccountTable { + + private static final class Fields implements AbstractAccountTable.Fields { + + private Fields() { + } + + public static final String ACCOUNT = "account"; + public static final String USER_PHONE = "user_phone"; + public static final String USER_NAME = "user_name"; + } + + private static final String NAME = "wa_contacts"; + private static final String[] PROJECTION = new String[] { Fields.ACCOUNT, Fields.USER_PHONE, Fields.USER_NAME }; + static final boolean DEFAULT_EXPANDED = true; + + private final DatabaseManager databaseManager; + private SQLiteStatement writeStatement; + private final Object writeLock; + + private final static WAContacts instance; + + static { + instance = new WAContacts(DatabaseManager.getInstance()); + DatabaseManager.getInstance().addTable(instance); + } + + public static WAContacts getInstance() { + return instance; + } + + private WAContacts(DatabaseManager databaseManager) { + this.databaseManager = databaseManager; + writeStatement = null; + writeLock = new Object(); + } + + @Override + public void create(SQLiteDatabase db) { + String sql = "CREATE TABLE IF NOT EXISTS " + NAME + " (" + + Fields.ACCOUNT + " TEXT," + + Fields.USER_PHONE + " TEXT," + + Fields.USER_NAME + " TEXT);"; + DatabaseManager.execSQL(db, sql); + } + + @Override + public void migrate(SQLiteDatabase db, int toVersion) { + super.migrate(db, toVersion); + // No migration at this time + } + + public void addContact(String account, String user, String name) { + synchronized (writeLock) { + if (writeStatement == null) { + SQLiteDatabase db = databaseManager.getWritableDatabase(); + create(db); + writeStatement = db.compileStatement("INSERT OR REPLACE INTO " + + NAME + " (" + Fields.ACCOUNT + ", " + + Fields.USER_PHONE + ", " + + Fields.USER_NAME + ") VALUES (?, ?, ?);"); + } + writeStatement.bindString(1, account); + writeStatement.bindString(2, user); + writeStatement.bindString(3, name); + writeStatement.execute(); + } + } + + public void removeContact(String account, String user) { + synchronized (writeLock) { + if (writeStatement == null) { + SQLiteDatabase db = databaseManager.getWritableDatabase(); + create(db); + writeStatement = db.compileStatement("DELETE FROM " + + NAME + " WHERE " + Fields.USER_PHONE + " = " + + "? AND " + Fields.ACCOUNT + " = ?;"); + } + writeStatement.bindString(1, user); + writeStatement.bindString(2, account); + writeStatement.execute(); + } + } + + @Override + protected String getTableName() { + return NAME; + } + + @Override + protected String[] getProjection() { + return PROJECTION; + } + + public Vector < Vector > getContacts(String account) { + Vector < Vector > ret = new Vector < Vector >(); + + SQLiteDatabase db = databaseManager.getWritableDatabase(); + create(db); + Cursor c = db.query(NAME, PROJECTION, "account = '" + account + "'", null, null, null, null); + + if (c.moveToFirst()) { + do { + String user = c.getString(1); + String name = c.getString(2); + Vector < String > e = new Vector (); + e.add(user); + e.add(name); + ret.add(e); + } while(c.moveToNext()); + } + + return ret; + } + +} + diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 130044cbac..4ccb10e073 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -59,7 +59,6 @@ public Tree read_tree(DataBuffer data) { return t; }else if (type == 2) { data.popData(1); - System.out.println("NO data in this tree\n"); return new Tree("treeerr"); // No data in this tree... } @@ -83,7 +82,6 @@ Tree parse_tree(DataBuffer data) { int bflag = (data.getInt(1,0) & 0xF0)>>4; int bsize = data.getInt(2,1); if (bsize > data.size()-3) { - System.out.println("NO data enough\n"); return new Tree("treeerr"); // Next message incomplete, return consumed data } data.popData(3); @@ -119,7 +117,6 @@ public int pushIncomingData(byte [] data) { if (!t.getTag().equals("treeerr")) this.processPacket(t); - System.out.println("Received tree!\n"); System.out.println(t.toString(0)); } while (!t.getTag().equals("treeerr") && db.size() >= 3); @@ -190,7 +187,6 @@ else if (t.getTag().equals("presence")) { } else if (t.getTag().equals("iq")) { if (t.hasAttribute("from") && t.hasAttribute("id") && t.hasChild("ping")) { - System.out.println("Received PING!\n"); this.doPong(t.getAttribute("id"),t.getAttribute("from")); } } @@ -217,6 +213,7 @@ else if (t.getTag().equals("message")) { if (tb.hasAttributeValue("type","image")) { this.receiveMessage( new ImageMessage(from,time,id,tb.getAttribute("url"),tb.getData(),author)); + addContact(from,false); } } } @@ -408,12 +405,20 @@ public void subscribePresence(String user) { new HashMap < String,String >() {{ put("type","subscribe"); put("to",username); }} ); outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(request,true))); + + // Meanwhile add the user presence... + Presence.Mode mode = Presence.Mode.away; + Presence presp = new Presence(Presence.Type.available); + presp.setMode(mode); + presp.setFrom(MiscUtil.getUser(user)); + presp.setTo(MiscUtil.getUser(phone)); + received_packets.add(presp); } public byte [] getWriteData() { byte [] r = outbuffer.getPtr(); - System.out.println("retrieveing data to send " + String.valueOf(r.length)); + System.out.println("Sending some bytes ... " + String.valueOf(r.length)); outbuffer = new DataBuffer(); return r; } From 0c8304fa491a8a9c7356af81e0f64b07d227cb77 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sat, 24 Aug 2013 20:57:41 +0200 Subject: [PATCH 19/33] Fixed small issue with contact addition startup :) --- src/net/davidgf/android/WAConnection.java | 4 +--- .../davidgf/android/WhatsappConnection.java | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index df27187682..647da8084c 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -401,9 +401,7 @@ public void sendPacket(Packet packet) { String name = saved_contacts.get(i).get(1); rr.addRosterItem(new RosterPacket.Item(user, name)); // Subscribe presence - // FIXME: addContact fails when not connected, should - // add a fallback to a queue or something... - //waconnection.addContact(user,true); + waconnection.addContact(user,true); } submitPacket(rr); diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 4ccb10e073..797e7734da 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -167,6 +167,10 @@ else if (t.getTag().equals("success")) { //this->sendInitial(); //this->updateGroups(); + // Resend contact status query (for already added contacts) + for (int i = 0; i < contacts.size(); i++) + subscribePresence(contacts.get(i).phone); + //std::cout << "Logged in!!!" << std::endl; //std::cout << "Account " << phone << " status: " << account_status << " kind: " << account_type << // " expires: " << account_expiration << " creation: " << account_creation << std::endl; @@ -389,14 +393,21 @@ void sendAuthResponse() { public void addContact(String user, boolean user_request) { user = MiscUtil.getUser(user); + boolean found = false; for (int i = 0; i < contacts.size(); i++) - if (contacts.get(i).phone.equals(user)) + if (contacts.get(i).phone.equals(user)) { + found = true; return; + } - Contact c = new Contact(user, user_request); - contacts.add(c); + if (!found) { + Contact c = new Contact(user, user_request); + contacts.add(c); + } - subscribePresence(user); + if (conn_status == SessionStatus.SessionConnected) { + subscribePresence(user); + } } public void subscribePresence(String user) { From b406df20bd029553a2af7814439d5a29e2c1418b Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sun, 25 Aug 2013 12:49:00 +0200 Subject: [PATCH 20/33] Added avatar support. Also fixed a small bug with user status. --- src/net/davidgf/android/MiscUtil.java | 13 ++++ src/net/davidgf/android/WAConnection.java | 69 ++++++++++++------- .../davidgf/android/WhatsappConnection.java | 69 ++++++++++++++++++- 3 files changed, 123 insertions(+), 28 deletions(-) diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java index 6757ad4d3b..96ee7633ce 100644 --- a/src/net/davidgf/android/MiscUtil.java +++ b/src/net/davidgf/android/MiscUtil.java @@ -1,6 +1,9 @@ package net.davidgf.android; +import java.security.MessageDigest; +import java.math.BigInteger; + public class MiscUtil { private static byte [] base64_chars = new byte []{'A','B','C','D','E','F','G','H','I','J','K','L', @@ -129,6 +132,16 @@ public static String getUser(String user) { return user.split("@")[0]; } + public static String getEncodedSha1Sum( byte [] data ) { + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + md.update(data); + return new BigInteger(1, md.digest()).toString(16); + } + catch (Exception e) { + return new String(""); + } + } } diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index 647da8084c..a32b2c20f1 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -20,6 +20,8 @@ import org.jivesoftware.smack.packet.Registration; import org.jivesoftware.smack.packet.RosterPacket; import org.jivesoftware.smack.packet.IQ; +import com.xabber.xmpp.vcard.VCard; +import com.xabber.xmpp.vcard.BinaryPhoto; import com.xabber.android.data.connection.ConnectionThread; import net.davidgf.android.WhatsappConnection; @@ -367,6 +369,16 @@ public void sendPacket(Packet packet) { Registration r = (Registration)packet; System.out.println(r.getChildElementXML()); } + if (packet instanceof VCard) { + // The request for a VCard has been queued, now we should provide the Avatar! + packet.setFrom(packet.getTo()); + VCard vc = (VCard)packet; + + BinaryPhoto bp = new BinaryPhoto(); + bp.setData(waconnection.getUserAvatar(packet.getFrom())); + vc.getPhotos().add(bp); + submitPacket(packet); + } if (packet instanceof RosterPacket) { RosterPacket r = (RosterPacket)packet; // Check Add/Remove Contact/Group: @@ -412,6 +424,9 @@ public void sendPacket(Packet packet) { // Notify others this.firePacketSendingListeners(packet); + + // DO stuff again + processLoop(); } private void popWriteData() { @@ -618,31 +633,7 @@ private void readPackets(Thread thisThread, InputStream istream) { } } - // Proceed to push data to underlying connection class - synchronized ( waconnection ) { - synchronized (inbuffer) { - int used = waconnection.pushIncomingData(inbuffer); - inbuffer = Arrays.copyOfRange(inbuffer, used, inbuffer.length); - } - } - - // Process stuff - this.popWriteData(); // Ready data might be waiting ... - - if (waconnection.isConnected() && !waconnected) { - connectionOK(); // Notify the connection status - waconnected = true; - } - - if (listenerExecutor == null) { - System.out.println("Null!!! Shit\n"); - } - - Packet p = waconnection.getNextPacket(); - while (p != null) { - submitPacket(p); - p = waconnection.getNextPacket(); - } + processLoop(); } while (r >= 0); }catch (IOException e) { System.out.println("Error!\n" + e.toString()); @@ -653,6 +644,34 @@ private void readPackets(Thread thisThread, InputStream istream) { // Signal the writer thread so it can also end writewait.release(); } + + private void processLoop() { + // Proceed to push data to underlying connection class + synchronized ( waconnection ) { + synchronized (inbuffer) { + int used = waconnection.pushIncomingData(inbuffer); + inbuffer = Arrays.copyOfRange(inbuffer, used, inbuffer.length); + } + } + + // Process stuff + this.popWriteData(); // Ready data might be waiting ... + + if (waconnection.isConnected() && !waconnected) { + connectionOK(); // Notify the connection status + waconnected = true; + } + + if (listenerExecutor == null) { + System.out.println("Null!!! Shit\n"); + } + + Packet p = waconnection.getNextPacket(); + while (p != null) { + submitPacket(p); + p = waconnection.getNextPacket(); + } + } /** * Establishes a connection to the XMPP server and performs an automatic login diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 797e7734da..3e62260a0a 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -13,7 +13,10 @@ import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.RosterPacket; import org.jivesoftware.smackx.packet.MessageEvent; +import com.xabber.xmpp.vcard.VCard; +import com.xabber.xmpp.avatar.VCardUpdate; import java.util.*; @@ -35,6 +38,8 @@ private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChall private Vector received_packets; private Vector contacts; + + private int iqid; public WhatsappConnection(String phone, String pass, String nick) { session_key = new byte[20]; @@ -47,6 +52,7 @@ public WhatsappConnection(String phone, String pass, String nick) { outbuffer = new DataBuffer(); received_packets = new Vector (); contacts = new Vector (); + iqid = 0; } public Tree read_tree(DataBuffer data) { @@ -168,8 +174,11 @@ else if (t.getTag().equals("success")) { //this->updateGroups(); // Resend contact status query (for already added contacts) - for (int i = 0; i < contacts.size(); i++) + System.out.println("SUbscribe delayed\n"); + for (int i = 0; i < contacts.size(); i++) { subscribePresence(contacts.get(i).phone); + queryPreview(contacts.get(i).phone); + } //std::cout << "Logged in!!!" << std::endl; //std::cout << "Account " << phone << " status: " << account_status << " kind: " << account_type << @@ -193,6 +202,14 @@ else if (t.getTag().equals("iq")) { if (t.hasAttribute("from") && t.hasAttribute("id") && t.hasChild("ping")) { this.doPong(t.getAttribute("id"),t.getAttribute("from")); } + + if (t.hasAttributeValue("type","result") && t.hasAttribute("from")) { + Tree tb = t.getChild("picture"); + if (tb != null) { + if (tb.hasAttributeValue("type","preview")) + this.addPreviewPicture(t.getAttribute("from"),tb.getData()); + } + } } else if (t.getTag().equals("message")) { if (t.hasAttributeValue("type","chat") && t.hasAttribute("from")) { @@ -257,6 +274,51 @@ private DataBuffer generateResponse(final String from, final String type, final return serialize_tree(mes,true); } + private void queryPreview(final String user) { + final String fuser = user+"@"+whatsappserver; + final String reqid = String.valueOf(++iqid); + Tree pic = new Tree ("picture", + new HashMap < String,String >() {{ put("xmlns","w:profile:picture"); put("type","preview"); }} ); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to",fuser); }} ); + + req.addChild(pic); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + + public byte[] getUserAvatar(String user) { + // Look for preview + user = MiscUtil.getUser(user); + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(user)) { + return contacts.get(i).ppprev; + } + } + return new byte[0]; + } + + private void addPreviewPicture(String user, byte [] picture) { + System.out.println("Received preview...\n"); + user = MiscUtil.getUser(user); + + // Save preview + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(user)) { + contacts.get(i).ppprev = picture; + break; + } + } + + VCardUpdate vc = new VCardUpdate(); + vc.setPhotoHash(MiscUtil.getEncodedSha1Sum(picture)); + Presence p = new Presence(Presence.Type.subscribed); + p.setTo(phone); + p.setFrom(user); + p.addExtension(vc); + received_packets.add(p); + } + private void receiveMessage(AbstractMessage msg) { received_packets.add(msg.serializePacket()); } @@ -407,11 +469,12 @@ public void addContact(String user, boolean user_request) { if (conn_status == SessionStatus.SessionConnected) { subscribePresence(user); + queryPreview(user); } } public void subscribePresence(String user) { - final String username = MiscUtil.getUser(user); + final String username = MiscUtil.getUser(user)+"@"+whatsappserver; Tree request = new Tree("presence", new HashMap < String,String >() {{ put("type","subscribe"); put("to",username); }} ); @@ -520,7 +583,7 @@ public class Contact { String status; long last_seen, last_status; boolean mycontact; - String ppprev, pppicture; + byte[] ppprev, pppicture; boolean subscribed; Contact(String phone, boolean myc) { From e3a3d7020895713bb72eeea27ee0f99427e2bc79 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sun, 25 Aug 2013 19:32:14 +0200 Subject: [PATCH 21/33] Disabling debug output --- src/net/davidgf/android/WhatsappConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 3e62260a0a..215425d21d 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -123,7 +123,7 @@ public int pushIncomingData(byte [] data) { if (!t.getTag().equals("treeerr")) this.processPacket(t); - System.out.println(t.toString(0)); + //System.out.println(t.toString(0)); } while (!t.getTag().equals("treeerr") && db.size() >= 3); return data.length - db.size(); From 6bdd1e79a6db390d8e8b8c7fa5c46a52e232c30c Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Tue, 27 Aug 2013 15:27:58 +0200 Subject: [PATCH 22/33] Support for timestamp in incoming messages. Now it's possible to know the sending time of a message when you receive it after being offline. --- src/net/davidgf/android/WhatsappConnection.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 215425d21d..44d3c6ba18 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -15,6 +15,7 @@ import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.RosterPacket; import org.jivesoftware.smackx.packet.MessageEvent; +import org.jivesoftware.smackx.packet.DelayInformation; import com.xabber.xmpp.vcard.VCard; import com.xabber.xmpp.avatar.VCardUpdate; @@ -553,6 +554,13 @@ public Packet serializePacket() { message.setType(Message.Type.chat); message.setBody(this.message); + // XXX: Criteria for adding Delay info is + // if the timestamp and the current time differ in more than 10 seconds + DelayInformation d = new DelayInformation(new Date(time*1000)); + long epoch = System.currentTimeMillis()/1000; + if (Math.abs(time - epoch) > 10) + message.addExtension(d); + return message; } } @@ -572,6 +580,13 @@ public Packet serializePacket() { message.setType(Message.Type.chat); message.setBody(url); + // XXX: Criteria for adding Delay info is + // if the timestamp and the current time differ in more than 10 seconds + DelayInformation d = new DelayInformation(new Date(time*1000)); + long epoch = System.currentTimeMillis()/1000; + if (Math.abs(time - epoch) > 10) + message.addExtension(d); + return message; } } From e41b76a5ca82580719648a9613131952203c0bfd Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Tue, 27 Aug 2013 16:00:11 +0200 Subject: [PATCH 23/33] Adding presence sending. Available and Chat statuses map to "online" whatsapp status, the rest map to "unavailable". --- src/net/davidgf/android/WAConnection.java | 9 +++++++++ src/net/davidgf/android/WhatsappConnection.java | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index a32b2c20f1..5f339665e6 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -379,6 +379,15 @@ public void sendPacket(Packet packet) { vc.getPhotos().add(bp); submitPacket(packet); } + if (packet instanceof Presence) { + // Sending our presence :) + String status = "unavailable"; + Presence pres = (Presence)packet; + if (pres.getMode() == Presence.Mode.chat || pres.getMode() == Presence.Mode.available) + status = "available"; + + waconnection.setMyPresence(status); + } if (packet instanceof RosterPacket) { RosterPacket r = (RosterPacket)packet; // Check Add/Remove Contact/Group: diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 44d3c6ba18..18f22a312c 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -346,6 +346,11 @@ public Packet getNextPacket() { return r; } + public void setMyPresence(String pres) { + mypresence = pres; + notifyMyPresence(); + } + private void notifyMyPresence() { // Send the nickname and the current status Tree pres = new Tree("presence", new HashMap < String,String >() {{ put("name",nickname); put("type",mypresence); }} ); From c57f39f8c396b1b0ead7752717643be45608c92e Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Tue, 27 Aug 2013 16:31:48 +0200 Subject: [PATCH 24/33] Adding feature: sending status message along with status. --- src/net/davidgf/android/MiscUtil.java | 9 +++++++++ src/net/davidgf/android/WAConnection.java | 2 +- .../davidgf/android/WhatsappConnection.java | 18 ++++++++++++++++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java index 96ee7633ce..6e976b58ff 100644 --- a/src/net/davidgf/android/MiscUtil.java +++ b/src/net/davidgf/android/MiscUtil.java @@ -128,6 +128,15 @@ public static String bytesToUTF8(byte [] ba) { } } + public static byte [] UTF8ToBytes(String st) { + try { + return st.getBytes("UTF-8"); + } + catch (Exception e) { + return new byte [0]; + } + } + public static String getUser(String user) { return user.split("@")[0]; } diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index 5f339665e6..d76e38cd18 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -386,7 +386,7 @@ public void sendPacket(Packet packet) { if (pres.getMode() == Presence.Mode.chat || pres.getMode() == Presence.Mode.available) status = "available"; - waconnection.setMyPresence(status); + waconnection.setMyPresence(status, pres.getStatus()); } if (packet instanceof RosterPacket) { RosterPacket r = (RosterPacket)packet; diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 18f22a312c..e04a9bdd5a 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -41,6 +41,7 @@ private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChall private Vector contacts; private int iqid; + private String mymessage = ""; public WhatsappConnection(String phone, String pass, String nick) { session_key = new byte[20]; @@ -346,16 +347,29 @@ public Packet getNextPacket() { return r; } - public void setMyPresence(String pres) { + public void setMyPresence(String pres, String msg) { mypresence = pres; + mymessage = msg; notifyMyPresence(); } private void notifyMyPresence() { // Send the nickname and the current status Tree pres = new Tree("presence", new HashMap < String,String >() {{ put("name",nickname); put("type",mypresence); }} ); - + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(pres,true))); + + Tree xhash = new Tree("x", new HashMap < String,String >() {{ put("xmlns","jabber:x:event"); }} ); + xhash.addChild(new Tree("server")); + Tree tbody = new Tree ("body"); tbody.setData(MiscUtil.UTF8ToBytes(this.mymessage)); + + String stime = String.valueOf(System.currentTimeMillis()/1000); + final String iqtime_id = stime + "-" + String.valueOf(++iqid); + Tree mes = new Tree("message", new HashMap < String,String >() {{ + put("to","s.us"); put("type","chat"); put("id",iqtime_id); }} ); + mes.addChild(xhash); mes.addChild(tbody); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(mes,true))); } void doPong(final String id, final String from) { From 015a730e1187ae79fb12ab706a2808720e6bc28f Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sat, 31 Aug 2013 14:37:31 +0200 Subject: [PATCH 25/33] Fix small NullPointerException bug due to presence setting when disconnected --- src/net/davidgf/android/WhatsappConnection.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index e04a9bdd5a..d50abec9f2 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -350,7 +350,8 @@ public Packet getNextPacket() { public void setMyPresence(String pres, String msg) { mypresence = pres; mymessage = msg; - notifyMyPresence(); + if (conn_status == SessionStatus.SessionConnected) + notifyMyPresence(); } private void notifyMyPresence() { From 4b7f81cbaade3a97f99dd10fa1ee63f79b186cd8 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sun, 1 Sep 2013 13:38:18 +0200 Subject: [PATCH 26/33] Adding support for group chats. Temporary disabled status message pushing due to some bug causing stream-not-well-formed messages. Need to add "nickname" option in WhatsApp account settings so we can send our name to show in messages. --- .../data/extension/muc/MUCManager.java | 7 +- .../android/data/extension/muc/RoomChat.java | 18 +- src/net/davidgf/android/MiscUtil.java | 5 + src/net/davidgf/android/WAConnection.java | 23 +- .../davidgf/android/WhatsappConnection.java | 229 +++++++++++++++++- 5 files changed, 261 insertions(+), 21 deletions(-) diff --git a/src/com/xabber/android/data/extension/muc/MUCManager.java b/src/com/xabber/android/data/extension/muc/MUCManager.java index a6a35c6388..617382b23f 100644 --- a/src/com/xabber/android/data/extension/muc/MUCManager.java +++ b/src/com/xabber/android/data/extension/muc/MUCManager.java @@ -130,7 +130,7 @@ private void onLoaded(Collection roomChats, * @param room * @return null if does not exists. */ - private RoomChat getRoomChat(String account, String room) { + public RoomChat getRoomChat(String account, String room) { AbstractChat chat = MessageManager.getInstance().getChat(account, room); if (chat != null && chat instanceof RoomChat) return (RoomChat) chat; @@ -220,19 +220,20 @@ public void run() { * @param password */ public void createRoom(String account, String room, String nickname, - String password, boolean join) { + String password, boolean join, String subject) { removeInvite(getInvite(account, room)); AbstractChat chat = MessageManager.getInstance().getChat(account, room); RoomChat roomChat; if (chat == null || !(chat instanceof RoomChat)) { if (chat != null) MessageManager.getInstance().removeChat(chat); - roomChat = new RoomChat(account, room, nickname, password); + roomChat = new RoomChat(account, room, nickname, password, subject); MessageManager.getInstance().addChat(roomChat); } else { roomChat = (RoomChat) chat; roomChat.setNickname(nickname); roomChat.setPassword(password); + roomChat.setSubject(subject); } requestToWriteRoom(account, room, nickname, password, join); if (join) diff --git a/src/com/xabber/android/data/extension/muc/RoomChat.java b/src/com/xabber/android/data/extension/muc/RoomChat.java index 2825ec4cfc..7e3ca4c91d 100644 --- a/src/com/xabber/android/data/extension/muc/RoomChat.java +++ b/src/com/xabber/android/data/extension/muc/RoomChat.java @@ -82,6 +82,18 @@ public class RoomChat extends AbstractChat { * Invited user for the sent packet ID. */ private final Map invites; + + RoomChat(String account, String user, String nickname, String password, String subject) { + super(account, user); + this.nickname = nickname; + this.password = password; + requested = false; + state = RoomState.unavailable; + this.subject = subject; + multiUserChat = null; + occupants = new HashMap(); + invites = new HashMap(); + } RoomChat(String account, String user, String nickname, String password) { super(account, user); @@ -156,6 +168,10 @@ String getSubject() { return subject; } + void setSubject(String s) { + this.subject = s; + } + MultiUserChat getMultiUserChat() { return multiUserChat; } @@ -486,4 +502,4 @@ protected void onDisconnect() { setState(RoomState.waiting); } -} \ No newline at end of file +} diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java index 6e976b58ff..3be9244153 100644 --- a/src/net/davidgf/android/MiscUtil.java +++ b/src/net/davidgf/android/MiscUtil.java @@ -140,6 +140,11 @@ public static String bytesToUTF8(byte [] ba) { public static String getUser(String user) { return user.split("@")[0]; } + + public static String getUserAndResource(String user) { + String [] re = user.split("/"); + return user.split("@")[0] + "/" + re[re.length-1]; + } public static String getEncodedSha1Sum( byte [] data ) { try { diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index d76e38cd18..f1c78da281 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -23,6 +23,8 @@ import com.xabber.xmpp.vcard.VCard; import com.xabber.xmpp.vcard.BinaryPhoto; import com.xabber.android.data.connection.ConnectionThread; +import com.xabber.android.data.account.AccountItem; +import com.xabber.android.data.connection.ConnectionItem; import net.davidgf.android.WhatsappConnection; import net.davidgf.android.WAContacts; @@ -60,6 +62,7 @@ public class WAConnection extends Connection { private String user = null; private boolean connected = false; private boolean waconnected = false; + private String account_name = null; /** * Flag that indicates if the user is currently authenticated with the server. */ @@ -158,6 +161,10 @@ public Thread newThread(Runnable runnable) { return thread; } }); + + ConnectionItem citem = cthread.getConnectionItem(); + AccountItem aitem = ((AccountItem)citem); + account_name = aitem.getAccount(); } public String getConnectionID() { @@ -426,6 +433,7 @@ public void sendPacket(Packet packet) { } submitPacket(rr); + waconnection.pushGroupUpdate(); } System.out.println(r.toXML()); @@ -509,7 +517,7 @@ private void connectUsingConfiguration(ConnectionConfiguration config) throws XM // Create WA connection API object // FIXME: Set proper nickname msgid = 0; - waconnection = new WhatsappConnection(config.getUsername(), config.getPassword(), config.getUsername()); + waconnection = new WhatsappConnection(config.getUsername(), config.getPassword(), config.getUsername(), account_name); try { if (config.getSocketFactory() == null) { @@ -656,6 +664,8 @@ private void readPackets(Thread thisThread, InputStream istream) { private void processLoop() { // Proceed to push data to underlying connection class + if (inbuffer == null) return; + synchronized ( waconnection ) { synchronized (inbuffer) { int used = waconnection.pushIncomingData(inbuffer); @@ -675,12 +685,17 @@ private void processLoop() { System.out.println("Null!!! Shit\n"); } - Packet p = waconnection.getNextPacket(); + Packet p; + synchronized (waconnection) { + p = waconnection.getNextPacket(); + } while (p != null) { submitPacket(p); - p = waconnection.getNextPacket(); - } + synchronized (waconnection) { + p = waconnection.getNextPacket(); + } } + } /** * Establishes a connection to the XMPP server and performs an automatic login diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index d50abec9f2..75381c8f3a 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -14,8 +14,13 @@ import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.RosterPacket; +import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smackx.packet.MessageEvent; +import org.jivesoftware.smackx.packet.Nick; +import org.jivesoftware.smackx.packet.MUCUser; import org.jivesoftware.smackx.packet.DelayInformation; +import org.jivesoftware.smackx.packet.MUCInitialPresence; +import com.xabber.android.data.extension.muc.MUCManager; import com.xabber.xmpp.vcard.VCard; import com.xabber.xmpp.avatar.VCardUpdate; @@ -39,11 +44,16 @@ private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChall private Vector received_packets; private Vector contacts; + private Map groups; + boolean groups_updated; + int gq_stat; + int gw1,gw2; private int iqid; private String mymessage = ""; + private String account_name = null; - public WhatsappConnection(String phone, String pass, String nick) { + public WhatsappConnection(String phone, String pass, String nick, String aname) { session_key = new byte[20]; this.phone = phone; @@ -55,6 +65,13 @@ public WhatsappConnection(String phone, String pass, String nick) { received_packets = new Vector (); contacts = new Vector (); iqid = 0; + + groups = new HashMap (); + groups_updated = false; + gq_stat = 0; + gw1 = gw2 = 0; + + account_name = aname; } public Tree read_tree(DataBuffer data) { @@ -125,7 +142,7 @@ public int pushIncomingData(byte [] data) { if (!t.getTag().equals("treeerr")) this.processPacket(t); - //System.out.println(t.toString(0)); + System.out.println(t.toString(0)); } while (!t.getTag().equals("treeerr") && db.size() >= 3); return data.length - db.size(); @@ -173,18 +190,13 @@ else if (t.getTag().equals("success")) { this.notifyMyPresence(); //this->sendInitial(); - //this->updateGroups(); + this.updateGroups(); // Resend contact status query (for already added contacts) - System.out.println("SUbscribe delayed\n"); for (int i = 0; i < contacts.size(); i++) { subscribePresence(contacts.get(i).phone); queryPreview(contacts.get(i).phone); } - - //std::cout << "Logged in!!!" << std::endl; - //std::cout << "Account " << phone << " status: " << account_status << " kind: " << account_type << - // " expires: " << account_expiration << " creation: " << account_creation << std::endl; } else if (t.getTag().equals("presence")) { // Receives the presence of the user @@ -201,10 +213,12 @@ else if (t.getTag().equals("presence")) { } } else if (t.getTag().equals("iq")) { + // PING if (t.hasAttribute("from") && t.hasAttribute("id") && t.hasChild("ping")) { this.doPong(t.getAttribute("id"),t.getAttribute("from")); } + // Preview query if (t.hasAttributeValue("type","result") && t.hasAttribute("from")) { Tree tb = t.getChild("picture"); if (tb != null) { @@ -212,6 +226,54 @@ else if (t.getTag().equals("iq")) { this.addPreviewPicture(t.getAttribute("from"),tb.getData()); } } + + // Group stuff + Vector childs = t.getChildren(); + int acc = 0; + for (int j = 0; j < childs.size(); j++) { + if (childs.get(j).getTag().equals("group")) { + boolean rep = groups.containsKey(MiscUtil.getUser(childs.get(j).getAttribute("id"))); + if (!rep) { + groups.put( + MiscUtil.getUser(childs.get(j).getAttribute("id")), + new Group( MiscUtil.getUser(childs.get(j).getAttribute("id")), + childs.get(j).getAttribute("subject"), + MiscUtil.getUser(childs.get(j).getAttribute("owner")) ) ); + + // Query group participants + final String iid = String.valueOf(++iqid); + final String pid = childs.get(j).getAttribute("id"); + final String subj = childs.get(j).getAttribute("subject"); + Tree iq = new Tree("list",new HashMap < String,String >(){{put("xmlns","w:g");}}); + Tree req = new Tree("iq", + new HashMap < String,String >(){{ + put("id",iid); + put("type","get"); + put("to",pid+"@g.us"); + }}); + req.addChild(iq); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + + // Add group as a contact + pushGroupUpdate(); + } + } + else if (childs.get(j).getTag().equals("participant")) { + String gid = MiscUtil.getUser(t.getAttribute("from")); + String pt = MiscUtil.getUser(childs.get(j).getAttribute("jid")); + if (groups.containsKey(gid)) { + groups.get(gid).participants.add(pt); + + pushGroupUpdate(); + } + } + } + + Tree tb = t.getChild("group"); + if (tb != null) { + if (tb.hasAttributeValue("type","preview")) + this.addPreviewPicture(t.getAttribute("from"),t.getData()); + } } else if (t.getTag().equals("message")) { if (t.hasAttributeValue("type","chat") && t.hasAttribute("from")) { @@ -222,6 +284,12 @@ else if (t.getTag().equals("message")) { String id = t.getAttribute("id"); String author = t.getAttribute("author"); + // Group nickname + if (from.contains("@g.us") && t.hasChild("notify")) { + author = t.getChild("notify").getAttribute("name"); + from = from + "/" + author; + } + Tree tb = t.getChild("body"); if (tb != null) { // TODO: Add user here and at UI in case we don't have it @@ -268,6 +336,64 @@ else if (t.getTag().equals("message")) { }*/ } + public void pushGroupUpdate() { + for (Map.Entry entry : groups.entrySet()) { + // Create room + MUCManager.getInstance().createRoom( + account_name, entry.getValue().id, phone, "", false, entry.getValue().subject); + + // Send the room status (chek MultiUserChat.java),matched the filter + /*Presence.Mode mode = Presence.Mode.chat; + Presence presp = new Presence(Presence.Type.available); + presp.setMode(mode); + presp.setFrom(entry.getValue().id + "/" + phone); + presp.setTo(MiscUtil.getUser(phone)); + // Add MUC User + MUCUser user = new MUCUser(); + user.setItem(new MUCUser.Item("member","participant")); + presp.addExtension(user); + received_packets.add(presp);*/ + + for (int i = 0; i < entry.getValue().participants.size(); i++) { + // Send the room status (chek MultiUserChat.java),matched the filter + Presence.Mode mode = Presence.Mode.chat; + Presence presp = new Presence(Presence.Type.available); + presp.setMode(mode); + presp.setFrom(entry.getValue().id + "/" + entry.getValue().participants.get(i)); + presp.setTo(MiscUtil.getUser(phone)); + // Add MUC User + MUCUser user = new MUCUser(); + user.setItem(new MUCUser.Item("member","participant")); + presp.addExtension(user); + received_packets.add(presp); + } + + //rr.addRosterItem(new RosterPacket.Item(, entry.getValue().subject)); + } + +/* // Add group as a contact + RosterPacket rr = new RosterPacket(); + rr.setType(IQ.Type.SET); + for (Map.Entry entry : groups.entrySet()) { + rr.addRosterItem(new RosterPacket.Item(entry.getValue().id, entry.getValue().subject)); + } + received_packets.add(rr); + + // Set group presence to "Chat" + for (Map.Entry entry : groups.entrySet()) { + Presence.Mode mode = Presence.Mode.chat; + Presence presp = new Presence(Presence.Type.available); + presp.setMode(mode); + presp.setFrom(entry.getValue().id); + presp.setTo(MiscUtil.getUser(phone)); + + // MUC + presp.addExtension(new MUCInitialPresence()); + + received_packets.add(presp); + }*/ + } + private DataBuffer generateResponse(final String from, final String type, final String id, final String ans) { Tree received = new Tree(ans,new HashMap < String,String >() {{ put("xmlns","urn:xmpp:receipts"); }} ); Tree mes = new Tree("message",new HashMap < String,String >() {{ @@ -289,6 +415,66 @@ private void queryPreview(final String user) { outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); } + private void updateGroups() { + groups.clear(); + { + final String reqid = String.valueOf(++iqid); + gw1 = iqid; + Tree iq = new Tree("list",new HashMap < String,String >() {{ put("xmlns","w:g"); put("type","owning");}} ); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to","g.us");}} ); + + req.addChild(iq); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + { + final String reqid = String.valueOf(++iqid); + gw2 = iqid; + Tree iq = new Tree("list", + new HashMap < String,String >() {{ put("xmlns","w:g"); put("type","participating");}} ); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to","g.us");}} ); + req.addChild(iq); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + gq_stat = 1; // Queried the groups + } + + private void manageParticipant(final String group, final String participant, final String command) { + Tree part = new Tree("participant",new HashMap < String,String >() {{ put("jid",participant); }} ); + Tree iq = new Tree(command,new HashMap < String,String >() {{ put("xmlns","w:g"); }} ); + iq.addChild(part); + final String reqid = String.valueOf(++iqid); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to",group+"@g.us");}} ); + req.addChild(iq); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + + private void leaveGroup(final String group) { + Tree gr = new Tree("group", new HashMap < String,String >() {{ put("id",group+"@g.us"); }} ); + Tree iq = new Tree("leave", new HashMap < String,String >() {{ put("xmlns","w:g"); }} ); + iq.addChild(gr); + final String reqid = String.valueOf(++iqid); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to","g.us");}} ); + req.addChild(iq); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + + void addGroup(final String subject) { + Tree gr = new Tree("group", + new HashMap < String,String >() {{ put("xmlns","w:g"); put("action","create"); put("subject",subject);}} ); + final String reqid = String.valueOf(++iqid); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to","g.us");}} ); + req.addChild(gr); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + public byte[] getUserAvatar(String user) { // Look for preview user = MiscUtil.getUser(user); @@ -301,7 +487,6 @@ public byte[] getUserAvatar(String user) { } private void addPreviewPicture(String user, byte [] picture) { - System.out.println("Received preview...\n"); user = MiscUtil.getUser(user); // Save preview @@ -370,7 +555,7 @@ private void notifyMyPresence() { put("to","s.us"); put("type","chat"); put("id",iqtime_id); }} ); mes.addChild(xhash); mes.addChild(tbody); - outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(mes,true))); + //outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(mes,true))); } void doPong(final String id, final String from) { @@ -475,6 +660,7 @@ void sendAuthResponse() { public void addContact(String user, boolean user_request) { user = MiscUtil.getUser(user); + if (user.contains("-")) return; // Do not add groups as contacts boolean found = false; for (int i = 0; i < contacts.size(); i++) @@ -531,7 +717,8 @@ public void subscribePresence(String user) { long epoch = System.currentTimeMillis()/1000; String stime = String.valueOf(epoch); Map < String,String > attrs = new HashMap (); - attrs.put("to",to+"@"+whatsappserver); + String full_to = to + "@" + (to.contains("-") ? whatsappservergroup : whatsappserver); + attrs.put("to",full_to); attrs.put("type","chat"); attrs.put("id",stime+"-"+String.valueOf(id)); attrs.put("t",stime); @@ -570,7 +757,7 @@ public ChatMessage(String from, long time, String id, String message, String aut public Packet serializePacket() { Message message = new Message(); message.setTo(MiscUtil.getUser(phone)); - message.setFrom(MiscUtil.getUser(this.from)); + message.setFrom(MiscUtil.getUserAndResource(this.from)); message.setType(Message.Type.chat); message.setBody(this.message); @@ -596,10 +783,13 @@ public ImageMessage(String from, long time, String id, String url, byte [] prev, public Packet serializePacket() { Message message = new Message(); message.setTo(MiscUtil.getUser(phone)); - message.setFrom(MiscUtil.getUser(this.from)); + message.setFrom(MiscUtil.getUserAndResource(this.from)); message.setType(Message.Type.chat); message.setBody(url); + if (author != null && author.length() != 0) + message.addExtension(new Nick(author)); + // XXX: Criteria for adding Delay info is // if the timestamp and the current time differ in more than 10 seconds DelayInformation d = new DelayInformation(new Date(time*1000)); @@ -630,6 +820,19 @@ public class Contact { this.status = ""; } }; + + public class Group { + String id, subject, owner; + Vector participants; + + Group(String id, String subject, String owner) { + this.id = id; + this.subject = subject; + this.owner = owner; + participants = new Vector (); + } + }; + } From 44cf3121a0654a2d9f3d8cd7d3d626dcc5d0885c Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sun, 1 Sep 2013 14:22:21 +0200 Subject: [PATCH 27/33] Adding "RESOURCE" as nickname for WA accounts. That is the name that will appear in the group chat. --- res/xml/account_editor_wapp.xml | 6 ++++++ src/net/davidgf/android/WAConnection.java | 5 ++++- src/net/davidgf/android/WhatsappConnection.java | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/res/xml/account_editor_wapp.xml b/res/xml/account_editor_wapp.xml index dbf312b8ac..9900c4f9d1 100644 --- a/res/xml/account_editor_wapp.xml +++ b/res/xml/account_editor_wapp.xml @@ -42,6 +42,12 @@ android:password="true" android:singleLine="true" /> + () {{ put("xmlns","urn:xmpp:receipts"); }} ); - Tree notify = new Tree ("notify", new HashMap < String,String >() {{ put("xmlns","urn:xmpp:whatsapp"); put("name",to); }} ); + Tree notify = new Tree ("notify", new HashMap < String,String >() {{ put("xmlns","urn:xmpp:whatsapp"); put("name",nickname); }} ); Tree xhash = new Tree ("x", new HashMap < String,String >() {{ put("xmlns","jabber:x:event"); }} ); xhash.addChild(new Tree("server")); Tree tbody = new Tree("body"); From fbdb3704041634ae72e86a6bf61010a8d87b7267 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Mon, 2 Sep 2013 21:00:49 +0200 Subject: [PATCH 28/33] Correct small bug which prevented build. --- src/com/xabber/android/ui/MUCEditor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/xabber/android/ui/MUCEditor.java b/src/com/xabber/android/ui/MUCEditor.java index 4c69378319..bb38f4cc62 100644 --- a/src/com/xabber/android/ui/MUCEditor.java +++ b/src/com/xabber/android/ui/MUCEditor.java @@ -206,7 +206,7 @@ public void onClick(View view) { .removeMessageNotification(this.account, this.room); } MUCManager.getInstance() - .createRoom(account, room, nick, password, join); + .createRoom(account, room, nick, password, join, ""); finish(); break; default: From 8c1b0bc3ba64a2ece64282cfef151769435adc3f Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sun, 15 Sep 2013 12:42:23 +0200 Subject: [PATCH 29/33] Cleanup of dead code --- src/net/davidgf/android/WAConnection.java | 64 +------------------ .../davidgf/android/WhatsappConnection.java | 38 +---------- 2 files changed, 2 insertions(+), 100 deletions(-) diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index d9c9242e85..90ce8548b7 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -764,70 +764,8 @@ public boolean isAlive() { return true; } - /** - * Wrapper for server keep alive. - * - * @author alexander.ivanov - * - */ - private class AliveReader extends Reader { - final Reader wrappedReader; - - public AliveReader(Reader wrappedReader) { - this.wrappedReader = wrappedReader; - } - - private void onRead() { - //packetWriter.responseReceived(); - } - - @Override - public int read(char[] buf, int offset, int count) throws IOException { - final int result = wrappedReader.read(buf, offset, count); - onRead(); - return result; - } - - public void close() throws IOException { - wrappedReader.close(); - } - - public int read() throws IOException { - final int result = wrappedReader.read(); - onRead(); - return result; - } - - public int read(char buf[]) throws IOException { - final int result = wrappedReader.read(buf); - onRead(); - return result; - } - - public long skip(long n) throws IOException { - return wrappedReader.skip(n); - } - - public boolean ready() throws IOException { - return wrappedReader.ready(); - } - - public boolean markSupported() { - return wrappedReader.markSupported(); - } - - public void mark(int readAheadLimit) throws IOException { - wrappedReader.mark(readAheadLimit); - } - - public void reset() throws IOException { - wrappedReader.reset(); - } - } - - - /** + /** * A runnable to notify all listeners of a packet. */ private class ListenerNotification implements Runnable { diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 4828d6f594..89d243bd57 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -341,19 +341,7 @@ public void pushGroupUpdate() { // Create room MUCManager.getInstance().createRoom( account_name, entry.getValue().id, phone, "", false, entry.getValue().subject); - - // Send the room status (chek MultiUserChat.java),matched the filter - /*Presence.Mode mode = Presence.Mode.chat; - Presence presp = new Presence(Presence.Type.available); - presp.setMode(mode); - presp.setFrom(entry.getValue().id + "/" + phone); - presp.setTo(MiscUtil.getUser(phone)); - // Add MUC User - MUCUser user = new MUCUser(); - user.setItem(new MUCUser.Item("member","participant")); - presp.addExtension(user); - received_packets.add(presp);*/ - + for (int i = 0; i < entry.getValue().participants.size(); i++) { // Send the room status (chek MultiUserChat.java),matched the filter Presence.Mode mode = Presence.Mode.chat; @@ -367,31 +355,7 @@ public void pushGroupUpdate() { presp.addExtension(user); received_packets.add(presp); } - - //rr.addRosterItem(new RosterPacket.Item(, entry.getValue().subject)); - } - -/* // Add group as a contact - RosterPacket rr = new RosterPacket(); - rr.setType(IQ.Type.SET); - for (Map.Entry entry : groups.entrySet()) { - rr.addRosterItem(new RosterPacket.Item(entry.getValue().id, entry.getValue().subject)); } - received_packets.add(rr); - - // Set group presence to "Chat" - for (Map.Entry entry : groups.entrySet()) { - Presence.Mode mode = Presence.Mode.chat; - Presence presp = new Presence(Presence.Type.available); - presp.setMode(mode); - presp.setFrom(entry.getValue().id); - presp.setTo(MiscUtil.getUser(phone)); - - // MUC - presp.addExtension(new MUCInitialPresence()); - - received_packets.add(presp); - }*/ } private DataBuffer generateResponse(final String from, final String type, final String id, final String ans) { From 2f4f85742793501cf2f89954f84e6195b059031d Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sun, 15 Sep 2013 12:45:40 +0200 Subject: [PATCH 30/33] More cleanup --- Makefile | 6 +++--- src/org/jivesoftware/smack/packet/Message.java | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 884e116cf5..616f0bc0b6 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -android update project --path . --target android-10 --name Xabber - -ant release +all: + android update project --path . --target android-10 --name Xabber + ant release diff --git a/src/org/jivesoftware/smack/packet/Message.java b/src/org/jivesoftware/smack/packet/Message.java index 0ca9037c16..d28a9f4848 100644 --- a/src/org/jivesoftware/smack/packet/Message.java +++ b/src/org/jivesoftware/smack/packet/Message.java @@ -470,6 +470,7 @@ public String toXML() { return buf.toString(); } + public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; From 48f113b597d2e6c8407276ce5f523c0037b1a8a4 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sun, 15 Sep 2013 13:43:14 +0200 Subject: [PATCH 31/33] Add last seen at the VCard notes field. --- src/net/davidgf/android/WAConnection.java | 4 + .../davidgf/android/WhatsappConnection.java | 88 +++++++++++++++++-- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index 90ce8548b7..f26f5b132e 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -21,6 +21,7 @@ import org.jivesoftware.smack.packet.RosterPacket; import org.jivesoftware.smack.packet.IQ; import com.xabber.xmpp.vcard.VCard; +import com.xabber.xmpp.vcard.VCardProperty; import com.xabber.xmpp.vcard.BinaryPhoto; import com.xabber.android.data.connection.ConnectionThread; import com.xabber.android.data.account.AccountItem; @@ -383,6 +384,9 @@ public void sendPacket(Packet packet) { // The request for a VCard has been queued, now we should provide the Avatar! packet.setFrom(packet.getTo()); VCard vc = (VCard)packet; + + // Last seen at notes + vc.getProperties().put(VCardProperty.NOTE,waconnection.getNotes(packet.getFrom())); BinaryPhoto bp = new BinaryPhoto(); bp.setData(waconnection.getUserAvatar(packet.getFrom())); diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 89d243bd57..e7258bbfd6 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -196,6 +196,7 @@ else if (t.getTag().equals("success")) { for (int i = 0; i < contacts.size(); i++) { subscribePresence(contacts.get(i).phone); queryPreview(contacts.get(i).phone); + getLastSeen(contacts.get(i).phone); } } else if (t.getTag().equals("presence")) { @@ -210,6 +211,9 @@ else if (t.getTag().equals("presence")) { presp.setFrom(MiscUtil.getUser(t.getAttribute("from"))); presp.setTo(MiscUtil.getUser(phone)); received_packets.add(presp); + + // Schedule the last seen querying + getLastSeen(t.getAttribute("from")); } } else if (t.getTag().equals("iq")) { @@ -225,6 +229,12 @@ else if (t.getTag().equals("iq")) { if (tb.hasAttributeValue("type","preview")) this.addPreviewPicture(t.getAttribute("from"),tb.getData()); } + tb = t.getChild("query"); + if (tb != null) { + if (tb.hasAttributeValue("xmlns","jabber:iq:last") && tb.hasAttribute("seconds")) { + this.notifyLastSeen(t.getAttribute("from"),tb.getAttribute("seconds")); + } + } } // Group stuff @@ -336,6 +346,48 @@ else if (t.getTag().equals("message")) { }*/ } + private String last_seen_text(long t) { + if (t < 60) + return String.valueOf(t) + " seconds ago"; + else if (t < 60*60) + return String.valueOf(t/60) + " minute(s) ago"; + else if (t < 60*60*24) + return String.valueOf(t/60/60) + " hour(s) ago"; + else if (t < 48*60*60) + return "yesterday"; + else + return String.valueOf(t/60/60/24) + " day(s) ago"; + } + + private void notifyLastSeen(final String from, final String seconds) { + final String u = MiscUtil.getUser(from); + final long sec = Integer.parseInt(seconds); + + // Save last seen time + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(u)) { + contacts.get(i).last_seen = sec; + break; + } + } + + requestVCardUpdate(u); + } + + public String getNotes(String u) { + u = MiscUtil.getUser(u); + + String note = ""; + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(u)) { + note = "Last seen: " + last_seen_text(contacts.get(i).last_seen); + break; + } + } + + return note; + } + public void pushGroupUpdate() { for (Map.Entry entry : groups.entrySet()) { // Create room @@ -461,13 +513,24 @@ private void addPreviewPicture(String user, byte [] picture) { } } - VCardUpdate vc = new VCardUpdate(); - vc.setPhotoHash(MiscUtil.getEncodedSha1Sum(picture)); - Presence p = new Presence(Presence.Type.subscribed); - p.setTo(phone); - p.setFrom(user); - p.addExtension(vc); - received_packets.add(p); + requestVCardUpdate(user); + } + + private void requestVCardUpdate(String user) { + user = MiscUtil.getUser(user); + + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(user)) { + VCardUpdate vc = new VCardUpdate(); + vc.setPhotoHash(MiscUtil.getEncodedSha1Sum(contacts.get(i).ppprev)); + Presence p = new Presence(Presence.Type.subscribed); + p.setTo(phone); + p.setFrom(user); + p.addExtension(vc); + received_packets.add(p); + break; + } + } } private void receiveMessage(AbstractMessage msg) { @@ -641,6 +704,7 @@ public void addContact(String user, boolean user_request) { if (conn_status == SessionStatus.SessionConnected) { subscribePresence(user); queryPreview(user); + getLastSeen(user); } } @@ -660,6 +724,16 @@ public void subscribePresence(String user) { received_packets.add(presp); } + private void getLastSeen(final String user) { + final String id = String.valueOf(++iqid); + final String fuser = MiscUtil.getUser(user)+"@"+whatsappserver; + Tree iq = new Tree("iq", + new HashMap < String,String >() {{ put("id",id); put("type","get"); put("to",fuser); }} ); + Tree req = new Tree("query", new HashMap < String,String >() {{ put("xmlns","jabber:iq:last"); }} ); + iq.addChild(req); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(iq,true))); + } public byte [] getWriteData() { byte [] r = outbuffer.getPtr(); From 604c9ec682f01ec8d4d75a54817e0b32b14d646f Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sun, 15 Sep 2013 14:49:57 +0200 Subject: [PATCH 32/33] Start adding HTTPS interface for user statuses --- src/net/davidgf/android/MiscUtil.java | 30 ++++++++ .../davidgf/android/WhatsappConnection.java | 72 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java index 3be9244153..c669eb0218 100644 --- a/src/net/davidgf/android/MiscUtil.java +++ b/src/net/davidgf/android/MiscUtil.java @@ -3,6 +3,7 @@ import java.security.MessageDigest; import java.math.BigInteger; +import java.util.*; public class MiscUtil { @@ -34,6 +35,12 @@ private static byte findarray(byte what) { return bb; } + public static byte [] concat(byte [] array, byte [] array2) { + byte [] ret = Arrays.copyOf(array, array.length + array2.length); + System.arraycopy(array2,0, ret,ret.length, array2.length); + return ret; + } + public static byte [] base64_decode(byte [] encoded_string) { int in_len = encoded_string.length; int i = 0; @@ -156,6 +163,29 @@ public static String getEncodedSha1Sum( byte [] data ) { return new String(""); } } + + public static byte [] md5raw( byte [] data ) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(data); + return md.digest(); + } + catch (Exception e) { + return new byte[0]; + } + } + public static String md5hex( byte [] data ) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(data); + return new BigInteger(1, md.digest()).toString(16); + } + catch (Exception e) { + return new String(""); + } + } + + } diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index e7258bbfd6..8a6221f136 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -24,7 +24,9 @@ import com.xabber.xmpp.vcard.VCard; import com.xabber.xmpp.avatar.VCardUpdate; +import java.net.*; import java.util.*; +import java.io.*; public class WhatsappConnection { private RC4Decoder in, out; @@ -52,6 +54,10 @@ private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChall private int iqid; private String mymessage = ""; private String account_name = null; + + // HTTPS interface + private String sslnonce = ""; + private Thread http_thread; public WhatsappConnection(String phone, String pass, String nick, String aname) { session_key = new byte[20]; @@ -870,7 +876,73 @@ public class Group { participants = new Vector (); } }; + + private void doSyncAuth() { + http_thread = new Thread() { + public void run() { + performSyncAuth(); + } + }; + http_thread.setName("Socket data reader"); + http_thread.setDaemon(true); + http_thread.start(); + } + private void performSyncAuth() { + try { + String request = generateHeaders(generateHttpAuth("0"),0); + URL address = new URL("https://sro.whatsapp.net/v2/sync/a"); + HttpURLConnection connection = (HttpURLConnection)address.openConnection(); + connection.setReadTimeout(10000); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + connection.setRequestProperty("charset", "utf-8"); + connection.setRequestProperty("Content-Length", "" + Integer.toString(request.length())); + connection.setUseCaches (false); + + DataOutputStream wr = new DataOutputStream(connection.getOutputStream ()); + DataInputStream rr = new DataInputStream(connection.getInputStream ()); + wr.writeBytes(request); + wr.flush(); + wr.close(); + } catch (Exception e) { + + } + } + + String generateHttpAuth(String nonce) { + // cnonce is a 10 ascii char random string + String cnonce = ""; + for (int i = 0; i < 10; i++) { + char c = 'a'; + char rn = (char)(new Random()).nextInt(25); + c += rn; + cnonce += Character.toString(c); + } + String credentials =phone + ":s.whatsapp.net:" + MiscUtil.bytesToUTF8(MiscUtil.base64_decode(password.getBytes())); + byte [] cred = MiscUtil.md5raw(credentials.getBytes()); + cred = MiscUtil.concat(cred,(":"+nonce+":"+cnonce).getBytes()); + String response = MiscUtil.md5hex( (MiscUtil.md5hex(cred)+ + ":"+nonce+":00000001:"+cnonce+":auth:"+ + MiscUtil.md5hex("AUTHENTICATE:WAWA/s.whatsapp.net".getBytes())).getBytes() ); + + return "X-WAWA: username=\"" + phone + "\",digest-uri=\"WAWA/s.whatsapp.net\"" + + ",realm=\"s.whatsapp.net\",nonce=\"" + nonce + "\",cnonce=\"" + + cnonce + "\",nc=\"00000001\",qop=\"auth\",digest-uri=\"WAWA/s.whatsapp.net\"," + + "response=\"" + response + "\",charset=\"utf-8\""; + } + String generateHeaders(String auth, int content_length) { + String h = + "User-Agent: WhatsApp/2.4.7 S40Version/14.26 Device/Nokia302\r\n" + + "Accept: text/json\r\n" + + "Content-Type: application/x-www-form-urlencoded\r\n" + + "Authorization: " + auth + "\r\n" + + "Accept-Encoding: identity\r\n" + + "Content-Length: " + String.valueOf(content_length) + "\r\n"; + return h; + } } From 9be0f3ce8660056ba6fc0b09de86808de3df7e50 Mon Sep 17 00:00:00 2001 From: David Guillen Fandos Date: Sat, 1 Feb 2014 19:38:41 +0100 Subject: [PATCH 33/33] Update WA protocol to V1.4. Not working due to encryption issues (probably). Seems that the RC4 decoder or the key derivation is not working as it should. So, this is WIP! --- src/net/davidgf/android/DataBuffer.java | 65 ++-- src/net/davidgf/android/KeyGenerator.java | 63 ++-- src/net/davidgf/android/MiscUtil.java | 103 +++++-- src/net/davidgf/android/RC4Decoder.java | 3 +- src/net/davidgf/android/WAConnection.java | 10 +- .../davidgf/android/WhatsappConnection.java | 286 ++++++++---------- 6 files changed, 262 insertions(+), 268 deletions(-) diff --git a/src/net/davidgf/android/DataBuffer.java b/src/net/davidgf/android/DataBuffer.java index 3b24f6ffcf..3f11f6de4d 100644 --- a/src/net/davidgf/android/DataBuffer.java +++ b/src/net/davidgf/android/DataBuffer.java @@ -4,6 +4,7 @@ * Written by David Guillen Fandos (david@davidgf.net) based * on the sources of WhatsAPI PHP implementation and whatsapp * for libpurple. + * Updated to WA protocol v1.4 * * Share and enjoy! * @@ -41,28 +42,19 @@ DataBuffer addBuf(DataBuffer other) { return ret; } - DataBuffer decodedBuffer(RC4Decoder decoder, int clength, boolean dout) { + DataBuffer decodedBuffer(RC4Decoder decoder, int clength) { byte [] carray, array4; - if (dout) { - carray = decoder.cipher(Arrays.copyOfRange(this.buffer,0,clength-4)); - array4 = Arrays.copyOfRange(this.buffer,clength-4,clength); - DataBuffer deco = new DataBuffer(carray); - DataBuffer extra = new DataBuffer(array4); - return deco.addBuf(extra); - } - else { - carray = decoder.cipher(Arrays.copyOfRange(this.buffer,4,clength)); - array4 = Arrays.copyOfRange(this.buffer,0,4); - DataBuffer deco = new DataBuffer(carray); - DataBuffer extra = new DataBuffer(array4); - return extra.addBuf(deco); - } + carray = decoder.cipher(Arrays.copyOfRange(this.buffer,0,clength-4)); + array4 = Arrays.copyOfRange(this.buffer,clength-4,clength); + DataBuffer deco = new DataBuffer(carray); + DataBuffer extra = new DataBuffer(array4); + return deco.addBuf(extra); } - DataBuffer encodedBuffer(RC4Decoder decoder, byte [] key, boolean dout) { + DataBuffer encodedBuffer(RC4Decoder decoder, byte [] key, boolean dout, int seq) { DataBuffer deco = new DataBuffer(Arrays.copyOfRange(this.buffer,0,this.buffer.length)); deco.buffer = decoder.cipher(deco.buffer); - byte [] hmacint = KeyGenerator.calc_hmac(deco.buffer,key); + byte [] hmacint = KeyGenerator.calc_hmac(deco.buffer,key,seq); DataBuffer hmac = new DataBuffer(hmacint); DataBuffer res; @@ -169,21 +161,21 @@ String readRawString(int size) { } byte [] readByteString() { int type = readInt(1); - if (type > 4 && type < 0xf5) { - return MiscUtil.getDecoded(type).getBytes(); + if (type > 2 && type <= 236) { + String ret = MiscUtil.getDecoded(type); + if (ret == null) + ret = MiscUtil.getDecodedExt(type, readInt(1)); + return ret.getBytes(); } - else if (type == 0xfc) { + else if (type == 252) { int slen = readInt(1); return readRawByteString(slen); } - else if (type == 0xfd) { + else if (type == 253) { int slen = readInt(3); return readRawByteString(slen); } - else if (type == 0xfe) { - return MiscUtil.getDecoded(readInt(1)+0xf5).getBytes(); - } - else if (type == 0xfa) { + else if (type == 250) { String u = readString(); String s = readString(); @@ -198,24 +190,24 @@ String readString() { //if (blen == 0) // throw 0; int type = readInt(1); - if (type > 4 && type < 0xf5) { - return MiscUtil.getDecoded(type); + if (type > 2 && type <= 236) { + String ret = MiscUtil.getDecoded(type); + if (ret == null) + ret = MiscUtil.getDecodedExt(type, readInt(1)); + return ret; } else if (type == 0) { return ""; } - else if (type == 0xfc) { + else if (type == 252) { int slen = readInt(1); return readRawString(slen); } - else if (type == 0xfd) { + else if (type == 253) { int slen = readInt(3); return readRawString(slen); } - else if (type == 0xfe) { - return MiscUtil.getDecoded(readInt(1)+0xf5); - } - else if (type == 0xfa) { + else if (type == 250) { String u = readString(); String s = readString(); @@ -241,7 +233,12 @@ void putRawString(byte [] s) { } void putString(String s) { int lu = MiscUtil.lookupDecoded(s); - if (lu > 4 && lu < 0xf5) { + int sub_dict = (lu >> 8); + + if (sub_dict != 0) + putInt(sub_dict + 236 - 1, 1); // Put dict byte first! + + if (lu != 0) { putInt(lu,1); } else if (s.indexOf('@') >= 0) { diff --git a/src/net/davidgf/android/KeyGenerator.java b/src/net/davidgf/android/KeyGenerator.java index bf58df3260..37b0a136fc 100644 --- a/src/net/davidgf/android/KeyGenerator.java +++ b/src/net/davidgf/android/KeyGenerator.java @@ -14,7 +14,7 @@ public class KeyGenerator { password[i] = (char)(pass[i]&0xFF); SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - PBEKeySpec ks = new PBEKeySpec(password,salt,16,20*8); + PBEKeySpec ks = new PBEKeySpec(password,salt,2,20*8); // 2 rounds, 20*8 bits output SecretKey s = f.generateSecret(ks); return s.getEncoded(); @@ -22,58 +22,33 @@ public class KeyGenerator { private static byte [] hexmap = new byte[]{ '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; - public static byte [] generateKeyImei(String imei, byte [] salt) { - try { - String imeir = (new StringBuilder(imei)).reverse().toString(); - - byte [] hash = MessageDigest.getInstance("MD5").digest(imeir.getBytes()); - - byte [] hashhex = new byte[32]; - for (int i = 0; i < 16; i++) { - hashhex[2*i] = hexmap[(hash[i]>>4)&0xF]; - hashhex[2*i+1] = hexmap[hash[i]&0xF]; - } - - return PKCS5_PBKDF2_HMAC_SHA1 (hashhex,salt); - } - catch (Exception e) { - return new byte[0]; - } - } - public static byte[] generateKeyV2(String pw, byte [] salt) { + public static byte[][] generateKeyV14(String pw, byte [] salt) { try { byte [] decpass = MiscUtil.base64_decode(pw.getBytes()); - return PKCS5_PBKDF2_HMAC_SHA1 (decpass,salt); + byte [][] keys = new byte[4][]; + for (int i = 0; i < 4; i++) { + byte salt2 [] = Arrays.copyOf(salt,salt.length + 1); + salt2[salt.length] = (byte)(i+1); + keys[i] = PKCS5_PBKDF2_HMAC_SHA1 (decpass,salt2); + } + + return keys; } catch (Exception e) { - return new byte[0]; + return new byte[0][0]; } } - public static byte [] generateKeyMAC(String macaddr, byte [] salt) { - try { - macaddr = macaddr+macaddr; + public static byte [] calc_hmac(byte [] data, byte [] key, int seq) { + byte [] dataext = Arrays.copyOf(data, data.length + 4); + dataext[data.length +0] = (byte)(seq >> 24); + dataext[data.length +1] = (byte)(seq >> 16); + dataext[data.length +2] = (byte)(seq >> 8); + dataext[data.length +3] = (byte)(seq ); - byte [] hash = MessageDigest.getInstance("MD5").digest(macaddr.getBytes()); - - byte [] hashhex = new byte[32]; - for (int i = 0; i < 16; i++) { - hashhex[2*i] = hexmap[(hash[i]>>4)&0xF]; - hashhex[2*i+1] = hexmap[hash[i]&0xF]; - } + byte [] hash = HMAC_SHA1 (dataext,key); - return PKCS5_PBKDF2_HMAC_SHA1 (hashhex,salt); - } - catch (Exception e) { - return new byte[0]; - } - } - public static byte [] calc_hmac(byte [] data, byte [] key) { - byte [] hash = HMAC_SHA1 (data,key); - byte [] ret = new byte[4]; - for (int i = 0; i < 4; i++) - ret[i] = hash[i]; - return ret; + return Arrays.copyOf(hash,4); } private static byte [] HMAC_SHA1(byte [] text, byte [] key) { diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java index c669eb0218..ab808a4659 100644 --- a/src/net/davidgf/android/MiscUtil.java +++ b/src/net/davidgf/android/MiscUtil.java @@ -85,44 +85,85 @@ private static byte findarray(byte what) { } - private static String [] dictionary = new String[] - { "","","","","", "account","ack","action","active","add","after", - "ib","all","allow","apple","audio","auth","author","available","bad-protocol","bad-request", - "before","Bell.caf","body","Boing.caf","cancel","category","challenge","chat","clean","code", - "composing","config","conflict","contacts","count","create","creation","default","delay", - "delete","delivered","deny","digest","DIGEST-MD5-1","DIGEST-MD5-2","dirty","elapsed","broadcast", - "enable","encoding","duplicate","error","event","expiration","expired","fail","failure","false", - "favorites","feature","features","field","first","free","from","g.us","get","Glass.caf","google", - "group","groups","g_notify","g_sound","Harp.caf","http://etherx.jabber.org/streams", - "http://jabber.org/protocol/chatstates","id","image","img","inactive","index","internal-server-error", - "invalid-mechanism","ip","iq","item","item-not-found","user-not-found","jabber:iq:last","jabber:iq:privacy", - "jabber:x:delay","jabber:x:event","jid","jid-malformed","kind","last","latitude","lc","leave","leave-all", - "lg","list","location","longitude","max","max_groups","max_participants","max_subject","mechanism", - "media","message","message_acks","method","microsoft","missing","modify","mute","name","nokia","none", - "not-acceptable","not-allowed","not-authorized","notification","notify","off","offline","order","owner", - "owning","paid","participant","participants","participating","password","paused","picture","pin","ping", - "platform","pop_mean_time","pop_plus_minus","port","presence","preview","probe","proceed","prop","props", - "p_o","p_t","query","raw","reason","receipt","receipt_acks","received","registration","relay", - "remote-server-timeout","remove","Replaced by new connection","request","required","resource", - "resource-constraint","response","result","retry","rim","s.whatsapp.net","s.us","seconds","server", - "server-error","service-unavailable","set","show","sid","silent","sound","stamp","unsubscribe","stat", - "status","stream:error","stream:features","subject","subscribe","success","sync","system-shutdown", - "s_o","s_t","t","text","timeout","TimePassing.caf","timestamp","to","Tri-tone.caf","true","type", - "unavailable","uri","url","urn:ietf:params:xml:ns:xmpp-sasl","urn:ietf:params:xml:ns:xmpp-stanzas", - "urn:ietf:params:xml:ns:xmpp-streams","urn:xmpp:delay","urn:xmpp:ping","urn:xmpp:receipts", - "urn:xmpp:whatsapp","urn:xmpp:whatsapp:account","urn:xmpp:whatsapp:dirty","urn:xmpp:whatsapp:mms", - "urn:xmpp:whatsapp:push","user","username","value","vcard","version","video","w","w:g","w:p","w:p:r", - "w:profile:picture","wait","x","xml-not-well-formed","xmlns","xmlns:stream","Xylophone.caf","1","WAUTH-1", - "","","","","","","","","","","","XXX","","","","","","","" }; + private static String [] dictionary = new String[] { + "", "", "", "account", "ack", "action", "active", "add", "after", "all", "allow", "apple", + "auth", "author", "available", "bad-protocol", "bad-request", "before", "body", "broadcast", + "cancel", "category", "challenge", "chat", "clean", "code", "composing", "config", "contacts", + "count", "create", "creation", "debug", "default", "delete", "delivery", "delta", "deny", + "digest", "dirty", "duplicate", "elapsed", "enable", "encoding", "error", "event", + "expiration", "expired", "fail", "failure", "false", "favorites", "feature", "features", + "feature-not-implemented", "field", "first", "free", "from", "g.us", "get", "google", "group", + "groups", "http://etherx.jabber.org/streams", "http://jabber.org/protocol/chatstates", "ib", + "id", "image", "img", "index", "internal-server-error", "ip", "iq", "item-not-found", "item", + "jabber:iq:last", "jabber:iq:privacy", "jabber:x:event", "jid", "kind", "last", "leave", + "list", "max", "mechanism", "media", "message_acks", "message", "method", "microsoft", + "missing", "modify", "mute", "name", "nokia", "none", "not-acceptable", "not-allowed", + "not-authorized", "notification", "notify", "off", "offline", "order", "owner", "owning", + "p_o", "p_t", "paid", "participant", "participants", "participating", "paused", "picture", + "pin", "ping", "platform", "port", "presence", "preview", "probe", "prop", "props", "query", + "raw", "read", "reason", "receipt", "received", "relay", "remote-server-timeout", "remove", + "request", "required", "resource-constraint", "resource", "response", "result", "retry", + "rim", "s_o", "s_t", "s.us", "s.whatsapp.net", "seconds", "server-error", "server", + "service-unavailable", "set", "show", "silent", "stat", "status", "stream:error", + "stream:features", "subject", "subscribe", "success", "sync", "t", "text", "timeout", + "timestamp", "to", "true", "type", "unavailable", "unsubscribe", "uri", "url", + "urn:ietf:params:xml:ns:xmpp-sasl", "urn:ietf:params:xml:ns:xmpp-stanzas", + "urn:ietf:params:xml:ns:xmpp-streams", "urn:xmpp:ping", "urn:xmpp:receipts", + "urn:xmpp:whatsapp:account", "urn:xmpp:whatsapp:dirty", "urn:xmpp:whatsapp:mms", + "urn:xmpp:whatsapp:push", "urn:xmpp:whatsapp", "user", "user-not-found", "value", + "version", "w:g", "w:p:r", "w:p", "w:profile:picture", "w", "wait", "WAUTH-2", + "x", "xmlns:stream", "xmlns", "1", "chatstate", "crypto", "enc", "class", "off_cnt", + "w:g2", "promote", "demote", "creator" + }; + + private static String [] dictionary2 = new String[] { + "Bell.caf", "Boing.caf", "Glass.caf", "Harp.caf", "TimePassing.caf", "Tri-tone.caf", + "Xylophone.caf", "background", "backoff", "chunked", "context", "full", "in", "interactive", + "out", "registration", "sid", "urn:xmpp:whatsapp:sync", "flt", "s16", "u8", "adpcm", + "amrnb", "amrwb", "mp3", "pcm", "qcelp", "wma", "h263", "h264", "jpeg", "mpeg4", "wmv", + "audio/3gpp", "audio/aac", "audio/amr", "audio/mp4", "audio/mpeg", "audio/ogg", "audio/qcelp", + "audio/wav", "audio/webm", "audio/x-caf", "audio/x-ms-wma", "image/gif", "image/jpeg", + "image/png", "video/3gpp", "video/avi", "video/mp4", "video/mpeg", "video/quicktime", + "video/x-flv", "video/x-ms-asf", "302", "400", "401", "402", "403", "404", "405", "406", + "407", "409", "500", "501", "503", "504", "abitrate", "acodec", "app_uptime", "asampfmt", + "asampfreq", "audio", "bb_db", "clear", "conflict", "conn_no_nna", "cost", "currency", + "duration", "extend", "file", "fps", "g_notify", "g_sound", "gcm", "google_play", "hash", + "height", "invalid", "jid-malformed", "latitude", "lc", "lg", "live", "location", "log", + "longitude", "max_groups", "max_participants", "max_subject", "mimetype", "mode", + "napi_version", "normalize", "orighash", "origin", "passive", "password", "played", + "policy-violation", "pop_mean_time", "pop_plus_minus", "price", "pricing", "redeem", + "Replaced by new connection", "resume", "signature", "size", "sound", "source", + "system-shutdown", "username", "vbitrate", "vcard", "vcodec", "video", "width", + "xml-not-well-formed", "checkmarks", "image_max_edge", "image_max_kbytes", "image_quality", + "ka", "ka_grow", "ka_shrink", "newmedia", "library", "caption", "forward", "c0", "c1", "c2", + "c3", "clock_skew", "cts", "k0", "k1", "login_rtt", "m_id", "nna_msg_rtt", "nna_no_off_count", + "nna_offline_ratio", "nna_push_rtt", "no_nna_con_count", "off_msg_rtt", "on_msg_rtt", + "stat_name", "sts", "suspect_conn", "lists", "self", "qr", "web", "w:b", "recipient", + "w:stats", "forbidden", "aurora.m4r", "bamboo.m4r", "chord.m4r", "circles.m4r", "complete.m4r", + "hello.m4r", "input.m4r", "keys.m4r", "note.m4r", "popcorn.m4r", "pulse.m4r", "synth.m4r", + "filehash" + }; + public static String getDecoded(int n) { - return new String(dictionary[n & 255]); + if (n < 236) + return new String(dictionary[n & 255]); + return null; + } + public static String getDecodedExt(int n, int n2) { + if (n == 236) + return new String(dictionary2[n2 & 255]); + return ""; } public static int lookupDecoded(String value) { - for (int i = 0; i < 256; i++) { + for (int i = 0; i < dictionary.length; i++) { if (dictionary[i].equals(value)) return i; } + for (int i = 0; i < dictionary2.length; i++) { + if (dictionary2[i].equals(value)) + return i | 0x100; + } return 0; } diff --git a/src/net/davidgf/android/RC4Decoder.java b/src/net/davidgf/android/RC4Decoder.java index c33acd375d..6df280f213 100644 --- a/src/net/davidgf/android/RC4Decoder.java +++ b/src/net/davidgf/android/RC4Decoder.java @@ -32,7 +32,8 @@ public RC4Decoder(byte [] key, int drop) { i = j = 0; byte [] temp = new byte[drop]; - for (int k = 0; k < drop; k++) temp[k] = (byte)(k & 0xFF); + for (int k = 0; k < drop; k++) + temp[k] = (byte)0; cipher(temp); } diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java index f26f5b132e..71fbd1ceee 100644 --- a/src/net/davidgf/android/WAConnection.java +++ b/src/net/davidgf/android/WAConnection.java @@ -83,7 +83,7 @@ public class WAConnection extends Connection { private ExecutorService listenerExecutor; int msgid; - private static final String waUA = "WhatsApp/2.10.750 Android/4.2.1 Device/GalaxyS3"; + private static final String waUA = "Android-2.11.151-443"; private String nickname = ""; Roster roster = null; @@ -225,7 +225,12 @@ public synchronized void login(String username, String password, String resource // Start login! synchronized (waconnection) { - waconnection.doLogin(waUA); + try { + waconnection.doLogin(waUA); + } + catch (Exception e) { + e.printStackTrace(); + } } popWriteData(); } @@ -546,6 +551,7 @@ private void connectUsingConfiguration(ConnectionConfiguration config) throws XM throw new XMPPException(errorMessage, new XMPPError( XMPPError.Condition.remote_server_error, errorMessage), ioe); } + System.out.println("Connected to " + host + " " + String.valueOf(port)); initConnection(); connected = true; } diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java index 8a6221f136..121227306e 100644 --- a/src/net/davidgf/android/WhatsappConnection.java +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -30,9 +30,10 @@ public class WhatsappConnection { private RC4Decoder in, out; - private byte session_key[]; + private byte session_key[][]; private DataBuffer outbuffer; private byte []challenge_data; + private int frame_seq; private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChallenge, SessionWaitingAuthOK, SessionConnected }; private SessionStatus conn_status; @@ -60,7 +61,7 @@ private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChall private Thread http_thread; public WhatsappConnection(String phone, String pass, String nick, String aname) { - session_key = new byte[20]; + session_key = new byte[4][20]; this.phone = phone; this.password = pass.trim(); @@ -120,14 +121,16 @@ Tree parse_tree(DataBuffer data) { if ((bflag & 0x8) != 0) { // Decode data, buffer conversion if (this.in != null) { - DataBuffer decoded_data = data.decodedBuffer(this.in,bsize,false); - + DataBuffer decoded_data = data.decodedBuffer(this.in,bsize); + + Tree tt = read_tree(decoded_data); + + data.popData(bsize); // Pop data unencrypted for next parsing! + // Remove hash decoded_data.popData(4); - - // Call recursive - data.popData(bsize); // Pop data unencrypted for next parsing! - return read_tree(decoded_data); + + return tt; }else{ data.popData(bsize); return new Tree("treeerr"); @@ -164,18 +167,12 @@ private void processPacket(Tree t) { // Generate a session key using the challege & the password assert(conn_status == SessionStatus.SessionWaitingChallenge); - if (password.length() == 15) { - session_key = KeyGenerator.generateKeyImei(password,t.getData()); - } - else if (password.contains(":")) { - session_key = KeyGenerator.generateKeyMAC(password,t.getData()); - } - else { - session_key = KeyGenerator.generateKeyV2(password,t.getData()); - } + // Update key generation to V1.4 + session_key = KeyGenerator.generateKeyV14(password,t.getData()); + System.out.println(password); - this.in = new RC4Decoder(session_key, 256); - this.out = new RC4Decoder(session_key, 256); + this.in = new RC4Decoder(session_key[2], 768); + this.out = new RC4Decoder(session_key[0], 768); conn_status = SessionStatus.SessionWaitingAuthOK; challenge_data = t.getData(); @@ -205,12 +202,19 @@ else if (t.getTag().equals("success")) { getLastSeen(contacts.get(i).phone); } } + else if (t.getTag().equals("notification")) { + DataBuffer reply = generateResponse(t.getAttribute("from"), + t.getAttribute("type"), + t.getAttribute("id")); + outbuffer = outbuffer.addBuf(reply); + } else if (t.getTag().equals("presence")) { // Receives the presence of the user - if ( t.hasAttribute("from") && t.hasAttribute("type") ) { - Presence.Mode mode = Presence.Mode.away; - if (t.getAttribute("type").equals("available")) - mode = Presence.Mode.available; + if ( t.hasAttribute("from") ) { + Presence.Mode mode = Presence.Mode.available; + if (t.hasAttribute("type")) + if (t.getAttribute("type").equals("unavailable")) + mode = Presence.Mode.away; Presence presp = new Presence(Presence.Type.available); presp.setMode(mode); @@ -224,7 +228,7 @@ else if (t.getTag().equals("presence")) { } else if (t.getTag().equals("iq")) { // PING - if (t.hasAttribute("from") && t.hasAttribute("id") && t.hasChild("ping")) { + if (t.hasAttribute("from") && t.hasAttribute("id") && t.hasChild("urn:xmpp:ping")) { this.doPong(t.getAttribute("id"),t.getAttribute("from")); } @@ -237,12 +241,21 @@ else if (t.getTag().equals("iq")) { } tb = t.getChild("query"); if (tb != null) { - if (tb.hasAttributeValue("xmlns","jabber:iq:last") && tb.hasAttribute("seconds")) { + if (tb.hasAttribute("seconds")) { this.notifyLastSeen(t.getAttribute("from"),tb.getAttribute("seconds")); } } } + // Status message + if (t.hasChild("status")) { + Vector childs = t.getChildren(); + for (int j = 0; j < childs.size(); j++) { + if (childs.get(j).getTag().equals("user")) + this.notifyStatus(childs.get(j).getAttribute("jid"),MiscUtil.bytesToUTF8(childs.get(j).getData())); + } + } + // Group stuff Vector childs = t.getChildren(); int acc = 0; @@ -260,12 +273,13 @@ else if (t.getTag().equals("iq")) { final String iid = String.valueOf(++iqid); final String pid = childs.get(j).getAttribute("id"); final String subj = childs.get(j).getAttribute("subject"); - Tree iq = new Tree("list",new HashMap < String,String >(){{put("xmlns","w:g");}}); + Tree iq = new Tree("list"); Tree req = new Tree("iq", new HashMap < String,String >(){{ put("id",iid); put("type","get"); put("to",pid+"@g.us"); + put("xmlns","w:g"); }}); req.addChild(iq); outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); @@ -292,13 +306,14 @@ else if (childs.get(j).getTag().equals("participant")) { } } else if (t.getTag().equals("message")) { - if (t.hasAttributeValue("type","chat") && t.hasAttribute("from")) { + if (t.hasAttribute("from") && + (t.hasAttributeValue("type","text") || t.hasAttributeValue("type","media")) ) { long time = 0; if (t.hasAttribute("t")) time = Integer.parseInt(t.getAttribute("t")); String from = t.getAttribute("from"); String id = t.getAttribute("id"); - String author = t.getAttribute("author"); + String author = t.getAttribute("participant"); // Group nickname if (from.contains("@g.us") && t.hasChild("notify")) { @@ -317,33 +332,52 @@ else if (t.getTag().equals("message")) { tb = t.getChild("media"); if (tb != null) { // Photo/audio - if (tb.hasAttributeValue("type","image")) { + if (tb.hasAttributeValue("type","image") || + tb.hasAttributeValue("type","audio") || + tb.hasAttributeValue("type","video")) { + this.receiveMessage( new ImageMessage(from,time,id,tb.getAttribute("url"),tb.getData(),author)); addContact(from,false); } } } - if (t.hasChild("composing")) { - gotTyping(t.getAttribute("from"),true); - } - if (t.hasChild("paused")) { - gotTyping(t.getAttribute("from"),false); + else if (t.hasAttributeValue("type", "notification") && t.hasAttribute("from")) { + /* If the nofitication comes from a group, assume we have to reload groups ;) */ + updateGroups(); } // Received ACK if (t.hasAttribute("type") && t.hasAttribute("from")) { - String answer = "received"; - if (t.hasChild("received")) - answer = "ack"; DataBuffer reply = generateResponse(t.getAttribute("from"), t.getAttribute("type"), - t.getAttribute("id"), - answer); + t.getAttribute("id")); outbuffer = outbuffer.addBuf(reply); } } + else if (t.getTag().equals("chatstate")) { + if (t.hasChild("composing")) { + gotTyping(t.getAttribute("from"),true); + } + if (t.hasChild("paused")) { + gotTyping(t.getAttribute("from"),false); + } + } + else if (t.getTag().equals("receipt")) { + final String pid = t.getAttribute("id"); + final String typed = t.getAttribute("type"); + final String typef = (typed.equals("") ? "delivery" : typed); + Tree req = new Tree("ack", + new HashMap < String,String >(){{ + put("id",pid); + put("type",typef); + put("class","receipt"); + }}); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + + /*else if (treelist[i].getTag() == "failure") { if (conn_status == SessionWaitingAuthOK) this->notifyError(errorAuth); @@ -379,6 +413,20 @@ private void notifyLastSeen(final String from, final String seconds) { requestVCardUpdate(u); } + + private void notifyStatus(final String from, final String status) { + final String u = MiscUtil.getUser(from); + + // Save last seen time + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(u)) { + contacts.get(i).status = status; + break; + } + } + + requestVCardUpdate(u); + } public String getNotes(String u) { u = MiscUtil.getUser(u); @@ -416,11 +464,9 @@ public void pushGroupUpdate() { } } - private DataBuffer generateResponse(final String from, final String type, final String id, final String ans) { - Tree received = new Tree(ans,new HashMap < String,String >() {{ put("xmlns","urn:xmpp:receipts"); }} ); - Tree mes = new Tree("message",new HashMap < String,String >() {{ - put("to",from); put("type",type); put("id",id); }} ); - mes.addChild(received); + private DataBuffer generateResponse(final String from, final String type, final String id) { + Tree mes = new Tree("receipt",new HashMap < String,String >() {{ + put("to",from); put("id",id); }} ); return serialize_tree(mes,true); } @@ -428,9 +474,15 @@ private void queryPreview(final String user) { final String fuser = user+"@"+whatsappserver; final String reqid = String.valueOf(++iqid); Tree pic = new Tree ("picture", - new HashMap < String,String >() {{ put("xmlns","w:profile:picture"); put("type","preview"); }} ); + new HashMap < String,String >() {{ put("type","preview"); }} ); Tree req = new Tree("iq", - new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to",fuser); }} ); + new HashMap < String,String >() {{ + put("id",reqid); + put("type","get"); + put("to",fuser); + put("xmlns","w:profile:picture"); + }} + ); req.addChild(pic); @@ -442,9 +494,9 @@ private void updateGroups() { { final String reqid = String.valueOf(++iqid); gw1 = iqid; - Tree iq = new Tree("list",new HashMap < String,String >() {{ put("xmlns","w:g"); put("type","owning");}} ); + Tree iq = new Tree("list",new HashMap < String,String >() {{ put("type","owning");}} ); Tree req = new Tree("iq", - new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to","g.us");}} ); + new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to","g.us"); put("xmlns","w:g"); }} ); req.addChild(iq); outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); @@ -453,9 +505,9 @@ private void updateGroups() { final String reqid = String.valueOf(++iqid); gw2 = iqid; Tree iq = new Tree("list", - new HashMap < String,String >() {{ put("xmlns","w:g"); put("type","participating");}} ); + new HashMap < String,String >() {{ put("type","participating");}} ); Tree req = new Tree("iq", - new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to","g.us");}} ); + new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to","g.us"); put("xmlns","w:g"); }} ); req.addChild(iq); outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); } @@ -464,11 +516,14 @@ private void updateGroups() { private void manageParticipant(final String group, final String participant, final String command) { Tree part = new Tree("participant",new HashMap < String,String >() {{ put("jid",participant); }} ); - Tree iq = new Tree(command,new HashMap < String,String >() {{ put("xmlns","w:g"); }} ); + Tree iq = new Tree(command); iq.addChild(part); final String reqid = String.valueOf(++iqid); Tree req = new Tree("iq", - new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to",group+"@g.us");}} ); + new HashMap < String,String >() {{ + put("id",reqid); put("type","set"); put("to",group+"@g.us"); put("xmlns","w:g"); + }} + ); req.addChild(iq); outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); @@ -476,11 +531,11 @@ private void manageParticipant(final String group, final String participant, fin private void leaveGroup(final String group) { Tree gr = new Tree("group", new HashMap < String,String >() {{ put("id",group+"@g.us"); }} ); - Tree iq = new Tree("leave", new HashMap < String,String >() {{ put("xmlns","w:g"); }} ); + Tree iq = new Tree("leave"); iq.addChild(gr); final String reqid = String.valueOf(++iqid); Tree req = new Tree("iq", - new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to","g.us");}} ); + new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to","g.us"); put("xmlns","w:g"); }} ); req.addChild(iq); outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); @@ -488,10 +543,10 @@ private void leaveGroup(final String group) { void addGroup(final String subject) { Tree gr = new Tree("group", - new HashMap < String,String >() {{ put("xmlns","w:g"); put("action","create"); put("subject",subject);}} ); + new HashMap < String,String >() {{ put("action","create"); put("subject",subject);}} ); final String reqid = String.valueOf(++iqid); Tree req = new Tree("iq", - new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to","g.us");}} ); + new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to","g.us"); put("xmlns","w:g"); }} ); req.addChild(gr); outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); @@ -544,7 +599,7 @@ private void receiveMessage(AbstractMessage msg) { } private void gotTyping(final String user, boolean typing) { - Message msg = new Message(); + Message msg = new Message(); msg.setTo(MiscUtil.getUser(phone)); msg.setFrom(MiscUtil.getUser(user)); @@ -577,18 +632,6 @@ private void notifyMyPresence() { Tree pres = new Tree("presence", new HashMap < String,String >() {{ put("name",nickname); put("type",mypresence); }} ); outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(pres,true))); - - Tree xhash = new Tree("x", new HashMap < String,String >() {{ put("xmlns","jabber:x:event"); }} ); - xhash.addChild(new Tree("server")); - Tree tbody = new Tree ("body"); tbody.setData(MiscUtil.UTF8ToBytes(this.mymessage)); - - String stime = String.valueOf(System.currentTimeMillis()/1000); - final String iqtime_id = stime + "-" + String.valueOf(++iqid); - Tree mes = new Tree("message", new HashMap < String,String >() {{ - put("to","s.us"); put("type","chat"); put("id",iqtime_id); }} ); - mes.addChild(xhash); mes.addChild(tbody); - - //outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(mes,true))); } void doPong(final String id, final String from) { @@ -623,13 +666,19 @@ public DataBuffer write_tree(Tree tree) { } public DataBuffer serialize_tree(Tree tree, boolean crypt) { + + System.out.println("OUT"); + System.out.println(tree.toString(0)); + DataBuffer data = write_tree(tree); int flag = 0; if (crypt) { - data = data.encodedBuffer(this.out,this.session_key,true); - flag = 0x10; + data = data.encodedBuffer(this.out,this.session_key[1],true,frame_seq++); + flag = 0x80; } + System.out.println("OUTDONE"); + DataBuffer ret = new DataBuffer(); ret.putInt(flag,1); ret.putInt(data.size(),2); @@ -645,26 +694,20 @@ public void doLogin(String useragent) { auth.put("resource", useragent); auth.put("to", whatsappserver); Tree t = new Tree("start",auth); - first.addData( new byte [] {'W','A',1,2} ); + first.addData( new byte [] {'W','A',1,4} ); first = first.addBuf(serialize_tree(t,false)); } // Send features { Tree p = new Tree("stream:features"); - p.addChild(new Tree("receipt_acks")); - p.addChild(new Tree("w:profile:picture",new HashMap < String,String >() {{ put("type","all"); }} )); - p.addChild(new Tree("w:profile:picture",new HashMap < String,String >() {{ put("type","group"); }} )); - p.addChild(new Tree("notification",new HashMap < String,String >() {{ put("type","participant"); }} )); - p.addChild(new Tree("status")); first = first.addBuf(serialize_tree(p,false)); } // Send auth request { Map < String,String > auth = new HashMap (); - auth.put("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); - auth.put("mechanism", "WAUTH-1"); + auth.put("mechanism", "WAUTH-2"); auth.put("user", phone); Tree t = new Tree("auth",auth); t.forceDataWrite(); @@ -676,7 +719,7 @@ public void doLogin(String useragent) { } void sendAuthResponse() { - Tree t = new Tree("response",new HashMap < String,String >() {{ put("xmlns","urn:ietf:params:xml:ns:xmpp-sasl"); }}); + Tree t = new Tree("response"); long epoch = System.currentTimeMillis()/1000; String stime = String.valueOf(epoch); @@ -685,7 +728,7 @@ void sendAuthResponse() { eresponse.addData(challenge_data); eresponse.addData(stime.getBytes()); - eresponse = eresponse.encodedBuffer(this.out,this.session_key,false); + eresponse = eresponse.encodedBuffer(this.out,this.session_key[1],false, frame_seq++); t.setData(eresponse.getPtr()); outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(t,false))); @@ -734,9 +777,11 @@ private void getLastSeen(final String user) { final String id = String.valueOf(++iqid); final String fuser = MiscUtil.getUser(user)+"@"+whatsappserver; Tree iq = new Tree("iq", - new HashMap < String,String >() {{ put("id",id); put("type","get"); put("to",fuser); }} ); - Tree req = new Tree("query", new HashMap < String,String >() {{ put("xmlns","jabber:iq:last"); }} ); - iq.addChild(req); + new HashMap < String,String >() {{ + put("id",id); put("type","get"); put("to",fuser); put("xmlns","jabber:iq:last"); + }} + ); + iq.addChild(new Tree("query")); outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(iq,true))); } @@ -751,10 +796,6 @@ private void getLastSeen(final String user) { // Helper for Message class public byte [] serializeMessage(final String to, String message, int id) { try { - Tree request = new Tree ("request",new HashMap < String,String >() {{ put("xmlns","urn:xmpp:receipts"); }} ); - Tree notify = new Tree ("notify", new HashMap < String,String >() {{ put("xmlns","urn:xmpp:whatsapp"); put("name",nickname); }} ); - Tree xhash = new Tree ("x", new HashMap < String,String >() {{ put("xmlns","jabber:x:event"); }} ); - xhash.addChild(new Tree("server")); Tree tbody = new Tree("body"); tbody.setData(message.getBytes("UTF-8")); @@ -768,8 +809,7 @@ private void getLastSeen(final String user) { attrs.put("t",stime); Tree mes = new Tree("message",attrs); - mes.addChild(xhash); mes.addChild(notify); - mes.addChild(request); mes.addChild(tbody); + mes.addChild(tbody); return serialize_tree(mes,true).getPtr(); }catch (Exception e) { @@ -876,73 +916,7 @@ public class Group { participants = new Vector (); } }; - - private void doSyncAuth() { - http_thread = new Thread() { - public void run() { - performSyncAuth(); - } - }; - http_thread.setName("Socket data reader"); - http_thread.setDaemon(true); - http_thread.start(); - } - private void performSyncAuth() { - try { - String request = generateHeaders(generateHttpAuth("0"),0); - URL address = new URL("https://sro.whatsapp.net/v2/sync/a"); - HttpURLConnection connection = (HttpURLConnection)address.openConnection(); - connection.setReadTimeout(10000); - connection.setRequestMethod("POST"); - connection.setDoOutput(true); - connection.setDoInput(true); - connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - connection.setRequestProperty("charset", "utf-8"); - connection.setRequestProperty("Content-Length", "" + Integer.toString(request.length())); - connection.setUseCaches (false); - - DataOutputStream wr = new DataOutputStream(connection.getOutputStream ()); - DataInputStream rr = new DataInputStream(connection.getInputStream ()); - wr.writeBytes(request); - wr.flush(); - wr.close(); - } catch (Exception e) { - - } - } - - String generateHttpAuth(String nonce) { - // cnonce is a 10 ascii char random string - String cnonce = ""; - for (int i = 0; i < 10; i++) { - char c = 'a'; - char rn = (char)(new Random()).nextInt(25); - c += rn; - cnonce += Character.toString(c); - } - String credentials =phone + ":s.whatsapp.net:" + MiscUtil.bytesToUTF8(MiscUtil.base64_decode(password.getBytes())); - byte [] cred = MiscUtil.md5raw(credentials.getBytes()); - cred = MiscUtil.concat(cred,(":"+nonce+":"+cnonce).getBytes()); - String response = MiscUtil.md5hex( (MiscUtil.md5hex(cred)+ - ":"+nonce+":00000001:"+cnonce+":auth:"+ - MiscUtil.md5hex("AUTHENTICATE:WAWA/s.whatsapp.net".getBytes())).getBytes() ); - - return "X-WAWA: username=\"" + phone + "\",digest-uri=\"WAWA/s.whatsapp.net\"" + - ",realm=\"s.whatsapp.net\",nonce=\"" + nonce + "\",cnonce=\"" + - cnonce + "\",nc=\"00000001\",qop=\"auth\",digest-uri=\"WAWA/s.whatsapp.net\"," + - "response=\"" + response + "\",charset=\"utf-8\""; - } - String generateHeaders(String auth, int content_length) { - String h = - "User-Agent: WhatsApp/2.4.7 S40Version/14.26 Device/Nokia302\r\n" + - "Accept: text/json\r\n" + - "Content-Type: application/x-www-form-urlencoded\r\n" + - "Authorization: " + auth + "\r\n" + - "Accept-Encoding: identity\r\n" + - "Content-Length: " + String.valueOf(content_length) + "\r\n"; - return h; - } }