GenericWebServer.java
/*
** Module : GenericWebServer.java
** Abstract : generic design for an embedded web server.
**
** Copyright (c) 2013-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------------Description------------------------------------------
** 001 MAG 20131105 First version based on jetty 9.1
** 002 MAG 20131129 Import server KeyStore.
** 003 AB 20150515 Removed configWsContext() and now directly use the handler collection.
** 004 SBI 20160220 Changed to set the websocket parameters from the server provided configuration.
** 005 HC 20170612 Changes related to implementation of new GWT-based Admin client.
** 006 SBI 20171116 Changed to set SSL context factory to look up its web store first.
** 008 CA 20190717 Let Jetty provide the HTTPS, for secure connections.
** CA 20190719 Allow listening on both secure and insecure ports.
** 009 HC 20190911 Implemented ad-hoc handler registration.
** 010 CA 20191122 Allow optional secure port.
** 20191203 Added APIs to load WAR web apps in distinct Jetty servers.
** 011 CA 20200122 Javadoc fixes.
** 012 CA 20200915 Upgraded to Jetty 9.4.22.
** Added getWebAppConnectors - used by third-party apps running in the same JVM as FWD.
** 014 IAS 20210422 Use daemon thread pool for ScheduledExecutorScheduler in the ServerConnector.
** IAS 20210420 Use daemon thread pool for ScheduledExecutorScheduler in the Server.
** SBI 20211003 Used HttpConfigurationConstants.
** CA 20211103 Do not allow a web server to start in HTTPS mode if SSLContext can't be initialized.
** CA 20211214 Renamed the authKeyStore, authKeyStorePassword and authKeyEntryPassword nodes to be the
** same node names as used by AppServerConnectionPool (keyStore, storePassword and
** entryPassword).
** VVT 20220316 getWarFile(): now the argument can be an absolute file path.
** TJD 20220331 Replaced deprecated new SslContextFactory() with SslContextFactory.Server()
** HC 20220427 Added Conscrypt SSL provider support for Web GUI JS Client - Java Client channel.
** IAS 20230109 Replace import of 'edu.emory.mathcs.backport.java.util.concurrent.atomic'
** with 'java.util.concurrent.atomic'.
** 015 CA 20230509 Either the secure or insecure ports must be configured for a webapp running within the FWD
** server JVM.
** 016 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 017 EVL 20230608 Fix from SBI to resolve socket availability issue.
** 018 CA 20230630 Isolate FWD jar (p2j.jar) and other dependencies from the webapp, except jars needed by
** the jetty server runtime running this webapp. This allows the webapp to have different
** versions of the jars used by FWD.
** 019 GBB 20230825 SecurityManager context methods calls updated.
** 020 CA 20231026 Allow cleanup of HTTP sessions created for 3rd web apps running within FWD server.
** The timeout is set at <webapp>/sessionTimeout, with a default value of 3600 seconds.
** 021 CA 20240524 All 'jetty-' prefixed jars are exposed to the .war app.
** 022 CA 20240614 Added websocket related jars to be exposed to the .war app.
** 023 GBB 20240709 Renamed HttpConfigurationConstants to WebConfigurationConstants.
*/
/*
** 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.web;
import java.io.*;
import java.net.*;
import java.nio.file.Paths;
import java.security.*;
import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.logging.*;
import javax.net.ssl.*;
import javax.servlet.DispatcherType;
import javax.servlet.http.*;
import com.goldencode.p2j.util.logging.*;
import org.conscrypt.*;
import org.eclipse.jetty.annotations.*;
import org.eclipse.jetty.http.*;
import org.eclipse.jetty.plus.webapp.*;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.*;
import org.eclipse.jetty.server.session.*;
import org.eclipse.jetty.util.ssl.*;
import org.eclipse.jetty.util.thread.*;
import org.eclipse.jetty.webapp.*;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.websocket.servlet.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.*;
/**
* A generic design for an embedded web server having support for WebSocket's.
*
* @author mag
*
*/
public class GenericWebServer
implements WebConfigurationConstants
{
/** Logger. */
protected static final CentralLogger LOG = CentralLogger.get(GenericWebServer.class.getName());
/** ScheduledExecutorScheduler ID generator */
private static final AtomicInteger SCHEDULER_ID = new AtomicInteger();
/** Default session timeout - 3600 seconds - for 3rd party web apps running in the FWD server. */
private static final int DEFAULT_SESSION_TIMEOUT = 3600;
/** The common http configuration settings */
private final HttpConfiguration httpConfiguration;
/** Exported server key store */
private ServerKeyStore serverKeyStore;
/** The bootstrap configuration */
private BootstrapConfig config;
/** Embedded jetty server */
private Server server;
/** Context Handlers */
private HandlerCollection handlerCollection = new HandlerCollection(true);
/** Context path */
private String contextPath = "/";
/** Server port */
private int port;
/** Server host name */
private String host;
/** The thread pool. */
private QueuedThreadPool pool;
/** Whether Conscrypt SSL provider will be enabled */
private boolean enableConscrypt;
/**
* Create a new server.
*/
public GenericWebServer()
{
this(false);
}
/**
* Create a new server.
*
* @param enableConscrypt
* A flag to enable Conscrypt SSL provider.
*/
public GenericWebServer(boolean enableConscrypt)
{
this.enableConscrypt = enableConscrypt;
pool = new QueuedThreadPool();
pool.setDaemon(true);
httpConfiguration = new HttpConfiguration();
server = new Server(pool);
Scheduler scheduler = new ScheduledExecutorScheduler(
String.format("Session-HouseKeeper-%x", hashCode()),
true
);
server.addBean(scheduler);
}
/**
* Resolve the WAR file with the given name - this will be a file relative to the current class
* location.
*
* @param warName
* The WAR file name (including extension).
*
* @return The {@link File} referencing this resource.
*/
public static File getWarFile(String warName)
{
File warFile = new File(warName);
if (warFile.isAbsolute())
{
return warFile;
}
URL url = GenericWebServer.class.getProtectionDomain().getCodeSource().getLocation();
String classPath = null;
try
{
classPath = url.toURI().getPath();
}
catch (URISyntaxException e)
{
return null;
}
File jarFile = new File(classPath);
if (!jarFile.exists())
{
return null;
}
// default path
String defaultPath = jarFile.getParentFile().getPath();
if (!defaultPath.endsWith(File.separator))
{
defaultPath += File.separator;
}
return new File(defaultPath + warName);
}
/**
* Get the HTTP and HTTPS connectors to run the specified web application, embedded in the specified Jetty
* server.
*
* @param name
* The web application name. The directory configuration is found in the <code>[name]WebApp</code>
* node - this includes the port, secure port, and host.
* @param server
* The Jetty server.
*
* @return The secure and insecure HTTP connectors.
*
* @throws Exception
* In case of configuration problems.
*/
public static ServerConnector[] getWebAppConnectors(String name, Server server)
throws Exception
{
String appName = name + "WebApp";
int insecurePort = Utils.getDirectoryNodeInt(null, appName + "/port", -1, false);
int securePort = Utils.getDirectoryNodeInt(null, appName + "/secureport", -1, false);
String host = Utils.getDirectoryNodeString(null, appName + "/host", null, false);
if (insecurePort == -1 && securePort == -1)
{
throw new IllegalStateException("No port was not configured for " + name + " web app.");
}
List<ServerConnector> connectors = new ArrayList<>(2);
if (securePort > 0)
{
// String alias = Utils.getDirectoryNodeString(null, appName + "/authAlias", null, false);
String ksfile = Utils.getDirectoryNodeString(null, appName + "/keyStore", null, false);
String kspass = Utils.getDirectoryNodeString(null, appName + "/storePassword",
null, false);
String kepass = Utils.getDirectoryNodeString(null, appName + "/entryPassword",
null, false);
if (ksfile == null || kspass == null || kepass == null)
{
throw new IllegalStateException("The " + name +" web app is configured to start, but " +
" the authentication details are not configured.");
}
SecureWebServer sslServer = new SecureWebServer(ksfile, kspass, kepass);
connectors.add(configSslConnector(server,
host,
securePort,
sslServer.getSslContextFactory()));
}
// do this always after the secure, so we know the secure connector is first
if (insecurePort >= 0)
{
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(insecurePort);
connectors.add(connector);
}
return connectors.toArray(new ServerConnector[2]);
}
/**
* Initialize a WEB app, from a specified WAR file.
* <p>
* Web application's configuration will be read from the directory, using the
* <code>name + "WebApp"</code> node, with these settings:
* <ul>
* <li><code>port</code>, with the insecure port. Optional.</li>
* <li><code>secureport</code>, with the secure port. Optional. If secure port is set, then:
* <ul>
* <li><code>keyStore</code>, the store with the FWD process private-key and
* certificate, to authenticate to FWD server.</li>
* <li><code>storePassword</code>, the password for the private-key.</li>
* <li><code>entryPassword</code>, the password for the certificate.</li>
* </ul>
* </li>
* <li><code>basepath</code>, with the base path serving this web app.</li>
* <li><code>warfile</code>, the WAR file name, relative to FWD server's working dir.
* Defaults to <code>defaultWar</code></li>
* </ul>
*
* @param name
* The WEB app name.
* @param defaultWar
* The default WAR file name.
* @param servletName
* The servlet name serving this WEB app.
*
* @return A {@link GenericWebServer} instance.
*/
public static GenericWebServer initializeWebApp(String name,
String defaultWar,
String servletName)
{
String appName = name + "WebApp";
int port = Utils.getDirectoryNodeInt(null, appName + "/port", -1, false);
int securePort = Utils.getDirectoryNodeInt(null, appName + "/secureport", -1, false);
String host = Utils.getDirectoryNodeString(null, appName + "/host", null, false);
if (port == -1 && securePort == -1)
{
throw new IllegalStateException("A secure or insecure port must be configured for " + name + " web app.");
}
String basepath = Utils.getDirectoryNodeString(null, appName + "/basepath", "/", false);
String webAppWar = Utils.getDirectoryNodeString(null, appName + "/warfile", defaultWar, false);
File warFile = getWarFile(webAppWar);
if (warFile == null || !warFile.exists())
{
throw new RuntimeException("WAR file can not be found: " + warFile.getAbsolutePath());
}
WebAppContext webapp = new WebAppContext();
String javaClassPath = System.getProperty("java.class.path");
if (javaClassPath != null)
{
ClasspathPattern pat = webapp.getServerClasspathPattern();
String[] dependencies =
{
"javax.servlet-api",
"log4j",
"tuscany-sdo",
"jetty-",
"javax.websocket-",
"javax-websocket-",
"websocket-"
};
HashSet<String> allow = new HashSet<>(Arrays.asList(dependencies));
Class<?>[] omit =
{
GenericWebServer.class, // p2j.jar
WebAppContext.class, // jetty
};
for (Class<?> cls : omit)
{
URL res = cls.getResource(cls.getSimpleName() + ".class");
String file = res.getFile();
int idx = file.indexOf("!/");
if (idx >= 0)
{
file = file.substring(0, idx);
}
file = file.substring(file.lastIndexOf(File.separator) + 1);
allow.add(file);
}
// everything in the 'allow' list will be exposed to the webapp; otherwise, the .jar file is isolated
// from the webapp, allowing it to have a different version.
l1:
for (String path : javaClassPath.split(File.pathSeparator))
{
try
{
path = Paths.get(path).toRealPath().toString();
}
catch (IOException e)
{
LOG.warning("Could not resolve real path:" + path);
}
// resolve absolute name of the path, so symlinks do not appear in the path
for (String tok : allow)
{
if (path.indexOf(tok) >= 0)
{
continue l1;
}
}
pat.add("file:" + path);
}
}
try
{
// ensure jasperreports_extension.properties is loaded only from the webapp.
// this is not enough - jasper looks into all parent classloaders, a custom ExtensionsRegistry needs
// to be implemented by the webapp
webapp.setClassLoader(new WebAppClassLoader(Thread.currentThread().getContextClassLoader(), webapp)
{
@Override
public Enumeration<URL> getResources(String name)
throws IOException
{
if (name.contains("jasperreports_extension.properties"))
{
List<URL> res = new ArrayList<>();
Enumeration<URL> urls = this.findResources(name);
while(urls != null && urls.hasMoreElements())
{
URL url = urls.nextElement();
res.add(url);
}
return Collections.enumeration(res);
}
return super.getResources(name);
}
});
}
catch (IOException exc)
{
throw new RuntimeException(exc);
}
webapp.setContextPath(basepath);
webapp.setWar(warFile.getAbsolutePath());
webapp.setExtractWAR(true);
webapp.setConfigurations(new Configuration[]
{
new AnnotationConfiguration(),
new EnvConfiguration(),
new FragmentConfiguration(),
new JettyWebXmlConfiguration(),
new MetaInfConfiguration(),
new PlusConfiguration(),
new WebInfConfiguration(),
new WebXmlConfiguration(),
});
webapp.addFilter(WebAuthFilter.class,
"/*",
EnumSet.of(DispatcherType.ASYNC,
DispatcherType.ERROR,
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST));
GenericWebServer server;
if (securePort > 0)
{
// String alias = Utils.getDirectoryNodeString(null, appName + "/authAlias", null, false);
String ksfile = Utils.getDirectoryNodeString(null, appName + "/keyStore", null, false);
String kspass = Utils.getDirectoryNodeString(null, appName + "/storePassword",
null, false);
String kepass = Utils.getDirectoryNodeString(null, appName + "/entryPassword",
null, false);
if (ksfile == null || kspass == null || kepass == null)
{
throw new IllegalStateException("The " + name +" web app is configured to start, but " +
" the authentication details are not configured.");
}
server = new SecureWebServer(ksfile, kspass, kepass);
}
else
{
server = new GenericWebServer();
}
int dirTimeout = Utils.getDirectoryNodeInt(null, appName + "/sessionTimeout", DEFAULT_SESSION_TIMEOUT,
false);
// do not allow sessions to live forever
int timeout = dirTimeout <= 0 ? DEFAULT_SESSION_TIMEOUT : dirTimeout;
DefaultSessionIdManager idmanager = new DefaultSessionIdManager(server.getServer());
SessionHandler sessions = new SessionHandler();
sessions.addEventListener(new SessionCleaner());
sessions.setSessionIdManager(idmanager);
webapp.setSessionHandler(sessions);
HouseKeeper houseKeeper = new HouseKeeper();
idmanager.setSessionHouseKeeper(houseKeeper);
server.addHandler(webapp);
Thread t = new Thread(() ->
{
try
{
server.getServer().setSessionIdManager(idmanager);
server.startup(host, securePort, port);
// these need to be set *after* the server is started
webapp.getSessionHandler().setMaxInactiveInterval(timeout);
try
{
houseKeeper.setIntervalSec(timeout);
}
catch (Exception e1)
{
e1.printStackTrace();
}
server.join();
}
catch (Exception e)
{
throw new RuntimeException("Error starting up the " + name + " Secure Servlet", e);
}
});
t.setDaemon(true);
t.setName(servletName);
t.start();
return server;
}
/**
* Startup the embedded server.
* The sever will listen on all interfaces.
* The port number is dynamically allocated by the OS.
*
* @throws Exception
* If an error condition occur during the server startup.
*/
public void startup()
throws Exception
{
this.startup(null, 0);
}
/**
* Startup server.
*
* @param host
* Host name or IP address. If <b>null</b> server will listen on
* all interfaces.
* @param port
* Port number to listen. If <b>0</b> the port is OS dynamically
* allocated.
*
* @throws Exception
* If an error condition occur during the server startup.
*/
public void startup(String host, int port)
throws Exception
{
startup(host, port, -1);
}
/**
* Startup server.
*
* @param host
* Host name or IP address. If <b>null</b> server will listen on
* all interfaces.
* @param securePort
* Port number to listen securely. If <b>0</b> the port is OS dynamically
* allocated.
* @param insecurePort
* Port number to listen insecurely. This is optional, set to a positive value to
* use it.
*
* @throws Exception
* If an error condition occur during the server startup.
*/
public void startup(String host, int securePort, int insecurePort)
throws Exception
{
// SSL Context Factory for HTTPS
SslContextFactory sslContextFactory = getSslContextFactory();
if (sslContextFactory == null && securePort >= 0)
{
String shost = (host == null ? "localhost" : host);
String msg = String.format("Could not initialize the SSL context for %s:%s", shost, securePort);
throw new RuntimeException(msg);
}
server.manage(pool);
if (securePort >= 0)
{
server.addConnector(configSslConnector(server,
host,
securePort,
sslContextFactory,
httpConfiguration));
}
// do this always after the secure, so we know the secure connector is first
if (insecurePort >= 0)
{
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(insecurePort);
connector.setReuseAddress(true);
server.addConnector(connector);
}
server.setHandler(handlerCollection);
server.setStopAtShutdown(true);
server.start();
// get host and port after server is starting
getRuntimeParams();
LOG.logp(Level.INFO,
"GenericWebServer.startup",
"",
CentralLogger.generate("Server URL: https://%s:%d%s",
getHost(),
getPort(),
contextPath));
if (securePort >= 0 && sslContextFactory != null)
{
String provider = sslContextFactory.getProvider();
LOG.log(Level.INFO, "SSL provider: " + (provider == null ? "<default>" : provider));
}
}
/**
* Shutdown server.
*
* @throws Exception
* If the server cannot be stopped.
*/
public void shutdown()
throws Exception
{
if (server != null && server.isRunning())
{
server.stop();
}
}
/**
* Waits for server thread pool to die.
*
* @throws Exception
* If any error condition occur.
*/
public void join()
throws Exception
{
if (server != null && server.isRunning())
{
server.join();
}
}
/**
* Get the {@link #server jetty server} instance.
*
* @return See above.
*/
public Server getServer()
{
return server;
}
/**
* Find out if server is running.
*
* @return <code>true</code> if server is running <code>false</code> otherwise.
*/
public boolean isRunning()
{
return server != null && server.isRunning();
}
/**
* Set the bootstrap configuration structure if any.
*
* @param config
* The <code>BootstrapConfig</code> instance or <b>null</b>.
*/
public void setBootstrapConfig(BootstrapConfig config)
{
this.config = config;
}
/**
* Get the server runtime host and port number.
*
*/
private void getRuntimeParams()
{
if (server.isRunning())
{
Connector[] connectors = server.getConnectors();
ServerConnector connector = (ServerConnector) connectors[0];
port = connector.getLocalPort();
host = connector.getHost();
}
}
/**
* Register a list of handlers.
*
* @param handlers
* A list of handlers to be registered.
*/
public void addHandler(Handler[] handlers)
{
for (Handler handler : handlers)
{
addHandler(handler);
}
}
/**
* Register a new handler.
*
* @param handler
* A Handler instance.
*/
public void addHandler(Handler handler)
{
if (handler != null)
{
handlerCollection.addHandler(handler);
}
}
/**
* Removes the handler from the list of handlers.
*
* @param handler
* The handler to remove.
*/
public void removeHandler(Handler handler)
{
handlerCollection.removeHandler(handler);
}
/**
* Register a new WebSocket.
*
* @param target
* The WebSocket target.
* @param webSocket
* A POJO decorated with <code>@WebSocket</code> annotation.
*/
public void addWebSocketHandler(String target, Class<?> webSocket)
{
Handler wsHandler = new PojoWebSocketHandler(target, webSocket, new WebSocketConfig(config));
handlerCollection.addHandler(wsHandler);
}
/**
* Register a new WebSocket by hand using a custom WebSocket creator.
*
* @param target
* The WebSocket target.
* @param creator
* A custom <code>WebSocketCreator</code> instance.
*/
public void addWebSocketHandler(String target, WebSocketCreator creator)
{
Handler wsHandler = new PojoWebSocketHandler(target, creator, new WebSocketConfig(config));
handlerCollection.addHandler(wsHandler);
}
/**
* Returns the common http configuration.
*
* @return The common http configuration
*/
public HttpConfiguration getHttpConfiguration()
{
return httpConfiguration;
}
/**
* Construct a SSL Context Factory for HTTPS.
*
* @return The <code>SslContextFactory</code> instance.
*
* @throws Exception
* If any error conditions occur.
*/
protected SslContextFactory getSslContextFactory()
throws Exception
{
SecurityManager sm = SecurityManager.getInstance();
SslContextFactory sslContextFactory = new SslContextFactory.Server();
if (serverKeyStore != null && serverKeyStore.isWebValid())
{
// Import sever SSL context
ByteArrayInputStream stream = new ByteArrayInputStream(serverKeyStore.getWebKeyStore());
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(stream, serverKeyStore.getWebKeyStorePassword().toCharArray());
sslContextFactory.setKeyStore(keyStore);
sslContextFactory.setKeyManagerPassword(serverKeyStore.getWebKeyManagerPassword());
}
else if (config == null)
{
SSLContext sslContext = sm.contextSm.getSecureSocketContext();
sslContextFactory.setSslContext(sslContext);
}
else
{
if (serverKeyStore != null && serverKeyStore.isValid())
{
// Import sever SSL context
ByteArrayInputStream stream = new ByteArrayInputStream(serverKeyStore.getKeyStore());
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(stream, serverKeyStore.getKeyStorePassword().toCharArray());
sslContextFactory.setKeyStore(keyStore);
sslContextFactory.setKeyManagerPassword(serverKeyStore.getKeyManagerPassword());
}
else
{
SSLContext sslContext = sm.contextSm.getSecureSocketContext(config);
sslContextFactory.setSslContext(sslContext);
}
}
if (enableConscrypt && sslContextFactory != null)
{
if (Security.getProvider("Conscrypt") == null)
{
Security.addProvider(new OpenSSLProvider());
}
sslContextFactory.setProvider("Conscrypt");
}
return sslContextFactory;
}
/**
* Create and configure a SSL server connector.
*
* @param server
* The server instance.
* @param host
* The server host name or IP address.
* @param port
* The port number to listen.
* @param sslContextFactory
* The <code>SslContextFactory</code> instance.
*
* @return The SSL server connector
*/
private static ServerConnector configSslConnector(Server server,
String host,
int port,
SslContextFactory sslContextFactory)
{
return configSslConnector(server, host, port, sslContextFactory, new HttpConfiguration());
}
/**
* Create and configure a SSL server connector.
*
* @param server
* The server instance.
* @param host
* The server host name or IP address.
* @param port
* The port number to listen.
* @param sslContextFactory
* The <code>SslContextFactory</code> instance.
* @param httpConfig
* The common http configuration settings
*
* @return The SSL server connector
*/
private static ServerConnector configSslConnector(Server server,
String host,
int port,
SslContextFactory sslContextFactory,
HttpConfiguration httpConfig)
{
String protocol = HttpVersion.HTTP_1_1.asString();
httpConfig.addCustomizer(new SecureRequestCustomizer());
Scheduler scheduler = new ScheduledExecutorScheduler(
String.format("Connector-Scheduler-%x", SCHEDULER_ID.incrementAndGet()), true);
// for GWT development comment out SslConnectionFactory,
// GTW's superdevmode doesn't run with SSL
ServerConnector sslConnector = new ServerConnector(server,
null, scheduler, null, -1, -1,
new SslConnectionFactory(sslContextFactory, protocol),
new HttpConnectionFactory(httpConfig));
sslConnector.setHost(host);
sslConnector.setPort(port);
sslConnector.setReuseAddress(true);
return sslConnector;
}
/**
* Get host site local address.
*
* @return Site local host address if any or <b>localhost</b> if none.
*/
private String getHostLocalAddress()
{
String hostAddress = "localhost";
try
{
InetAddress address = getSiteLocalAddress();
if (address != null)
{
hostAddress = address.getHostAddress();
}
}
catch (Exception e)
{
LOG.logp(Level.SEVERE,
"GenericWebServer.getHostLocalAddress()",
"",
"Error!",
e);
}
return hostAddress;
}
/**
* Search on the network interfaces of this machine for
* <code>InetAddress</code> which is a site local address.
*
* @return The InetAddress of the first site local address.
*
* @throws Exception
* If an error condition occur.
*/
private InetAddress getSiteLocalAddress()
throws Exception
{
// enumerate network interfaces
Enumeration<NetworkInterface> netInterfaces =
NetworkInterface.getNetworkInterfaces();
if (netInterfaces != null)
{
while (netInterfaces.hasMoreElements())
{
NetworkInterface netInterface = netInterfaces.nextElement();
if (netInterface.isUp())
{
// enumerate addresses bound to this network interface.
Enumeration<InetAddress> addresses =
netInterface.getInetAddresses();
while (addresses.hasMoreElements())
{
InetAddress address = addresses.nextElement();
if (address.isSiteLocalAddress())
{
return address;
}
}
}
}
}
return null;
}
/**
* Set the server context path root.
*
* @param contextPath
* A string representing the context path root.
*/
public void setContextPath(String contextPath)
{
this.contextPath = contextPath;
}
/**
* Get the server context path root.
*
* @return A string representing the context path root.
*/
public String getContextPath()
{
return contextPath;
}
/**
* Get server runtime port number.
*
* @return The server runtime port number.
*/
public int getPort()
{
return port;
}
/**
* Get the server runtime host.
*
* @return The server runtime host.
*
*/
public String getHost()
{
return (host == null) ? getHostLocalAddress() : host;
}
/**
* Set imported key store
*
* @param keyStore
* Imported key store if any or <code>null</code>
*/
public void setKeyStore(ServerKeyStore keyStore)
{
this.serverKeyStore = keyStore;
}
/**
* A session listener to clean the FWD context for destroyed sessions.
*/
private static class SessionCleaner
implements HttpSessionListener
{
/**
* Notification when Jetty housekeeping will destroy inactive/timeout sessions. This will terminate the
* FWD session associated with this HTTP session.
*
* @param se
* The session event.
*/
@Override
public void sessionDestroyed(HttpSessionEvent se)
{
HttpSession session = se.getSession();
ContextSwitcher switcher = (ContextSwitcher) session.getAttribute("fwd.switcher");
if (switcher != null)
{
Object sessionId = session.getId();
SecurityManager sm = SecurityManager.getInstance();
try
{
sm.sessionSm.terminateSessionById(sessionId);
}
catch (RestrictedUseException e)
{
throw new RuntimeException(e);
}
}
}
@Override
public void sessionCreated(HttpSessionEvent se)
{
// no-op
}
}
}