blob: 596344b2b714e870b54add9bbf7632efde1781be [file] [log] [blame]
/*
* 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.ignite.internal.util.nio.ssl;
import org.apache.ignite.*;
import org.apache.ignite.internal.util.nio.*;
import org.apache.ignite.internal.util.typedef.internal.*;
import javax.net.ssl.*;
import java.io.*;
import java.nio.*;
import static org.apache.ignite.internal.util.nio.GridNioSessionMetaKey.*;
/**
* Implementation of SSL filter using {@link SSLEngine}
*/
public class GridNioSslFilter extends GridNioFilterAdapter {
/** SSL handshake future metadata key. */
public static final int HANDSHAKE_FUT_META_KEY = GridNioSessionMetaKey.nextUniqueKey();
/** Logger to use. */
private IgniteLogger log;
/** Set to true if engine should request client authentication. */
private boolean wantClientAuth;
/** Set to true if engine should require client authentication. */
private boolean needClientAuth;
/** Array of enabled cipher suites, optional. */
private String[] enabledCipherSuites;
/** Array of enabled protocols. */
private String[] enabledProtos;
/** SSL context to use. */
private SSLContext sslCtx;
/** Whether SSLEngine should use client mode. */
private boolean clientMode;
/** Whether direct mode is used. */
private boolean directMode;
/**
* Creates SSL filter.
*
* @param sslCtx SSL context.
* @param log Logger to use.
*/
public GridNioSslFilter(SSLContext sslCtx, IgniteLogger log) {
super("SSL filter");
this.log = log;
this.sslCtx = sslCtx;
}
/**
* @param clientMode Flag indicating whether SSLEngine should use client mode..
*/
public void clientMode(boolean clientMode) {
this.clientMode = clientMode;
}
/**
*
* @param directMode Flag indicating whether direct mode is used.
*/
public void directMode(boolean directMode) {
this.directMode = directMode;
}
/**
* @return Flag indicating whether direct mode is used.
*/
public boolean directMode() {
return directMode;
}
/**
* Sets flag indicating whether client authentication will be requested during handshake.
*
* @param wantClientAuth {@code True} if client authentication should be requested.
*/
public void wantClientAuth(boolean wantClientAuth) {
this.wantClientAuth = wantClientAuth;
}
/**
* Sets flag indicating whether client authentication will be required.
*
* @param needClientAuth {@code True} if client authentication is required.
*/
public void needClientAuth(boolean needClientAuth) {
this.needClientAuth = needClientAuth;
}
/**
* Sets a set of cipher suites that will be enabled for this filter.
*
* @param enabledCipherSuites Enabled cipher suites.
*/
public void enabledCipherSuites(String... enabledCipherSuites) {
this.enabledCipherSuites = enabledCipherSuites;
}
/**
* Sets enabled secure protocols for this filter.
*
* @param enabledProtos Enabled protocols.
*/
public void enabledProtocols(String... enabledProtos) {
this.enabledProtos = enabledProtos;
}
/** {@inheritDoc} */
@Override public void onSessionOpened(GridNioSession ses) throws IgniteCheckedException {
if (log.isDebugEnabled())
log.debug("Remote client connected, creating SSL handler and performing initial handshake: " + ses);
SSLEngine engine = sslCtx.createSSLEngine();
engine.setUseClientMode(clientMode);
if (!clientMode) {
engine.setWantClientAuth(wantClientAuth);
engine.setNeedClientAuth(needClientAuth);
}
if (enabledCipherSuites != null)
engine.setEnabledCipherSuites(enabledCipherSuites);
if (enabledProtos != null)
engine.setEnabledProtocols(enabledProtos);
try {
GridNioSslHandler hnd = new GridNioSslHandler(this, ses, engine, log);
ses.addMeta(SSL_HANDLER.ordinal(), hnd);
hnd.handshake();
}
catch (SSLException e) {
U.error(log, "Failed to start SSL handshake (will close inbound connection): " + ses, e);
ses.close();
}
}
/** {@inheritDoc} */
@Override public void onSessionClosed(GridNioSession ses) throws IgniteCheckedException {
GridNioSslHandler hnd = sslHandler(ses);
try {
GridNioFutureImpl<?> fut = ses.removeMeta(HANDSHAKE_FUT_META_KEY);
if (fut != null)
fut.onDone(new IgniteCheckedException("SSL handshake failed (connection closed)."));
hnd.shutdown();
}
finally {
proceedSessionClosed(ses);
}
}
/** {@inheritDoc} */
@Override public void onExceptionCaught(GridNioSession ses, IgniteCheckedException ex)
throws IgniteCheckedException {
proceedExceptionCaught(ses, ex);
}
/**
* @param ses Session.
* @return SSL handshake flag.
*/
@SuppressWarnings("LockAcquiredButNotSafelyReleased")
public boolean lock(GridNioSession ses) {
GridNioSslHandler hnd = sslHandler(ses);
hnd.lock();
return hnd.isHandshakeFinished();
}
/**
* @param ses NIO session.
*/
public void unlock(GridNioSession ses) {
sslHandler(ses).unlock();
}
/**
* @param ses Session.
* @param input Data to encrypt.
* @return Output buffer with encrypted data.
* @throws SSLException If failed to encrypt.
*/
public ByteBuffer encrypt(GridNioSession ses, ByteBuffer input) throws SSLException {
GridNioSslHandler hnd = sslHandler(ses);
hnd.lock();
try {
assert hnd.isHandshakeFinished();
return hnd.encrypt(input);
}
finally {
hnd.unlock();
}
}
/** {@inheritDoc} */
@Override public GridNioFuture<?> onSessionWrite(GridNioSession ses, Object msg) throws IgniteCheckedException {
if (directMode)
return proceedSessionWrite(ses, msg);
ByteBuffer input = checkMessage(ses, msg);
if (!input.hasRemaining())
return new GridNioFinishedFuture<Object>(null);
GridNioSslHandler hnd = sslHandler(ses);
hnd.lock();
try {
if (hnd.isOutboundDone())
return new GridNioFinishedFuture<Object>(new IOException("Failed to send data (secure session was " +
"already closed): " + ses));
if (hnd.isHandshakeFinished()) {
hnd.encrypt(input);
return hnd.writeNetBuffer();
}
else {
if (log.isDebugEnabled())
log.debug("Write request received during handshake, scheduling deferred write: " + ses);
return hnd.deferredWrite(input);
}
}
catch (SSLException e) {
throw new GridNioException("Failed to encode SSL data: " + ses, e);
}
finally {
hnd.unlock();
}
}
/** {@inheritDoc} */
@Override public void onMessageReceived(GridNioSession ses, Object msg) throws IgniteCheckedException {
ByteBuffer input = checkMessage(ses, msg);
GridNioSslHandler hnd = sslHandler(ses);
hnd.lock();
try {
hnd.messageReceived(input);
// Handshake may become finished on incoming message, flush writes, if any.
if (hnd.isHandshakeFinished())
hnd.flushDeferredWrites();
ByteBuffer appBuf = hnd.getApplicationBuffer();
appBuf.flip();
if (appBuf.hasRemaining())
proceedMessageReceived(ses, appBuf);
appBuf.compact();
if (hnd.isInboundDone() && !hnd.isOutboundDone()) {
if (log.isDebugEnabled())
log.debug("Remote peer closed secure session (will close connection): " + ses);
shutdownSession(ses, hnd);
}
}
catch (SSLException e) {
throw new GridNioException("Failed to decode SSL data: " + ses, e);
}
finally {
hnd.unlock();
}
}
/** {@inheritDoc} */
@Override public GridNioFuture<Boolean> onSessionClose(GridNioSession ses) throws IgniteCheckedException {
GridNioSslHandler hnd = sslHandler(ses);
hnd.lock();
try {
return shutdownSession(ses, hnd);
}
finally {
hnd.unlock();
}
}
/**
* Sends SSL <tt>close_notify</tt> message and closes underlying TCP connection.
*
* @param ses Session to shutdown.
* @param hnd SSL handler.
* @throws GridNioException If failed to forward requests to filter chain.
* @return Close future.
*/
private GridNioFuture<Boolean> shutdownSession(GridNioSession ses, GridNioSslHandler hnd)
throws IgniteCheckedException {
try {
hnd.closeOutbound();
hnd.writeNetBuffer();
}
catch (SSLException e) {
U.warn(log, "Failed to shutdown SSL session gracefully (will force close) [ex=" + e + ", ses=" + ses + ']');
}
return proceedSessionClose(ses);
}
/** {@inheritDoc} */
@Override public void onSessionIdleTimeout(GridNioSession ses) throws IgniteCheckedException {
proceedSessionIdleTimeout(ses);
}
/** {@inheritDoc} */
@Override public void onSessionWriteTimeout(GridNioSession ses) throws IgniteCheckedException {
proceedSessionWriteTimeout(ses);
}
/**
* Gets ssl handler from the session.
*
* @param ses Session instance.
* @return SSL handler.
*/
private GridNioSslHandler sslHandler(GridNioSession ses) {
GridNioSslHandler hnd = ses.meta(SSL_HANDLER.ordinal());
if (hnd == null)
throw new IgniteException("Failed to process incoming message (received message before SSL handler " +
"was created): " + ses);
return hnd;
}
/**
* Checks type of the message passed to the filter and converts it to a byte buffer (since SSL filter
* operates only on binary data).
*
* @param ses Session instance.
* @param msg Message passed in.
* @return Message that was cast to a byte buffer.
* @throws GridNioException If msg is not a byte buffer.
*/
private ByteBuffer checkMessage(GridNioSession ses, Object msg) throws GridNioException {
if (!(msg instanceof ByteBuffer))
throw new GridNioException("Invalid object type received (is SSL filter correctly placed in filter " +
"chain?) [ses=" + ses + ", msgClass=" + msg.getClass().getName() + ']');
return (ByteBuffer)msg;
}
/**
* Expands the given byte buffer to the requested capacity.
*
* @param original Original byte buffer.
* @param cap Requested capacity.
* @return Expanded byte buffer.
*/
public static ByteBuffer expandBuffer(ByteBuffer original, int cap) {
ByteBuffer res = ByteBuffer.allocate(cap);
res.order(ByteOrder.nativeOrder());
original.flip();
res.put(original);
return res;
}
/**
* Copies the given byte buffer.
*
* @param original Byte buffer to copy.
* @return Copy of the original byte buffer.
*/
public static ByteBuffer copy(ByteBuffer original) {
ByteBuffer cp = ByteBuffer.allocate(original.remaining());
cp.put(original);
cp.flip();
return cp;
}
}