LegacySocketLibrary.java
/*
** Module : ABLSocketLibrary.java
** Abstract : Implementation of the builtin class.
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA 20190526 First version, stubs taken by converting the skeleton using FWD.
** 002 IAS 20190527 Added multipart support, got rid of "abl" in names.
** 003 CA 20191024 Added method support levels and updated the class support level.
** 004 IAS 20191108 Added HTTP Redirect support.
** 005 MP 20200602 Add STUB constructors, properties and methods requested by calls made
** by LegacySocketLibraryBuilder class.
** 006 ME 20210128 Add query parameters and fragment when executing request.
** 20210130 Fix call to 'next' iterator method.
** 20210210 Add support for proxy, use version info from request.
** CA 20210219 OO input parameter definitions for functions/procedures must not register with ObjectOps
** - TypeFactory.initInput will take care of this.
** 007 CA 20210221 Fixed 'qualified', 'extent' and 'returns' annotations at the legacy signature.
** 008 ME 20210426 Removed default ctor, add destructor to clean-up client socket and connect params.
** 009 ME 20210524 Added request timeout, improve error handling (4GL style).
** CA 20220120 Do not used TypeFactory.object when the OO reference must not be tracked or registered.
** Do not use TypeFactory.object for internal usages, use ObjectVar if the reference must
** be tracked.
** All TypeFactory.object variable definitions must be done outside of the top-level block.
** ME 20210929 Check for unknown port number.
** ME 20220918 Fix check for port number, should not be negative not unknown.
** CA 20220923 Variable definitions (including associated with parameters) must be done always outside of
** the BlockManager API
** CA 20221026 Do not pass null to 'readTerminatedHandler' call (all arguments must handle unknown as
** BDT instances).
** 010 CA 20230313 In 'execute', catch and throw any ConditionException (as there is a 'catch Exception'
** block for anything else).
** 011 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 012 CA 20230704 Added a name for the Timer thread and made it daemon.
** 013 CA 20230817 Removed ObjectOps.register, as this registration is handled by runtime.
** 014 SBI 20231002 Changed _execute(...) to fix url encoding for query.
** 015 CA 20231113 The 'execute' method must be annotated with LegacySignature Type.Execute, and also can be
** dropped if is a no-op.
** 016 AL2 20240930 Delete temporary ByteBuckets; they are still in memory due to positive reference count.
** 017 CA 20250515 'execute' must log any exception being thrown; also, Content-Length and Transfer-Encoding
** must not be written to the request, as Apache HttpClient will write them always by
** itself, when needed - if they appear twice, the request fails.
** 018 AL2 20250530 Added extra logging for failing requests without known cause.
*/
/*
** 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.oo.net.http.lib.sockets;
import static com.goldencode.p2j.util.BlockManager.externalProcedure;
import static com.goldencode.p2j.util.BlockManager.function;
import static com.goldencode.p2j.report.ReportConstants.*;
import static com.goldencode.p2j.util.BlockManager.internalProcedure;
import static com.goldencode.p2j.util.BlockManager.onBlockLevel;
import static com.goldencode.p2j.util.BlockManager.returnNormal;
import static com.goldencode.p2j.util.BlockManager.undoThrow;
import java.io.*;
import java.net.*;
import java.nio.charset.Charset;
import java.security.*;
import java.security.cert.*;
import java.util.*;
import java.util.logging.*;
import java.util.stream.Collectors;
import javax.net.ssl.*;
import com.goldencode.p2j.util.logging.*;
import org.apache.http.*;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolException;
import org.apache.http.client.*;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.*;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.*;
import org.apache.http.config.*;
import org.apache.http.conn.*;
import org.apache.http.conn.socket.*;
import org.apache.http.conn.ssl.*;
import org.apache.http.impl.client.*;
import org.apache.http.impl.conn.*;
import org.apache.http.protocol.*;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.oo.core.*;
import com.goldencode.p2j.oo.core.collections.IStringStringMap;
import com.goldencode.p2j.oo.core.collections.Iiterator;
import com.goldencode.p2j.oo.json.objectmodel.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.oo.logging.ISupportLogging;
import com.goldencode.p2j.oo.logging.VoidLogger;
import com.goldencode.p2j.oo.net.*;
import com.goldencode.p2j.oo.net.http.*;
import com.goldencode.p2j.oo.net.http.filter.payload.*;
import com.goldencode.p2j.oo.net.http.filter.writer.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.BlockManager.Action;
import com.goldencode.p2j.util.BlockManager.Condition;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.InternalEntry.*;
import com.goldencode.p2j.xml.*;
/**
*
* Performs actual HTTP request
*
*/
@LegacyResource(resource = "OpenEdge.Net.HTTP.Lib.ABLSockets.ABLSocketLibrary")
@LegacyResourceSupport(supportLvl = CVT_LVL_PARTIAL|RT_LVL_PARTIAL)
public class LegacySocketLibrary
extends com.goldencode.p2j.oo.lang.BaseObject
implements IHttpClientLibrary, ISupportLogging
{
@LegacySignature(type = Type.PROPERTY, name = "Logger")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
private object<? extends com.goldencode.p2j.oo.logging.IlogWriter> logger = TypeFactory.object(com.goldencode.p2j.oo.logging.IlogWriter.class);
@LegacySignature(type = Type.PROPERTY, name = "LibraryName")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
private character libraryName = TypeFactory.character();
@LegacySignature(type = Type.PROPERTY, name = "LibraryVersion")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
private character libraryVersion = TypeFactory.character();
@LegacySignature(type = Type.PROPERTY, name = "Options")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
private object<? extends com.goldencode.p2j.oo.net.http.ClientOptions> options = TypeFactory.object(com.goldencode.p2j.oo.net.http.ClientOptions.class);
private object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> moSocketLib = TypeFactory.object(com.goldencode.p2j.oo.net.serverconnection.ClientSocket.class);
private object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocketConnectionParameters> moSocketConnectionParam =
TypeFactory.object(com.goldencode.p2j.oo.net.serverconnection.ClientSocketConnectionParameters.class);
/** The root node for configuration */
private static final String CONFIG_ROOT = "/security/http-client";
/** Buffer size */
private static final int BUFSIZE = 4096;
/** logger */
private static final CentralLogger LOG = CentralLogger.get(LegacySocketLibrary.class.getName());
/** SSL Context */
private static final ContextLocal<SSLContext> SSL_CONTEXT = new ContextLocal<SSLContext>()
{
/**
* Create initial value
*/
@Override
protected SSLContext initialValue()
{
try
{
return getSSLContext();
}
catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException
| CertificateException | IOException e)
{
throw new RuntimeException("Failed to create SSLContext", e);
}
}
};
/** HttpClientConnectionManager */
private static final ContextLocal<HttpClientConnectionManager> CONNECTION_NANAGER =
new ContextLocal<HttpClientConnectionManager>()
{
/**
* Create initial value
*/
@Override
protected HttpClientConnectionManager initialValue()
{
return getHttpClientConnectionManager(SSL_CONTEXT.get());
}
};
/**
* Create SSLContext
*
* @return SSLContext
*
* @throws KeyManagementException
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws CertificateException
* @throws IOException
*/
private static SSLContext getSSLContext()
throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException,
CertificateException, IOException {
DirectoryService ds = DirectoryService.getInstance();
if (!ds.bind())
{
throw new RuntimeException("Directory bind failed");
}
Boolean acceptSelfSigned = ds.getNodeBoolean(CONFIG_ROOT + "/accept-self-signed", "value");
Boolean acceptAll = ds.getNodeBoolean(CONFIG_ROOT + "/accept-all", "value");
String ksFile = ds.getNodeString(CONFIG_ROOT + "/keystore/file", "value");
String ksPwd = ds.getNodeString(CONFIG_ROOT + "/keystore/password", "value");
TrustStrategy trustStrategy;
if (!Boolean.FALSE.equals(acceptAll))
{
LOG.log(Level.INFO, "Http client accepts all certificates");
trustStrategy = new TrustStrategy()
{
@Override
public boolean isTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
return true;
}
};
}
else if (!Boolean.FALSE.equals(acceptSelfSigned))
{
LOG.log(Level.INFO, "Http client accepts self-signed certificates");
trustStrategy = TrustSelfSignedStrategy.INSTANCE;
}
else
{
trustStrategy = new TrustStrategy()
{
@Override
public boolean isTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
return false;
}
};
}
if (ksFile != null)
{
File ks = new File(ksFile);
LOG.log(Level.INFO, "Using trusted keystrore: " + ks.getAbsolutePath());
return new SSLContextBuilder().loadTrustMaterial(ks,
ksPwd != null ? ksPwd.toCharArray() : null , trustStrategy).build();
}
return new SSLContextBuilder().loadTrustMaterial(trustStrategy).build();
}
/**
* Create HttpClientConnectionManager
* @param sslContext SSLContet
*
* @return HttpClientConnectionManager
*/
private static HttpClientConnectionManager getHttpClientConnectionManager(SSLContext sslContext)
{
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
SSLConnectionSocketFactory sslSocketFactory =
new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
org.apache.http.config.Registry<ConnectionSocketFactory> socketFactoryRegistry =
RegistryBuilder.<ConnectionSocketFactory>create().
register("http", PlainConnectionSocketFactory.getSocketFactory()).
register("https", sslSocketFactory).
build();
return new PoolingHttpClientConnectionManager(socketFactoryRegistry);
}
/**
* Get Library name.
*/
@LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "LibraryName")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
@Override
public character getLibraryName()
{
return function(LegacySocketLibrary.class, this, "LibraryName", character.class, new Block((Body) () ->
{
returnNormal(libraryName);
}));
}
/**
* Get Library version
*/
@LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "LibraryVersion")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
@Override
public character getLibraryVersion()
{
return function(LegacySocketLibrary.class, this, "LibraryVersion", character.class, new Block((Body) () ->
{
returnNormal(libraryVersion);
}));
}
/**
* Get Client options
*/
@LegacySignature(returns = "OBJECT", qualified = "OpenEdge.Net.HTTP.ClientOptions", type = Type.GETTER, name = "Options")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
@Override
public object<? extends com.goldencode.p2j.oo.net.http.ClientOptions> getOptions()
{
return function(LegacySocketLibrary.class, this, "Options", object.class, new Block((Body) () ->
{
returnNormal(options);
}));
}
/**
* Get Client logger
*/
@LegacySignature(returns = "OBJECT", qualified = "OpenEdge.Logging.ILogWriter", type = Type.GETTER, name = "Logger")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_PARTIAL)
@Override
public object<? extends com.goldencode.p2j.oo.logging.IlogWriter> getLogger()
{
return function(LegacySocketLibrary.class, this, "Logger", object.class, new Block((Body) () ->
{
// TODO: use logger builder when available instead of void logger
if (!logger._isValid())
logger.assign(ObjectOps.newInstance(VoidLogger.class));
returnNormal(logger);
}));
}
/**
* Set Client logger
*/
@Override
@LegacySignature(type = Type.SETTER, name = "Logger", parameters =
{
@LegacyParameter(qualified = "OpenEdge.Logging.ILogWriter", name = "var", type = "OBJECT", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
public void setLogger(final object<? extends com.goldencode.p2j.oo.logging.IlogWriter> _var)
{
object<? extends com.goldencode.p2j.oo.logging.IlogWriter> var = TypeFactory.initInput(_var);
internalProcedure(LegacySocketLibrary.class, this, "Logger", new Block((Body) () ->
{
logger.assign(var);
}));
}
/**
* Execute HTTP request
*
* @param _poRequest HTTP request
* @param _poResponse HTTP response
*/
@LegacySignature(type = Type.METHOD, name = "Execute", parameters =
{
@LegacyParameter(name = "poRequest", type = "OBJECT",
qualified = IHTTPREQUEST, mode = "INPUT"),
@LegacyParameter(name = "poResponse", type = "OBJECT",
qualified = IHTTPRESPONSE, mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
@Override
public void execute(object<? extends IhttpRequest> _poRequest, object<? extends IhttpResponse> _poResponse)
{
object<? extends IhttpRequest> poRequest = TypeFactory.initInput(_poRequest);
object<? extends IhttpResponse> poResponse = TypeFactory.initInput(_poResponse);
object<? extends Uri> proxyUri = TypeFactory.object(Uri.class);
object<? extends MessageWriter> entityWriter = TypeFactory.object(MessageWriter.class);
object[][] headers = { TypeFactory.objectExtent(HttpHeader.class, false) };
internalProcedure(this, "execute", new Block((Body) () ->
{
Assert.notNull(poRequest, new character("HTTP request"));
Assert.notNull(poResponse, new character("HTTP response"));
integer port = poRequest.ref().getUri().ref().getPort();
// too high port number checked by client socket probably, comes before host check
if (!port.isUnknown() && port.intValue() > 65535)
{
undoThrow(AppError.newInstance(String.format("Port is too large: %d", port.intValue()), 0));
}
character host = poRequest.ref().getUri().ref().getHost();
// spaces in host name is breaking the connection params (-H xxxx yyyy)
if (TextOps.numEntries(host, " ").intValue() > 1)
{
ErrorManager.recordOrThrowError(new int[] { 301, 5509, 5510 },
new String[] {
String.format("** Could not recognize argument: %s",
TextOps.entry(2, host, " ").toStringMessage()),
"Unable to process parameters", "Invalid parameter string" },
false);
}
// negative port is breaking the connection params (-S -xxxx)
if (!port.isUnknown() && port.intValue() < 0)
{
ErrorManager.recordOrThrowError(new int[] { 1403, 5509, 5510 },
new String[] { "You have not supplied a parameter for argument -S",
"Unable to process parameters", "Invalid parameter string" },
false);
}
try
{
_execute(poRequest, poResponse, proxyUri, entityWriter, headers);
}
catch (UnknownHostException e)
{
undoThrow(SysError.newInstance(String.format("Unknown hostname %s",
poRequest.ref().getUri().ref().getHost()), 5482, false, true));
}
catch (HttpHostConnectException e)
{
undoThrow(SysError.newInstance(
String.format("Connection failure for host %s port %d transport TCP",
e.getHost().getHostName(), e.getHost().getPort()),
9407, false, true));
}
catch (HttpException exc)
{
LOG.log(Level.SEVERE, "HttpException while executing HTTP request", exc);
// request terminated
readTerminatedHandler(new object(), new object());
}
catch (IOException e)
{
LOG.log(Level.WARNING, "IOException while executing HTTP request", e);
// request terminated
readTerminatedHandler(new object(), new object());
}
catch (ConditionException cex)
{
throw cex;
}
catch (Exception e)
{
String msg = String.format("Unknown exception: %s", e.getMessage());
if (LOG.isLoggable(Level.SEVERE))
{
LOG.severe(msg, e);
}
undoThrow(AppError.newInstance(msg, 0));
}
}));
}
/**
* Destructor
*/
@LegacySignature(type = Type.DESTRUCTOR)
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
public void __net_http_lib_sockets_LegacySocketLibrary_destructor__()
{
internalProcedure(this, "__net_http_lib_ablsockets_ABLSocketLibrary_destructor__", new Block((Body) () ->
{
moSocketLib.setUnknown();
moSocketConnectionParam.setUnknown();
}));
}
@LegacySignature(type = Type.CONSTRUCTOR, parameters =
{
@LegacyParameter(name = "pcLibraryName", type = "CHARACTER", mode = "INPUT"),
@LegacyParameter(name = "pcLibraryVersion", type = "CHARACTER", mode = "INPUT"),
@LegacyParameter(name = "poSocket", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
@LegacyParameter(name = "poSocketConnectionParams", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocketconnectionparameters", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
public void __net_http_lib_sockets_LegacySocketLibrary_constructor__(
final character _pcLibraryName,
final character _pcLibraryVersion,
final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _poSocket,
final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocketConnectionParameters> _poSocketConnectionParams)
{
__net_http_lib_sockets_LegacySocketLibrary_constructor__(_pcLibraryName, _pcLibraryVersion, _poSocket, _poSocketConnectionParams,
ObjectOps.newInstance(com.goldencode.p2j.oo.net.http.ClientOptions.class));
}
@LegacySignature(type = Type.CONSTRUCTOR, parameters =
{
@LegacyParameter(name = "pcLibraryName", type = "CHARACTER", mode = "INPUT"),
@LegacyParameter(name = "pcLibraryVersion", type = "CHARACTER", mode = "INPUT"),
@LegacyParameter(name = "poSocket", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
@LegacyParameter(name = "poSocketConnectionParams", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocketconnectionparameters", mode = "INPUT"),
@LegacyParameter(name = "poOptions", type = "OBJECT", qualified = "openedge.net.http.clientoptions", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
public void __net_http_lib_sockets_LegacySocketLibrary_constructor__(
final character _pcLibraryName,
final character _pcLibraryVersion,
final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _poSocket,
final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocketConnectionParameters> _poSocketConnectionParams,
final object<? extends com.goldencode.p2j.oo.net.http.ClientOptions> _poOptions)
{
character pcLibraryName = TypeFactory.initInput(_pcLibraryName);
character pcLibraryVersion = TypeFactory.initInput(_pcLibraryVersion);
object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> poSocket = TypeFactory.initInput(_poSocket);
object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocketConnectionParameters> poSocketConnectionParams = TypeFactory.initInput(_poSocketConnectionParams);
object<? extends com.goldencode.p2j.oo.net.http.ClientOptions> poOptions = TypeFactory.initInput(_poOptions);
internalProcedure(LegacySocketLibrary.class, this, "__net_http_lib_ablsockets_ABLSocketLibrary_constructor__", new Block((Body) () ->
{
__lang_BaseObject_constructor__();
Assert.notNull(poSocket, new character("Client socket"));
Assert.notNull(poSocketConnectionParams, new character("Default connection parameters"));
Assert.notNullOrEmpty(pcLibraryName, new character("Library Name"));
Assert.notNullOrEmpty(pcLibraryVersion, new character("Library Version"));
Assert.notNull(poOptions, new character("Client options"));
libraryName.assign(pcLibraryName);
libraryVersion.assign(pcLibraryVersion);
options.assign(poOptions);
moSocketLib.assign(poSocket);
moSocketConnectionParam.assign(poSocketConnectionParams);
}));
}
/**
* Execute method
*/
@LegacySignature(type = Type.EXECUTE)
public void __net_http_lib_sockets_LegacySocketLibrary_execute__()
{
onBlockLevel(Condition.ERROR, Action.THROW);
}
@LegacySignature(type = Type.METHOD, name = "CreateTunnel", parameters =
{
@LegacyParameter(name = "pProxyReq", type = "OBJECT", qualified = "openedge.net.http.isupportproxy", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_STUB)
protected void createTunnel(final object<? extends com.goldencode.p2j.oo.net.http.ISupportProxy> _pProxyReq)
{
object<? extends com.goldencode.p2j.oo.net.http.ISupportProxy> pProxyReq = TypeFactory.initInput(_pProxyReq);
internalProcedure(LegacySocketLibrary.class, this, "CreateTunnel", new Block((Body) () ->
{
UnimplementedFeature.missing("ABLSocketLibrary:CreateTunnel METHOD");
}));
}
@LegacySignature(type = Type.METHOD, name = "DataReceivedHandler", parameters =
{
@LegacyParameter(name = "poSender", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
@LegacyParameter(name = "poEventArgs", type = "OBJECT", qualified = "openedge.net.serverconnection.socketreadeventargs", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_STUB)
public void dataReceivedHandler(final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _poSender, final object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> _poEventArgs)
{
object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> poSender = TypeFactory.initInput(_poSender);
object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> poEventArgs = TypeFactory.initInput(_poEventArgs);
internalProcedure(LegacySocketLibrary.class, this, "DataReceivedHandler", new Block((Body) () ->
{
UnimplementedFeature.missing("ABLSocketLibrary:DataReceivedHandler METHOD");
}));
}
@LegacySignature(type = Type.METHOD, name = "MakeSyncRequest", parameters =
{
@LegacyParameter(name = "pServer", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
@LegacyParameter(name = "pRequest", type = "OBJECT", qualified = "openedge.net.http.ihttprequest", mode = "INPUT"),
@LegacyParameter(name = "pResponse", type = "OBJECT", qualified = "openedge.net.http.ihttpresponse", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_STUB)
protected void makeSyncRequest(final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _pServer, final object<? extends com.goldencode.p2j.oo.net.http.IhttpRequest> _pRequest, final object<? extends com.goldencode.p2j.oo.net.http.IhttpResponse> _pResponse)
{
object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> pServer = TypeFactory.initInput(_pServer);
object<? extends com.goldencode.p2j.oo.net.http.IhttpRequest> pRequest = TypeFactory.initInput(_pRequest);
object<? extends com.goldencode.p2j.oo.net.http.IhttpResponse> pResponse = TypeFactory.initInput(_pResponse);
internalProcedure(LegacySocketLibrary.class, this, "MakeSyncRequest", new Block((Body) () ->
{
UnimplementedFeature.missing("ABLSocketLibrary:MakeSyncRequest METHOD");
}));
}
@LegacySignature(type = Type.METHOD, name = "ReadTerminatedHandler", parameters =
{
@LegacyParameter(name = "poSender", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
@LegacyParameter(name = "poEventArgs", type = "OBJECT", qualified = "openedge.net.serverconnection.socketreadeventargs", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
public void readTerminatedHandler(final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _poSender, final object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> _poEventArgs)
{
object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> poSender = TypeFactory.initInput(_poSender);
object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> poEventArgs = TypeFactory.initInput(_poEventArgs);
internalProcedure(LegacySocketLibrary.class, this, "ReadTerminatedHandler", new Block((Body) () ->
{
undoThrow(AppError.newInstance(TextOps.substitute("Read terminated for &1",
moSocketConnectionParam.ref().getUri().ref().toLegacyString()), new integer(105)));
}));
}
@LegacySignature(type = Type.METHOD, name = "ReadTimeoutHandler", parameters =
{
@LegacyParameter(name = "poSender", type = "OBJECT", qualified = "openedge.net.serverconnection.clientsocket", mode = "INPUT"),
@LegacyParameter(name = "poEventArgs", type = "OBJECT", qualified = "openedge.net.serverconnection.socketreadeventargs", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
public void readTimeoutHandler(final object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> _poSender, final object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> _poEventArgs)
{
object<? extends com.goldencode.p2j.oo.net.serverconnection.ClientSocket> poSender = TypeFactory.initInput(_poSender);
object<? extends com.goldencode.p2j.oo.net.serverconnection.SocketReadEventArgs> poEventArgs = TypeFactory.initInput(_poEventArgs);
internalProcedure(LegacySocketLibrary.class, this, "ReadTimeoutHandler", new Block((Body) () ->
{
undoThrow(ObjectOps.newInstance(HttpRequestError.class, "III",
new character(StatusCodeEnum.requestTimeOut),
new character("read"),
moSocketConnectionParam.ref().getUri().ref().toLegacyString()));
}));
}
/**
* Execute HTTP request and polulate HTTP response
*
* @param req
* request
* @param rsp
* response
* @throws Exception
*/
private void _execute(object<? extends IhttpRequest> req,
object<? extends IhttpResponse> rsp,
object<? extends Uri> proxyUri,
object<? extends MessageWriter> entityWriter,
object[][] headers)
throws Exception
{
Uri uri = req.ref().getUri().ref();
String method = req.ref().getMethod().getValue();
String baseUri = uri.getBaseUri().getValue();
String path = uri.getPath().getValue();
String scheme = uri.getScheme().getValue();
String host = uri.getHost().getValue();
//URI java class will assign -1 to port if is not defined
//we change it to unknown to match our logic
int port = -1;
if (!uri.getPort().isUnknown())
{
port = uri.getPort().intValue();
}
java.net.URI _uri;
URIBuilder builder = new URIBuilder().setScheme(scheme).setHost(host).setPort(port)
.setPath(path);
String encoding = I18nOps.getJavaCharset(req.ref().getCharacterEncoding());
if (encoding != null && Charset.isSupported(encoding))
builder.setCharset(Charset.forName(encoding));
object<? extends IStringStringMap> qryMap = uri.getQueryMap_1();
if (qryMap._isValid() && qryMap.ref().getSize().intValue() > 0)
{
object<? extends Iiterator> iterator = qryMap.ref().getKeySet().ref().iterator();
while (iterator.ref().hasNext().booleanValue())
{
object<? extends _BaseObject_> key = iterator.ref().next_();
object<? extends _BaseObject_> value = qryMap.ref().get(key);
builder.addParameter(key.ref().toLegacyString().getValue(),
Uri.decode(value.ref().toLegacyString(), new character(encoding)).getValue());
}
}
String fragment = uri.getFragment().getValue();
if (!TextOps.isEmpty(fragment))
builder.setFragment(fragment);
_uri = builder.build();
String uriAscii = _uri.toASCIIString();
uriAscii = uriAscii.replaceAll("%3A", ":");//4GL does not encode colons in query string.
org.apache.http.client.methods.RequestBuilder requestBuilder = org.apache.http.client.methods.RequestBuilder
.create(method).setUri(uriAscii);
if (req.ref().hasHeader(new character("Content-Type")).booleanValue())
{
HttpEntity httpEntity = createEntity(req);
requestBuilder.setEntity(httpEntity);
}
setHeaders(req, requestBuilder, headers);
String[] ver = req.ref().getVersion().getValue().split("[/.]+");
int majVer = ver.length > 1 ? Integer.parseInt(ver[1]) : 1;
int minVer = ver.length > 2 ? Integer.parseInt(ver[2]) : 1;
ProtocolVersion pv = new ProtocolVersion(ver[0], majVer, minVer);
requestBuilder.setVersion(pv);
HttpUriRequest request = requestBuilder.build();
if (ObjectOps.typeOf(req, ISupportProxy.class).booleanValue())
{
proxyUri.assign(ObjectOps.cast(req, ISupportProxy.class).ref().getProxyURI());
}
org.apache.http.client.HttpClient client = createHttpClient(proxyUri);
// request hard timeout
double reqTimeout = options.ref().getRequestTimeout().doubleValue();
if (reqTimeout > 0)
{
Timer timer = new Timer("Request timeout timer", true);
TimerTask task = new TimerTask()
{
@Override
public void run()
{
if (request != null)
{
request.abort();
}
timer.cancel();
}
};
timer.schedule(task, (long) (reqTimeout * 1000));
}
HttpResponse response = client.execute(request);
StatusLine sline = response.getStatusLine();
rsp.ref().setStatusCode(new integer(sline.getStatusCode()));
rsp.ref().setStatusReason(new character(sline.getReasonPhrase()));
rsp.ref().setVersion(new character(
sline.getProtocolVersion().toString()));
extractCookies(response, rsp.ref());
extractHeaders(response, rsp.ref());
if (!"HEAD".equalsIgnoreCase(method))
{
try
{
extractEntity(response, rsp, entityWriter);
}
catch (IOException e)
{
LOG.log(Level.WARNING, "HTTP response processing failed", e);
rsp.ref().setStatusCode(new integer(500));
rsp.ref().setStatusReason(new character("HTTP response processing failed"));
}
}
}
/**
* Create HTTP client
*
* @return Apache HTTPClient
*/
private org.apache.http.client.HttpClient createHttpClient(object<? extends Uri> proxyUri)
{
SSLContext sslContext = SSL_CONTEXT.get();
HttpClientConnectionManager connectionManager = CONNECTION_NANAGER.get();
HttpClientBuilder builder = HttpClientBuilder.create().
setConnectionManager(connectionManager).
// Redirect should be handled by the RequstStatusFilter for all methods
setRedirectStrategy(new RedirectStrategy()
{
@Override
public boolean isRedirected(HttpRequest arg0, HttpResponse arg1, HttpContext arg2)
throws ProtocolException
{
return false;
}
@Override
public HttpUriRequest getRedirect(HttpRequest arg0, HttpResponse arg1, HttpContext arg2)
throws ProtocolException
{
return null;
}
}).
setSSLContext(sslContext).
setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build());
if (proxyUri._isValid())
{
HttpHost proxy = new HttpHost(proxyUri.ref().getHost().toStringMessage(), proxyUri.ref().getPort().intValue());
builder.setProxy(proxy);
}
int reqTimeout = options.ref().getRequestTimeout().intValue() * 1000;
if (reqTimeout > 0)
{
builder.setDefaultRequestConfig(RequestConfig.custom().setConnectTimeout(reqTimeout)
.setConnectionRequestTimeout(reqTimeout).build());
}
return builder.build();
}
/**
* Set request headers
*
* @param req HTTP request
* @param requestBuilder Apache HTTP request builder
*/
private void setHeaders(object<? extends IhttpRequest> req,
org.apache.http.client.methods.RequestBuilder requestBuilder,
object[][] headers)
{
int nh = req.ref().getHeaders(
new OutputExtentParameter<object<? extends HttpHeader>>()
{
@Override
public object<? extends HttpHeader>[] getVariable()
{
return headers[0];
}
@Override
public void setVariable(object<? extends HttpHeader>[] reference)
{
headers[0] = reference;
}
}).intValue();
Set<String> unsupported = new HashSet<>(Arrays.asList("Content-Length", "Transfer-Encoding"));
for (int hn = 0; hn < nh; hn++)
{
HttpHeader hdr = (HttpHeader) headers[0][hn].ref();
if (!hdr.getName().isUnknown() && unsupported.contains(hdr.getName().toJavaType()))
{
LOG.log(Level.WARNING,
"FWD HttpClient does not allow header " + hdr.getName().toJavaType() +
" - will not be sent to the request.");
continue;
}
String hdrParams = hdr.getParameterValues().getValue();
String value = hdrParams.isEmpty() ? hdr.getValue().getValue() : hdr.getBaseValue().getValue() + hdrParams;
requestBuilder.addHeader(hdr.getName().getValue(), value);
}
}
/**
* Create request body
*
* @param req HTTP request
*
* @return Apache HTTP request entity
*/
private HttpEntity createEntity(object<? extends IhttpRequest> req)
{
object<? extends _BaseObject_> entity = req.ref().getEntity();
if (entity.isValid().booleanValue())
{
object<? extends ByteBucket> bb = null;
object<? extends MessageWriter> writer = null;
try
{
character boundary = TypeFactory.character();
if (ObjectOps.typeOf(entity, ISupportMultipartEntity.class).booleanValue())
{
boundary.assign(
ObjectOps.cast(entity, ISupportMultipartEntity.class).ref().getBoundary()
);
req.ref().getHeader(new character("Content-Type")).ref().
setParameterValue(new character("boundary"), boundary);
}
bb = ByteBucket.instance();
writer = BodyWriterBuilder.build_1(req).ref().writeTo(bb).ref().getWriter();
if (ObjectOps.typeOf(writer, ISupportMultipartEntity.class).booleanValue())
{
ObjectOps.cast(writer, ISupportMultipartEntity.class).ref().
setBoundary(boundary);
}
writer.ref().open();
writer.ref().write(entity);
writer.ref().close();
int64 clen = bb.ref().getSize();
int64 ptr = bb.ref().getBytes().ref().getPointerValue();
memptr mptr = TypeFactory.memptr();
mptr.setPointerValue(ptr);
mptr.setLength(clen);
return EntityBuilder.create().setBinary(mptr.getByteArray()).build();
}
finally
{
if (bb != null && bb._isValid())
{
ObjectOps.delete(bb);
}
if (writer != null && writer._isValid())
{
ObjectOps.delete(writer);
}
}
}
return EntityBuilder.create().setBinary(new byte[0]).build();
}
/**
* Extract Entity from the response
*
* @param response Apache HTTP response
* @param rsp OE HTTP response
*
* @throws UnsupportedOperationException
* @throws IOException
*/
private void extractEntity(HttpResponse response, object<? extends IhttpResponse> rsp, object<? extends MessageWriter> entityWriter)
throws UnsupportedOperationException, IOException
{
object<? extends ByteBucket> rspBody = ByteBucket.instance();
try
{
InputStream is = response.getEntity().getContent();
byte[] bb = new byte[BUFSIZE];
int len = is.read(bb);
while(len >= 0)
{
if (len > 0)
{
rspBody.ref().putBytes(new memptr(Arrays.copyOf(bb, len)));
}
len = is.read(bb);
}
is.close();
entityWriter.assign(EntityWriterBuilder.build_1(rsp).ref().getWriter());
if (!entityWriter.isValid().booleanValue())
{
return;
}
integer clen = TypeFactory.integer();
entityWriter.ref().open();
clen.assign(entityWriter.ref().write(rspBody));
entityWriter.ref().close();
rsp.ref().setEntity(entityWriter.ref().getEntity());
rsp.ref().setContentLength(clen);
}
finally
{
if (rspBody._isValid())
{
ObjectOps.delete(rspBody);
}
}
}
/**
* Extract Headers from the response
*
* @param response Apache HTTP response
* @param rsp OE HTTP response
*/
private void extractHeaders(HttpResponse response, IhttpResponse rsp)
{
Arrays.stream(response.getAllHeaders())
.filter(hdr -> !hdr.getName().equalsIgnoreCase("set-cookie"))
.map(this::makeHeader)
.forEach(rsp::setHeader);
object<? extends HttpHeader> contentType =
rsp.getHeader(new character(HttpHeaders.CONTENT_TYPE));
if (!ObjectOps.typeOf(contentType, NullHeader.class).booleanValue())
{
rsp.setContentType(contentType.ref().getValue());
}
}
/**
* Create OE HTTP header from Apache one
*
* @param hdr Apache HTTP Header
*
* @return OE HTTP Header
*/
private object<? extends HttpHeader> makeHeader(Header hdr)
{
object<? extends HttpHeaderBuilder> builder =
HttpHeaderBuilder.build(new character(hdr.getName()));
builder.assign(builder.ref().value(new character(hdr.getValue())));
return builder.ref().getHeader();
}
/**
* Extract payload from the HTTP response
*
* @param response Apache HTTP response
* @param contentType response content type
*
* @return response payload
* @throws IOException
*/
private object<? extends _BaseObject_> getPayload(HttpResponse response, String contentType)
throws IOException
{
int paramPos = contentType.indexOf(';');
if (paramPos > 0)
{
contentType = contentType.substring(0, paramPos);
}
HttpEntity entity = response.getEntity();
switch (contentType)
{
case "application/json":
String json = EntityUtils.toString(entity);
object<? extends ObjectModelParser> parser =
ObjectOps.newInstance(ObjectModelParser.class);
return parser.ref().parse(new longchar(json));
case "text/xml":
byte[] xml = EntityUtils.toByteArray(entity);
memptr ptr = new memptr(xml);
XDocumentImpl doc = new XDocumentImpl();
doc.load("memptr", ptr, false);
return ObjectOps.newInstance(WidgetHandle.class, "I", new handle(doc));
case "text/plain":
String text = EntityUtils.toString(entity);
return ObjectOps.newInstance(LegacyString.class, "I", new longchar(text));
case "application/octet-stream":
default:
raw bytes = new raw(EntityUtils.toByteArray(entity));
return ObjectOps.newInstance(Memptr.class, "I", bytes);
}
}
/**
* Extract Cookies from the response
*
* @param response Apache HTTP response
* @param rsp OE HTTP response
*/
private void extractCookies(HttpResponse response, IhttpResponse rsp)
{
List<Header> headerList = Arrays.asList(response.getAllHeaders());
List<Header> cookieHeaders = headerList.stream()
.filter(p -> p.getName().equalsIgnoreCase("set-cookie"))
.collect(Collectors.toList());
cookieHeaders
.stream()
.map(cookieHeader -> makeCookie(cookieHeader))
.forEach(parsedCookie -> rsp.setCookie(parsedCookie));
}
/**
* Create OE HTTP Cookie from Apache one
*
* @param hdr Apache HTTP Header
*
* @return OE HTTP Cookie
*/
private object<? extends Cookie> makeCookie(Header hdr)
{
object<? extends Cookie> buildCookie =
Cookie.parse(new character(hdr.getValue()));
return buildCookie;
}
}