All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jivesoftware.openfire.net.StanzaHandler Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.openfire.net;

import org.dom4j.Element;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.StreamIDFactory;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.http.FlashCrossDomainServlet;
import org.jivesoftware.openfire.session.LocalSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.spi.BasicStreamIDFactory;
import org.jivesoftware.openfire.streammanagement.StreamManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.*;

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. The rest of the stanzas are just forwarded to the router.
 *
 * @author Gaston Dombiak
 */
public abstract class StanzaHandler {

    private static final Logger Log = LoggerFactory.getLogger(StanzaHandler.class);

    /**
     * A factory that generates random stream IDs
     */
    private static final StreamIDFactory STREAM_ID_FACTORY = new BasicStreamIDFactory();

    /**
     * The utf-8 charset for decoding and encoding Jabber packet streams.
     */
    protected static String CHARSET = "UTF-8";
    protected 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;
    /**
     * SASL status based on the last SASL interaction
     */
    private SASLAuthentication.Status saslStatus;

    // DANIELE: Indicate if a stream:stream is arrived to complete compression
    private boolean waitingCompressionACK = false;

    /**
     * Session associated with the socket reader.
     */
    protected LocalSession session;

    /**
     * Router used to route incoming packets to the correct channels.
     */
    private PacketRouter router;

    /**
     * Creates a dedicated reader for a socket.
     *
     * @param router     the router for sending packets that were read.
     * @param connection the connection being read.
     */
    public StanzaHandler(PacketRouter router, Connection connection) {
        this.router = router;
        this.connection = connection;
    }

    @Deprecated
    public StanzaHandler(PacketRouter router, String serverName, Connection connection) {
        this.router = router;
        this.connection = connection;
    }

    public void setSession(LocalSession session) {
        this.session = session;
    }

    public void process(String stanza, XMPPPacketReader reader) throws Exception {

        boolean initialStream = stanza.startsWith("")) {
                    String crossDomainText = FlashCrossDomainServlet.CROSS_DOMAIN_TEXT +
                            XMPPServer.getInstance().getConnectionManager().getClientListenerPort() +
                            FlashCrossDomainServlet.CROSS_DOMAIN_END_TEXT + '\0';
                    connection.deliverRawText(crossDomainText);
                    return;
                }
                else {
                    // Ignore 
                    return;
                }
            }
            // Found an stream:stream tag...
            if (!sessionCreated) {
                sessionCreated = true;
                MXParser parser = reader.getXPPParser();
                parser.setInput(new StringReader(stanza));
                createSession(parser);
            }
            else if (startedTLS) {
                startedTLS = false;
                tlsNegotiated();
            }
            else if (startedSASL && saslStatus == SASLAuthentication.Status.authenticated) {
                startedSASL = false;
                saslSuccessful();
            }
            else if (waitingCompressionACK) {
                waitingCompressionACK = false;
                compressionSuccessful();
            }
            return;
        }

        // Verify if end of stream was requested
        if (stanza.equals("")) {
            if (session != null) {
                session.getStreamManager().formalClose();
                session.close();
            }
            return;
        }
        // Ignore  stanzas sent by clients
        if (stanza.startsWith("
     * Subclasses may redefine this method for different reasons such as modifying the sender
     * of the packet to avoid spoofing, rejecting the packet or even process the packet in
     * another thread.

* * @param packet the received packet. * @throws org.jivesoftware.openfire.auth.UnauthorizedException * if service is not available to sender. */ protected void processIQ(IQ packet) throws UnauthorizedException { router.route(packet); session.incrementClientPacketCount(); } /** * Process the received Presence packet. Registered * {@link org.jivesoftware.openfire.interceptor.PacketInterceptor} will be invoked before * and after the packet was routed. *

* Subclasses may redefine this method for different reasons such as modifying the sender * of the packet to avoid spoofing, rejecting the packet or even process the packet in * another thread.

* * @param packet the received packet. * @throws org.jivesoftware.openfire.auth.UnauthorizedException * if service is not available to sender. */ protected void processPresence(Presence packet) throws UnauthorizedException { router.route(packet); session.incrementClientPacketCount(); } /** * Process the received Message packet. Registered * {@link org.jivesoftware.openfire.interceptor.PacketInterceptor} will be invoked before * and after the packet was routed. *

* Subclasses may redefine this method for different reasons such as modifying the sender * of the packet to avoid spoofing, rejecting the packet or even process the packet in * another thread.

* * @param packet the received packet. * @throws org.jivesoftware.openfire.auth.UnauthorizedException * if service is not available to sender. */ protected void processMessage(Message packet) throws UnauthorizedException { router.route(packet); session.incrementClientPacketCount(); } /** * Returns true if a received packet of an unkown type (i.e. not a Message, Presence * or IQ) has been processed. If the packet was not processed then an exception will * be thrown which will make the thread to stop processing further packets. * * @param doc the DOM element of an unkown type. * @return true if a received packet has been processed. * @throws UnauthorizedException if stanza failed to be processed. Connection will be closed. */ abstract boolean processUnknowPacket(Element doc) throws UnauthorizedException; /** * 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 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 { startTLS(); } catch (Exception e) { Log.error("Error while negotiating TLS", e); connection.deliverRawText(""); connection.close(); return false; } return true; } abstract void startTLS() throws Exception; /** * 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(getStreamHeader()); sb.append(""); // Include available SASL Mechanisms sb.append(SASLAuthentication.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(""); 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(getStreamHeader()); sb.append(""); // 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(""); 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 doc the element sent by the client requesting compression. Compression method is * included. * @return true if it was possible to use compression. */ private boolean compressClient(Element doc) { String error = null; if (connection.getCompressionPolicy() == Connection.CompressionPolicy.disabled) { // Client requested compression but this feature is disabled error = ""; // 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 = ""; // 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 { // Check that the requested method is supported String method = doc.elementText("method"); if (!"zlib".equals(method)) { error = ""; // 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 { // Start using compression for incoming traffic connection.addCompression(); // Indicate client that he can proceed and compress the socket connection.deliverRawText(""); // Start using compression for outgoing traffic 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(getStreamHeader()); sb.append(""); // Include SASL mechanisms only if client has not been authenticated if (session.getStatus() != Session.STATUS_AUTHENTICATED) { // Include available SASL Mechanisms sb.append(SASLAuthentication.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(""); connection.deliverRawText(sb.toString()); } /** * Determines whether stanza's namespace matches XEP-0198 namespace * @param stanza Stanza to be checked * @return whether stanza's namespace matches XEP-0198 namespace */ private boolean isStreamManagementStanza(Element stanza) { return StreamManager.NAMESPACE_V2.equals(stanza.getNamespace().getStringValue()) || StreamManager.NAMESPACE_V3.equals(stanza.getNamespace().getStringValue()); } private String getStreamHeader() { StringBuilder sb = new StringBuilder(200); sb.append(""); if (connection.isFlashClient()) { 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. */ protected void createSession(XmlPullParser xpp) throws XmlPullParserException, IOException { for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) { eventType = xpp.next(); } final String serverName = XMPPServer.getInstance().getServerInfo().getXMPPDomain(); // 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"); StreamError streamError = null; if (validateHost() && isHostUnknown(host)) { streamError = new StreamError(StreamError.Condition.host_unknown); // 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); } // Validate the stream namespace else if (!"http://etherx.jabber.org/streams".equals(xpp.getNamespace()) && !"http://www.jabber.com/streams/flash".equals(xpp.getNamespace())) { // Include the invalid-namespace in the response streamError = new StreamError(StreamError.Condition.invalid_namespace); // Log a warning so that admins can track this cases from the server side Log.warn("Closing session due to invalid namespace in stream header. Namespace: " + xpp.getNamespace() + ". 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 stanza should be received else if (!createSession(xpp.getNamespace(null), serverName, xpp, connection)) { // http://xmpp.org/rfcs/rfc6120.html#streams-error-conditions-invalid-namespace // "or the content namespace declared as the default namespace is not supported (e.g., something other than "jabber:client" or "jabber:server")." streamError = new StreamError(StreamError.Condition.invalid_namespace); // Log a warning so that admins can track this cases from the server side Log.warn("Closing session due to invalid namespace in stream header. Prefix: " + xpp.getNamespace(null) + ". Connection: " + connection); } if (streamError != null) { StringBuilder sb = new StringBuilder(250); if (host == null) host = serverName; sb.append(""); // Append stream header sb.append(""); sb.append(streamError.toXML()); // Deliver stanza connection.deliverRawText(sb.toString()); // Close the underlying connection connection.close(); } } 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 (XMPPServer.getInstance().getServerInfo().getXMPPDomain().equals( host )) { // requested host matched the server name return false; } return true; } /** * Obtain the address of the XMPP entity for which this StanzaHandler * handles stanzas. * * Note that the value that is returned for this method can * change over time. For example, if no session has been established yet, * this method will return null, or, if resource binding occurs, * the returned value might change. Values obtained from this method are * therefore best not cached. * * @return The address of the XMPP entity for. */ public JID getAddress() { if (session == null) { return null; } return session.getAddress(); } /** * 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(); /** * Returns true if the value of the 'to' attribute of {@link IQ}, {@link Presence} and * {@link Message} must be validated. Connection Managers perform their own * JID validation so there is no need to validate JIDs again but when clients are * directly connected to the server then we need to validate JIDs. * * @return rue if the value of the 'to' attribute of IQ, Presence and Messagemust be validated. */ abstract boolean validateJIDs(); /** * Creates the appropriate {@link Session} subclass based on the specified namespace. * * @param namespace the namespace sent in the stream element. eg. jabber:client. * @return the created session or null. * @throws org.xmlpull.v1.XmlPullParserException * */ abstract boolean createSession(String namespace, String serverName, XmlPullParser xpp, Connection connection) throws XmlPullParserException; }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy