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

org.apache.mina.filter.ssl.SslFilter Maven / Gradle / Ivy

There is a newer version: 3.0.0-M2
Show newest version
/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.apache.mina.filter.ssl;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.filterchain.IoFilter;
import org.apache.mina.core.filterchain.IoFilterAdapter;
import org.apache.mina.core.filterchain.IoFilterChain;
import org.apache.mina.core.future.DefaultWriteFuture;
import org.apache.mina.core.future.IoFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.future.WriteFuture;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.session.AttributeKey;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.write.DefaultWriteRequest;
import org.apache.mina.core.write.WriteRequest;
import org.apache.mina.core.write.WriteToClosedSessionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An SSL filter that encrypts and decrypts the data exchanged in the session.
 * Adding this filter triggers SSL handshake procedure immediately by sending
 * a SSL 'hello' message, so you don't need to call
 * {@link #startSsl(IoSession)} manually unless you are implementing StartTLS
 * (see below).  If you don't want the handshake procedure to start
 * immediately, please specify {@code false} as {@code autoStart} parameter in
 * the constructor.
 * 

* This filter uses an {@link SSLEngine} which was introduced in Java 5, so * Java version 5 or above is mandatory to use this filter. And please note that * this filter only works for TCP/IP connections. * *

Implementing StartTLS

*

* You can use {@link #DISABLE_ENCRYPTION_ONCE} attribute to implement StartTLS: *

 * public void messageReceived(IoSession session, Object message) {
 *    if (message instanceof MyStartTLSRequest) {
 *        // Insert SSLFilter to get ready for handshaking
 *        session.getFilterChain().addFirst(sslFilter);
 *
 *        // Disable encryption temporarily.
 *        // This attribute will be removed by SSLFilter
 *        // inside the Session.write() call below.
 *        session.setAttribute(SSLFilter.DISABLE_ENCRYPTION_ONCE, Boolean.TRUE);
 *
 *        // Write StartTLSResponse which won't be encrypted.
 *        session.write(new MyStartTLSResponse(OK));
 *
 *        // Now DISABLE_ENCRYPTION_ONCE attribute is cleared.
 *        assert session.getAttribute(SSLFilter.DISABLE_ENCRYPTION_ONCE) == null;
 *    }
 * }
 * 
* * @author Apache MINA Project * @org.apache.xbean.XBean */ public class SslFilter extends IoFilterAdapter { /** The logger */ private static final Logger LOGGER = LoggerFactory.getLogger(SslFilter.class); /** * A session attribute key that stores underlying {@link SSLSession} * for each session. */ public static final AttributeKey SSL_SESSION = new AttributeKey(SslFilter.class, "session"); /** * A session attribute key that makes next one write request bypass * this filter (not encrypting the data). This is a marker attribute, * which means that you can put whatever as its value. ({@link Boolean#TRUE} * is preferred.) The attribute is automatically removed from the session * attribute map as soon as {@link IoSession#write(Object)} is invoked, * and therefore should be put again if you want to make more messages * bypass this filter. This is especially useful when you implement * StartTLS. */ public static final AttributeKey DISABLE_ENCRYPTION_ONCE = new AttributeKey(SslFilter.class, "disableOnce"); /** * A session attribute key that makes this filter to emit a * {@link IoHandler#messageReceived(IoSession, Object)} event with a * special message ({@link SslEvent#SECURED} or {@link SslEvent#UNSECURED}). * This is a marker attribute, which means that you can put whatever as its * value. ({@link Boolean#TRUE} is preferred.) By default, this filter * doesn't emit any events related with SSL session flow control. */ public static final AttributeKey USE_NOTIFICATION = new AttributeKey(SslFilter.class, "useNotification"); /** * A session attribute key that should be set to an {@link InetSocketAddress}. * Setting this attribute causes * {@link SSLContext#createSSLEngine(String, int)} to be called passing the * hostname and port of the {@link InetSocketAddress} to get an * {@link SSLEngine} instance. If not set {@link SSLContext#createSSLEngine()} * will be called. *
* Using this feature {@link SSLSession} objects may be cached and reused * when in client mode. * * @see SSLContext#createSSLEngine(String, int) */ public static final AttributeKey PEER_ADDRESS = new AttributeKey(SslFilter.class, "peerAddress"); /** An attribute containing the next filter */ private static final AttributeKey NEXT_FILTER = new AttributeKey(SslFilter.class, "nextFilter"); private static final AttributeKey SSL_HANDLER = new AttributeKey(SslFilter.class, "handler"); /** The SslContext used */ /* No qualifier */final SSLContext sslContext; /** A flag used to tell the filter to start the handshake immediately */ private final boolean autoStart; /** A flag used to determinate if the handshake should start immediately */ public static final boolean START_HANDSHAKE = true; /** A flag used to determinate if the handshake should wait for the client to initiate the handshake */ public static final boolean CLIENT_HANDSHAKE = false; private boolean client; private boolean needClientAuth; private boolean wantClientAuth; private String[] enabledCipherSuites; private String[] enabledProtocols; /** * Creates a new SSL filter using the specified {@link SSLContext}. * The handshake will start immediately after the filter has been added * to the chain. * * @param sslContext The SSLContext to use */ public SslFilter(SSLContext sslContext) { this(sslContext, START_HANDSHAKE); } /** * Creates a new SSL filter using the specified {@link SSLContext}. * If the autostart flag is set to true, the * handshake will start immediately after the filter has been added * to the chain. * * @param sslContext The SSLContext to use * @param autoStart The flag used to tell the filter to start the handshake immediately */ public SslFilter(SSLContext sslContext, boolean autoStart) { if (sslContext == null) { throw new IllegalArgumentException("sslContext"); } this.sslContext = sslContext; this.autoStart = autoStart; } /** * Returns the underlying {@link SSLSession} for the specified session. * * @param session The current session * @return null if no {@link SSLSession} is initialized yet. */ public SSLSession getSslSession(IoSession session) { return (SSLSession) session.getAttribute(SSL_SESSION); } /** * (Re)starts SSL session for the specified session if not started yet. * Please note that SSL session is automatically started by default, and therefore * you don't need to call this method unless you've used TLS closure. * * @param session The session that will be switched to SSL mode * @return true if the SSL session has been started, false if already started. * @throws SSLException if failed to start the SSL session */ public boolean startSsl(IoSession session) throws SSLException { SslHandler sslHandler = getSslSessionHandler(session); boolean started; try { synchronized (sslHandler) { if (sslHandler.isOutboundDone()) { NextFilter nextFilter = (NextFilter) session.getAttribute(NEXT_FILTER); sslHandler.destroy(); sslHandler.init(); sslHandler.handshake(nextFilter); started = true; } else { started = false; } } sslHandler.flushScheduledEvents(); } catch (SSLException se) { sslHandler.release(); throw se; } return started; } /** * An extended toString() method for sessions. If the SSL handshake * is not yet completed, we will print (ssl) in small caps. Once it's * completed, we will use SSL capitalized. */ /* no qualifier */String getSessionInfo(IoSession session) { StringBuilder sb = new StringBuilder(); if (session.getService() instanceof IoAcceptor) { sb.append("Session Server"); } else { sb.append("Session Client"); } sb.append('[').append(session.getId()).append(']'); SslHandler sslHandler = (SslHandler) session.getAttribute(SSL_HANDLER); if (sslHandler == null) { sb.append("(no sslEngine)"); } else if (isSslStarted(session)) { if (sslHandler.isHandshakeComplete()) { sb.append("(SSL)"); } else { sb.append("(ssl...)"); } } return sb.toString(); } /** * @return true if and only if the specified session is * encrypted/decrypted over SSL/TLS currently. This method will start * to return false after TLS close_notify message * is sent and any messages written after then is not going to get encrypted. * * @param session the session we want to check */ public boolean isSslStarted(IoSession session) { SslHandler sslHandler = (SslHandler) session.getAttribute(SSL_HANDLER); if (sslHandler == null) { return false; } synchronized (sslHandler) { return !sslHandler.isOutboundDone(); } } /** * @return true if and only if the conditions for * {@link #isSslStarted(IoSession)} are met, and the handhake has * completed. * * @param session the session we want to check */ public boolean isSecured(IoSession session) { SslHandler sslHandler = (SslHandler) session.getAttribute(SSL_HANDLER); if (sslHandler == null) { return false; } synchronized (sslHandler) { return !sslHandler.isOutboundDone() && sslHandler.isHandshakeComplete(); } } /** * Stops the SSL session by sending TLS close_notify message to * initiate TLS closure. * * @param session the {@link IoSession} to initiate TLS closure * @return The Future for the initiated closure * @throws SSLException if failed to initiate TLS closure */ public WriteFuture stopSsl(IoSession session) throws SSLException { SslHandler sslHandler = getSslSessionHandler(session); NextFilter nextFilter = (NextFilter) session.getAttribute(NEXT_FILTER); WriteFuture future; try { synchronized (sslHandler) { future = initiateClosure(nextFilter, session); } sslHandler.flushScheduledEvents(); } catch (SSLException se) { sslHandler.release(); throw se; } return future; } /** * @return true if the engine is set to use client mode * when handshaking. */ public boolean isUseClientMode() { return client; } /** * Configures the engine to use client (or server) mode when handshaking. * * @param clientMode true when we are in client mode, false when in server mode */ public void setUseClientMode(boolean clientMode) { this.client = clientMode; } /** * @return true if the engine will require client authentication. * This option is only useful to engines in the server mode. */ public boolean isNeedClientAuth() { return needClientAuth; } /** * Configures the engine to require client authentication. * This option is only useful for engines in the server mode. * * @param needClientAuth A flag set when we need to authenticate the client */ public void setNeedClientAuth(boolean needClientAuth) { this.needClientAuth = needClientAuth; } /** * @return true if the engine will request client authentication. * This option is only useful to engines in the server mode. */ public boolean isWantClientAuth() { return wantClientAuth; } /** * Configures the engine to request client authentication. * This option is only useful for engines in the server mode. * * @param wantClientAuth A flag set when we want to check the client authentication */ public void setWantClientAuth(boolean wantClientAuth) { this.wantClientAuth = wantClientAuth; } /** * @return the list of cipher suites to be enabled when {@link SSLEngine} * is initialized. null means 'use {@link SSLEngine}'s default.' */ public String[] getEnabledCipherSuites() { return enabledCipherSuites; } /** * Sets the list of cipher suites to be enabled when {@link SSLEngine} * is initialized. * * @param cipherSuites null means 'use {@link SSLEngine}'s default.' */ public void setEnabledCipherSuites(String[] cipherSuites) { this.enabledCipherSuites = cipherSuites; } /** * @return the list of protocols to be enabled when {@link SSLEngine} * is initialized. null means 'use {@link SSLEngine}'s default.' */ public String[] getEnabledProtocols() { return enabledProtocols; } /** * Sets the list of protocols to be enabled when {@link SSLEngine} * is initialized. * * @param protocols null means 'use {@link SSLEngine}'s default.' */ public void setEnabledProtocols(String[] protocols) { this.enabledProtocols = protocols; } /** * Executed just before the filter is added into the chain, we do : *
    *
  • check that we don't have a SSL filter already present *
  • we update the next filter *
  • we create the SSL handler helper class *
  • and we store it into the session's Attributes *
*/ @Override public void onPreAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws SSLException { // Check that we don't have a SSL filter already present in the chain if (parent.contains(SslFilter.class)) { String msg = "Only one SSL filter is permitted in a chain."; LOGGER.error(msg); throw new IllegalStateException(msg); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Adding the SSL Filter {} to the chain", name); } IoSession session = parent.getSession(); session.setAttribute(NEXT_FILTER, nextFilter); // Create a SSL handler and start handshake. SslHandler sslHandler = new SslHandler(this, session); // Adding the supported ciphers in the SSLHandler if ((enabledCipherSuites == null) || (enabledCipherSuites.length == 0)) { enabledCipherSuites = sslContext.getServerSocketFactory().getSupportedCipherSuites(); } sslHandler.init(); session.setAttribute(SSL_HANDLER, sslHandler); } @Override public void onPostAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws SSLException { if (autoStart == START_HANDSHAKE) { initiateHandshake(nextFilter, parent.getSession()); } } @Override public void onPreRemove(IoFilterChain parent, String name, NextFilter nextFilter) throws SSLException { IoSession session = parent.getSession(); stopSsl(session); session.removeAttribute(NEXT_FILTER); session.removeAttribute(SSL_HANDLER); } // IoFilter impl. @Override public void sessionClosed(NextFilter nextFilter, IoSession session) throws SSLException { SslHandler sslHandler = getSslSessionHandler(session); try { synchronized (sslHandler) { // release resources sslHandler.destroy(); } } finally { // notify closed session nextFilter.sessionClosed(session); } } @Override public void messageReceived(NextFilter nextFilter, IoSession session, Object message) throws SSLException { if (LOGGER.isDebugEnabled()) { LOGGER.debug("{}: Message received : {}", getSessionInfo(session), message); } SslHandler sslHandler = getSslSessionHandler(session); synchronized (sslHandler) { if (!isSslStarted(session) && sslHandler.isInboundDone()) { // The SSL session must be established first before we // can push data to the application. Store the incoming // data into a queue for a later processing sslHandler.scheduleMessageReceived(nextFilter, message); } else { IoBuffer buf = (IoBuffer) message; try { if (sslHandler.isOutboundDone()) { sslHandler.destroy(); throw new SSLException("Outbound done"); } // forward read encrypted data to SSL handler sslHandler.messageReceived(nextFilter, buf.buf()); // Handle data to be forwarded to application or written to net handleSslData(nextFilter, sslHandler); if (sslHandler.isInboundDone()) { if (sslHandler.isOutboundDone()) { sslHandler.destroy(); } else { initiateClosure(nextFilter, session); } if (buf.hasRemaining()) { // Forward the data received after closure. sslHandler.scheduleMessageReceived(nextFilter, buf); } } } catch (SSLException ssle) { if (!sslHandler.isHandshakeComplete()) { SSLException newSsle = new SSLHandshakeException("SSL handshake failed."); newSsle.initCause(ssle); ssle = newSsle; // Close the session immediately, the handshake has failed session.closeNow(); } else { // Free the SSL Handler buffers sslHandler.release(); } throw ssle; } } } sslHandler.flushScheduledEvents(); } @Override public void messageSent(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) { if (writeRequest instanceof EncryptedWriteRequest) { EncryptedWriteRequest wrappedRequest = (EncryptedWriteRequest) writeRequest; nextFilter.messageSent(session, wrappedRequest.getParentRequest()); } else { // ignore extra buffers used for handshaking } } @Override public void exceptionCaught(NextFilter nextFilter, IoSession session, Throwable cause) throws Exception { if (cause instanceof WriteToClosedSessionException) { // Filter out SSL close notify, which is likely to fail to flush // due to disconnection. WriteToClosedSessionException e = (WriteToClosedSessionException) cause; List failedRequests = e.getRequests(); boolean containsCloseNotify = false; for (WriteRequest r : failedRequests) { if (isCloseNotify(r.getMessage())) { containsCloseNotify = true; break; } } if (containsCloseNotify) { if (failedRequests.size() == 1) { // close notify is the only failed request; bail out. return; } List newFailedRequests = new ArrayList<>(failedRequests.size() - 1); for (WriteRequest r : failedRequests) { if (!isCloseNotify(r.getMessage())) { newFailedRequests.add(r); } } if (newFailedRequests.isEmpty()) { // the failedRequests were full with close notify; bail out. return; } cause = new WriteToClosedSessionException(newFailedRequests, cause.getMessage(), cause.getCause()); } } nextFilter.exceptionCaught(session, cause); } private boolean isCloseNotify(Object message) { if (!(message instanceof IoBuffer)) { return false; } IoBuffer buf = (IoBuffer) message; int offset = buf.position(); return (buf.get(offset + 0) == 0x15) /* Alert */ && (buf.get(offset + 1) == 0x03) /* TLS/SSL */ && ((buf.get(offset + 2) == 0x00) /* SSL 3.0 */ || (buf.get(offset + 2) == 0x01) /* TLS 1.0 */ || (buf.get(offset + 2) == 0x02) /* TLS 1.1 */ || (buf.get(offset + 2) == 0x03)) /* TLS 1.2 */ && (buf.get(offset + 3) == 0x00); /* close_notify */ } @Override public void filterWrite(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws SSLException { if (LOGGER.isDebugEnabled()) { LOGGER.debug("{}: Writing Message : {}", getSessionInfo(session), writeRequest); } boolean needsFlush = true; SslHandler sslHandler = getSslSessionHandler(session); try { synchronized (sslHandler) { if (!isSslStarted(session)) { sslHandler.scheduleFilterWrite(nextFilter, writeRequest); } // Don't encrypt the data if encryption is disabled. else if (session.containsAttribute(DISABLE_ENCRYPTION_ONCE)) { // Remove the marker attribute because it is temporary. session.removeAttribute(DISABLE_ENCRYPTION_ONCE); sslHandler.scheduleFilterWrite(nextFilter, writeRequest); } else { // Otherwise, encrypt the buffer. IoBuffer buf = (IoBuffer) writeRequest.getMessage(); if (sslHandler.isWritingEncryptedData()) { // data already encrypted; simply return buffer sslHandler.scheduleFilterWrite(nextFilter, writeRequest); } else if (sslHandler.isHandshakeComplete()) { // SSL encrypt sslHandler.encrypt(buf.buf()); IoBuffer encryptedBuffer = sslHandler.fetchOutNetBuffer(); writeRequest.setMessage( encryptedBuffer ); sslHandler.scheduleFilterWrite(nextFilter, new EncryptedWriteRequest(writeRequest, encryptedBuffer)); } else { if (session.isConnected()) { // Handshake not complete yet. sslHandler.schedulePreHandshakeWriteRequest(nextFilter, writeRequest); } needsFlush = false; } } } if (needsFlush) { sslHandler.flushScheduledEvents(); } } catch (SSLException se) { sslHandler.release(); throw se; } } @Override public void filterClose(final NextFilter nextFilter, final IoSession session) throws SSLException { SslHandler sslHandler = (SslHandler) session.getAttribute(SSL_HANDLER); if (sslHandler == null) { // The connection might already have closed, or // SSL might have not started yet. nextFilter.filterClose(session); return; } WriteFuture future = null; try { synchronized (sslHandler) { if (isSslStarted(session)) { future = initiateClosure(nextFilter, session); future.addListener(new IoFutureListener() { @Override public void operationComplete(IoFuture future) { nextFilter.filterClose(session); } }); } } sslHandler.flushScheduledEvents(); } catch (SSLException se) { sslHandler.release(); throw se; } finally { if (future == null) { nextFilter.filterClose(session); } } } /** * Initiate the SSL handshake. This can be invoked if you have set the 'autoStart' to * false when creating the SslFilter instance. * * @param session The session for which the SSL handshake should be done * @throws SSLException If the handshake failed */ public void initiateHandshake(IoSession session) throws SSLException { IoFilterChain filterChain = session.getFilterChain(); if (filterChain == null) { throw new SSLException("No filter chain"); } IoFilter.NextFilter nextFilter = filterChain.getNextFilter(SslFilter.class); if (nextFilter == null) { throw new SSLException("No SSL next filter in the chain"); } initiateHandshake(nextFilter, session); } private void initiateHandshake(NextFilter nextFilter, IoSession session) throws SSLException { if (LOGGER.isDebugEnabled()) { LOGGER.debug("{} : Starting the first handshake", getSessionInfo(session)); } SslHandler sslHandler = getSslSessionHandler(session); try { synchronized (sslHandler) { sslHandler.handshake(nextFilter); } sslHandler.flushScheduledEvents(); } catch (SSLException se) { sslHandler.release(); throw se; } } private WriteFuture initiateClosure(NextFilter nextFilter, IoSession session) throws SSLException { SslHandler sslHandler = getSslSessionHandler(session); WriteFuture future = null; // if already shut down try { synchronized(sslHandler) { if (!sslHandler.closeOutbound()) { return DefaultWriteFuture.newNotWrittenFuture(session, new IllegalStateException( "SSL session is shut down already.")); } // there might be data to write out here? future = sslHandler.writeNetBuffer(nextFilter); if (future == null) { future = DefaultWriteFuture.newWrittenFuture(session); } if (sslHandler.isInboundDone()) { sslHandler.destroy(); } } // Inform that the session is not any more secured session.getFilterChain().fireEvent(SslEvent.UNSECURED); } catch (SSLException se) { sslHandler.release(); throw se; } return future; } // Utilities private void handleSslData(NextFilter nextFilter, SslHandler sslHandler) throws SSLException { if (LOGGER.isDebugEnabled()) { LOGGER.debug("{}: Processing the SSL Data ", getSessionInfo(sslHandler.getSession())); } // Flush any buffered write requests occurred before handshaking. if (sslHandler.isHandshakeComplete()) { sslHandler.flushPreHandshakeEvents(); } // Write encrypted data to be written (if any) sslHandler.writeNetBuffer(nextFilter); // handle app. data read (if any) handleAppDataRead(nextFilter, sslHandler); } private void handleAppDataRead(NextFilter nextFilter, SslHandler sslHandler) { // forward read app data IoBuffer readBuffer = sslHandler.fetchAppBuffer(); if (readBuffer.hasRemaining()) { sslHandler.scheduleMessageReceived(nextFilter, readBuffer); } } private SslHandler getSslSessionHandler(IoSession session) { SslHandler sslHandler = (SslHandler) session.getAttribute(SSL_HANDLER); if (sslHandler == null) { throw new IllegalStateException(); } synchronized(sslHandler) { if (sslHandler.getSslFilter() != this) { throw new IllegalArgumentException("Not managed by this filter."); } } return sslHandler; } /** * A message that is sent from {@link SslFilter} when the connection became * secure or is not secure anymore. * * @author Apache MINA Project */ public static class SslFilterMessage { private final String name; private SslFilterMessage(String name) { this.name = name; } @Override public String toString() { return name; } } /** * A private class used to store encrypted messages. This is necessary * to be able to emit the messageSent event with the proper original * message, but not for handshake messages, which will be swallowed. * */ /* package protected */ static class EncryptedWriteRequest extends DefaultWriteRequest { // Thee encrypted messagee private final IoBuffer encryptedMessage; // The original message private WriteRequest parentRequest; /** * Create a new instance of an EncryptedWriteRequest * @param writeRequest The parent request * @param encryptedMessage The encrypted message */ private EncryptedWriteRequest(WriteRequest writeRequest, IoBuffer encryptedMessage) { super(encryptedMessage); parentRequest = writeRequest; this.encryptedMessage = encryptedMessage; } /** * @return teh encrypted message */ @Override public Object getMessage() { return encryptedMessage; } /** * @return The parent WriteRequest */ public WriteRequest getParentRequest() { return parentRequest; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy