Java tutorial
/** * $RCSfile$ * $Revision: $ * $Date: $ * * Copyright (C) 2006 Jive Software. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), * a copy of which is included in this distribution. */ package org.jivesoftware.multiplexer.net; import org.dom4j.Element; import org.dom4j.io.XMPPPacketReader; import org.jivesoftware.multiplexer.*; import org.jivesoftware.util.Log; import org.jivesoftware.util.StringUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import java.io.IOException; import java.io.StringReader; /** * A StanzaHandler is the main responsible for handling incoming stanzas. Some stanzas like startTLS * are totally managed by this class. Other stanzas are just forwarded to the server. * * @author Gaston Dombiak */ abstract class StanzaHandler { /** * The utf-8 charset for decoding and encoding Jabber packet streams. */ protected static String CHARSET = "UTF-8"; /** * Reuse the same factory for all the connections. */ private static XmlPullParserFactory factory = null; private Connection connection; // DANIELE: Indicate if a session is already created private boolean sessionCreated = false; // Flag that indicates that the client requested to use TLS and TLS has been negotiated. Once the // client sent a new initial stream header the value will return to false. private boolean startedTLS = false; // Flag that indicates that the client requested to be authenticated. Once the // authentication process is over the value will return to false. private boolean startedSASL = false; // DANIELE: Indicate if a stream:stream is arrived to complete compression private boolean waitingCompressionACK = false; /** * Session associated with the socket reader. */ protected Session session; /** * Server name for which we are attending clients. */ private String serverName; /** * Router used to route incoming packets to the correct channels. */ private PacketRouter router; static { try { factory = XmlPullParserFactory.newInstance(MXParser.class.getName(), null); factory.setNamespaceAware(true); } catch (XmlPullParserException e) { Log.error("Error creating a parser factory", e); } } /** * Creates a dedicated reader for a socket. * * @param router the router for sending packets that were read. * @param serverName the name of the server this socket is working for. * @param connection the connection being read. * @throws org.xmlpull.v1.XmlPullParserException of an error while parsing occurs. */ public StanzaHandler(PacketRouter router, String serverName, Connection connection) throws XmlPullParserException { this.serverName = serverName; this.router = router; this.connection = connection; } public void process(String stanza, XmlPullParser parser) throws Exception { boolean initialStream = stanza.startsWith("<stream:stream") || stanza.startsWith("<flash:stream"); if (!sessionCreated || initialStream) { if (!initialStream) { // Ignore <?xml version="1.0"?> return; } // Found an stream:stream tag... if (!sessionCreated) { sessionCreated = true; parser.setInput(new StringReader(stanza)); createSession(parser); } else if (startedTLS) { startedTLS = false; tlsNegotiated(); } else if (startedSASL && session.getStatus() == Session.STATUS_AUTHENTICATED) { startedSASL = false; saslSuccessful(); } else if (waitingCompressionACK) { waitingCompressionACK = false; compressionSuccessful(); } return; } // Verify if end of stream was requested if (stanza.equals("</stream:stream>")) { session.close(); return; } // Ignore <?xml version="1.0"?> stanzas sent by clients if (stanza.startsWith("<?xml")) { return; } // Reset XPP parser with new stanza parser.setInput(new StringReader(stanza)); parser.next(); String tag = parser.getName(); // Verify that XML stanza is valid (i.e. well-formed) boolean valid; try { valid = validateStanza(stanza, parser); } catch (IllegalArgumentException e) { // Specify TO address was incorrect so do not process this stanza return; } if (!valid) { session.close(); return; } if ("starttls".equals(tag)) { // Negotiate TLS if (negotiateTLS()) { startedTLS = true; } else { connection.close(); session = null; } } else if ("auth".equals(tag)) { // User is trying to authenticate using SASL startedSASL = true; // Forward packet to the server route(stanza); } else if ("compress".equals(tag)) { // Client is trying to initiate compression if (compressClient(stanza)) { // Compression was successful so open a new stream and offer // resource binding and session establishment (to client sessions only) waitingCompressionACK = true; } } else { route(stanza); } } private boolean validateStanza(String stanza, XmlPullParser parser) { // TODO Detect when XML stanza is not complete int eventType; try { eventType = parser.getEventType(); } catch (XmlPullParserException e) { Log.error("Error parsing XML stanza: " + stanza, e); return false; } if (eventType == XmlPullParser.START_TAG) { String to = parser.getAttributeValue("", "to"); if (to != null) { // Validate the to address if (!StringUtils.validateJID(to)) { StringBuilder reply = new StringBuilder(); String stanzaType; if (parser.getName().equals("message")) { stanzaType = "message"; } else if (parser.getName().equals("iq")) { stanzaType = "iq"; } else if (parser.getName().equals("presence")) { stanzaType = "presence"; } else { return false; } reply.append("<").append(stanzaType).append(" type='error'"); String id = parser.getAttributeValue("", "id"); if (id != null) { reply.append(" id='").append(id).append("'"); } reply.append(">"); reply.append( "<error type='modify'><jid-malformed xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"); reply.append("</error>"); reply.append("</").append(stanzaType).append(">"); connection.deliverRawText(reply.toString()); throw new IllegalArgumentException("Illegal TO address"); } } } try { while (eventType != XmlPullParser.END_DOCUMENT) { eventType = parser.next(); } } catch (Exception e) { Log.error("Error parsing XML stanza: " + stanza, e); return false; } return true; } private void route(String stanza) { // Ensure that connection was secured if TLS was required if (connection.getTlsPolicy() == Connection.TLSPolicy.required && !connection.isSecure()) { closeNeverSecuredConnection(); return; } router.route(stanza, session.getStreamID()); } /** * Tries to secure the connection using TLS. If the connection is secured then reset * the parser to use the new secured reader. But if the connection failed to be secured * then send a <failure> stanza and close the connection. * * @return true if the connection was secured. */ private boolean negotiateTLS() { if (connection.getTlsPolicy() == Connection.TLSPolicy.disabled) { // Set the not_authorized error StreamError error = new StreamError(StreamError.Condition.not_authorized); // Deliver stanza connection.deliverRawText(error.toXML()); // Close the underlying connection connection.close(); // Log a warning so that admins can track this case from the server side Log.warn("TLS requested by initiator when TLS was never offered by server. " + "Closing connection : " + connection); return false; } // Client requested to secure the connection using TLS. Negotiate TLS. try { connection.startTLS(false, null); } catch (Exception e) { Log.error("Error while negotiating TLS", e); connection.deliverRawText("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">"); connection.close(); return false; } return true; } /** * TLS negotiation was successful so open a new stream and offer the new stream features. * The new stream features will include available SASL mechanisms and specific features * depending on the session type such as auth for Non-SASL authentication and register * for in-band registration. */ private void tlsNegotiated() { // Offer stream features including SASL Mechanisms StringBuilder sb = new StringBuilder(620); sb.append(geStreamHeader()); sb.append("<stream:features>"); // Include available SASL Mechanisms sb.append(ConnectionManager.getInstance().getServerSurrogate().getSASLMechanisms(session)); // Include specific features such as auth and register for client sessions String specificFeatures = session.getAvailableStreamFeatures(); if (specificFeatures != null) { sb.append(specificFeatures); } sb.append("</stream:features>"); connection.deliverRawText(sb.toString()); } /** * After SASL authentication was successful we should open a new stream and offer * new stream features such as resource binding and session establishment. Notice that * resource binding and session establishment should only be offered to clients (i.e. not * to servers or external components) */ private void saslSuccessful() { StringBuilder sb = new StringBuilder(420); sb.append(geStreamHeader()); sb.append("<stream:features>"); // Include specific features such as resource binding and session establishment // for client sessions String specificFeatures = session.getAvailableStreamFeatures(); if (specificFeatures != null) { sb.append(specificFeatures); } sb.append("</stream:features>"); connection.deliverRawText(sb.toString()); } /** * Start using compression but first check if the connection can and should use compression. * The connection will be closed if the requested method is not supported, if the connection * is already using compression or if client requested to use compression but this feature * is disabled. * * @param stanza the XML stanza sent by the client requesting compression. Compression method is * included. * @return true if it was possible to use compression. */ private boolean compressClient(String stanza) { String error = null; if (connection.getCompressionPolicy() == Connection.CompressionPolicy.disabled) { // Client requested compression but this feature is disabled error = "<failure xmlns='http://jabber.org/protocol/compress'><setup-failed/></failure>"; // Log a warning so that admins can track this case from the server side Log.warn("Client requested compression while compression is disabled. Closing " + "connection : " + connection); } else if (connection.isCompressed()) { // Client requested compression but connection is already compressed error = "<failure xmlns='http://jabber.org/protocol/compress'><setup-failed/></failure>"; // Log a warning so that admins can track this case from the server side Log.warn("Client requested compression and connection is already compressed. Closing " + "connection : " + connection); } else { XMPPPacketReader xmppReader = new XMPPPacketReader(); xmppReader.setXPPFactory(factory); Element doc; try { doc = xmppReader.read(new StringReader(stanza)).getRootElement(); } catch (Exception e) { Log.error("Error parsing compression stanza: " + stanza, e); connection.close(); return false; } // Check that the requested method is supported String method = doc.elementText("method"); if (!"zlib".equals(method)) { error = "<failure xmlns='http://jabber.org/protocol/compress'><unsupported-method/></failure>"; // Log a warning so that admins can track this case from the server side Log.warn("Requested compression method is not supported: " + method + ". Closing connection : " + connection); } } if (error != null) { // Deliver stanza connection.deliverRawText(error); return false; } else { // Indicate client that he can proceed and compress the socket connection.deliverRawText("<compressed xmlns='http://jabber.org/protocol/compress'/>"); // Start using compression connection.startCompression(); return true; } } /** * After compression was successful we should open a new stream and offer * new stream features such as resource binding and session establishment. Notice that * resource binding and session establishment should only be offered to clients (i.e. not * to servers or external components) */ private void compressionSuccessful() { StringBuilder sb = new StringBuilder(340); sb.append(geStreamHeader()); sb.append("<stream:features>"); // Include SASL mechanisms only if client has not been authenticated if (session.getStatus() != Session.STATUS_AUTHENTICATED) { // Include available SASL Mechanisms sb.append(ConnectionManager.getInstance().getServerSurrogate().getSASLMechanisms(session)); } // Include specific features such as resource binding and session establishment // for client sessions String specificFeatures = session.getAvailableStreamFeatures(); if (specificFeatures != null) { sb.append(specificFeatures); } sb.append("</stream:features>"); connection.deliverRawText(sb.toString()); } private String geStreamHeader() { StringBuilder sb = new StringBuilder(200); sb.append("<?xml version='1.0' encoding='"); sb.append(CHARSET); sb.append("'?>"); if (connection.isFlashClient()) { sb.append("<flash:stream xmlns:flash=\"http://www.jabber.com/streams/flash\" "); } else { sb.append("<stream:stream "); } sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\""); sb.append(getNamespace()); sb.append("\" from=\""); sb.append(serverName); sb.append("\" id=\""); sb.append(session.getStreamID()); sb.append("\" xml:lang=\""); sb.append(connection.getLanguage()); sb.append("\" version=\""); sb.append(Session.MAJOR_VERSION).append(".").append(Session.MINOR_VERSION); sb.append("\">"); return sb.toString(); } /** * Close the connection since TLS was mandatory and the entity never negotiated TLS. Before * closing the connection a stream error will be sent to the entity. */ void closeNeverSecuredConnection() { // Set the not_authorized error StreamError error = new StreamError(StreamError.Condition.not_authorized); // Deliver stanza connection.deliverRawText(error.toXML()); // Close the underlying connection connection.close(); // Log a warning so that admins can track this case from the server side Log.warn("TLS was required by the server and connection was never secured. " + "Closing connection : " + connection); } /** * Uses the XPP to grab the opening stream tag and create an active session * object. The session to create will depend on the sent namespace. In all * cases, the method obtains the opening stream tag, checks for errors, and * either creates a session or returns an error and kills the connection. * If the connection remains open, the XPP will be set to be ready for the * first packet. A call to next() should result in an START_TAG state with * the first packet in the stream. * @param xpp * @throws java.io.IOException * @throws org.xmlpull.v1.XmlPullParserException */ protected void createSession(XmlPullParser xpp) throws XmlPullParserException, IOException { for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) { eventType = xpp.next(); } // Check that the TO attribute of the stream header matches the server name or a valid // subdomain. If the value of the 'to' attribute is not valid then return a host-unknown // error and close the underlying connection. String host = xpp.getAttributeValue("", "to"); if (validateHost() && isHostUnknown(host)) { StringBuilder sb = new StringBuilder(250); sb.append("<?xml version='1.0' encoding='"); sb.append(CHARSET); sb.append("'?>"); // Append stream header sb.append("<stream:stream "); sb.append("from=\"").append(serverName).append("\" "); sb.append("id=\"").append(StringUtils.randomString(5)).append("\" "); sb.append("xmlns=\"").append(xpp.getNamespace(null)).append("\" "); sb.append("xmlns:stream=\"").append(xpp.getNamespace("stream")).append("\" "); sb.append("version=\"1.0\">"); // Set the host_unknown error StreamError error = new StreamError(StreamError.Condition.host_unknown); sb.append(error.toXML()); // Deliver stanza connection.deliverRawText(sb.toString()); // Close the underlying connection connection.close(); // Log a warning so that admins can track this cases from the server side Log.warn("Closing session due to incorrect hostname in stream header. Host: " + host + ". Connection: " + connection); } // Create the correct session based on the sent namespace. At this point the server // may offer the client to secure the connection. If the client decides to secure // the connection then a <starttls> stanza should be received else if (!createSession(xpp.getNamespace(null), serverName, xpp, connection)) { // No session was created because of an invalid namespace prefix so answer a stream // error and close the underlying connection StringBuilder sb = new StringBuilder(250); sb.append("<?xml version='1.0' encoding='"); sb.append(CHARSET); sb.append("'?>"); // Append stream header sb.append("<stream:stream "); sb.append("from=\"").append(serverName).append("\" "); sb.append("id=\"").append(StringUtils.randomString(5)).append("\" "); sb.append("xmlns=\"").append(xpp.getNamespace(null)).append("\" "); sb.append("xmlns:stream=\"").append(xpp.getNamespace("stream")).append("\" "); sb.append("version=\"1.0\">"); // Include the bad-namespace-prefix in the response StreamError error = new StreamError(StreamError.Condition.bad_namespace_prefix); sb.append(error.toXML()); connection.deliverRawText(sb.toString()); // Close the underlying connection connection.close(); // Log a warning so that admins can track this cases from the server side Log.warn("Closing session due to bad_namespace_prefix in stream header. Prefix: " + xpp.getNamespace(null) + ". Connection: " + connection); } } private boolean isHostUnknown(String host) { if (host == null) { // Answer false since when using server dialback the stream header will not // have a TO attribute return false; } if (serverName.equals(host)) { // requested host matched the server name return false; } return true; } /** * Returns the stream namespace. (E.g. jabber:client, jabber:server, etc.). * * @return the stream namespace. */ abstract String getNamespace(); /** * Returns true if the value of the 'to' attribute in the stream header should be * validated. If the value of the 'to' attribute is not valid then a host-unknown error * will be returned and the underlying connection will be closed. * * @return true if the value of the 'to' attribute in the initial stream header should be * validated. */ abstract boolean validateHost(); /** * Creates the appropriate {@link Session} subclass based on the specified namespace. * * @param namespace the namespace sent in the stream element. eg. jabber:client. * @param serverName * @param xpp * @param connection * @return the created session or null. * @throws org.xmlpull.v1.XmlPullParserException */ abstract boolean createSession(String namespace, String serverName, XmlPullParser xpp, Connection connection) throws XmlPullParserException; }