WebServiceHandler.java
/*
** Module : WebServiceHandler.java
** Abstract : Handler for WebHandler calls.
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA 20190703 First version.
** 002 CA 20190710 Fixed a problem in addService, wrong WebHandler class was used.
** CA 20190723 The path matching is done case-insensitive.
** 003 CA 20200116 Javadoc fixes.
** GES 20200213 NPE protection in termination code.
** 004 CA 20200427 Fixed to allow session context details.
** 005 CA 20200514 Fixed error handling for handleWebService.
** 006 CA 20220405 Added authentication and authorization for web requests. When this is enabled, the target
** API call will be executed under the authenticated FWD context, and not the agent's context.
** 007 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 008 GBB 20230825 WebHandler renamed to WebDriverHandler.
** 009 CA 20240219 Target web handler resolution will use the explicit path match if it exists together with
** a path token match like '/Test' vs '/{Other}'.
** 010 GBB 20240610 LegacyAppServerWork replaced by Consumer<LegacyServiceWorker>.
** 011 CA 20240925 When matching paths do determine the web handler, first configuration matching this
** request's target will be resolved.
** Ensure the configurations are sorted.
** 012 GBB 20250403 Replace service type with enum constant.
** 013 RB2 20250530 The handle method now calls the handle method with an extra parameter, the request. This
** was done in order to handle multiple URLs in the Access-Control-Allow-Origin field
*/
/*
** 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.main;
import java.io.*;
import java.util.*;
import javax.servlet.http.*;
import com.goldencode.p2j.util.logging.*;
import org.eclipse.jetty.http.*;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.*;
import com.goldencode.p2j.util.*;
/**
* Specialized handler to process {@link com.goldencode.p2j.oo.web.WebHandler} paths.
* <p>
* <p>
* Service registration is being performed via {@link SourceNameMapper}, which loads the
* programs marked with <code>with-services</code>, via {@link #addService} API.
* <p>
* Any converted Java code mapped to a service must have a class-level {@link LegacyService}
* annotation. This annotation specifies one or more {@link LegacyWebPath paths} to be served by
* this handler.
* <p>
* The implicit basepath is <code>/web</code>, and the implicit timeout is 10 seconds (set to 0
* to disable it). This timeout is read from the appserver's timeout.
* <p>
* The values which need to be configured in the directory are:
* <ul>
* <li><code>WebHandler/basepath</code>, to specify the basepath.
* Defaults to <code>/web</code>.</li>
* <li><code>WebHandler/alias</code>, a process certificate alias associated with the workers
* which will process the WebHandler requests; these will delegate the call to the
* appserver. This context is automatically marked as HEADLESS.</li>
* <li><code>WebHandler/keyStore</code>, the key store with the certificate to authenticate
* this process. If is missing, the store will be built from the directory.</li>
* <li><code>WebHandler/storePassword</code>, the store password, only if
* <code>WebHandler/keyStore</code> is set.</li>
* <li><code>WebHandler/poolSize</code> - the pool size of workers. Defaults to 10.</li>
* <li><code>WebHandler/appserver</code> - the name of the appserver handling the requests.
* This appserver must be in the same FWD server as the handler (can't use a remote one).
* </li>
* </ul>
* <p>
* Each queue of workers is ordered by the number of pending tasks to be executed - for each
* incoming request, the worker with the smallest number of pending tasks will be chosen.
*/
public class WebServiceHandler
extends LegacyServiceHandler
{
/** The singleton instance for this handler. */
private static WebServiceHandler instance;
/** The context handler. */
private static ContextHandler handler;
/** The exposed WebHandler services. */
private Map<String, List<WebHandlerConfig>> services = new HashMap<>();
/** Logger. */
private static final CentralLogger LOG = CentralLogger.get(WebServiceHandler.class);
/**
* Initialize this handler with the 'WebHandler' type and '/web' default basepath.
*/
public WebServiceHandler()
{
super(ServiceType.WEB, "/web");
}
/**
* Initialize the WebHandler worker(s) and start the workers.
*
* @return The handler for WebHandler services.
*/
public static AbstractHandler initialize()
{
synchronized (WebServiceHandler.class)
{
if (handler != null)
{
return handler;
}
instance = new WebServiceHandler();
instance.initializeHandler();
handler = new ContextHandler();
handler.setContextPath(instance.getBasePath());
handler.setHandler(instance);
return handler;
}
}
/**
* Determine if this web service is configured to authenticate the APIs.
*
* @return <code>true</code> if the authentication is enabled for this web service, in the directory.
*/
public static boolean useWebServiceAuthentication()
{
synchronized (WebServiceHandler.class)
{
return instance != null && instance.useAuthorization();
}
}
/**
* Terminate the rest worker threads.
*/
public static void terminate()
{
synchronized (WebServiceHandler.class)
{
if (instance != null)
{
instance.terminateWorkers();
}
}
}
/**
* Register a legacy service associated with a {@link com.goldencode.p2j.oo.web.WebHandler}
* implementation.
*
* @param pls
* The annotation describing the service.
* @param cls
* The WebHandler implementation.
*/
public static void addService(LegacyService pls, Class<?> cls)
{
synchronized (WebServiceHandler.class)
{
if (instance == null)
{
LOG.warning("Trying to register WebHandler service " + pls.name() +
", but WebHandler service is not enabled.");
return;
}
}
if (!com.goldencode.p2j.oo.web.WebHandler.class.isAssignableFrom(cls))
{
throw new IllegalArgumentException(cls.toString() + " is not a sub-class of WebHandler!");
}
String section = pls.name().toLowerCase();
List<WebHandlerConfig> cfgs = instance.services.get(section);
if (cfgs == null)
{
instance.services.put(section, cfgs = new ArrayList<>());
}
for (LegacyWebPath path : pls.paths())
{
cfgs.add(new WebHandlerConfig(path.path(),
path.order(),
(Class<? extends WebDriverHandler>) cls));
}
// ensure they are sorted, 'initialize' is called to early, and no need for a 'postInitialize' as the
// size of the configs is small
Collections.sort(cfgs);
}
/**
* Code to process web requests.
*/
@Override
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException
{
if (baseRequest.isHandled())
{
return;
}
// first check the authentication
super.handle(target, baseRequest, request, response);
if (baseRequest.isHandled())
{
// authentication failed, nothing else to do
return;
}
baseRequest.setHandled(true);
String[] targetPaths = getPaths(target);
WebHandlerConfig found = null;
outer: for (String section : services.keySet())
{
List<WebHandlerConfig> cfgs = services.get(section);
for (WebHandlerConfig cfg : cfgs)
{
if (cfg.matches(targetPaths))
{
found = cfg;
break outer;
}
}
}
if (found == null)
{
response.setStatus(HttpStatus.NOT_FOUND_404);
response.getOutputStream().write(("Can't find service for path " + target).getBytes());
return;
}
WebHandlerConfig cfg = found;
LegacyAppServerWork work = (worker) ->
{
// now do the authorization - must be in the worker thread
String token;
if (useAuthorization())
{
token = authorize(target, request, response);
if (token == null)
{
return;
}
}
else
{
token = null;
}
try
{
AppServerInvocationResult result = worker.getHelper().handleWebService(instance.getTimeout(),
cfg.handler,
instance.getBasePath(),
cfg.paths,
token,
target,
request,
response);
if (result.getError() != null)
{
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
try
{
response.getOutputStream().write(result.getError().getMessage().getBytes());
}
catch (IOException e)
{
// ignore
}
}
}
finally
{
postHandle(token);
}
};
handle(instance, work, target, response, request);
}
/**
* A configuration for a WebHandler, as it was defined in <code>openedge.properties</code>.
* This will sort them based on the original <code>handler#</code> key.
*/
private static class WebHandlerConfig
implements Comparable<WebHandlerConfig>
{
/** The split path served by this handler.*/
private final String[] paths;
/** The order. */
private final int order;
/** The handler. */
private final Class<? extends WebDriverHandler> handler;
/**
* Configure this webhandler to service the given path.
*
* @param path
* The served path.
* @param order
* The path order.
* @param handler
* The handler class.
*/
public WebHandlerConfig(String path, int order, Class<? extends WebDriverHandler> handler)
{
this.paths = getPaths(path);
this.order = order;
this.handler = handler;
}
/**
* Compare this to another instance, by their {@link #order}.
*/
@Override
public int compareTo(WebHandlerConfig o)
{
return this.order - o.order;
}
/**
* Check if this configured web handler's paths match the given target. The match doesn't take into
* consideration if the configuration ends with a {@code /*} or not, this is always assumed - so, if
* the configuration is a prefix for this target (considering path parameters), then this will be a
* match.
*
* @param target
* The target path to match.
*
* @return See above.
*/
public boolean matches(String[] target)
{
int plen = paths.length;
if (plen == 0)
{
// here we match on anything
return true;
}
String last = paths[plen - 1];
if ("*".equals(last) || "**".equals(last))
{
plen = plen - 1;
}
if (plen == 0)
{
// match on anything
return true;
}
if (plen > target.length)
{
// if more configured paths then this target, we can't match, as configuration can't be a prefix
// for this configuration
return false;
}
// target can have more parts as in this configuration, so walk only the configured parts
for (int i = 0; i < plen; i++)
{
if (paths[i].startsWith("{"))
{
// this is a path parameter, we can match
continue;
}
if (!paths[i].equalsIgnoreCase(target[i]))
{
// this part doesn't match
return false;
}
// the current part matches, we can continue
}
// we reached here; it means all tokens in the configured path match, and if there is or not a '*'
// suffix doesn't matter, we always match
return true;
}
}
}