ConnectionMultiplexerSession.java 17 KB
Newer Older
Gaston Dombiak's avatar
Gaston Dombiak committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
/**
 * $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.wildfire.multiplex;

import org.dom4j.Element;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.jivesoftware.wildfire.*;
import org.jivesoftware.wildfire.auth.AuthFactory;
import org.jivesoftware.wildfire.auth.UnauthorizedException;
import org.jivesoftware.wildfire.net.SASLAuthentication;
import org.jivesoftware.wildfire.net.SocketConnection;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.StreamError;

import java.io.IOException;
import java.io.Writer;
import java.util.Collection;

/**
 * Represents a session between the server and a connection manager.<p>
 *
 * Each Connection Manager has its own domain. Each connection from the same connection manager
 * uses a different resource. Unlike any other session, connection manager sessions are not
 * present in the routing table. This means that connection managers are not reachable entities.
 * In other words, entities cannot send packets to connection managers but clients being hosted
 * by them. The main reason behind this design decision is that connection managers are private
 * components of the server so they can only be contacted by the server. Connection Manager
 * sessions are present in {@link SessionManager} but not in {@link RoutingTable}. Use
 * {@link SessionManager#getConnectionMultiplexerSessions(String)} to get all sessions or
 * {@link ConnectionMultiplexerManager#getMultiplexerSession(String)}
 * to get a random session to a given connection manager.
 *
 * @author Gaston Dombiak
 */
public class ConnectionMultiplexerSession extends Session {

    private static Connection.TLSPolicy tlsPolicy;
    private static Connection.CompressionPolicy compressionPolicy;

    /**
     * Milliseconds a connection has to be idle to be closed. Default is 30 minutes. Sending
     * stanzas to the client is not considered as activity. We are only considering the connection
     * active when the client sends some data or hearbeats (i.e. whitespaces) to the server.
     * The reason for this is that sending data will fail if the connection is closed. And if
     * the thread is blocked while sending data (because the socket is closed) then the clean up
     * thread will close the socket anyway.
     */
    private static long idleTimeout;

    static {
        // Set the TLS policy stored as a system property
        String policyName = JiveGlobals.getProperty("xmpp.multiplex.tls.policy",
                Connection.TLSPolicy.optional.toString());
        tlsPolicy = Connection.TLSPolicy.valueOf(policyName);

        // Set the Compression policy stored as a system property
        policyName = JiveGlobals.getProperty("xmpp.multiplex.compression.policy",
                Connection.CompressionPolicy.disabled.toString());
        compressionPolicy = Connection.CompressionPolicy.valueOf(policyName);

77 78
        // Set the default read idle timeout. If none was set then assume 5 minutes
        idleTimeout = JiveGlobals.getIntProperty("xmpp.multiplex.idle", 5 * 60 * 1000);
Gaston Dombiak's avatar
Gaston Dombiak committed
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
    }

    public static Session createSession(String serverName, XMPPPacketReader reader,
            SocketConnection connection) throws XmlPullParserException, IOException,
            UnauthorizedException {
        XmlPullParser xpp = reader.getXPPParser();
        String domain = xpp.getAttributeValue("", "to");

        Log.debug("[ConMng] Starting registration of new connection manager for domain: " + domain);

        Writer writer = connection.getWriter();
        // Default answer header in case of an error
        StringBuilder sb = new StringBuilder();
        sb.append("<?xml version='1.0' encoding='");
        sb.append(CHARSET);
        sb.append("'?>");
        sb.append("<stream:stream ");
        sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" ");
        sb.append("xmlns=\"jabber:connectionmanager\" from=\"");
        sb.append(domain);
99
        sb.append("\" version=\"1.0\">");
Gaston Dombiak's avatar
Gaston Dombiak committed
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116

        // Check that a domain was provided in the stream header
        if (domain == null) {
            Log.debug("[ConMng] Domain not specified in stanza: " + xpp.getText());
            // Include the bad-format in the response
            StreamError error = new StreamError(StreamError.Condition.bad_format);
            sb.append(error.toXML());
            writer.write(sb.toString());
            writer.flush();
            // Close the underlying connection
            connection.close();
            return null;
        }

        // Get the requested domain
        JID address = new JID(domain);
        // Check that a secret key was configured in the server
117
        String secretKey = ConnectionMultiplexerManager.getDefaultSecret();
Gaston Dombiak's avatar
Gaston Dombiak committed
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
        if (secretKey == null) {
            Log.debug("[ConMng] A shared secret for connection manager was not found.");
            // Include the internal-server-error in the response
            StreamError error = new StreamError(StreamError.Condition.internal_server_error);
            sb.append(error.toXML());
            writer.write(sb.toString());
            writer.flush();
            // Close the underlying connection
            connection.close();
            return null;
        }
        // Check that the requested subdomain is not already in use
        if (SessionManager.getInstance().getConnectionMultiplexerSession(address) != null) {
            Log.debug("[ConMng] Another connection manager is already using domain: " + domain);
            // Domain already occupied so return a conflict error and close the connection
            // Include the conflict error in the response
            StreamError error = new StreamError(StreamError.Condition.conflict);
            sb.append(error.toXML());
            writer.write(sb.toString());
            writer.flush();
            // Close the underlying connection
            connection.close();
            return null;
        }

        // Indicate the TLS policy to use for this connection
        connection.setTlsPolicy(tlsPolicy);

        // Indicate the compression policy to use for this connection
        connection.setCompressionPolicy(compressionPolicy);

        // Set the max number of milliseconds the connection may not receive data from the
        // client before closing the connection
        connection.setIdleTimeout(idleTimeout);

        // Set the connection manager domain to use delivering a packet fails
        ((MultiplexerPacketDeliverer) connection.getPacketDeliverer())
                .setConnectionManagerDomain(address.getDomain());

        // Create a ConnectionMultiplexerSession for the new session originated
        // from the connection manager
        Session session =
                SessionManager.getInstance().createMultiplexerSession(connection, address);
        // Set the address of the new session
        session.setAddress(address);

        try {
            Log.debug("[ConMng] Send stream header with ID: " + session.getStreamID() +
                    " for connection manager with domain: " +
                    domain);
            // Build the start packet response
            sb = new StringBuilder();
            sb.append("<?xml version='1.0' encoding='");
            sb.append(CHARSET);
            sb.append("'?>");
            sb.append("<stream:stream ");
            sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" ");
            sb.append("xmlns=\"jabber:connectionmanager\" from=\"");
            sb.append(domain);
            sb.append("\" id=\"");
            sb.append(session.getStreamID().toString());
179
            sb.append("\" version=\"1.0\" >");
Gaston Dombiak's avatar
Gaston Dombiak committed
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
            writer.write(sb.toString());
            writer.flush();

            // Announce stream features.

            sb = new StringBuilder(490);
            sb.append("<stream:features>");
            if (tlsPolicy != Connection.TLSPolicy.disabled) {
                sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
                if (tlsPolicy == Connection.TLSPolicy.required) {
                    sb.append("<required/>");
                }
                sb.append("</starttls>");
            }
            // Include Stream features
            String specificFeatures = session.getAvailableStreamFeatures();
            if (specificFeatures != null) {
                sb.append(specificFeatures);
            }
            sb.append("</stream:features>");

            writer.write(sb.toString());
            writer.flush();

            return session;
        }
        catch (Exception e) {
            Log.error("An error occured while creating a ComponentSession", e);
            // Close the underlying connection
            connection.close();
            return null;
        }
    }

    public ConnectionMultiplexerSession(String serverName, Connection connection, StreamID streamID) {
        super(serverName, connection, streamID);
    }

    public String getAvailableStreamFeatures() {
        if (conn.getTlsPolicy() == Connection.TLSPolicy.required && !conn.isSecure()) {
            return null;
        }

        // Include Stream Compression Mechanism
        if (conn.getCompressionPolicy() != Connection.CompressionPolicy.disabled &&
                !conn.isCompressed()) {
            return "<compression xmlns=\"http://jabber.org/features/compress\"><method>zlib</method></compression>";
        }
        return null;
    }

    public void process(Packet packet) {
        deliver(packet);
    }

    /**
     * Authenticates the connection manager. Shared secret is validated with the one provided
     * by the connection manager. If everything went fine then the session will have a status
     * of "authenticated" and the connection manager will receive the client configuration
     * options.
     *
     * @param digest the digest provided by the connection manager with the handshake stanza.
     * @return true if the connection manager was sucessfully authenticated.
     */
    public boolean authenticate(String digest) {
        // Perform authentication. Wait for the handshake (with the secret key)
246 247
        String anticipatedDigest = AuthFactory.createDigest(getStreamID().getID(),
                ConnectionMultiplexerManager.getDefaultSecret());
Gaston Dombiak's avatar
Gaston Dombiak committed
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
        // Check that the provided handshake (secret key + sessionID) is correct
        if (!anticipatedDigest.equalsIgnoreCase(digest)) {
            Log.debug("[ConMng] Incorrect handshake for connection manager with domain: " +
                    getAddress().getDomain());
            //  The credentials supplied by the initiator are not valid (answer an error
            // and close the connection)
            conn.deliverRawText(new StreamError(StreamError.Condition.not_authorized).toXML());
            // Close the underlying connection
            conn.close();
            return false;
        }
        else {
            // Component has authenticated fine
            setStatus(Session.STATUS_AUTHENTICATED);
            // Send empty handshake element to acknowledge success
            conn.deliverRawText("<handshake></handshake>");
            Log.debug("[ConMng] Connection manager was AUTHENTICATED with domain: " + getAddress());
            sendClientOptions();
            return true;
        }
    }

    /**
     * Send to the Connection Manager the connection options available for clients. The info
     * to send includes:
     * <ul>
     *  <li>if TLS is available, optional or required
     *  <li>SASL mechanisms available before TLS is negotiated
     *  <li>if compression is available
     *  <li>if Non-SASL authentication is available
     *  <li>if In-Band Registration is available
     * </ul
     */
    private void sendClientOptions() {
        IQ options = new IQ(IQ.Type.set);
        Element child = options.setChildElement("configuration",
                "http://jabber.org/protocol/connectionmanager");
        // Add info about TLS
        if (ClientSession.getTLSPolicy() != Connection.TLSPolicy.disabled) {
            Element tls = child.addElement("starttls", "urn:ietf:params:xml:ns:xmpp-tls");
            if (ClientSession.getTLSPolicy() != Connection.TLSPolicy.required) {
                tls.addElement("required");
            }

        }
        // Add info about SASL mechanisms
        Collection<String> mechanisms = SASLAuthentication.getSupportedMechanisms();
        if (!mechanisms.isEmpty()) {
            Element sasl = child.addElement("mechanisms", "urn:ietf:params:xml:ns:xmpp-sasl");
            for (String mechanism : mechanisms) {
                sasl.addElement("mechanism").setText(mechanism);
            }
        }
        // Add info about Stream Compression
        if (ClientSession.getCompressionPolicy() == Connection.CompressionPolicy.optional) {
            Element comp = child.addElement("compression", "http://jabber.org/features/compress");
            comp.addElement("method").setText("zlib");
        }
        // Add info about Non-SASL authentication
307
        child.addElement("auth", "http://jabber.org/features/iq-auth");
Gaston Dombiak's avatar
Gaston Dombiak committed
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
        // Add info about In-Band Registration
        if (XMPPServer.getInstance().getIQRegisterHandler().isInbandRegEnabled()) {
            child.addElement("register", "http://jabber.org/features/iq-register");
        }
        // Send the options
        try {
            conn.deliver(options);
        }
        catch (UnauthorizedException e) {
            // Do nothing. Should never happen
        }
    }

    void deliver(Packet packet) {
        if (conn != null && !conn.isClosed()) {
            try {
                conn.deliver(packet);
            }
            catch (Exception e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        }
    }

    /**
     * Returns whether TLS is mandatory, optional or is disabled for clients. When TLS is
     * mandatory clients are required to secure their connections or otherwise their connections
     * will be closed. On the other hand, when TLS is disabled clients are not allowed to secure
     * their connections using TLS. Their connections will be closed if they try to secure the
     * connection. in this last case.
     *
     * @return whether TLS is mandatory, optional or is disabled.
     */
    public static SocketConnection.TLSPolicy getTLSPolicy() {
        return tlsPolicy;
    }

    /**
     * Sets whether TLS is mandatory, optional or is disabled for clients. When TLS is
     * mandatory clients are required to secure their connections or otherwise their connections
     * will be closed. On the other hand, when TLS is disabled clients are not allowed to secure
     * their connections using TLS. Their connections will be closed if they try to secure the
     * connection. in this last case.
     *
     * @param policy whether TLS is mandatory, optional or is disabled.
     */
    public static void setTLSPolicy(SocketConnection.TLSPolicy policy) {
        tlsPolicy = policy;
        JiveGlobals.setProperty("xmpp.multiplex.tls.policy", tlsPolicy.toString());
    }

    /**
     * Returns whether compression is optional or is disabled for clients.
     *
     * @return whether compression is optional or is disabled.
     */
    public static SocketConnection.CompressionPolicy getCompressionPolicy() {
        return compressionPolicy;
    }

    /**
     * Sets whether compression is optional or is disabled for clients.
     *
     * @param policy whether compression is optional or is disabled.
     */
    public static void setCompressionPolicy(SocketConnection.CompressionPolicy policy) {
        compressionPolicy = policy;
        JiveGlobals.setProperty("xmpp.multiplex.compression.policy", compressionPolicy.toString());
    }

    /**
     * Returns the number of milliseconds a connection has to be idle to be closed. Default is
     * 30 minutes. Sending stanzas to the client is not considered as activity. We are only
     * considering the connection active when the client sends some data or hearbeats
     * (i.e. whitespaces) to the server.
     *
     * @return the number of milliseconds a connection has to be idle to be closed.
     */
    public static long getIdleTimeout() {
        return idleTimeout;
    }

    /**
     * Sets the number of milliseconds a connection has to be idle to be closed. Default is
     * 30 minutes. Sending stanzas to the client is not considered as activity. We are only
     * considering the connection active when the client sends some data or hearbeats
     * (i.e. whitespaces) to the server.
     *
     * @param timeout the number of milliseconds a connection has to be idle to be closed.
     */
    public static void setIdleTimeout(long timeout) {
        idleTimeout = timeout;
        JiveGlobals.setProperty("xmpp.multiplex.idle", Long.toString(idleTimeout));
    }
}