NIOConnection.java 13.9 KB
Newer Older
1 2 3 4
/**
 * $Revision: $
 * $Date: $
 *
5
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
6 7
 *
 * This software is published under the terms of the GNU Public License (GPL),
8 9
 * a copy of which is included in this distribution, or a commercial license
 * agreement with Jive.
10 11
 */

12
package org.jivesoftware.openfire.nio;
13 14 15 16 17 18 19

import org.apache.mina.common.ByteBuffer;
import org.apache.mina.common.IoFilterChain;
import org.apache.mina.common.IoSession;
import org.apache.mina.filter.CompressionFilter;
import org.apache.mina.filter.SSLFilter;
import org.dom4j.io.OutputFormat;
20 21 22 23 24 25 26 27
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.ConnectionCloseListener;
import org.jivesoftware.openfire.PacketDeliverer;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.openfire.net.SSLJiveKeyManagerFactory;
import org.jivesoftware.openfire.net.SSLJiveTrustManagerFactory;
import org.jivesoftware.openfire.net.ServerTrustManager;
28
import org.jivesoftware.openfire.net.ClientTrustManager;
29
import org.jivesoftware.openfire.session.LocalSession;
30
import org.jivesoftware.openfire.session.Session;
31 32 33
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.XMLWriter;
34 35 36 37
import org.xmpp.packet.Packet;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
38
import javax.net.ssl.SSLSession;
39
import javax.net.ssl.TrustManager;
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.security.KeyStore;

/**
 * Implementation of {@link Connection} inteface specific for NIO connections when using
 * the MINA framework.<p>
 *
 * MINA project can be found at <a href="http://mina.apache.org">here</a>.
 *
 * @author Gaston Dombiak
 */
public class NIOConnection implements Connection {

    /**
     * The utf-8 charset for decoding and encoding XMPP packet streams.
     */
    public static final String CHARSET = "UTF-8";

61
    private LocalSession session;
62 63
    private IoSession ioSession;

64
    private ConnectionCloseListener closeListener;
Gaston Dombiak's avatar
Gaston Dombiak committed
65

66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
    /**
     * Deliverer to use when the connection is closed or was closed when delivering
     * a packet.
     */
    private PacketDeliverer backupDeliverer;
    private boolean flashClient = false;
    private int majorVersion = 1;
    private int minorVersion = 0;
    private String language = null;

    // TODO Uso el #checkHealth????
    /**
     * TLS policy currently in use for this connection.
     */
    private TLSPolicy tlsPolicy = TLSPolicy.optional;

    /**
     * Compression policy currently in use for this connection.
     */
    private CompressionPolicy compressionPolicy = CompressionPolicy.disabled;
86
    private static ThreadLocal encoder = new ThreadLocalEncoder();
87 88 89 90 91 92 93
    /**
     * Flag that specifies if the connection should be considered closed. Closing a NIO connection
     * is an asynch operation so instead of waiting for the connection to be actually closed just
     * keep this flag to avoid using the connection between #close was used and the socket is actually
     * closed.
     */
    private boolean closed;
94 95 96 97 98


    public NIOConnection(IoSession session, PacketDeliverer packetDeliverer) {
        this.ioSession = session;
        this.backupDeliverer = packetDeliverer;
99
        closed = false;
100 101 102 103 104 105 106 107 108 109
    }

    public boolean validate() {
        if (isClosed()) {
            return false;
        }
        deliverRawText(" ");
        return !isClosed();
    }

110 111 112 113
    public void registerCloseListener(ConnectionCloseListener listener, Object ignore) {
        if (closeListener != null) {
            throw new IllegalStateException("Close listener already configured");
        }
114
        if (isClosed()) {
115
            listener.onConnectionClose(session);
116 117
        }
        else {
118
            closeListener = listener;
119 120 121 122
        }
    }

    public void removeCloseListener(ConnectionCloseListener listener) {
123 124 125
        if (closeListener == listener) {
            closeListener = null;
        }
126 127
    }

128 129 130 131 132 133 134 135 136 137
    public byte[] getAddress() throws UnknownHostException {
        return ((InetSocketAddress) ioSession.getRemoteAddress()).getAddress().getAddress();
    }

    public String getHostAddress() throws UnknownHostException {
        return ((InetSocketAddress) ioSession.getRemoteAddress()).getAddress().getHostAddress();
    }

    public String getHostName() throws UnknownHostException {
        return ((InetSocketAddress) ioSession.getRemoteAddress()).getAddress().getHostName();
138 139
    }

140 141 142 143
    public SSLSession getSSLSession() {
        return (SSLSession) ioSession.getAttribute(SSLFilter.SSL_SESSION);
    }

144 145 146 147 148
    public PacketDeliverer getPacketDeliverer() {
        return backupDeliverer;
    }

    public void close() {
149
        boolean closedSuccessfully = false;
150 151 152
        synchronized (this) {
            if (!isClosed()) {
                try {
153
                    deliverRawText(flashClient ? "</flash:stream>" : "</stream:stream>", false);
154 155 156
                } catch (Exception e) {
                    // Ignore
                }
157 158 159
                if (session != null) {
                    session.setStatus(Session.STATUS_CLOSED);
                }
160 161 162
                ioSession.close();
                closed = true;
                closedSuccessfully = true;
163 164
            }
        }
165
        if (closedSuccessfully) {
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
            notifyCloseListeners();
        }
    }

    public void systemShutdown() {
        deliverRawText("<stream:error><system-shutdown " +
                "xmlns='urn:ietf:params:xml:ns:xmpp-streams'/></stream:error>");
        close();
    }

    /**
     * Notifies all close listeners that the connection has been closed.
     * Used by subclasses to properly finish closing the connection.
     */
    private void notifyCloseListeners() {
181
        if (closeListener != null) {
Gaston Dombiak's avatar
Gaston Dombiak committed
182
            try {
183 184 185
                closeListener.onConnectionClose(session);
            } catch (Exception e) {
                Log.error("Error notifying listener: " + closeListener, e);
186 187 188 189
            }
        }
    }

190
    public void init(LocalSession owner) {
191 192 193 194 195
        session = owner;
    }

    public boolean isClosed() {
        if (session == null) {
196
            return closed;
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
        }
        return session.getStatus() == Session.STATUS_CLOSED;
    }

    public boolean isSecure() {
        return ioSession.getFilterChain().contains("tls");
    }

    public void deliver(Packet packet) throws UnauthorizedException {
        if (isClosed()) {
            backupDeliverer.deliver(packet);
        }
        else {
            ByteBuffer buffer = ByteBuffer.allocate(4096);
            buffer.setAutoExpand(true);

            boolean errorDelivering = false;
            try {
215 216
                XMLWriter xmlSerializer =
                        new XMLWriter(new ByteBufferWriter(buffer, (CharsetEncoder) encoder.get()), new OutputFormat());
217 218 219 220 221 222 223 224 225
                xmlSerializer.write(packet.getElement());
                xmlSerializer.flush();
                if (flashClient) {
                    buffer.put((byte) '\0');
                }
                buffer.flip();
                ioSession.write(buffer);
            }
            catch (Exception e) {
226
                Log.debug("NIOConnection: Error delivering packet" + "\n" + this.toString(), e);
227 228 229 230 231 232 233 234
                errorDelivering = true;
            }
            if (errorDelivering) {
                close();
                // Retry sending the packet again. Most probably if the packet is a
                // Message it will be stored offline
                backupDeliverer.deliver(packet);
            }
235 236 237
            else {
                session.incrementServerPacketCount();
            }
238 239 240 241
        }
    }

    public void deliverRawText(String text) {
242 243 244 245 246
        // Deliver the packet in asynchronous mode
        deliverRawText(text, true);
    }

    private void deliverRawText(String text, boolean asynchronous) {
247 248 249 250 251 252 253 254 255 256 257 258 259
        if (!isClosed()) {
            ByteBuffer buffer = ByteBuffer.allocate(text.length());
            buffer.setAutoExpand(true);

            boolean errorDelivering = false;
            try {
                //Charset charset = Charset.forName(CHARSET);
                //buffer.putString(text, charset.newEncoder());
                buffer.put(text.getBytes(CHARSET));
                if (flashClient) {
                    buffer.put((byte) '\0');
                }
                buffer.flip();
260 261 262 263
                if (asynchronous) {
                    ioSession.write(buffer);
                }
                else {
264 265 266 267 268 269
                    // Send stanza and wait for ACK (using a 2 seconds default timeout)
                    boolean ok =
                            ioSession.write(buffer).join(JiveGlobals.getIntProperty("connection.ack.timeout", 2000));
                    if (!ok) {
                        Log.warn("No ACK was received when sending stanza to: " + this.toString());
                    }
270
                }
271 272
            }
            catch (Exception e) {
273
                Log.debug("NIOConnection: Error delivering raw text" + "\n" + this.toString(), e);
274 275
                errorDelivering = true;
            }
276 277
            // Close the connection if delivering text fails and we are already not closing the connection
            if (errorDelivering && asynchronous) {
278 279 280 281 282
                close();
            }
        }
    }

283
    public void startTLS(boolean clientMode, String remoteServer, ClientAuth authentication) throws Exception {
284
        boolean c2s = (remoteServer == null);
285 286 287
        KeyStore ksKeys = SSLConfig.getKeyStore();
        String keypass = SSLConfig.getKeyPassword();

288 289 290 291
        KeyStore ksTrust = (c2s ? SSLConfig.getc2sTrustStore() : SSLConfig.gets2sTrustStore() );
        String trustpass = (c2s ? SSLConfig.getc2sTrustPassword() : SSLConfig.gets2sTrustPassword() );
        if (c2s)  Log.debug("NIOConnection: startTLS: using c2s");
        else Log.debug("NIOConnection: startTLS: using s2s");
292 293 294 295 296
        // KeyManager's decide which key material to use.
        KeyManager[] km = SSLJiveKeyManagerFactory.getKeyManagers(ksKeys, keypass);

        // TrustManager's decide whether to allow connections.
        TrustManager[] tm = SSLJiveTrustManagerFactory.getTrustManagers(ksTrust, trustpass);
297

298
        if (clientMode || authentication == ClientAuth.needed || authentication == ClientAuth.wanted) {
299 300 301 302 303 304 305 306
            // We might need to verify a certificate from our peer, so get different TrustManager[]'s
            if(c2s) {
                // Check if we can trust certificates presented by the client
                tm = new TrustManager[]{new ClientTrustManager(ksTrust)};
            } else {
                // Check if we can trust certificates presented by the server
                tm = new TrustManager[]{new ServerTrustManager(remoteServer, ksTrust)};
            }
307 308 309 310 311 312 313 314
        }

        SSLContext tlsContext = SSLContext.getInstance("TLS");

        tlsContext.init(km, tm, null);

        SSLFilter filter = new SSLFilter(tlsContext);
        filter.setUseClientMode(clientMode);
315 316 317 318 319 320 321 322
        if (authentication == ClientAuth.needed) {
            filter.setNeedClientAuth(true);
        }
        else if (authentication == ClientAuth.wanted) {
            // Just indicate that we would like to authenticate the client but if client
            // certificates are self-signed or have no certificate chain then we are still
            // good
            filter.setWantClientAuth(true);
323
        }
324 325 326 327
        // TODO Temporary workaround (placing SSLFilter before ExecutorFilter) to avoid deadlock. Waiting for
        // MINA devs feedback
        ioSession.getFilterChain().addBefore("org.apache.mina.common.ExecutorThreadModel", "tls", filter);
        //ioSession.getFilterChain().addAfter("org.apache.mina.common.ExecutorThreadModel", "tls", filter);
328 329 330 331 332 333 334
        ioSession.setAttribute(SSLFilter.DISABLE_ENCRYPTION_ONCE, Boolean.TRUE);
        if (!clientMode) {
            // Indicate the client that the server is ready to negotiate TLS
            deliverRawText("<proceed xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
        }
    }

335
    public void addCompression() {
336 337 338 339 340
        IoFilterChain chain = ioSession.getFilterChain();
        String baseFilter = "org.apache.mina.common.ExecutorThreadModel";
        if (chain.contains("tls")) {
            baseFilter = "tls";
        }
341 342 343 344 345 346
        chain.addAfter(baseFilter, "compression", new CompressionFilter(true, false, CompressionFilter.COMPRESSION_MAX));
    }

    public void startCompression() {
        CompressionFilter ioFilter = (CompressionFilter) ioSession.getFilterChain().get("compression");
        ioFilter.setCompressOutbound(true);
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
    }

    public boolean isFlashClient() {
        return flashClient;
    }

    public void setFlashClient(boolean flashClient) {
        this.flashClient = flashClient;
    }

    public int getMajorXMPPVersion() {
        return majorVersion;
    }

    public int getMinorXMPPVersion() {
        return minorVersion;
    }

    public void setXMPPVersion(int majorVersion, int minorVersion) {
        this.majorVersion = majorVersion;
        this.minorVersion = minorVersion;
    }

    public String getLanguage() {
        return language;
    }

    public void setLanaguage(String language) {
        this.language = language;
    }

    public boolean isCompressed() {
        return ioSession.getFilterChain().contains("compression");
    }

    public CompressionPolicy getCompressionPolicy() {
        return compressionPolicy;
    }

    public void setCompressionPolicy(CompressionPolicy compressionPolicy) {
        this.compressionPolicy = compressionPolicy;
    }

    public TLSPolicy getTlsPolicy() {
        return tlsPolicy;
    }

    public void setTlsPolicy(TLSPolicy tlsPolicy) {
        this.tlsPolicy = tlsPolicy;
    }

    public String toString() {
        return super.toString() + " MINA Session: " + ioSession;
    }
401 402 403 404 405 406 407

    private static class ThreadLocalEncoder extends ThreadLocal {

        protected Object initialValue() {
            return Charset.forName(CHARSET).newEncoder();
        }
    }
408
}