/* * 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.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.locks.ReentrantLock; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteException; import org.apache.ignite.IgniteLogger; import org.apache.ignite.internal.util.nio.GridNioEmbeddedFuture; import org.apache.ignite.internal.util.nio.GridNioException; 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.typedef.internal.U; import org.apache.ignite.lang.IgniteInClosure; import static javax.net.ssl.SSLEngineResult.HandshakeStatus; import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED; import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_TASK; import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_UNWRAP; import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; import static javax.net.ssl.SSLEngineResult.Status; import static javax.net.ssl.SSLEngineResult.Status.BUFFER_UNDERFLOW; import static javax.net.ssl.SSLEngineResult.Status.CLOSED; import static org.apache.ignite.internal.util.nio.ssl.GridNioSslFilter.HANDSHAKE_FUT_META_KEY; /** * Class that encapsulate the per-session SSL state, encoding and decoding logic. */ class GridNioSslHandler extends ReentrantLock { /** */ private static final long serialVersionUID = 0L; /** Grid logger. */ private IgniteLogger log; /** SSL engine. */ private SSLEngine sslEngine; /** Order. */ private ByteOrder order; /** Allocate direct buffer or heap buffer. */ private boolean directBuf; /** Session of this handler. */ private GridNioSession ses; /** Handshake completion flag. */ private boolean handshakeFinished; /** Flag to initiate session opened event on first handshake. */ private boolean initHandshakeComplete; /** Engine handshake status. */ private HandshakeStatus handshakeStatus; /** Output buffer into which encrypted data will be written. */ private ByteBuffer outNetBuf; /** Input buffer from which SSL engine will decrypt data. */ private ByteBuffer inNetBuf; /** Empty buffer used in handshake procedure. */ private ByteBuffer handshakeBuf = ByteBuffer.allocate(0); /** Application buffer. */ private ByteBuffer appBuf; /** Parent filter. */ private GridNioSslFilter parent; /** Pre-handshake write requests. */ private Queue<WriteRequest> deferredWriteQueue = new LinkedList<>(); /** * Creates handler. * * @param parent Parent SSL filter. * @param ses Session for which this handler was created. * @param engine SSL engine instance for this handler. * @param log Logger to use. * @param directBuf Direct buffer flag. * @param order Byte order. * @param handshake is handshake required. * @param encBuf encoded buffer to be used. * @throws SSLException If exception occurred when starting SSL handshake. */ GridNioSslHandler(GridNioSslFilter parent, GridNioSession ses, SSLEngine engine, boolean directBuf, ByteOrder order, IgniteLogger log, boolean handshake, ByteBuffer encBuf) throws SSLException { assert parent != null; assert ses != null; assert engine != null; assert log != null; this.parent = parent; this.ses = ses; this.order = order; this.directBuf = directBuf; this.log = log; sslEngine = engine; if (handshake) sslEngine.beginHandshake(); else { handshakeFinished = true; initHandshakeComplete = true; } handshakeStatus = sslEngine.getHandshakeStatus(); // Allocate a little bit more so SSL engine would not return buffer overflow status. int netBufSize = sslEngine.getSession().getPacketBufferSize() + 50; outNetBuf = directBuf ? ByteBuffer.allocateDirect(netBufSize) : ByteBuffer.allocate(netBufSize); outNetBuf.order(order); inNetBuf = directBuf ? ByteBuffer.allocateDirect(netBufSize) : ByteBuffer.allocate(netBufSize); inNetBuf.order(order); if (encBuf != null) { encBuf.flip(); inNetBuf.put(encBuf); // Buffer contains bytes read but not handled by sslEngine at BlockingSslHandler. } // Initially buffer is empty. outNetBuf.position(0); outNetBuf.limit(0); int appBufSize = Math.max(sslEngine.getSession().getApplicationBufferSize() + 50, netBufSize * 2); appBuf = directBuf ? ByteBuffer.allocateDirect(appBufSize) : ByteBuffer.allocate(appBufSize); appBuf.order(order); if (log.isDebugEnabled()) log.debug("Started SSL session [netBufSize=" + netBufSize + ", appBufSize=" + appBufSize + ']'); } /** * @return Application buffer with decoded data. */ ByteBuffer getApplicationBuffer() { return appBuf; } /** * Shuts down the handler. */ void shutdown() { try { sslEngine.closeInbound(); } catch (SSLException e) { // According to javadoc, the only case when exception is thrown is when no close_notify // message was received before TCP connection get closed. if (log.isDebugEnabled()) log.debug("Unable to correctly close inbound data stream (will ignore) [msg=" + e.getMessage() + ", ses=" + ses + ']'); } } /** * Performs handshake procedure with remote peer. * * @throws GridNioException If filter processing has thrown an exception. * @throws SSLException If failed to process SSL data. */ void handshake() throws IgniteCheckedException, SSLException { if (log.isDebugEnabled()) log.debug("Entered handshake(): [handshakeStatus=" + handshakeStatus + ", ses=" + ses + ']'); lock(); try { boolean loop = true; while (loop) { switch (handshakeStatus) { case NOT_HANDSHAKING: case FINISHED: { SSLSession sslSes = sslEngine.getSession(); if (log.isDebugEnabled()) log.debug("Finished ssl handshake [protocol=" + sslSes.getProtocol() + ", cipherSuite=" + sslSes.getCipherSuite() + ", ses=" + ses + ']'); handshakeFinished = true; if (!initHandshakeComplete) { initHandshakeComplete = true; GridNioFutureImpl<?> fut = ses.removeMeta(HANDSHAKE_FUT_META_KEY); if (fut != null) fut.onDone(); parent.proceedSessionOpened(ses); } loop = false; break; } case NEED_TASK: { if (log.isDebugEnabled()) log.debug("Need to run ssl tasks: " + ses); handshakeStatus = runTasks(); break; } case NEED_UNWRAP: { if (log.isDebugEnabled()) log.debug("Need to unwrap incoming data: " + ses); Status status = unwrapHandshake(); if (status == BUFFER_UNDERFLOW && handshakeStatus != FINISHED || sslEngine.isInboundDone()) // Either there is no enough data in buffer or session was closed. loop = false; break; } case NEED_WRAP: { // If the output buffer has remaining data, clear it. if (outNetBuf.hasRemaining()) U.warn(log, "Output net buffer has unsent bytes during handshake (will clear): " + ses); outNetBuf.clear(); SSLEngineResult res = sslEngine.wrap(handshakeBuf, outNetBuf); outNetBuf.flip(); handshakeStatus = res.getHandshakeStatus(); if (log.isDebugEnabled()) log.debug("Wrapped handshake data [status=" + res.getStatus() + ", handshakeStatus=" + handshakeStatus + ", ses=" + ses + ']'); writeNetBuffer(null); break; } default: { throw new IllegalStateException("Invalid handshake status in handshake method [handshakeStatus=" + handshakeStatus + ", ses=" + ses + ']'); } } } } finally { unlock(); } if (log.isDebugEnabled()) log.debug("Leaved handshake(): [handshakeStatus=" + handshakeStatus + ", ses=" + ses + ']'); } /** * Called by SSL filter when new message was received. * * @param buf Received message. * @throws GridNioException If exception occurred while forwarding events to underlying filter. * @throws SSLException If failed to process SSL data. */ void messageReceived(ByteBuffer buf) throws IgniteCheckedException, SSLException { if (buf.limit() > inNetBuf.remaining()) { inNetBuf = expandBuffer(inNetBuf, inNetBuf.capacity() + buf.limit() * 2); appBuf = expandBuffer(appBuf, inNetBuf.capacity() * 2); if (log.isDebugEnabled()) log.debug("Expanded buffers [inNetBufCapacity=" + inNetBuf.capacity() + ", appBufCapacity=" + appBuf.capacity() + ", ses=" + ses + ", "); } // append buf to inNetBuffer inNetBuf.put(buf); if (!handshakeFinished) handshake(); else unwrapData(); if (isInboundDone()) { int newPosition = buf.position() - inNetBuf.position(); if (newPosition >= 0) { buf.position(newPosition); // If we received close_notify but not all bytes has been read by SSL engine, print a warning. if (buf.hasRemaining()) U.warn(log, "Got unread bytes after receiving close_notify message (will ignore): " + ses); } inNetBuf.clear(); } } /** * Encrypts data to be written to the network. * * @param src data to encrypt. * @throws SSLException on errors. * @return Output buffer with encrypted data. */ ByteBuffer encrypt(ByteBuffer src) throws SSLException { assert handshakeFinished; assert isHeldByCurrentThread(); // The data buffer is (must be) empty, we can reuse the entire // buffer. outNetBuf.clear(); // Loop until there is no more data in src while (src.hasRemaining()) { int outNetRemaining = outNetBuf.capacity() - outNetBuf.position(); if (outNetRemaining < src.remaining() * 2) { outNetBuf = expandBuffer(outNetBuf, Math.max( outNetBuf.position() + src.remaining() * 2, outNetBuf.capacity() * 2)); if (log.isDebugEnabled()) log.debug("Expanded output net buffer [outNetBufCapacity=" + outNetBuf.capacity() + ", ses=" + ses + ']'); } SSLEngineResult res = sslEngine.wrap(src, outNetBuf); if (log.isDebugEnabled()) log.debug("Encrypted data [status=" + res.getStatus() + ", handshakeStaus=" + res.getHandshakeStatus() + ", ses=" + ses + ']'); if (res.getStatus() == SSLEngineResult.Status.OK) { if (res.getHandshakeStatus() == NEED_TASK) runTasks(); } else throw new SSLException("Failed to encrypt data (SSL engine error) [status=" + res.getStatus() + ", handshakeStatus=" + res.getHandshakeStatus() + ", ses=" + ses + ']'); } outNetBuf.flip(); return outNetBuf; } /** * Checks if SSL handshake is finished. * * @return {@code True} if handshake is finished. */ boolean isHandshakeFinished() { return handshakeFinished; } /** * @return {@code True} if inbound data stream has ended, i.e. SSL engine received * <tt>close_notify</tt> message. */ boolean isInboundDone() { return sslEngine.isInboundDone(); } /** * @return {@code True} if outbound data stream has closed, i.e. SSL engine encoded * <tt>close_notify</tt> message. */ boolean isOutboundDone() { return sslEngine.isOutboundDone(); } /** * Adds write request to the queue. * * @param buf Buffer to write. * @param ackC Closure invoked when message ACK is received. * @return Write future. */ GridNioFuture<?> deferredWrite(ByteBuffer buf, IgniteInClosure<IgniteException> ackC) { assert isHeldByCurrentThread(); GridNioEmbeddedFuture<Object> fut = new GridNioEmbeddedFuture<>(); ByteBuffer cp = copy(buf); deferredWriteQueue.offer(new WriteRequest(fut, cp, ackC)); return fut; } /** * Flushes all deferred write events. * @throws GridNioException If failed to forward writes to the filter. */ void flushDeferredWrites() throws IgniteCheckedException { assert isHeldByCurrentThread(); assert handshakeFinished; while (!deferredWriteQueue.isEmpty()) { WriteRequest req = deferredWriteQueue.poll(); req.future().onDone((GridNioFuture<Object>)parent.proceedSessionWrite(ses, req.buffer(), true, req.ackC)); } } /** * Writes close_notify message to the network output buffer. * * @throws SSLException If wrap failed or SSL engine does not get closed * after wrap. * @return {@code True} if <tt>close_notify</tt> message was encoded, {@code false} if outbound * stream was already closed. */ boolean closeOutbound() throws SSLException { assert isHeldByCurrentThread(); if (!sslEngine.isOutboundDone()) { sslEngine.closeOutbound(); outNetBuf.clear(); SSLEngineResult res = sslEngine.wrap(handshakeBuf, outNetBuf); if (res.getStatus() != CLOSED) throw new SSLException("Incorrect SSL engine status after closeOutbound call [status=" + res.getStatus() + ", handshakeStatus=" + res.getHandshakeStatus() + ", ses=" + ses + ']'); outNetBuf.flip(); return true; } return false; } /** * Copies data from out net buffer and passes it to the underlying chain. * * @return Write future. * @param ackC Closure invoked when message ACK is received. * @throws GridNioException If send failed. */ GridNioFuture<?> writeNetBuffer(IgniteInClosure<IgniteException> ackC) throws IgniteCheckedException { assert isHeldByCurrentThread(); ByteBuffer cp = copy(outNetBuf); return parent.proceedSessionWrite(ses, cp, true, ackC); } /** * Unwraps user data to the application buffer. * * @throws SSLException If failed to process SSL data. * @throws GridNioException If failed to pass events to the next filter. */ private void unwrapData() throws IgniteCheckedException, SSLException { if (log.isDebugEnabled()) log.debug("Unwrapping received data: " + ses); // Flip buffer so we can read it. inNetBuf.flip(); SSLEngineResult res = unwrap0(); // prepare to be written again inNetBuf.compact(); checkStatus(res); renegotiateIfNeeded(res); } /** * Unwraps handshake data and processes it. * * @return Status. * @throws SSLException If SSL exception occurred while unwrapping. * @throws GridNioException If failed to pass event to the next filter. */ private Status unwrapHandshake() throws SSLException, IgniteCheckedException { // Flip input buffer so we can read the collected data. inNetBuf.flip(); SSLEngineResult res = unwrap0(); handshakeStatus = res.getHandshakeStatus(); checkStatus(res); // If handshake finished, no data was produced, and the status is still ok, // try to unwrap more if (handshakeStatus == FINISHED && res.getStatus() == Status.OK && inNetBuf.hasRemaining()) { res = unwrap0(); handshakeStatus = res.getHandshakeStatus(); // prepare to be written again inNetBuf.compact(); renegotiateIfNeeded(res); } else // prepare to be written again inNetBuf.compact(); return res.getStatus(); } /** * Check status and retry the negotiation process if needed. * * @param res Result. * @throws GridNioException If exception occurred during handshake. * @throws SSLException If failed to process SSL data */ private void renegotiateIfNeeded(SSLEngineResult res) throws IgniteCheckedException, SSLException { if (res.getStatus() != CLOSED && res.getStatus() != BUFFER_UNDERFLOW && res.getHandshakeStatus() != NOT_HANDSHAKING) { // Renegotiation required. handshakeStatus = res.getHandshakeStatus(); if (log.isDebugEnabled()) log.debug("Renegotiation requested [status=" + res.getStatus() + ", handshakeStatus = " + handshakeStatus + "ses=" + ses + ']'); handshakeFinished = false; handshake(); } } /** * @param res SSL engine result. * @throws SSLException If status is not acceptable. */ private void checkStatus(SSLEngineResult res) throws SSLException { SSLEngineResult.Status status = res.getStatus(); if (status != Status.OK && status != CLOSED && status != BUFFER_UNDERFLOW) throw new SSLException("Failed to unwrap incoming data (SSL engine error) [ses" + ses + ", status=" + status + ']'); } /** * Performs raw unwrap from network read buffer. * * @return Result. * @throws SSLException If SSL exception occurs. */ private SSLEngineResult unwrap0() throws SSLException { SSLEngineResult res; do { res = sslEngine.unwrap(inNetBuf, appBuf); if (log.isDebugEnabled()) log.debug("Unwrapped raw data [status=" + res.getStatus() + ", handshakeStatus=" + res.getHandshakeStatus() + ", ses=" + ses + ']'); if (res.getStatus() == Status.BUFFER_OVERFLOW) appBuf = expandBuffer(appBuf, appBuf.capacity() * 2); } while ((res.getStatus() == Status.OK || res.getStatus() == Status.BUFFER_OVERFLOW) && (handshakeFinished && res.getHandshakeStatus() == NOT_HANDSHAKING || res.getHandshakeStatus() == NEED_UNWRAP)); return res; } /** * Runs all tasks needed to continue SSL work. * * @return Handshake status after running all tasks. */ private HandshakeStatus runTasks() { Runnable runnable; while ((runnable = sslEngine.getDelegatedTask()) != null) { if (log.isDebugEnabled()) log.debug("Running SSL engine task [task=" + runnable + ", ses=" + ses + ']'); runnable.run(); } if (log.isDebugEnabled()) log.debug("Finished running SSL engine tasks [handshakeStatus=" + sslEngine.getHandshakeStatus() + ", ses=" + ses + ']'); return sslEngine.getHandshakeStatus(); } /** * Expands the given byte buffer to the requested capacity. * * @param original Original byte buffer. * @param cap Requested capacity. * @return Expanded byte buffer. */ private ByteBuffer expandBuffer(ByteBuffer original, int cap) { ByteBuffer res = directBuf ? ByteBuffer.allocateDirect(cap) : ByteBuffer.allocate(cap); res.order(order); 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. */ private ByteBuffer copy(ByteBuffer original) { ByteBuffer cp = directBuf ? ByteBuffer.allocateDirect(original.remaining()) : ByteBuffer.allocate(original.remaining()); cp.order(order); cp.put(original); cp.flip(); return cp; } /** * Write request for cases while handshake is not finished yet. */ private static class WriteRequest { /** Future that should be completed. */ private final GridNioEmbeddedFuture<Object> fut; /** Buffer needed to be written. */ private final ByteBuffer buf; /** */ private final IgniteInClosure<IgniteException> ackC; /** * Creates write request. * * @param fut Future. * @param buf Buffer to write. * @param ackC Closure invoked when message ACK is received. */ private WriteRequest(GridNioEmbeddedFuture<Object> fut, ByteBuffer buf, IgniteInClosure<IgniteException> ackC) { this.fut = fut; this.buf = buf; this.ackC = ackC; } /** * @return Future. */ public GridNioEmbeddedFuture<Object> future() { return fut; } /** * @return Buffer. */ public ByteBuffer buffer() { return buf; } } }