# LowLevelSocketListenerImpl.java

    /*
    ** Module   : LowLevelSocketListenerImpl.java
    ** Abstract : Implementation of a server socket.
    **
    ** Copyright (c) 2013-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description-----------------------------------
    ** 001 CA  20130908 Created initial version.
    ** 002 HC  20131031 Changed resource ID data type to Long, see issue #2183.
    ** 003 OM  20131018 Updated to conform the new SessionListener API.
    ** 004 CA  20140805 ThinClient is explicitly imported, to isolate ChUI class usage outside of ChUI
    **                  packages.
    ** 005 EVL 20160217 Clean up comments from symbols invalid for Solaris to compile.
    ** 006 OM  20160321 Improved enableSSLConnections() method.
    ** 007 EVL 20160406 Javadoc fix.
    ** 008 RFB 20210902 Added protocol parameter so that additional socket protocols like TLSv1.2 can be used.
    **                  Also fixed NPE when the store cannot be found. Ref. #4366.
    **     CA  20220329 Do not attempt to cleanup sockets if the WorkArea has not been created (as the client
    **                  didn't use them).
    **     CA  20220514 The active session is now set for leaf sessions, too - this is required for a change in
    **                  RemoteObject.obtainInstance, where first a check is done for an existing local proxy, and
    **                  after that a network instance is obtained (a requirement for the server-side OS resources
    **                  support, like memptr).
    ** 009 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 010 GBB 20240826 Moving LowLevelSocketImpl to osresource package.
    */
    /*
    ** 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.util;

    import java.io.*;
    import java.security.*;
    import java.util.*;
    import java.util.logging.*;
    import java.net.*;
    import java.net.Socket;
    import javax.net.ssl.*;
    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.net.Session;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.ui.chui.ThinClient;
    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.p2j.util.osresource.*;

    /**
     * This class acts as a delegate for the server-side to the create java socket.
     */
    public class LowLevelSocketListenerImpl
    implements LowLevelSocketListener,
               SessionListener
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(LowLevelSocketListenerImpl.class);

       /** Token used to authenticate with the dispatcher when registering APIs. */
       private static Object modToken = null;

       /** The context-local data of this class: a registry of server sockets by their ID. */
       private static ContextLocal<Map<Integer, ServerSocketData>> sockets =
       new ContextLocal<Map<Integer, ServerSocketData>>()
       {
          protected Map<Integer,ServerSocketData> initialValue()
          {
             return new HashMap<>();
          };
       };

       /** The single instance available of this class. */
       private static final LowLevelSocketListenerImpl instance = new LowLevelSocketListenerImpl();

       /**
        * The password for access to server-side SSL certificate store. Default is blank.
        */
       private static String keyStorePassword = null;

       /**
        * Private c'tor, to not allow explicit usage of this class. Its intended goal is to be
        * exported as a network server.
        */
       private LowLevelSocketListenerImpl()
       {
          // no-op
       }

       /**
        * Initialize the client-side socket support:
        * <ul>
        *    <li>export the singleton instance of this class as a network server;</li>
        *    <li>initialize server side SSL by setting keystore access password.</li>
        * </ul>
        *
        * @param   cfg
        *          The configuration to use for setup of the client.
        * @param   session
        *          The P2J session. May be <code>null</code> if running in embedded mode.
        * @param   local
        *          <code>true</code> to register purely on a local basis (no remote/network access to these
        *          methods).
        */
       public static synchronized void initialize(BootstrapConfig cfg,
                                                  Session session,
                                                  boolean local)
       {
          final Class<?> ifc = LowLevelSocketListener.class;
          modToken = RemoteObject.registerServer(ifc, instance, modToken, local);

          if (session != null)
          {
             session.addSessionListener(instance);
          }
          keyStorePassword = cfg.getString("ssl-socket", "keystore", "password", "");
       }

       /**
        * Create a server socket and let it listen on the given port.
        *
        * @param    resourceId
        *           The ID of the associated {@link SocketImpl} resource on server side.
        * @param    port
        *           The port number.
        * @param    qsize
        *           The backlog size for this socket.
        *
        * @return   The ID of the associated {@link LowLevelSocketListenerImpl} instance when
        *           the connection was established.
        *
        * @throws   IOException
        *           In case of errors while starting the server.
        */
       @Override
       public int enableConnections(long resourceId, int port, int qsize)
       throws IOException
       {
          ServerSocket socket = new ServerSocket(port, qsize);

          int id = Utils.uniqueId();

          Map<Integer, ServerSocketData> map = sockets.get();
          synchronized (map)
          {
             ServerSocketData data = new ServerSocketData(id, resourceId, socket, null);
             map.put(id, data);

             data.startThreads();
          }

          return id;
       }

       /**
        * Create a SSL server socket and let it listen on the given port.
        * <p>
        * Following are some findings/implementation notes:
        * <ul>
        *    <li>{@code nosessioncache} - this disables the SSL session cache. To emulate this, we use
        *        {@link SSLSessionContext#setSessionCacheSize} to set the cache size to 1;</li>
        *    <li>{@code sessiontimeout} - represents "The maximum amount of time, in seconds, that the
        *        server waits before it rejects a SSL client's request to resume a session. The
        *        default value is 180 seconds."  This parameter looks like is something which should
        *        be set by the client and not by the server, as (CA thinking) it means "if client is
        *        idle more than x seconds, than terminate connection". <br>
        *        We use {@link SSLSessionContext#setSessionTimeout} to invalidate the the
        *        {@link SSLSession} object so future connections cannot resume the SSL session;</li>
        *    <li>documentation states that OpenEdge keeps the private keys in a global, per-
        *        installation store, is the {@code <DLC>/keys} folder. These files are in ASCII PEM
        *        format and include both the key and the certificate. Similarly, we store each alias
        *        in separate store files, similar to 4GL, however, the format differ and the keys/
        *        certificates must be imported (but keeping same name);</li>
        *    <li>there is a {@code default_server} key with the default password set to
        *        {@code password}. This is used when connecting in SSL mode, but no key alias or
        *        password is specified in the connection string. The default values were automatically
        *        selected on P2J server-side before calling this method;</li>
        *    <li>the password specified via {@code keyaliaspasswd} option in ENABLE-CONNECTIONS is
        *        encrypted in the 4GL source code, but it was already decrypted before passing it in
        *        as argument.</li>
        * </ul>
        *
        * @param   resourceId
        *          The ID of the associated {@link SocketImpl} resource on server side.
        * @param   port
        *          The port number.
        * @param   qsize
        *          The backlog size for this socket.
        * @param   keyalias
        *          The alias for the digital certificate.
        * @param   keyaliaspasswd
        *          The password for the key of the digital certificate.
        * @param   nosessioncache
        *          Flag indicating the SSL client session cache is disabled.
        * @param   sessiontimeout
        *          The number of seconds before the SSL client request is timeout.
        * @param   protocol
        *          The standard name of the requested protocol as defined by the SSLContext class.
        *
        * @return  The ID of the associated {@link LowLevelSocketListenerImpl} instance when
        *          the connection was established.
        *
        * @throws  IOException
        *          In case of errors while starting the server socket.
        * @throws  GeneralSecurityException
        *          In case of SSL-related errors while starting the server socket.
        */
       @Override
       public int enableSSLConnections(long    resourceId,
                                       int     port,
                                       int     qsize,
                                       String  keyalias,
                                       String  keyaliaspasswd,
                                       boolean nosessioncache,
                                       int     sessiontimeout,
                                       String  protocol)
       throws IOException,
              GeneralSecurityException
       {
          int id = -1;

          String keyStoreName = ThinClient.getLocalFileName(keyalias + ".store");

          // When null, KeyStore.load fails with NPE
          if (keyStoreName == null)
          {
              return -1;
          }

          // file-level password for keystore. Same for all stores. The password is passed in as
          // a bootstrap parameter value with "ssl-socket:keystore:password" key
          char keyStorePass[] = keyStorePassword.toCharArray();

          // keyaliaspasswd is already decrypted on p2j-server side
          char ctPass[] = keyaliaspasswd.toCharArray();

          // open key store, check the existence of the requested alias
          KeyStore ks = KeyStore.getInstance("JKS");
          ks.load(new FileInputStream(keyStoreName), keyStorePass);
          if (!ks.isKeyEntry(keyalias))
          {
             // requested alias was not found in keystore
             return -1;
          }

          // create a key manager
          KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
          kmf.init(ks, ctPass);

          // create the SSL context
          SSLContext sslCtx = SSLContext.getInstance(protocol);
          sslCtx.init(kmf.getKeyManagers(), null, null);

          SSLSessionContext clSessCtx = sslCtx.getClientSessionContext();
          if (clSessCtx != null)
          {
             if (nosessioncache)
             {
                clSessCtx.setSessionCacheSize(1);
             }
             clSessCtx.setSessionTimeout(sessiontimeout);
          }
          else
          {
             if (LOG.isLoggable(Level.FINE))
             {
                LOG.log(Level.FINE,
                        "nosessioncache and sessiontimeout could not be set in SSL context");
             }
          }
          SSLServerSocketFactory factory = sslCtx.getServerSocketFactory();
          SSLServerSocket secure = (SSLServerSocket) factory.createServerSocket(port);

          id = Utils.uniqueId();

          Map<Integer, ServerSocketData> map = sockets.get();
          synchronized (map)
          {
             ServerSocketData data = new ServerSocketData(id, resourceId, null, secure);
             map.put(id, data);

             data.listener.start();
          }

          return id;
       }

       /**
        * Terminates the listening loop for this server socket. The existing established connections
        * are not terminated.
        *
        * @param    id
        *           The ID of {@link LowLevelSocketImpl} instance on P2J Client side.
        *
        * @return   <code>true</code> if no errors were encountered during disconnect.
        */
       @Override
       public boolean disableConnections(int id)
       {
          Map<Integer, ServerSocketData> map = sockets.get();
          synchronized (map)
          {
             ServerSocketData server = map.remove(id);
             server.terminate();
          }

          return true;
       }

       /**
        * When the P2J client session is ending, terminate all active server sockets.
        *
        * @param    session
        *           The session that is ending.
        */
       @Override
       public void terminate(Session session)
       {
          Map<Integer, ServerSocketData> data = sockets.get(false);

          if (data == null)
          {
             // no sockets were created in this client session.
             return;
          }

          synchronized (data)
          {
             Map<Integer, ServerSocketData> copy = new HashMap<Integer, ServerSocketData>(data);
             for (ServerSocketData server : copy.values())
             {
                disableConnections(server.id);
             }
          }
       }

       /**
        * This callback will be called after the security context has been set up.
        *
        * @param    session
        *           The session that is starting.
        */
       @Override
       public void initialize(Session session)
       {
          // no-ops
       }

       /**
        * Container of various server-socket related data.
        */
       private static class ServerSocketData
       {
          /** The local ID of this server socket. */
          private final int id;

          /** The non-ssl server socket. <code>null</code> if ssl mode. */
          private final ServerSocket socket;

          /** The ssl server socket. <code>null</code> if no-ssl mode. */
          private final SSLServerSocket sslSocket;

          /** Instance of this class will start listening for connections in a different thread. */
          private final ConnectListener listener;

          /** Flag indicating that the server socket is terminated. */
          private boolean terminated = false;

          /**
           * Save the given data in a new {@link ServerSocketData} instance.
           *
           * @param    id
           *           The local ID for this server socket.
           * @param    resourceId
           *           The resource ID of the server socket on P2J serer side.
           * @param    socket
           *           The server-socket for non-ssl mode; <code>null</code> if ssl mode.
           * @param    sslSocket
           *           The server-socket for ssl mode; <code>null</code> if no-ssl mode.
           */
          public ServerSocketData(int             id,
                                  long            resourceId,
                                  ServerSocket    socket,
                                  SSLServerSocket sslSocket)
          {
             this.id = id;
             this.socket = socket;
             this.sslSocket = sslSocket;

             this.listener = new ConnectListener(resourceId, this);
          }

          /**
           * Start the {@link #listener connect listener} thread.
           */
          public void startThreads()
          {
             this.listener.start();
          }

          /**
           * Terminate this server socket.
           */
          public synchronized void terminate()
          {
             this.terminated = true;

             try
             {
                if (socket != null)
                {
                   socket.close();
                }
                else
                {
                   sslSocket.close();
                }
             }
             catch (IOException e)
             {
                if (LOG.isLoggable(Level.FINER))
                {
                   final String msg = "Error terminating server socket [%d]";
                   LOG.log(Level.FINER, String.format(msg, id), e);
                }

                // ignore exceptions triggered by socket close
             }
          }

          /**
           * Check if this server socket is terminated.
           *
           * @return   The {@link #terminated} flag.
           */
          public synchronized boolean isTerminated()
          {
             return terminated;
          }
       }

       /**
        * Start a loop in a different thread and listen for incoming connections.
        */
       private static class ConnectListener
       implements Runnable
       {
          /** The remote resource ID for this server-socket. */
          private final long resourceId;

          /** Back reference to the data for this server socket. */
          private final ServerSocketData data;

          /**
           * Initialize this instance.
           *
           * @param    resourceId
           *           The ID of the {@link SocketListener resource} on P2J server side.
           * @param    data
           *           The associated {@link ServerSocketData}.
           */
          public ConnectListener(long resourceId, ServerSocketData data)
          {
             this.resourceId = resourceId;
             this.data = data;
          }

          /**
           * Listen for incoming connections, in a different thread.
           */
          public void run()
          {
             try
             {
                ServerExports server = ThinClient.getInstance().getServer();

                while (!data.isTerminated())
                {
                   LowLevelSocketImpl.SocketData clientData = accept();

                   if (clientData != null)
                   {
                      server.connectEvent(resourceId, clientData.getResourceId());
                   }
                }
             }
             finally
             {
                SessionManager.get().deregisterAsyncThread(Thread.currentThread());
             }
          }

          /**
           * Start a new thread, in which this instance will be executed.  The thread's name will be
           * <code>Server socket listener [%d]</code>, where <code>%d</code> is replaced with the
           * {@link LowLevelSocketImpl.SocketData#id} value.
           * <p>
           * This thread is marked as async, as all outgoing requests must be processed async, in
           * their own {@link Dispatcher dispatcher}.
           */
          public void start()
          {
             Thread t = new AssociatedThread(this);
             // mark this thread as async
             SessionManager.get().registerAsyncThread(t);
             String name = String.format("Server socket listener [%d]", data.id);
             t.setName(name);
             t.start();
          }

          /**
           * Wait for a new incoming connection.
           *
           * @return   The {@link LowLevelSocketImpl.SocketData} for this client socket.
           */
          private LowLevelSocketImpl.SocketData accept()
          {
             boolean ssl = (data.sslSocket != null);
             LowLevelSocketImpl.SocketData cldata = null;

             try
             {
                Socket client = ssl ? data.sslSocket.accept() : data.socket.accept();

                cldata = LowLevelSocketImpl.registerClientSocket(client, ssl);
             }
             catch (IOException e)
             {
                if (LOG.isLoggable(Level.FINER))
                {
                   final String msg = "Error while accepting a new connection on [%d]";
                   LOG.log(Level.FINER, String.format(msg, data.id), e);
                }

                // ignore errors
             }

             return cldata;
          }
       }
    }