# 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;
       }
    }