package com.voxeo.rayo.client.io; import java.io.Reader; import java.net.SocketException; import java.util.Collection; import java.util.concurrent.ConcurrentLinkedQueue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmlpull.mxp1.MXParser; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import com.voxeo.rayo.client.XmppConnectionListener; import com.voxeo.rayo.client.auth.AuthenticationListener; import com.voxeo.rayo.client.filter.XmppObjectFilter; import com.voxeo.rayo.client.filter.XmppObjectFilterSupport; import com.voxeo.rayo.client.listener.StanzaListener; import com.voxeo.rayo.client.listener.StanzaListenerSupport; import com.voxeo.rayo.client.util.XmppObjectParser; import com.voxeo.rayo.client.xmpp.stanza.Error; import com.voxeo.rayo.client.xmpp.stanza.IQ; import com.voxeo.rayo.client.xmpp.stanza.Message; import com.voxeo.rayo.client.xmpp.stanza.Presence; import com.voxeo.rayo.client.xmpp.stanza.XmppObject; import com.voxeo.rayo.client.xmpp.stanza.Error.Condition; import com.voxeo.rayo.client.xmpp.stanza.Error.Type; import com.voxeo.rayo.client.xmpp.stanza.sasl.Challenge; import com.voxeo.rayo.client.xmpp.stanza.sasl.Success; public class XmppReaderWorker implements Runnable, StanzaListenerSupport, XmppObjectFilterSupport { private Logger log = LoggerFactory.getLogger(XmppReaderWorker.class); private XmlPullParser parser; private String connectionId; private boolean done; private Reader reader; private Collection<XmppConnectionListener> listeners = new ConcurrentLinkedQueue<XmppConnectionListener>(); private Collection<AuthenticationListener> authListeners = new ConcurrentLinkedQueue<AuthenticationListener>(); private MessageDispatcher messageDispatcher; public XmppReaderWorker() { messageDispatcher = new UnboundedQueueMessageDispatcher(); } @Override public void run() { parse(); } public void addXmppConnectionListener(XmppConnectionListener listener) { listeners.add(listener); } public void removeXmppConnectionListener(XmppConnectionListener listener) { listeners.remove(listener); } public void addStanzaListener(StanzaListener listener) { messageDispatcher.addStanzaListener(listener); } public void removeStanzaListener(StanzaListener listener) { messageDispatcher.removeStanzaListener(listener); } public void addAuthenticationListener(AuthenticationListener authListener) { authListeners.add(authListener); } public void removeAuthenticationListener(AuthenticationListener authListener) { authListeners.remove(authListener); } public void addFilter(XmppObjectFilter filter) { messageDispatcher.addFilter(filter); } public void removeFilter(XmppObjectFilter filter) { messageDispatcher.removeFilter(filter); } public void resetParser(Reader reader) { log("Reseting parser"); try { this.reader = reader; parser = new MXParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); parser.setInput(reader); } catch (XmlPullParserException xppe) { xppe.printStackTrace(); } } /** * Parse top-level packets in order to process them further. * * @param thread the thread that is being used by the reader to parse incoming packets. */ private void parse() { try { int eventType = parser.getEventType(); do { if (eventType == XmlPullParser.START_TAG) { if (parser.getName().equals("message")) { final Message message = XmppObjectParser.parseMessage(parser); log(message); messageDispatcher.dispatch(message); } else if (parser.getName().equals("iq")) { final IQ iq = XmppObjectParser.parseIQ(parser); if (iq.hasChild("error")) { handleError(iq.getError()); } log(iq); messageDispatcher.dispatch(iq); } else if (parser.getName().equals("presence")) { final Presence presence = XmppObjectParser.parsePresence(parser); log(presence); messageDispatcher.dispatch(presence); } // We found an opening stream. Record information about it, then notify // the connectionID lock so that the packet reader startup can finish. else if (parser.getName().equals("stream")) { // Ensure the correct jabber:client namespace is being used. if ("jabber:client".equals(parser.getNamespace(null))) { // Get the connection id. for (int i=0; i<parser.getAttributeCount(); i++) { if (parser.getAttributeName(i).equals("id")) { // Save the connectionID connectionId = parser.getAttributeValue(i); log("Received new connection stream with id: " + connectionId); if (!"1.0".equals(parser.getAttributeValue("", "version"))) { // Notify that a stream has been opened if the // server is not XMPP 1.0 compliant otherwise make the // notification after TLS has been negotiated or if TLS // is not supported connectionEstablished(); } } else if (parser.getAttributeName(i).equals("from")) { } } } } else if (parser.getName().equals("error")) { Error error = XmppObjectParser.parseError(parser); log(error); handleError(error); } else if (parser.getName().equals("features")) { log("Received features"); parseFeatures(parser); } else if (parser.getName().equals("proceed")) { } else if (parser.getName().equals("failure")) { } else if (parser.getName().equals("challenge")) { final Challenge challenge = new Challenge().setText(parser.nextText()); for (final AuthenticationListener listener: authListeners) { listener.authChallenge(challenge); } } else if (parser.getName().equals("success")) { final Success success = new Success().setText(parser.nextText()); log(success); for (final AuthenticationListener listener: authListeners) { listener.authSuccessful(success); } // We now need to bind a resource for the connection // Open a new stream and wait for the response for (final XmppConnectionListener listener: listeners) { listener.connectionReset(connectionId); } // Reset the state of the parser since a new stream element is going // to be sent by the server resetParser(reader); } else if (parser.getName().equals("compressed")) { } } else if (eventType == XmlPullParser.END_TAG) { if (parser.getName().equals("stream")) { // Disconnect the connection for (final XmppConnectionListener listener: listeners) { listener.connectionFinished(connectionId); } } } if (parser == null) { log("Parser is null. Exiting."); done = true; } else { eventType = parser.next(); } } while (!done && eventType != XmlPullParser.END_DOCUMENT); } catch (SocketException se) { if (!done) { se.printStackTrace(); handleError(new Error(Condition.gone, Type.cancel, se.getMessage())); } } catch (Exception e) { e.printStackTrace(); handleError(new Error(Condition.undefined_condition, Type.cancel, e.getMessage())); } } private void parseFeatures(XmlPullParser parser) throws Exception { boolean startTLSReceived = false; boolean startTLSRequired = false; boolean done = false; while (!done) { int eventType = parser.next(); if (eventType == XmlPullParser.START_TAG) { if (parser.getName().equals("starttls")) { startTLSReceived = true; } else if (parser.getName().equals("mechanisms")) { log("Received mechanisms"); // The server is reporting available SASL mechanisms. Store this information // which will be used later while logging (i.e. authenticating) into // the server final Collection<String> mechanisms = XmppObjectParser.parseMechanisms(parser); for (final AuthenticationListener listener: authListeners) { listener.authSettingsReceived(mechanisms); } } else if (parser.getName().equals("bind")) { log("Received bind"); for (final AuthenticationListener listener: authListeners) { listener.authBindingRequired(); } } else if (parser.getName().equals("session")) { log("Received session"); for (final AuthenticationListener listener: authListeners) { listener.authSessionsSupported(); } } else if (parser.getName().equals("compression")) { // The server supports stream compression } else if (parser.getName().equals("register")) { } } else if (eventType == XmlPullParser.END_TAG) { if (parser.getName().equals("starttls")) { // Confirm the server that we want to use TLS } else if (parser.getName().equals("required") && startTLSReceived) { startTLSRequired = true; } else if (parser.getName().equals("features")) { done = true; } } } //TODO: Lots of stuff to handle here. Code based in Packet reader from Smack // Release the lock after TLS has been negotiated or we are not insterested in TLS if (!startTLSReceived || (startTLSReceived && !startTLSRequired)) { connectionEstablished(); } } private void connectionEstablished() { if (connectionId != null) { for (final XmppConnectionListener listener: listeners) { listener.connectionEstablished(connectionId); } } } private void connectionFinished() { if (connectionId != null) { for (final XmppConnectionListener listener: listeners) { listener.connectionFinished(connectionId); } } } void handleError(Error e) { messageDispatcher.dispatch(e); } public void setDone(boolean done) { this.done = done; connectionFinished(); } public void reset() { resetParser(reader); cleanListeners(); } public void shutdown() { reader = null; parser = null; connectionId = null; cleanListeners(); messageDispatcher.shutdown(); } private void cleanListeners() { listeners.clear(); authListeners.clear(); messageDispatcher.reset(); }; public String getConnectionId() { return connectionId; } private void log(XmppObject object) { log(object.toString()); } private void log(String value) { log.debug(String.format("[IN ] [%s]", value)); } }