SSL.java

/*
** Module   : SSL.java
** Abstract : Implements abstract SSL FSM on the top of SSLEngine. 
**
** Copyright (c) 2016-2024, Golden Code Development Corporation.
**
** -#- -I- --Date--  ---------------------------------------Description---------------------------------------
** 001 IAS 20160805  Initial version
** 002 IAS 20200729  Process large messages
** 003 IAS 20210310  Added incomingBuffersGuard to prevent race conditions
**     IAS 20210323  Re-worked synchronization logic and added logging.
**     IAS 20210608  Provide more details on the unwrap() failure.
**     IAS 20210827  Fixed sporadic SSL failures
**     IAS 20220624  Added support for the Conscrypt JCE/JSSE provider
**     IAS 20220626  Added SSL JMX counters.
** 004 GBB 20230512  Logging methods replaced by CentralLogger/ConversionStatus.
** 005 EVL 20231124  All byte buffers are now direct ones to improve performance.
** 006 HC  20240222  Enabled JMX on FWD Client.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.net;

import java.io.*;
import java.nio.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.*;
import java.util.logging.*;

import javax.net.ssl.*;
import javax.net.ssl.SSLEngineResult.*;

import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.util.logging.CentralLogger;
import com.goldencode.util.*;

/**
 * Implements abstract SSL FSM.
 */
public abstract class SSL
implements Runnable
{
   /** Logger */
   protected static final CentralLogger LOG = CentralLogger.get(SSL.class.getName(), true, true);

   /** Set of HandshakeStatus values after the end of SSL handshake */
   public static final Set<HandshakeStatus> AFTER_HANDSHAKE = EnumSet.of(HandshakeStatus.NOT_HANDSHAKING, 
                                                                         HandshakeStatus.FINISHED);

   /** timer for measuring send time */
   private static final NanoTimer SEND_TIMER = NanoTimer.getInstance(
      FwdServerJMX.TimeStat.sslSend
   );

   /** timer for measuring wrap time */
   private static final NanoTimer WRAP_TIMER = NanoTimer.getInstance(
      FwdServerJMX.TimeStat.sslWrap
   );
 
   /** timer for measuring wrap time */
   private static final NanoTimer UNWRAP_TIMER = NanoTimer.getInstance(
      FwdServerJMX.TimeStat.sslUnwrap
   );

   /** SSLEngine instance */
   protected final SSLEngine engine;
   
   /** ExecutorService for internal tasks' execution */
   protected final ExecutorService fsmWorkers;
   
   /** Application buffer size */
   protected final int appBufferMax;
   
   /** Packet buffer size */
   protected final int netBufferMax;

   /** Incoming buffers */
   protected final ByteBuffer outWrap, outUnwrap;

   /** Outgoing buffers */
   protected final ByteBuffer inpWrap, inpUnwrap;
   
   /** Handshake done flag */
   protected volatile AtomicBoolean done = new AtomicBoolean(false);

   /** Input chunks' queue */
   protected final java.util.Queue<ByteBuffer> incomingBuffers = new ConcurrentLinkedQueue <>();
   
   /** Input stream guard */
   protected final ReentrantLock inpGuard = new ReentrantLock(true);
   
   /** Output stream guard */
   protected final ReentrantLock outGuard = new ReentrantLock(true);

   /** Max message size */
   private final int maxMessageSize; 
   
   /** Dump SSL packets flasg */
   private final boolean dumpSSL;
   
   /** Use packet seqNo instrumentation */
   private final boolean trackSeqNo; 
   
   /** Input packet seqNo holder */
   protected final AtomicLong inpSeqNo = new AtomicLong(0);

   /** Output packet seqNo generator */
   protected final AtomicLong outSeqNo = new AtomicLong(0);
   
   /** Current packet seqNo*/
   protected long cSeqNo = -1;
   
   /** Use packet seqNo instrumentation */
   protected boolean checkSeqNo = true;

   /** dump halper */  
   protected final HexDump hex = new HexDump(s -> LOG.log(Level.WARNING, s), 256);
   
   /**
    * Constructor
    * 
    * @param engine
    *        the SSLEngine instance
    * @param fsmWorkers
    *        ExecutorService for internal tasks' execution
    */
   public SSL(SSLEngine engine, ExecutorService fsmWorkers)
   {
      if (LOG.isLoggable(Level.FINER))
      {
         LOG.log(Level.FINER, "SSLEngine class: " + engine.getClass().getName());
      }

      int delta = engine.getClass().getName().startsWith("org.conscrypt") ? 0 : 50;
      // the wrap buffers' size adjustment and the value of delta where added
      // based on multiple experiments.
      SSLSession session = engine.getSession();
      this.appBufferMax = session.getApplicationBufferSize();
      this.netBufferMax = session.getPacketBufferSize();
      LOG.log(Level.FINE, String.format("appBufferMax: %d, netBufferMax: %d", appBufferMax, netBufferMax));
      this.outWrap = ByteBuffer.allocateDirect(appBufferMax + delta);
      this.inpWrap = ByteBuffer.allocateDirect(appBufferMax + 50);
      this.outUnwrap = ByteBuffer.allocateDirect(2 * netBufferMax);
      this.inpUnwrap = ByteBuffer.allocateDirect(2 * netBufferMax);
      // EVL: this is strange call      this.outUnwrap.limit(0);
      //      made twice                this.outUnwrap.limit(0);

      this.maxMessageSize = appBufferMax - 50 + delta;

      this.engine = engine;
      this.fsmWorkers = fsmWorkers;
      
      BootstrapConfig bc = SessionManager.get().config();
      this.dumpSSL = bc.getBoolean("net", "ssl", "dump", false); 
      this.trackSeqNo = bc.getBoolean("net", "ssl", "trackSeqNo", false); 
      this.checkSeqNo = trackSeqNo;
   }
   
   /**
    * Check if handshake is finished
    * @return <code>true</code> if handshake is finished
    */
   public boolean handshakeDone() 
   {
      return done.get();  
   }
   /**
    * Accept the next decrypted portion of the input
    *  
    * @param decrypted
    *        the next decrypted portion of the input
    */
   public abstract void nextChunk(ByteBuffer decrypted);

   /**
    * Check if more input is available for processing
    * @throws IOException
    *         on network failure
    */
   public abstract void checkInput() throws IOException;

   /**
    * Process the next portion of the encrypted output
    * 
    * @param encrypted
    *        the next portion of the encrypted output
    */
   public abstract void onOutput(ByteBuffer encrypted);

   /**
    * Called on the SSL session close
    */
   public abstract void onClosed();

   /**
    * Process the next portion of the decrypted input
    * 
    * @param decrypted
    *        the next portion of the decrypted input
    */
   public void onInput(ByteBuffer decrypted)
   {
      log("onInput(%s)", decrypted.remaining());
      nextChunk(decrypted);
   }

   /**
    * Process the next portion of the output
    * 
    * @param data
    *        the next portion of the output
    */
   public void send(final ByteBuffer data)
   {
      tryLock(outGuard);
      try
      {
         SEND_TIMER.start();
         outWrap.put(data);
         run();
      }
      finally 
      {
         SEND_TIMER.stop();
         outGuard.unlock();
      }
   }

   /**
    * Notify about new portion of the input
    * 
    * @param data
    *        a new portion of the input
    */
   public void notify(final ByteBuffer data)
   {
      if (dumpSSL)
      {
         hex.dump("notify", data, false);
      }
      log("notify(%s)", data.remaining());
      incomingBuffers.add(data);
      tryLock(inpGuard);
      try
      {
         while (appendIncoming())
            ;
      }
      finally
      {
         inpGuard.unlock();
      }
      run();
   }

   /** 
    * Perform next FSM step
    */
   @Override
   public void run()
   {
      while (this.handshake())
      {
         continue;
      }
   }

   /**
    * Report the handshake failure
    * 
    * @param ex
    *        The exception which caused the failure
    */
   protected abstract void onFailure(Exception ex);

   /**
    * Report the handshake success
    */
   protected void onSuccess()
   {
      if (done.compareAndSet(false, true))
      {
         LOG.log(Level.FINE, "done");
      }
   }

   /**
    * Get maximal message size
    * 
    * @return maximal message size;
    */
   protected int getMaxMessageSize()
   {
      return this.maxMessageSize;
   }

   /**
    *  Write message to stderr
    * 
    * @param message
    *        message to be written
    */
   protected void log(String message)
   {
      if (LOG.isLoggable(Level.FINER) && !done.get())
      {
         LOG.log(Level.FINER, message);
      }
   }

   /**
    * Write a message to stdout using the specified format string and
    * arguments.
    *
    * @param  fmt
    *         A format string
    *
    * @param  args
    *         Arguments referenced by the format specifiers in the format
    *         string. 
    */
   protected void log(String fmt, Object... args)
   {
      if (LOG.isLoggable(Level.FINER) && !done.get())
      {
         log(String.format(fmt, args));
      }
   }

   /**
    * Timed lock acquisition
    * @param lock
    *        lock to be acquired
    */
   protected void tryLock(Lock lock)
   {
      try
      {
         if (!lock.tryLock(100, TimeUnit.MILLISECONDS))
         {
            LOG.log(Level.WARNING, "Faild to acquire lock", new Exception());
            throw new RuntimeException("Faild to acquire lock");
         }
      }
      catch (InterruptedException e)
      {
         LOG.log(Level.WARNING, "InterruptedException", e);
         throw new RuntimeException("Faild to acquire lock");
      }
   }
   
   /**
    * Append input data to the processing buffer if possible. This is a precaution 
    * against possible BufferOverflow which where experienced in some rare situations   
    * 
    * @return <code>true</code> if data was appended to the incoming buffer
    */
   private boolean appendIncoming()
   {
      ByteBuffer bb = incomingBuffers.peek();
      if (bb == null)
      {
         return false;
      }
      if (dumpSSL)
      {
         hex.dump("append", bb, false);
      }
      int remaining = outUnwrap.remaining();
      int pended = bb.remaining(); 
      if (pended <= remaining)
      {
         try
         {
            outUnwrap.put(bb);
            incomingBuffers.remove();
         } 
         catch (BufferOverflowException e)
         {
            LOG.log(Level.WARNING, 
                  String.format("appendIncoming() failed: "+ 
                        "remaining: %d, pended: %d, "+
                        "outUnwrap.position(: %d), outUnwrap.remaining(): %d",
                  remaining, pended, outUnwrap.position(), outUnwrap.remaining()), e);
            return false;
         }
         log("appendIncoming(): remaining: %d, pended: %d", remaining, pended);
         if (dumpSSL)
         {
            hex.dump("after append", outUnwrap, true);
         }
         return true;
      }
      else if (remaining > 0)
      {
         byte[] b = new byte[remaining];
         bb.get(b);
         bb.compact().flip();
         try
         {
            outUnwrap.put(b);
         } 
         catch (BufferOverflowException e)
         {
            LOG.log(Level.WARNING, 
                  String.format("appendIncoming() failed: "+ 
                        "remaining: %d, pended: %d, "+
                        "outUnwrap.position(: %d), outUnwrap.remaining(): %d",
                  remaining, b.length, outUnwrap.position(), outUnwrap.remaining()), e);
            return false;
         }
         log("appendIncoming(): remaining: %d, written: %d, bb.remaining(): %d", 
               remaining, pended, bb.remaining());
         return false;
      }
      // OM : got these messages: cause under investigation
      System.out.println(
            "Potential buffer overflow prevented (bytes pended:" + pended + ", remaining:" + remaining + ")");
      return false;
   }

   /**
    * Make a next step of handshake  or process input/output data
    * 
    * @return <code>true</code> if the step was successful and the engine is ready for 
    *         the next step 
    */
   private boolean handshake()
   {
      HandshakeStatus handshakeStatus = engine.getHandshakeStatus();
      log("engine.getHandshakeStatus(): %s", handshakeStatus);
      switch (handshakeStatus)
      {
         case NOT_HANDSHAKING:
            boolean havePendingData = false;
            {
               if (outWrap.position() > 0)
                  havePendingData |= this.wrap();
               if (outUnwrap.position() > 0)
                  havePendingData |= this.unwrap();
            }
            if (done.compareAndSet(false, true))
            {
               log("done");
            }
            return havePendingData;

         case NEED_WRAP:
            if (!this.wrap())
               return false;
            break;

         case NEED_UNWRAP:
            if (!this.unwrap())
               return false;
            break;

         case NEED_TASK:
            final Runnable sslTask = engine.getDelegatedTask();
            if (sslTask != null)
            {
               fsmWorkers.execute(() -> {
                  sslTask.run();
                  run();
               });
            }
            return false;

         case FINISHED:
            throw new IllegalStateException("FINISHED");
      }

      return true;
   }

   /**
    * Encrypt the next portion of output (guarded).
    * 
    * @return <code>true</code> if the operation was successful
    */
   private boolean wrap()
   {
      tryLock(outGuard);
      try
      {
         WRAP_TIMER.start();
         return doWrap();
      }
      finally
      {
         WRAP_TIMER.stop();
         outGuard.unlock();
      }
   }
   /**
    * Encrypt the next portion of output.
    * 
    * @return <code>true</code> if the operation was successful
    */
   private boolean doWrap()
   {
      log("wrap()");
      if (trackSeqNo && checkSeqNo && engine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING)
      {
         inpWrap.putLong(outSeqNo.incrementAndGet());
      }
      SSLEngineResult wrapResult;
      try
      {
         if (dumpSSL)
         {
            hex.dump("wrapping", outWrap,true);
            hex.dump("before wrap", inpWrap, true);
         }
         outWrap.flip();
         wrapResult = engine.wrap(outWrap, inpWrap);
         outWrap.compact();
         if (dumpSSL)
         {
            hex.dump("after wrap", inpWrap, true);
         }
      }
      catch (SSLException exc)
      {
         SSLSession session = engine.getSession();
         LOG.log(Level.WARNING,
                  String.format("wrap() failed for %s; %s; [%d, %d] ", 
                           session.getProtocol(), session.toString(), 
                           outWrap.position(), outWrap.limit()
                  ), 
                  exc);
         this.onFailure(exc);
         return false;
      }

      Status status = wrapResult.getStatus();
      log("wrapResult.getStatus(): %s", status);
      switch (status)
      {
         case OK:
            if (inpWrap.position() > 0)
            {
               inpWrap.flip();
               this.onOutput(inpWrap);
               inpWrap.compact();
            }
            break;

         case BUFFER_UNDERFLOW:
            // try again later
            break;

         case BUFFER_OVERFLOW:
            throw new IllegalStateException("failed to wrap - buffer overflow");

         case CLOSED:
            this.onClosed();
            return false;
      }

      return true;
   }

   /**
    * Decrypt the next portion of input (guarded).
    * 
    * @return <code>true</code> if the operation was successful
    */
   private boolean unwrap()
   {
      tryLock(inpGuard);
      try
      {
         UNWRAP_TIMER.start();
         return doUnwrap();
      }
      finally
      {
         UNWRAP_TIMER.stop();
         inpGuard.unlock();
      }
   }

   /**
    * Decrypt the next portion of input.
    * 
    * @return <code>true</code> if the operation was successful
    */
   private boolean doUnwrap()
   {
      SSLEngineResult unwrapResult = null;
      try
      {
         outUnwrap.flip();
         if (dumpSSL)
         {
            hex.dump("before unwrap", outUnwrap, false);
         }
         if (trackSeqNo && engine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING && 
             outUnwrap.remaining() >= 8)
         {
            if (cSeqNo < 0)
            {
               outUnwrap.mark();
               cSeqNo  = outUnwrap.getLong();
               if (cSeqNo >> 48 != 0)
               {
                  outUnwrap.reset();
                  checkSeqNo = false;
               }
//               LOG.warning(
//                        String.format("cSeqNo:%x; inpSeqNo:%x; %s", cSeqNo, inpSeqNo.get(), 
//                        engine.getHandshakeStatus())
//               );
               if (checkSeqNo && !inpSeqNo.compareAndSet(cSeqNo-1, cSeqNo))
               {
                  throw new SSLException(
                     String.format("Out ot order: prev = %x, current = %x; %s", 
                              inpSeqNo.get(), cSeqNo, engine.getHandshakeStatus())
                  );
               }
            }
         }
         unwrapResult = engine.unwrap(outUnwrap, inpUnwrap);
         outUnwrap.compact();
         ByteBuffer decoded = inpUnwrap.duplicate();
         decoded.flip();
         if (decoded.limit() > 0)
         {
            cSeqNo = -1;       
         }
         if (dumpSSL)
         {
            hex.dump("after unwrap", decoded, false);
         }
      }
      catch (SSLException ex)
      {
      SSLSession session = engine.getSession();
         LOG.log(Level.WARNING,
                  String.format("%s: unwrap() failed for %s; %s; [%d, %d] ", 
                           Thread.currentThread().getName(),
                           session.getProtocol(), session.toString(), 
                           outUnwrap.position(), outUnwrap.limit()
                  ),
                  ex);
         this.onFailure(ex);
         return false;
      }

      Status status = unwrapResult.getStatus();
      log("unwrapResult.getStatus(): %s, got: %d", status, inpUnwrap.position());
      switch (status)
      {
         case OK:
            if (inpUnwrap.position() > 0)
            {
               inpUnwrap.flip();
               this.onInput(inpUnwrap);
               inpUnwrap.compact();
            }
            break;

         case CLOSED:
            this.onClosed();
            return false;

         case BUFFER_OVERFLOW:
            throw new IllegalStateException("failed to unwrap - buffer overflow");

         case BUFFER_UNDERFLOW:
            return false;
      }

      HandshakeStatus handshakeStatus = unwrapResult.getHandshakeStatus();
      log("unwrapResult.getHandshakeStatus()): %s", handshakeStatus);
      if (handshakeStatus == HandshakeStatus.FINISHED)
      {
         this.onSuccess();
         return false;
      }

      return true;
   }
}