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