diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a3529cbd0..2b65b3a7e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -10,6 +10,8 @@ + + accounts; + + public boolean connectionRunnig = false; private final IBinder mBinder = new XmppConnectionBinder(); @@ -26,9 +47,27 @@ public class XmppConnectionService extends Service { } } + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(LOGTAG,"recieved start command. been running for "+((System.currentTimeMillis() - startDate) / 1000)+"s"); + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (!connectionRunnig) { + for(Account account : accounts) { + Log.d(LOGTAG,"connection wasnt running"); + XmppConnection connection = new XmppConnection(account, pm); + Thread thread = new Thread(connection); + thread.start(); + } + connectionRunnig = true; + } + return START_STICKY; + } + @Override public void onCreate() { databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); + this.accounts = databaseBackend.getAccounts(); + startDate = System.currentTimeMillis(); } @Override diff --git a/src/de/gultsch/chat/ui/XmppActivity.java b/src/de/gultsch/chat/ui/XmppActivity.java index ce65ab5ec..66c92b72e 100644 --- a/src/de/gultsch/chat/ui/XmppActivity.java +++ b/src/de/gultsch/chat/ui/XmppActivity.java @@ -31,6 +31,7 @@ public abstract class XmppActivity extends Activity { @Override protected void onStart() { + startService(new Intent(this, XmppConnectionService.class)); super.onStart(); if (!xmppConnectionServiceBound) { Intent intent = new Intent(this, XmppConnectionService.class); diff --git a/src/de/gultsch/chat/utils/SASL.java b/src/de/gultsch/chat/utils/SASL.java new file mode 100644 index 000000000..266f0cb29 --- /dev/null +++ b/src/de/gultsch/chat/utils/SASL.java @@ -0,0 +1,24 @@ +package de.gultsch.chat.utils; + +import android.util.Base64; + +public class SASL { + public static String plain(String username, String password) { + byte[] userBytes = username.getBytes(); + int userLenght = userBytes.length; + byte[] passwordBytes = password.getBytes(); + byte[] saslBytes = new byte[userBytes.length+passwordBytes.length+2]; + saslBytes[0] = 0x0; + for(int i = 1; i < saslBytes.length; ++i) { + if (i<=userLenght) { + saslBytes[i] = userBytes[i-1]; + } else if (i==userLenght+1) { + saslBytes[i] = 0x0; + } else { + saslBytes[i] = passwordBytes[i-(userLenght+2)]; + } + } + + return Base64.encodeToString(saslBytes, Base64.DEFAULT); + } +} diff --git a/src/de/gultsch/chat/xml/Element.java b/src/de/gultsch/chat/xml/Element.java new file mode 100644 index 000000000..d6d1b23d7 --- /dev/null +++ b/src/de/gultsch/chat/xml/Element.java @@ -0,0 +1,65 @@ +package de.gultsch.chat.xml; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +public class Element { + protected String name; + protected Hashtable attributes = new Hashtable(); + protected String content; + protected List children = new ArrayList(); + + public Element(String name) { + this.name = name; + } + + public Element addChild(Element child) { + this.content = null; + children.add(child); + return this; + } + + public Element setContent(String content) { + this.content = content; + this.children.clear(); + return this; + } + + public Element setAttribute(String name, String value) { + this.attributes.put(name, value); + return this; + } + + public Element setAttributes(Hashtable attributes) { + this.attributes = attributes; + return this; + } + + public String toString() { + StringBuilder elementOutput = new StringBuilder(); + if ((content==null)&&(children.size() == 0)) { + Tag emptyTag = Tag.empty(name); + emptyTag.setAtttributes(this.attributes); + elementOutput.append(emptyTag.toString()); + } else { + Tag startTag = Tag.start(name); + startTag.setAtttributes(this.attributes); + elementOutput.append(startTag); + if (content!=null) { + elementOutput.append(content); + } else { + for(Element child : children) { + elementOutput.append(child.toString()); + } + } + Tag endTag = Tag.end(name); + elementOutput.append(endTag); + } + return elementOutput.toString(); + } + + public String getName() { + return name; + } +} diff --git a/src/de/gultsch/chat/xml/Tag.java b/src/de/gultsch/chat/xml/Tag.java new file mode 100644 index 000000000..275229cf4 --- /dev/null +++ b/src/de/gultsch/chat/xml/Tag.java @@ -0,0 +1,99 @@ +package de.gultsch.chat.xml; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Set; + +public class Tag { + public static final int NO = -1; + public static final int START = 0; + public static final int END = 1; + public static final int EMPTY = 2; + + protected int type; + protected String name; + protected Hashtable attributes = new Hashtable(); + + protected Tag(int type, String name) { + this.type = type; + this.name = name; + } + + + public static Tag no(String text) { + return new Tag(NO,text); + } + + public static Tag start(String name) { + return new Tag(START,name); + } + + public static Tag end(String name) { + return new Tag(END,name); + } + + public static Tag empty(String name) { + return new Tag(EMPTY,name); + } + + public String getName() { + return name; + } + + public String getAttribute(String attrName) { + return this.attributes.get(attrName); + } + + public Tag setAttribute(String attrName, String attrValue) { + this.attributes.put(attrName, attrValue); + return this; + } + + public Tag setAtttributes(Hashtable attributes) { + this.attributes = attributes; + return this; + } + + public boolean isStart(String needle) { + return (this.type == START) && (this.name.equals(needle)); + } + + public boolean isEnd(String needle) { + return (this.type == END) && (this.name.equals(needle)); + } + + public boolean isNo() { + return (this.type == NO); + } + + public String toString() { + StringBuilder tagOutput = new StringBuilder(); + tagOutput.append('<'); + if (type==END) { + tagOutput.append('/'); + } + tagOutput.append(name); + if(type!=END) { + Set> attributeSet = attributes.entrySet(); + Iterator> it = attributeSet.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + tagOutput.append(' '); + tagOutput.append(entry.getKey()); + tagOutput.append("=\""); + tagOutput.append(entry.getValue()); + tagOutput.append('"'); + } + } + if (type==EMPTY) { + tagOutput.append('/'); + } + tagOutput.append('>'); + return tagOutput.toString(); + } + + public Hashtable getAttributes() { + return this.attributes; + } +} diff --git a/src/de/gultsch/chat/xml/TagWriter.java b/src/de/gultsch/chat/xml/TagWriter.java new file mode 100644 index 000000000..35f27477f --- /dev/null +++ b/src/de/gultsch/chat/xml/TagWriter.java @@ -0,0 +1,46 @@ +package de.gultsch.chat.xml; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import android.util.Log; + +public class TagWriter { + + OutputStreamWriter writer; + + public TagWriter() { + + } + + public TagWriter(OutputStream out) { + this.setOutputStream(out); + } + + public void setOutputStream(OutputStream out) { + this.writer = new OutputStreamWriter(out); + } + + public TagWriter beginDocument() throws IOException { + writer.write(""); + return this; + } + + public TagWriter writeTag(Tag tag) throws IOException { + writer.write(tag.toString()); + return this; + } + + public void flush() throws IOException { + writer.flush(); + } + + public void writeString(String string) throws IOException { + writer.write(string); + } + + public void writeElement(Element element) throws IOException { + writer.write(element.toString()); + } +} diff --git a/src/de/gultsch/chat/xml/XmlReader.java b/src/de/gultsch/chat/xml/XmlReader.java new file mode 100644 index 000000000..e086c8ae2 --- /dev/null +++ b/src/de/gultsch/chat/xml/XmlReader.java @@ -0,0 +1,91 @@ +package de.gultsch.chat.xml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; +import android.util.Xml; + +public class XmlReader { + private static final String LOGTAG = "xmppService"; + private XmlPullParser parser; + private PowerManager.WakeLock wakeLock; + private InputStream is; + + public XmlReader(WakeLock wakeLock) { + this.parser = Xml.newPullParser(); + try { + this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,true); + } catch (XmlPullParserException e) { + Log.d(LOGTAG,"error setting namespace feature on parser"); + } + this.wakeLock = wakeLock; + } + + public void setInputStream(InputStream inputStream) { + this.is = inputStream; + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + Log.d(LOGTAG,"error setting input stream"); + } + } + + public void reset() { + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + Log.d(LOGTAG,"error resetting input stream"); + } + } + + public Tag readTag() throws XmlPullParserException, IOException { + if (wakeLock.isHeld()) { + //Log.d(LOGTAG,"there was a wake lock. releasing it till next event"); + wakeLock.release(); //release wake look while waiting on next parser event + } + while(parser.next() != XmlPullParser.END_DOCUMENT) { + //Log.d(LOGTAG,"found new event. acquiring wake lock"); + wakeLock.acquire(); + if (parser.getEventType() == XmlPullParser.START_TAG) { + Tag tag = Tag.start(parser.getName()); + for(int i = 0; i < parser.getAttributeCount(); ++i) { + tag.setAttribute(parser.getAttributeName(i), parser.getAttributeValue(i)); + } + return tag; + } else if (parser.getEventType() == XmlPullParser.END_TAG) { + Tag tag = Tag.end(parser.getName()); + return tag; + } else if (parser.getEventType() == XmlPullParser.TEXT) { + Tag tag = Tag.no(parser.getText()); + return tag; + } + } + if (wakeLock.isHeld()) { + wakeLock.release(); + } + return null; //end document; + } + + public Element readElement(Tag currentTag) throws XmlPullParserException, IOException { + Element element = new Element(currentTag.getName()); + element.setAttributes(currentTag.getAttributes()); + Tag nextTag = this.readTag(); + if(nextTag.isNo()) { + element.setContent(nextTag.getName()); + nextTag = this.readTag(); + } + while(!nextTag.isEnd(element.getName())) { + Element child = this.readElement(nextTag); + element.addChild(child); + nextTag = this.readTag(); + } + return element; + } +} diff --git a/src/de/gultsch/chat/xmpp/IqPacket.java b/src/de/gultsch/chat/xmpp/IqPacket.java new file mode 100644 index 000000000..062bf1c0f --- /dev/null +++ b/src/de/gultsch/chat/xmpp/IqPacket.java @@ -0,0 +1,26 @@ +package de.gultsch.chat.xmpp; + +import de.gultsch.chat.xml.Element; + +public class IqPacket extends Element { + + public static final int TYPE_SET = 0; + public static final int TYPE_RESULT = 1; + + private IqPacket(String name) { + super(name); + } + + public IqPacket(String id, int type) { + super("iq"); + this.setAttribute("id",id); + switch (type) { + case TYPE_SET: + this.setAttribute("type", "set"); + break; + default: + break; + } + } + +} diff --git a/src/de/gultsch/chat/xmpp/XmppConnection.java b/src/de/gultsch/chat/xmpp/XmppConnection.java new file mode 100644 index 000000000..942033a15 --- /dev/null +++ b/src/de/gultsch/chat/xmpp/XmppConnection.java @@ -0,0 +1,222 @@ +package de.gultsch.chat.xmpp; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.SecureRandom; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import org.xmlpull.v1.XmlPullParserException; + +import android.os.PowerManager; +import android.util.Log; +import de.gultsch.chat.entities.Account; +import de.gultsch.chat.utils.SASL; +import de.gultsch.chat.xml.Element; +import de.gultsch.chat.xml.Tag; +import de.gultsch.chat.xml.XmlReader; +import de.gultsch.chat.xml.TagWriter; + +public class XmppConnection implements Runnable { + + protected Account account; + private static final String LOGTAG = "xmppService"; + + private PowerManager.WakeLock wakeLock; + + private SecureRandom random = new SecureRandom(); + + private Socket socket; + private XmlReader tagReader; + private TagWriter tagWriter; + + private boolean isTlsEncrypted = false; + private boolean isAuthenticated = false; + + public XmppConnection(Account account, PowerManager pm) { + this.account = account; + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "XmppConnection"); + tagReader = new XmlReader(wakeLock); + tagWriter = new TagWriter(); + } + + protected void connect() { + try { + socket = new Socket(account.getServer(), 5222); + Log.d(LOGTAG, "starting new socket"); + OutputStream out = socket.getOutputStream(); + tagWriter.setOutputStream(out); + InputStream in = socket.getInputStream(); + tagReader.setInputStream(in); + } catch (UnknownHostException e) { + Log.d(LOGTAG, "error during connect. unknown host"); + } catch (IOException e) { + Log.d(LOGTAG, "error during connect. io exception. falscher port?"); + } + } + + @Override + public void run() { + connect(); + try { + tagWriter.beginDocument(); + sendStartStream(); + Tag nextTag; + while ((nextTag = tagReader.readTag()) != null) { + if (nextTag.isStart("stream")) { + processStream(nextTag); + } else { + Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName()); + } + } + } catch (XmlPullParserException e) { + Log.d(LOGTAG, + "xml error during normal read. maybe missformed xml? " + + e.getMessage()); + } catch (IOException e) { + Log.d(LOGTAG, "io exception during read. connection lost?"); + } + } + + private void processStream(Tag currentTag) throws XmlPullParserException, + IOException { + Log.d(LOGTAG, "process Stream"); + Tag nextTag; + while ((nextTag = tagReader.readTag()) != null) { + if (nextTag.isStart("error")) { + processStreamError(nextTag); + } else if (nextTag.isStart("features")) { + processStreamFeatures(nextTag); + if (!isTlsEncrypted) { + sendStartTLS(); + } + if ((!isAuthenticated) && (isTlsEncrypted)) { + sendSaslAuth(); + } + if ((isAuthenticated)&&(isTlsEncrypted)) { + sendBindRequest(); + } + } else if (nextTag.isStart("proceed")) { + switchOverToTls(nextTag); + } else if (nextTag.isStart("success")) { + isAuthenticated = true; + Log.d(LOGTAG,"read success tag in stream. reset again"); + tagReader.readTag(); + tagReader.reset(); + sendStartStream(); + processStream(tagReader.readTag()); + } else if (nextTag.isStart("iq")) { + processIq(nextTag); + } else if (nextTag.isEnd("stream")) { + break; + } else { + Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName() + + " as child of " + currentTag.getName()); + } + } + } + + private void processIq(Tag currentTag) throws XmlPullParserException, IOException { + int typ = -1; + if (currentTag.getAttribute("type").equals("result")) { + typ = IqPacket.TYPE_RESULT; + } + IqPacket iq = new IqPacket(currentTag.getAttribute("id"),typ); + Tag nextTag = tagReader.readTag(); + while(!nextTag.isEnd("iq")) { + Element element = tagReader.readElement(nextTag); + iq.addChild(element); + nextTag = tagReader.readTag(); + } + Log.d(LOGTAG,"this is what i understood: "+iq.toString()); + } + + private void sendStartTLS() throws XmlPullParserException, IOException { + Tag startTLS = Tag.empty("starttls"); + startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls"); + Log.d(LOGTAG, "sending starttls"); + tagWriter.writeTag(startTLS).flush(); + } + + private void switchOverToTls(Tag currentTag) throws XmlPullParserException, + IOException { + Tag nextTag = tagReader.readTag(); // should be proceed end tag + Log.d(LOGTAG, "now switch to ssl"); + SSLSocket sslSocket; + try { + sslSocket = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory + .getDefault()).createSocket(socket, socket.getInetAddress() + .getHostAddress(), socket.getPort(), true); + tagReader.setInputStream(sslSocket.getInputStream()); + Log.d(LOGTAG, "reset inputstream"); + tagWriter.setOutputStream(sslSocket.getOutputStream()); + Log.d(LOGTAG, "switch over seemed to work"); + isTlsEncrypted = true; + sendStartStream(); + processStream(tagReader.readTag()); + } catch (IOException e) { + Log.d(LOGTAG, "error on ssl" + e.getMessage()); + } + } + + private void sendSaslAuth() throws IOException, XmlPullParserException { + String saslString = SASL.plain(account.getUsername(), + account.getPassword()); + Element auth = new Element("auth"); + auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + auth.setAttribute("mechanism", "PLAIN"); + auth.setContent(saslString); + Log.d(LOGTAG,"sending sasl "+auth.toString()); + tagWriter.writeElement(auth); + tagWriter.flush(); + } + + private void processStreamFeatures(Tag currentTag) + throws XmlPullParserException, IOException { + Log.d(LOGTAG, "processStreamFeatures"); + + Element streamFeatures = new Element("features"); + + Tag nextTag = tagReader.readTag(); + while(!nextTag.isEnd("features")) { + Element element = tagReader.readElement(nextTag); + streamFeatures.addChild(element); + nextTag = tagReader.readTag(); + } + } + + private void sendBindRequest() throws IOException { + IqPacket iq = new IqPacket(nextRandomId(),IqPacket.TYPE_SET); + Element bind = new Element("bind"); + bind.setAttribute("xmlns","urn:ietf:params:xml:ns:xmpp-bind"); + iq.addChild(bind); + Log.d(LOGTAG,"sending bind request: "+iq.toString()); + tagWriter.writeElement(iq); + tagWriter.flush(); + } + + private void processStreamError(Tag currentTag) { + Log.d(LOGTAG, "processStreamError"); + } + + private void sendStartStream() throws IOException { + Tag stream = Tag.start("stream"); + stream.setAttribute("from", account.getJid()); + stream.setAttribute("to", account.getServer()); + stream.setAttribute("version", "1.0"); + stream.setAttribute("xml:lang", "en"); + stream.setAttribute("xmlns", "jabber:client"); + stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams"); + tagWriter.writeTag(stream).flush(); + } + + private String nextRandomId() { + return new BigInteger(50, random).toString(32); + } +}