WebServer.java
/*
** Module : WebServer.java
** Abstract : provides an embedded web server based on Jetty
**
** Copyright (c) 2009-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description----------------------
** 001 JJC 20090107 @41129 Created the first version of the file.
** 002 JJC 20090114 @41130 Add initialize() so WebServer can be started
** by the StandardServer.loadStartup().
** 003 NVS 20090304 @41399 Reworked and brought to compliance with the
** P2J coding standards. Renamed.
** 004 NVS 20090326 @41692 Added support for request parameters. They
** are coded using "?p1&p2..." syntax in URL.
** The "debug" parameter, if specified, causes
** the applet tag to specify JVM parameters for
** creating a debug mode applet's JVM.
** 005 SIY 20090513 @42158 Use daemon threads instead of regular ones to
** prevent server hang after graceful shutdown.
** 006 NVS 20090601 @42512 A custom account extension client side plugin is
** added to the ARCHIVE keyword of the applet page,
** if the P2J system is configured with one.
** 007 NVS 20090602 @42544 Fixed #006 to provide the archive file name, not
** the class name.
** 008 CA 20090603 @42554 Fixed #007: a NPE error when no plugin is provided.
** 009 NVS 20090615 @42695 Protected jetty server from serving arbitrary
** files. Now only the admin base and extension jars
** will be served through jetty.
** 010 NVS 20090617 @42722 Fixed a NPE when there is no admin extensions.
** The admin server won't start if the main p2jadmin
** jar is not found.
** 011 NVS 20090617 @42725 Fixed the client main jar file search procedure
** to avoid false positives for the p2j.jar.
** 012 NVS 20090821 @43712 Admin port is now searched correctly scoped to the
** per server scope.
** 013 GES 20100421 @44830 Handle host address/name better by using the
** base_request instead of request for getting the
** "local name".
** 014 GES 20110929 Moved this code here from AdminGate. Refactored it
** to be more generic.
** 015 CS 20130213 Added conversion support for Server, disambiguated
** the Server class.
** 016 MAG 20131101 Upgrade to jetty 9.1 embedded server.
** 017 MAG 20131108 Add WebSocket's capabilities by extending the
* new GenericWebServer.
** 018 MAG 20140217 Add terminate method.
** 019 SBI 20171116 Changed to set ServerKeyStore.
** 020 CA 20200514 Added getURL(), to return the URL and path served by this server.
** 021 SBI 20210413 Added getWebContentHandlers() to public static content.
** 20210621 Updated usages of Utils.getDirectoryNodeBeanList
** 20210812 Added createContextHandler() method to encapsulate and reuse this logic.
** 20211003 Moved common constants to HttpConfigurationConstants.
** 022 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 023 SBI 20231208 Fixed the default idle timeout for web server http configuration and replaced coded
** parameters with corresponding defined constants.
** 024 GBB 20240425 searchResourceJars() moved to JarUtil.
** 025 GBB 20240709 Moving here constants from HttpConfigurationConstants, no longer used anywhere else.
*/
/*
** 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.net.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import com.goldencode.p2j.util.logging.*;
import org.eclipse.jetty.server.handler.*;
import org.eclipse.jetty.util.resource.*;
import com.goldencode.p2j.classloader.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.web.*;
/**
* Provides an embedded web server based on Jetty 9.1
* <p>
* In particular, Jetty will be started using keystore info from p2j config
* that is already in memory.
*/
public class WebServer
extends GenericWebServer
{
/** HTTPS default port for listening. */
public static final int HTTPS_PORT = 9443;
/** Anonymous log instance. */
private static final CentralLogger LOG = CentralLogger.get(WebServer.class.getName());
/** The "maxOutputBufferSize" parameter name */
private static final String MAX_OUTPUT_BUFFER_SIZE_PARAM = "maxOutputBufferSize";
/** The "maxOutputAggregationSize" parameter name */
private static final String MAX_OUTPUT_AGGREGATION_SIZE_PARAM = "maxOutputAggregationSize";
/** The "webServerMaxIdleTimeout" parameter name */
private static final String WEB_SERVER_MAX_IDLE_TIMEOUT_PARAM = "webServerMaxIdleTimeout";
/** The "maxResponseHeaderSize" parameter name */
private static final String MAX_RESPONSE_HEADER_SIZE_PARAM = "maxResponseHeaderSize";
/** The "maxRequestHeaderSize" parameter name */
private static final String MAX_REQUEST_HEADER_SIZE_PARAM = "maxRequestHeaderSize";
/** Singleton instance of this class. */
private static WebServer instance = null;
/** Server port in use. */
private int port = HTTPS_PORT;
/**
* Constructor.
*
* @param handlers
* List of handlers to register.
*
* @throws Exception
* If something goes wrong.
*/
private WebServer(org.eclipse.jetty.server.Handler[] handlers)
throws Exception
{
// server-scoped directory search for the admin port number
port = Utils.getDirectoryNodeInt(null, "adminPort", HTTPS_PORT, false);
int maxOutputBufferSize = Utils.getDirectoryNodeInt(null,
MAX_OUTPUT_BUFFER_SIZE_PARAM,
OUTPUT_BUFFER_SIZE,
false);
getHttpConfiguration().setOutputBufferSize(maxOutputBufferSize);
int maxOutputAggregationSize = Utils.getDirectoryNodeInt(null,
MAX_OUTPUT_AGGREGATION_SIZE_PARAM,
OUTPUT_AGGREGATION_SIZE,
false);
if (maxOutputAggregationSize != OUTPUT_AGGREGATION_SIZE)
{
getHttpConfiguration().setOutputAggregationSize(maxOutputAggregationSize);
}
int maxIdleTimeout = Utils.getDirectoryNodeInt(null,
WEB_SERVER_MAX_IDLE_TIMEOUT_PARAM,
CONNECTION_TIMEOUT,
false);
getHttpConfiguration().setIdleTimeout(maxIdleTimeout);
int maxResponseHeaderSize = Utils.getDirectoryNodeInt(null,
MAX_RESPONSE_HEADER_SIZE_PARAM,
RESPONSE_HEADER_SIZE,
false);
if (maxResponseHeaderSize != RESPONSE_HEADER_SIZE)
{
getHttpConfiguration().setResponseHeaderSize(maxResponseHeaderSize);
}
int maxRequestHeaderSize = Utils.getDirectoryNodeInt(null,
MAX_REQUEST_HEADER_SIZE_PARAM,
REQUEST_HEADER_SIZE,
false);
if (maxRequestHeaderSize != REQUEST_HEADER_SIZE)
{
getHttpConfiguration().setRequestHeaderSize(maxRequestHeaderSize);
}
setKeyStore(ServerKeyStore.getStore());
addHandler(handlers);
startup(null, port);
}
/**
* Get the URL on which this server is running.
*
* @return See above.
*/
public static String getURL()
{
synchronized (WebServer.class)
{
if (instance == null)
{
throw new NullPointerException("The FWD web server was not started!");
}
return String.format("https://%s:%d%s",
instance.getHost(),
instance.getPort(),
instance.getContextPath());
}
}
/**
* Builds the list of web handlers for managing static web content given by the web-static-content
* directory settings.
*
* @return The list of web handlers for managing static web content
*
* @throws Throwable
* Iff errors occur while reading web-static-content settings from the directory
*/
public static ContextHandler[] getWebContentHandlers()
throws Throwable
{
DirectoryService ds = DirectoryService.getInstance();
List<WebContext> list = Utils.<WebContext>getDirectoryNodeBeanList(
ds,
"web-static-content",
null,
WebContext.class,
false,
false);
List<ContextHandler> webHandlers = new ArrayList<>(list.size());
for (WebContext entry : list)
{
try
{
ContextHandler webHandler = entry.createContextHandler(StandardServer::getResourceJarPaths);
webHandlers.add(webHandler);
}
catch (IOException e)
{
LOG.log(Level.SEVERE, e.getMessage());
}
catch (IllegalArgumentException e1)
{
LOG.log(Level.WARNING, e1.getMessage());
}
}
return webHandlers.toArray(new ContextHandler[webHandlers.size()]);
}
/**
* Called by StandardServer.loadStartup() to start the Web Server.
*
* @param handlers
* List of handlers to register.
*/
public static void initialize(org.eclipse.jetty.server.Handler[] handlers)
{
synchronized (WebServer.class)
{
if (instance == null)
{
// instantiate this class
try
{
instance = new WebServer(handlers);
}
catch (Exception exc)
{
LOG.logp(Level.SEVERE,
"WebServer.initialize()",
"",
"Could not startup web server!",
exc);
}
}
}
}
/**
* Shutdown web server instance if any.
*/
public static void terminate()
{
synchronized (WebServer.class)
{
if (instance != null)
{
try
{
instance.shutdown();
instance.join();
}
catch (Exception exc)
{
LOG.logp(Level.SEVERE,
"WebServer.terminate()",
"",
"Could not shutdown web server!",
exc);
}
}
}
}
/**
* Holds the web content resource description.
*/
public static class WebContext
{
/**
* The public context path
*/
public String context;
/**
* The path to the target resource
*/
public String resource;
/**
* Welcome file delivered by the server to the client
*/
public String file;
/**
* Creates the context handler based on this web context.
*
* @return The context handler.
*
* @throws IOException
* If an IO error occurs when trying to create new resource by the given path
* @throws IllegalArgumentException
* If the context path isn't set and/or the resource isn't found
*/
public ContextHandler createContextHandler(Supplier<List<String>> propathSupplier)
throws IOException
{
if (context == null)
{
throw new IllegalArgumentException("The context path isn't set");
}
ResourceHandler resourceHandler = new ResourceHandler();
if (file != null && !file.trim().isEmpty())
{
resourceHandler.setWelcomeFiles(new String[] {file.trim()});
}
resourceHandler.setDirectoriesListed(false);
ContextHandler webHandler = new ContextHandler();
webHandler.setContextPath(context);
webHandler.setHandler(resourceHandler);
Function<JarClassLoader, Function<String, String>> searchFn = jcl -> (n -> jcl.getResourceName(n, true));
JarUtil.JarResource jarEntry = JarUtil.searchResourceJars(LOG,
resource,
searchFn,
null,
propathSupplier);
URL urlJarResource = null;
if (jarEntry != null)
{
urlJarResource = jarEntry.getLoader().getResource(jarEntry.getName());
}
Resource targetResource = null;
if (urlJarResource != null)
{
targetResource = Resource.newResource(urlJarResource);
}
else
{
targetResource = Resource.newResource(resource);
}
if (targetResource == null || !targetResource.exists())
{
throw new IllegalArgumentException("The resource isn't found by the given path [" + resource +"]");
}
webHandler.setBaseResource(targetResource);
return webHandler;
}
}
}