/*
* 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 java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.internal.util.nio.GridNioException;
import org.apache.ignite.internal.util.nio.GridNioFilterAdapter;
import org.apache.ignite.internal.util.nio.GridNioFinishedFuture;
import org.apache.ignite.internal.util.nio.GridNioFuture;
import org.apache.ignite.internal.util.nio.GridNioFutureImpl;
import org.apache.ignite.internal.util.nio.GridNioSession;
import org.apache.ignite.internal.util.nio.GridNioSessionMetaKey;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteInClosure;
import static org.apache.ignite.internal.util.nio.GridNioSessionMetaKey.SSL_META;
/**
* 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;
/** Order. */
private ByteOrder order;
/** Allocate direct buffer or heap buffer. */
private boolean directBuf;
/** 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 directBuf Direct buffer flag.
* @param order Byte order.
* @param log Logger to use.
*/
public GridNioSslFilter(SSLContext sslCtx, boolean directBuf, ByteOrder order, IgniteLogger log) {
super("SSL filter");
this.log = log;
this.sslCtx = sslCtx;
this.directBuf = directBuf;
this.order = order;
}
/**
* @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;
boolean handshake;
GridSslMeta sslMeta = ses.meta(SSL_META.ordinal());
if (sslMeta == null) {
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);
sslMeta = new GridSslMeta();
ses.addMeta(SSL_META.ordinal(), sslMeta);
handshake = true;
}
else {
engine = sslMeta.sslEngine();
assert engine != null;
handshake = false;
}
try {
GridNioSslHandler hnd = new GridNioSslHandler(this,
ses,
engine,
directBuf,
order,
log,
handshake,
sslMeta.encodedBuffer());
sslMeta.handler(hnd);
hnd.handshake();
ByteBuffer alreadyDecoded = sslMeta.decodedBuffer();
if (alreadyDecoded != null)
proceedMessageReceived(ses, alreadyDecoded);
}
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,
boolean fut,
IgniteInClosure<IgniteException> ackC
) throws IgniteCheckedException {
if (directMode)
return proceedSessionWrite(ses, msg, fut, ackC);
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(ackC);
}
else {
if (log.isDebugEnabled())
log.debug("Write request received during handshake, scheduling deferred write: " + ses);
return hnd.deferredWrite(input, ackC);
}
}
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(null);
}
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) {
GridSslMeta sslMeta = ses.meta(SSL_META.ordinal());
assert sslMeta != null;
GridNioSslHandler hnd = sslMeta.handler();
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;
}
}