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