# SocketListenerImpl.java

    /*
    ** Module   : SocketListenerImpl.java
    ** Abstract : A class for implementing the SERVER-SOCKET method and attributes
    **
    ** Copyright (c) 2013-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------------------------Description---------------------------------
    ** 001 CS  20130204 Created initial version.
    ** 002 CA  20130221 Added Deletable support. setType was renamed to setResourceType.
    ** 003 OM  20130304 Refactored isValid and isUnknown of WrappedResource to valid and unknown.
    ** 004 CA  20130603 resourceDelete must return a boolean value.
    ** 005 CA  20130902 Added runtime support for server sockets.
    ** 006 CA  20130927 Resource type is computed from the annotation. Removed setUnknown(false) call.
    ** 007 HC  20131031 Changed resource ID data type to Long, see issue #2183.
    ** 008 CA  20131218 Fixed implementation of DISABLE-CONNECTIONS.
    ** 009 VIG 20131225 Added LAST-EVENT attrs setting during processing of CONNECT event
    ** 010 OM  20151103 Added GeneralSecurityException to exceptions caught at socket connection.
    ** 011 RFB 20210906 Added -nosessioncache to no value options and additional connect algorithms. Ref #4366.
    **     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).
    **     CA  20220930 Refactored the callback invocation to be performed via a call-site and InvokeConfig, to
    **                  allow caching of the resolved target.
    **     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
    ** 012 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 013 GBB 20240101 ConnectHelper renamed to ServerConnectHelper.
    ** 014 RFB 20240326 Make the parameters consistent with the documentation and add TLSv1.3 (RFC 8446) support.
    **     RFB 20240402 Reduce the parameter selection to the -ssl option the 4GL supports and -tls. -tls will be the same
    **                  as -ssl and perform the ssl negotiation.
    */
    /*
    ** 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.net.*;
    import java.security.*;
    import java.util.*;
    import java.util.logging.*;

    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.ui.client.event.*;
    import com.goldencode.p2j.util.logging.*;

    /**
     * A class for implementing the SERVER-SOCKET method and attributes.
     */
    public class SocketListenerImpl
    extends SensitiveResource
    implements SocketListener,
               Sensitive
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(SocketListenerImpl.class);

       /** The call-site for the CONNECT event callback. */
       private static final InvokeConfig CONNECT_CALL_SITE = new InvokeConfig();

       /** Client side network instance for handling sockets. */
       private static final ContextLocal<LowLevelSocketListener> remoteSocket =
       new ContextLocal<LowLevelSocketListener>()
       {
          protected LowLevelSocketListener initialValue()
          {
             Class<?> cls = LowLevelSocketListener.class;

             if (!SessionManager.get().isLeaf())
             {
                return (LowLevelSocketListener) RemoteObject.obtainNetworkInstance(cls);
             }
             else
             {
                return (LowLevelSocketListener) RemoteObject.obtainLocalInstance(cls, true);
             }
          };
       };

       /** The name of the procedure to be executed when new clients connect, if any. */
       private String connectProcedure = null;

       /** The context of the procedure to be executed when new clients connect, if any. */
       private WrappedResource connectProcedureContext = null;

       /** Boolean flag to track connection state. */
       private boolean connected = false;

       /** The associated server socket id on P2J client side. If not connected, defaults to -1. */
       private int serverSocketId = -1;

       /** Flag to track the valid state of this resource. */
       private boolean valid = true;

       /**
        * Default constructor which initializes SERVER-SOCKET specific settings.
        */
       public SocketListenerImpl()
       {
          // nothing special to do
       }

       /**
        * Reports if this object is valid for use.
        *
        * @return   <code>true</code> if we are valid (can be used).
        */
       public boolean valid()
       {
          return valid;
       }

       /**
        * Java implementation for the ENABLE-CONNECTION() method.  This will attempt to start
        * listening on the specified port and will return <code>true</code> if operation succeeded,
        * or <code>false</code> otherwise.
        * <p>
        * The following options can be specified as valid ENABLE-CONNECTIONS() options:
        * <ul>
        *    <li><b>-S port</b> The port to which to connect.</li>
        *    <li><b>-ssl</b> If specified the connection will be negotiated to the highest
        *         level of encryption available.</li>
        *    <li><b>-tls</b> Same option as -ssl, as the connection will be negotiated to the
        *         highest level of encryption available.</li>
        *    <li><b>-pf config-file-name</b> Specifies the file from which options will be read.</li>
        *    <li><b>-qsize maxconn</b> The maximum number of connections to handle  before refusing
        *        extra incoming connections.</li>
        *    <li><b>-keyalias alias</b> The alias for the digital certificate, default is:
        *        <b>default_server.</b></li>
        *    <li><b>-keyaliaspasswd pwd</b> The password for the key of the digital certificate.
        *        To use always with -keyalias option.</li>
        *    <li><b>-nossesioncache</b> Disables SSL client session cache.</li>
        *    <li><b>-sessiontimeout time</b> The number of seconds before the SSL client request is
        *        timeout, default value is 180.</li>
        * </ul>
        *
        * @param    option
        *           The set of options that define the enable-connections method.
        *
        * @return   <code>true</code> if operation succeeded, or <code>false</code> otherwise.
        */
       public logical enableConnections(String option)
       {
          return enableConnections(new character(option));
       }

       /**
        * Java implementation for the ENABLE-CONNECTION() method.  This will attempt to start
        * listening on the specified port and will return <code>true</code> if operation succeeded,
        * or <code>false</code> otherwise.
        * <p>
        * The following options can be specified as valid ENABLE-CONNECTIONS() options:
        * <ul>
        *    <li><b>-S port</b> The port to which to connect.</li>
        *    <li><b>-ssl</b> If specified the connection will be negotiated to the highest
        *         level of encryption available.</li>
        *    <li><b>-tls</b> Same option as -ssl, as the connection will be negotiated to the
        *         highest level of encryption available.</li>
        *    <li><b>-pf config-file-name</b> Specifies the file from which options will be read.</li>
        *    <li><b>-qsize maxconn</b> The maximum number of connections to handle  before refusing
        *        extra incoming connections.</li>
        *    <li><b>-keyalias alias</b> The alias for the digital certificate, default is:
        *        <b>default_server.</b></li>
        *    <li><b>-keyaliaspasswd pwd</b> The password for the key of the digital certificate.
        *        To use always with -keyalias option.</li>
        *    <li><b>-nossesioncache</b> Disables SSL client session cache.</li>
        *    <li><b>-sessiontimeout time</b> The number of seconds before the SSL client request is
        *        timeout, default value is 180.</li>
        * </ul>
        *
        * @param    option
        *           The set of options that define the enable-connections method.
        *
        * @return   <code>true</code> if operation succeeded, or <code>false</code> otherwise.
        */
       public logical enableConnections(character option)
       {
          if (connected)
          {
             final String msg = "Only one ENABLE-CONNECTIONS can be active at any one time";
             ErrorManager.recordOrShowError(9185, msg, false, false, false);

             return new logical(false);
          }

          Map<String, String> parms = parseOptions(option);

          if (parms != null && !parms.containsKey("-S"))
          {
             ErrorManager.recordOrShowError(5488, "The -S parameter must be specified to connect",
                                            false, false, false);
             return new logical(false);
          }

          if (parms == null)
          {
             ErrorManager.recordOrShowError(5510, "Invalid parameter string", false, false, false);
             return new logical(false);
          }

          int qsize = ServerConnectHelper.getInt(parms, "-qsize", 0);
          String serviceName = ServerConnectHelper.getString(parms, "-S", null);
          String saveServiceName = serviceName;

          int port = -1;
          try
          {
             port = Integer.parseInt(serviceName);
             serviceName = "";
          }
          catch (NumberFormatException e)
          {
             // in case of server sockets, the target service name must be registered in the
             // directory's "port_services" mapping
             port = Utils.getPortForService(null, serviceName);
          }

          if (port == -1)
          {
             final String msg = "Service name %s could not be resolved to a port!";
             throw new IllegalArgumentException(String.format(msg, serviceName));
          }

          boolean ssl    = parms.containsKey("-ssl") || parms.containsKey("-tls");
          long resId = handle.resourceId(this);

          try
          {
             serverSocketId = -1;
             if (!ssl)
             {
                serverSocketId = remoteSocket.get().enableConnections(resId, port, qsize);
             }
             else
             {
                String keyalias = ServerConnectHelper.getString(parms, "-keyalias", "default_server");
                String keyaliaspasswd = ServerConnectHelper.getString(parms, "-keyaliaspasswd",
                                                                      "20333c34252a2137" /* = encrypted default 'password' */);
                keyaliaspasswd = SymmetricEncryption.decrypt(keyaliaspasswd);

                boolean nosessioncache = parms.containsKey("-nosessioncache");
                int sessiontimeout = ServerConnectHelper.getInt(parms, "-sessiontimeout", 180);

                serverSocketId = remoteSocket.get().enableSSLConnections(resId,
                                                                         port,
                                                                         qsize,
                                                                         keyalias,
                                                                         keyaliaspasswd,
                                                                         nosessioncache,
                                                                         sessiontimeout,
                                                                         "SSL");

             }

             connected = (serverSocketId != -1);
          }
          catch (BindException e)
          {
             if (LOG.isLoggable(Level.FINER))
             {
                LOG.log(Level.FINER, "Error while establishing connection!", e);
             }

             final String msg = "Service %s transport TCP is busy";
             ErrorManager.recordOrShowError(5485, String.format(msg, serviceName), false);
          }
          catch (IOException | GeneralSecurityException e)
          {
             if (LOG.isLoggable(Level.FINER))
             {
                LOG.log(Level.FINER, "Error while establishing connection!", e);
             }

             final String msg = "Unexpected error %s when connecting port %s";
             String errMsg = String.format(msg, e.getMessage(), saveServiceName);
             ErrorManager.recordOrThrowError(5485, errMsg);
          }

          return new logical(connected);
       }

       /**
        * Java implementation for the DISABLE-CONNECTIONS() SERVER-SOCKET method.  This will stop
        * listening and accepting any new connections, but will interrupt the existing ones.
        *
        * @return    <code>true</code> if the operation succeeded or <code>false</code> otherwise.
        */
       public synchronized logical disableConnections()
       {
          boolean disabled = remoteSocket.get().disableConnections(serverSocketId);

          if (disabled)
          {
             this.connected = false;
             this.serverSocketId = -1;
          }

          return new logical(disabled);
       }

       /**
        * Java implementation for the SET-CONNECT-PROCEDURE() SERVER-SOCKET method.  This will set
        * the procedure to execute when a CONNECT event is received.
        *
        * @param    procedureName
        *           The name of the procedure to be executed on CONNECT event.
        *
        * @return   <code>true</code> if the operation was successful or <code>false</code> otherwise.
        */
       public logical setConnectProcedure(String procedureName)
       {
          return setConnectProcedure(new character(procedureName), ProcedureManager.thisProcedure());
       }

       /**
        * Java implementation for the SET-CONNECT-PROCEDURE() SERVER-SOCKET method.  This will set
        * the procedure to execute when a CONNECT event is received.
        *
        * @param    procedureName
        *           The name of the procedure to be executed on CONNECT event.
        *
        * @return   <code>true</code> if the operation was successful or <code>false</code> otherwise.
        */
       public logical setConnectProcedure(character procedureName)
       {
          return setConnectProcedure(procedureName, ProcedureManager.thisProcedure());
       }

       /**
        * Java implementation for the SET-CONNECT-PROCEDURE() SERVER-SOCKET method.  This will set
        * the procedure to execute when a CONNECT event is received.
        *
        * @param    procedureName
        *           The name of the procedure to be executed on CONNECT event.
        * @param    procHandle
        *           The context where the specified procedure belongs. Validation will be performed
        *           when the CONNECT event is executed.
        *
        * @return   <code>true</code> if the operation was successful or <code>false</code> otherwise.
        */
       public logical setConnectProcedure(String procedureName, handle procHandle)
       {
          return setConnectProcedure(new character(procedureName), procHandle);
       }

       /**
        * Java implementation for the SET-CONNECT-PROCEDURE() SERVER-SOCKET method.  This will set
        * the procedure to execute when a CONNECT event is received.
        *
        * @param    procedureName
        *           The name of the procedure to be executed on CONNECT event.
        * @param    procHandle
        *           The context where the specified procedure belongs. Validation will be performed
        *           when the CONNECT event is executed.
        *
        * @return   <code>true</code> if the operation was successful or <code>false</code> otherwise.
        */
       public logical setConnectProcedure(character procedureName, handle procHandle)
       {
          this.connectProcedure = procedureName.getValue();
          this.connectProcedureContext = procHandle.getResource();

          return new logical(true);
       }

       /**
        * Called by the P2J client side, it will inform the server that a server event was generated.
        * Although the event originates from the P2J Client Side, socket events are server-side
        * events.
        *
        * @param    clientSocketId
        *           The resource ID of the socket created for the new connected accepted by the
        *           server socket.
        *
        * @return   The ID of the generated server event or <code>-1</code> if a event could not be
        *           posted (i.e. the resource is not SENSITIVE).
        */
       public int connectEvent(final long clientSocketId)
       {
          if (!isSensitive().booleanValue())
          {
             // if the resource is not sensitive, will ignore the event.
             return -1;
          }

          long resId = handle.resourceId(this);

          Runnable task = new Runnable()
          {
             @Override
             public void run()
             {
                handle h = handle.fromResourceId(clientSocketId);

                invokeConnectProcedure(h);

                // the client socket is not deleted/disconnected implicitly
             }
          };

          KeyReader.setLabelWorker(Keyboard.CONNECT);

          // raise a CONNECT event
          ServerEvent event = new ServerEvent(resId, Keyboard.CONNECT, task);
          LogicalTerminal.postServerEvent(event);

          return event.id();
       }

       /**
        * Delete the resource.
        *
        * @return   <code>true</code> if the resource was deleted.
        */
       @Override
       protected boolean resourceDelete()
       {
          if (connected)
          {
             final String msg = "Server-Socket is still connected. Cannot DELETE";
             ErrorManager.recordOrThrowError(10096, msg);
             return false;
          }

          this.valid = false;

          ControlFlowOps.invalidateCallSiteCache(this);

          return true;
       }

       /**
        * Invoke the {@link #connectProcedure connect procedure}, as a new client has connected to
        * this socket.  If {@link #connectProcedure} is unknown, this is a no-op.
        *
        * @param    socket
        *           A handle referring the connected {@link SocketImpl client socket}.
        */
       private void invokeConnectProcedure(handle socket)
       {
          if (connectProcedure == null)
          {
             return;
          }

          // invoke the event procedure
          try
          {
             SelfManager.pushSelf(new handle(this));
             CONNECT_CALL_SITE.clone()
                              .setTarget(connectProcedure)
                              .setInHandle(new handle(connectProcedureContext))
                              .setModes("I")
                              .setArguments(socket)
                              .executeForResource(this);
          }
          finally
          {
             SelfManager.popSelf();
          }
       }

       /**
        * Utility method for parsing the {@link #enableConnections} parameters.
        *
        * @param    options
        *           The string to parse.
        *
        * @return   The parameter-to-value map or <code>null</code> if something could not be parsed.
        */
       private Map<String, String> parseOptions(character options)
       {
          final Set<String> knownOptions = new HashSet<String>();
          knownOptions.add("-S");
          knownOptions.add("-pf");
          knownOptions.add("-qsize");
          knownOptions.add("-ssl"); // no value
          knownOptions.add("-tls"); // no value
          knownOptions.add("-keyalias");
          knownOptions.add("-keyaliaspasswd");
          knownOptions.add("-nosessioncache"); // no value
          knownOptions.add("-sessiontimeout");

          final Set<String> noValueOptions = new HashSet<String>();
          noValueOptions.add("-ssl"); // no value
          noValueOptions.add("-tls"); // no value
          noValueOptions.add("-nosessioncache"); // no value

          final Map<String, Integer> intParams = new HashMap<String, Integer>();
          intParams.put("-qsize", 0);
          intParams.put("-sessiontimeout", 0);

          return ServerConnectHelper.parseOptions(options, knownOptions, noValueOptions, intParams);
       }
    }