/** * $Revision: $ * $Date: $ * * 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.nio; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; import java.security.KeyStore; import java.security.cert.Certificate; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; 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; 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.ClientTrustManager; import org.jivesoftware.openfire.net.SSLConfig; import org.jivesoftware.openfire.net.SSLJiveKeyManagerFactory; import org.jivesoftware.openfire.net.SSLJiveTrustManagerFactory; import org.jivesoftware.openfire.net.ServerTrustManager; import org.jivesoftware.openfire.session.LocalSession; import org.jivesoftware.openfire.session.Session; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.XMLWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.packet.Packet; /** * 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 { private static final Logger Log = LoggerFactory.getLogger(NIOConnection.class); /** * The utf-8 charset for decoding and encoding XMPP packet streams. */ public static final String CHARSET = "UTF-8"; private LocalSession session; private IoSession ioSession; private ConnectionCloseListener closeListener; /** * 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; private boolean usingSelfSignedCertificate; /** * Compression policy currently in use for this connection. */ private CompressionPolicy compressionPolicy = CompressionPolicy.disabled; private static ThreadLocal<CharsetEncoder> encoder = new ThreadLocalEncoder(); /** * 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; public NIOConnection(IoSession session, PacketDeliverer packetDeliverer) { this.ioSession = session; this.backupDeliverer = packetDeliverer; closed = false; } public boolean validate() { if (isClosed()) { return false; } deliverRawText(" "); return !isClosed(); } public void registerCloseListener(ConnectionCloseListener listener, Object ignore) { if (closeListener != null) { throw new IllegalStateException("Close listener already configured"); } if (isClosed()) { listener.onConnectionClose(session); } else { closeListener = listener; } } public void removeCloseListener(ConnectionCloseListener listener) { if (closeListener == listener) { closeListener = null; } } 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(); } public Certificate[] getLocalCertificates() { SSLSession sslSession = (SSLSession) ioSession.getAttribute(SSLFilter.SSL_SESSION); if (sslSession != null) { return sslSession.getLocalCertificates(); } return new Certificate[0]; } public Certificate[] getPeerCertificates() { try { SSLSession sslSession = (SSLSession) ioSession.getAttribute(SSLFilter.SSL_SESSION); if (sslSession != null) { return sslSession.getPeerCertificates(); } } catch (SSLPeerUnverifiedException e) { Log.warn("Error retrieving client certificates of: " + session, e); } return new Certificate[0]; } public void setUsingSelfSignedCertificate(boolean isSelfSigned) { this.usingSelfSignedCertificate = isSelfSigned; } public boolean isUsingSelfSignedCertificate() { return usingSelfSignedCertificate; } public PacketDeliverer getPacketDeliverer() { return backupDeliverer; } public void close() { boolean closedSuccessfully = false; synchronized (this) { if (!isClosed()) { try { deliverRawText(flashClient ? "</flash:stream>" : "</stream:stream>", false); } catch (Exception e) { // Ignore } if (session != null) { session.setStatus(Session.STATUS_CLOSED); } ioSession.close(); closed = true; closedSuccessfully = true; } } if (closedSuccessfully) { 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() { if (closeListener != null) { try { closeListener.onConnectionClose(session); } catch (Exception e) { Log.error("Error notifying listener: " + closeListener, e); } } } public void init(LocalSession owner) { session = owner; } public boolean isClosed() { if (session == null) { return closed; } 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 { XMLWriter xmlSerializer = new XMLWriter(new ByteBufferWriter(buffer, encoder.get()), new OutputFormat()); xmlSerializer.write(packet.getElement()); xmlSerializer.flush(); if (flashClient) { buffer.put((byte) '\0'); } buffer.flip(); ioSession.write(buffer); } catch (Exception e) { Log.debug("NIOConnection: Error delivering packet" + "\n" + this.toString(), e); 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); } else { session.incrementServerPacketCount(); } } } public void deliverRawText(String text) { // Deliver the packet in asynchronous mode deliverRawText(text, true); } private void deliverRawText(String text, boolean asynchronous) { 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(); if (asynchronous) { ioSession.write(buffer); } else { // 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()); } } } catch (Exception e) { Log.debug("NIOConnection: Error delivering raw text" + "\n" + this.toString(), e); errorDelivering = true; } // Close the connection if delivering text fails and we are already not closing the connection if (errorDelivering && asynchronous) { close(); } } } public void startTLS(boolean clientMode, String remoteServer, ClientAuth authentication) throws Exception { boolean c2s = (remoteServer == null); KeyStore ksKeys = SSLConfig.getKeyStore(); String keypass = SSLConfig.getKeyPassword(); 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"); // 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); if (clientMode || authentication == ClientAuth.needed || authentication == ClientAuth.wanted) { // 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, this)}; } } String algorithm = JiveGlobals.getProperty("xmpp.socket.ssl.algorithm", "TLS"); SSLContext tlsContext = SSLContext.getInstance(algorithm); tlsContext.init(km, tm, null); SSLFilter filter = new SSLFilter(tlsContext); filter.setUseClientMode(clientMode); 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); } // 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); 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\"/>"); } } public void addCompression() { IoFilterChain chain = ioSession.getFilterChain(); String baseFilter = "org.apache.mina.common.ExecutorThreadModel"; if (chain.contains("tls")) { baseFilter = "tls"; } chain.addAfter(baseFilter, "compression", new CompressionFilter(true, false, CompressionFilter.COMPRESSION_MAX)); } public void startCompression() { CompressionFilter ioFilter = (CompressionFilter) ioSession.getFilterChain().get("compression"); ioFilter.setCompressOutbound(true); } 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; } @Override public String toString() { return super.toString() + " MINA Session: " + ioSession; } private static class ThreadLocalEncoder extends ThreadLocal<CharsetEncoder> { @Override protected CharsetEncoder initialValue() { return Charset.forName(CHARSET).newEncoder() .onMalformedInput(CodingErrorAction.REPORT) .onUnmappableCharacter(CodingErrorAction.REPORT); } } }