/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2013-2014 ForgeRock AS
*/
package org.opends.server.protocols.http;
import static org.opends.messages.ConfigMessages.*;
import static org.opends.messages.ProtocolMessages.*;
import static org.opends.server.loggers.ErrorLogger.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.ServletException;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.ConnectionFactory;
import org.forgerock.json.resource.Resources;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.servlet.HttpServlet;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.rest2ldap.AuthorizationPolicy;
import org.forgerock.opendj.rest2ldap.Rest2LDAP;
import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory;
import org.glassfish.grizzly.http.HttpProbe;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.http.server.HttpServerMonitoringConfig;
import org.glassfish.grizzly.http.server.NetworkListener;
import org.glassfish.grizzly.http.server.ServerConfiguration;
import org.glassfish.grizzly.monitoring.MonitoringConfig;
import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
import org.glassfish.grizzly.servlet.WebappContext;
import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
import org.glassfish.grizzly.strategies.SameThreadIOStrategy;
import org.glassfish.grizzly.utils.Charsets;
import org.opends.messages.Message;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.ConnectionHandlerCfg;
import org.opends.server.admin.std.server.HTTPConnectionHandlerCfg;
import org.opends.server.api.AlertGenerator;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.ConnectionHandler;
import org.opends.server.api.KeyManagerProvider;
import org.opends.server.api.ServerShutdownListener;
import org.opends.server.api.TrustManagerProvider;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.extensions.NullKeyManagerProvider;
import org.opends.server.extensions.NullTrustManagerProvider;
import org.opends.server.loggers.HTTPAccessLogger;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.monitors.ClientConnectionMonitorProvider;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.DN;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.HostPort;
import org.opends.server.types.InitializationException;
import org.opends.server.types.ResultCode;
import org.opends.server.util.SelectableCertificateKeyManager;
import org.opends.server.util.StaticUtils;
/**
* This class defines a connection handler that will be used for communicating
* with clients over HTTP. The connection handler is responsible for
* starting/stopping the embedded web server.
*/
public class HTTPConnectionHandler extends
ConnectionHandler<HTTPConnectionHandlerCfg> implements
ConfigurationChangeListener<HTTPConnectionHandlerCfg>,
ServerShutdownListener, AlertGenerator
{
/** The tracer object for the debug logger. */
private static final DebugTracer TRACER = getTracer();
/** Default friendly name for this connection handler. */
private static final String DEFAULT_FRIENDLY_NAME = "HTTP Connection Handler";
/** SSL instance name used in context creation. */
private static final String SSL_CONTEXT_INSTANCE_NAME = "TLS";
private static final ObjectMapper JSON_MAPPER = new ObjectMapper().configure(
JsonParser.Feature.ALLOW_COMMENTS, true);
/** The initialization configuration. */
private HTTPConnectionHandlerCfg initConfig;
/** The current configuration. */
private HTTPConnectionHandlerCfg currentConfig;
/**
* Indicates whether the Directory Server is in the process of shutting down.
*/
private volatile boolean shutdownRequested;
/** Indicates whether this connection handler is enabled. */
private boolean enabled;
/** The set of listeners for this connection handler. */
private List<HostPort> listeners = new LinkedList<HostPort>();
/** The HTTP server embedded in OpenDJ. */
private HttpServer httpServer;
/** The HTTP probe that collects stats. */
private HTTPStatsProbe httpProbe;
/**
* Holds the current client connections. Using {@link ConcurrentHashMap} to
* ensure no concurrent reads/writes can happen and adds/removes are fast. We
* only use the keys, so it does not matter what value is put there.
*/
private Map<ClientConnection, ClientConnection> clientConnections =
new ConcurrentHashMap<ClientConnection, ClientConnection>();
/** The set of statistics collected for this connection handler. */
private HTTPStatistics statTracker;
/**
* The client connection monitor provider associated with this connection
* handler.
*/
private ClientConnectionMonitorProvider connMonitor;
/** The unique name assigned to this connection handler. */
private String handlerName;
/** The protocol used by this connection handler. */
private String protocol;
/**
* The condition variable that will be used by the start method to wait for
* the socket port to be opened and ready to process requests before
* returning.
*/
private final Object waitListen = new Object();
/** The friendly name of this connection handler. */
private String friendlyName;
/**
* The SSL engine configurator is used for obtaining default SSL parameters.
*/
private SSLEngineConfigurator sslEngineConfigurator;
/**
* Default constructor. It is invoked by reflection to create this
* {@link ConnectionHandler}.
*/
public HTTPConnectionHandler()
{
super(DEFAULT_FRIENDLY_NAME);
}
/**
* Returns whether unauthenticated HTTP requests are allowed. The server
* checks whether unauthenticated requests are allowed server-wide first then
* for the HTTP Connection Handler second.
*
* @return true if unauthenticated requests are allowed, false otherwise.
*/
public boolean acceptUnauthenticatedRequests()
{
// the global setting overrides the more specific setting here.
return !DirectoryServer.rejectUnauthenticatedRequests()
&& !this.currentConfig.isAuthenticationRequired();
}
/**
* Registers a client connection to track it.
*
* @param clientConnection
* the client connection to register
*/
void addClientConnection(ClientConnection clientConnection)
{
clientConnections.put(clientConnection, clientConnection);
}
/** {@inheritDoc} */
@Override
public ConfigChangeResult applyConfigurationChange(
HTTPConnectionHandlerCfg config)
{
// Create variables to include in the response.
boolean adminActionRequired = false;
List<Message> messages = new ArrayList<Message>();
if (anyChangeRequiresRestart(config))
{
adminActionRequired = true;
messages.add(ERR_CONNHANDLER_CONFIG_CHANGES_REQUIRE_RESTART.get("HTTP"));
}
// Reconfigure SSL if needed.
try
{
configureSSL(config);
}
catch (DirectoryException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
messages.add(e.getMessageObject());
return new ConfigChangeResult(e.getResultCode(), adminActionRequired,
messages);
}
if (config.isEnabled() && this.currentConfig.isEnabled() && isListening())
{ // server was running and will still be running
// if the "enabled" was flipped, leave it to the stop / start server to
// handle it
if (!this.currentConfig.isKeepStats() && config.isKeepStats())
{ // it must now keep stats while it was not previously
setHttpStatsProbe(this.httpServer);
}
else if (this.currentConfig.isKeepStats() && !config.isKeepStats()
&& this.httpProbe != null)
{ // it must NOT keep stats anymore
getHttpConfig(this.httpServer).removeProbes(this.httpProbe);
this.httpProbe = null;
}
}
this.initConfig = config;
this.currentConfig = config;
this.enabled = this.currentConfig.isEnabled();
return new ConfigChangeResult(ResultCode.SUCCESS, adminActionRequired,
messages);
}
private boolean anyChangeRequiresRestart(HTTPConnectionHandlerCfg newCfg)
{
return !equals(newCfg.getListenPort(), initConfig.getListenPort())
|| !equals(newCfg.getListenAddress(), initConfig.getListenAddress())
|| !equals(newCfg.getMaxRequestSize(), currentConfig
.getMaxRequestSize())
|| !equals(newCfg.isAllowTCPReuseAddress(), currentConfig
.isAllowTCPReuseAddress())
|| !equals(newCfg.isUseTCPKeepAlive(), currentConfig
.isUseTCPKeepAlive())
|| !equals(newCfg.isUseTCPNoDelay(), currentConfig.isUseTCPNoDelay())
|| !equals(newCfg.getMaxBlockedWriteTimeLimit(), currentConfig
.getMaxBlockedWriteTimeLimit())
|| !equals(newCfg.getBufferSize(), currentConfig.getBufferSize())
|| !equals(newCfg.getAcceptBacklog(), currentConfig.getAcceptBacklog())
|| !equals(newCfg.isUseSSL(), currentConfig.isUseSSL())
|| !equals(newCfg.getKeyManagerProviderDN(), currentConfig
.getKeyManagerProviderDN())
|| !equals(newCfg.getSSLCertNickname(), currentConfig
.getSSLCertNickname())
|| !equals(newCfg.getTrustManagerProviderDN(), currentConfig
.getTrustManagerProviderDN())
|| !equals(newCfg.getSSLProtocol(), currentConfig.getSSLProtocol())
|| !equals(newCfg.getSSLCipherSuite(), currentConfig
.getSSLCipherSuite())
|| !equals(newCfg.getSSLClientAuthPolicy(), currentConfig
.getSSLClientAuthPolicy());
}
private boolean equals(Object o1, Object o2)
{
if (o1 == null)
{
return o2 == null;
}
return o1.equals(o2);
}
private boolean equals(long l1, long l2)
{
return l1 == l2;
}
private boolean equals(boolean b1, boolean b2)
{
return b1 == b2;
}
private void configureSSL(HTTPConnectionHandlerCfg config)
throws DirectoryException
{
protocol = config.isUseSSL() ? "HTTPS" : "HTTP";
if (config.isUseSSL())
{
sslEngineConfigurator = createSSLEngineConfigurator(config);
}
else
{
sslEngineConfigurator = null;
}
}
/** {@inheritDoc} */
@Override
public void finalizeConnectionHandler(Message finalizeReason)
{
shutdownRequested = true;
// Unregister this as a change listener.
currentConfig.removeHTTPChangeListener(this);
if (connMonitor != null)
{
DirectoryServer.deregisterMonitorProvider(connMonitor);
}
if (statTracker != null)
{
DirectoryServer.deregisterMonitorProvider(statTracker);
}
}
/** {@inheritDoc} */
@Override
public Map<String, String> getAlerts()
{
Map<String, String> alerts = new LinkedHashMap<String, String>();
alerts.put(ALERT_TYPE_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES,
ALERT_DESCRIPTION_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES);
return alerts;
}
/** {@inheritDoc} */
@Override
public String getClassName()
{
return HTTPConnectionHandler.class.getName();
}
/** {@inheritDoc} */
@Override
public Collection<ClientConnection> getClientConnections()
{
return clientConnections.keySet();
}
/** {@inheritDoc} */
@Override
public DN getComponentEntryDN()
{
return currentConfig.dn();
}
/** {@inheritDoc} */
@Override
public String getConnectionHandlerName()
{
return handlerName;
}
/**
* Returns the current config of this connection handler.
*
* @return the current config of this connection handler
*/
HTTPConnectionHandlerCfg getCurrentConfig()
{
return this.currentConfig;
}
/** {@inheritDoc} */
@Override
public Collection<String> getEnabledSSLCipherSuites()
{
final SSLEngineConfigurator configurator = sslEngineConfigurator;
if (configurator != null)
{
return Arrays.asList(configurator.getEnabledCipherSuites());
}
return super.getEnabledSSLCipherSuites();
}
/** {@inheritDoc} */
@Override
public Collection<String> getEnabledSSLProtocols()
{
final SSLEngineConfigurator configurator = sslEngineConfigurator;
if (configurator != null)
{
return Arrays.asList(configurator.getEnabledProtocols());
}
return super.getEnabledSSLProtocols();
}
/** {@inheritDoc} */
@Override
public Collection<HostPort> getListeners()
{
return listeners;
}
/**
* Returns the listen port for this connection handler.
*
* @return the listen port for this connection handler.
*/
int getListenPort()
{
return this.initConfig.getListenPort();
}
/** {@inheritDoc} */
@Override
public String getProtocol()
{
return protocol;
}
/**
* Returns the SSL engine configured for this connection handler if SSL is
* enabled, null otherwise.
*
* @return the SSL engine if SSL is enabled, null otherwise
*/
SSLEngine getSSLEngine()
{
return sslEngineConfigurator.createSSLEngine();
}
/** {@inheritDoc} */
@Override
public String getShutdownListenerName()
{
return handlerName;
}
/**
* Retrieves the set of statistics maintained by this connection handler.
*
* @return The set of statistics maintained by this connection handler.
*/
public HTTPStatistics getStatTracker()
{
return statTracker;
}
/** {@inheritDoc} */
@Override
public void initializeConnectionHandler(HTTPConnectionHandlerCfg config)
throws ConfigException, InitializationException
{
if (friendlyName == null)
{
friendlyName = config.dn().getRDN().getAttributeValue(0).toString();
}
int listenPort = config.getListenPort();
for (InetAddress a : config.getListenAddress())
{
listeners.add(new HostPort(a.getHostAddress(), listenPort));
}
handlerName = getHandlerName(config);
// Configure SSL if needed.
try
{
configureSSL(config);
}
catch (DirectoryException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
throw new InitializationException(e.getMessageObject());
}
// Create and register monitors.
statTracker = new HTTPStatistics(handlerName + " Statistics");
DirectoryServer.registerMonitorProvider(statTracker);
connMonitor = new ClientConnectionMonitorProvider(this);
DirectoryServer.registerMonitorProvider(connMonitor);
// Register this as a change listener.
config.addHTTPChangeListener(this);
this.initConfig = config;
this.currentConfig = config;
this.enabled = this.currentConfig.isEnabled();
}
private String getHandlerName(HTTPConnectionHandlerCfg config)
{
StringBuilder nameBuffer = new StringBuilder();
nameBuffer.append(friendlyName);
for (InetAddress a : config.getListenAddress())
{
nameBuffer.append(" ");
nameBuffer.append(a.getHostAddress());
}
nameBuffer.append(" port ");
nameBuffer.append(config.getListenPort());
return nameBuffer.toString();
}
/** {@inheritDoc} */
@Override
public boolean isConfigurationAcceptable(ConnectionHandlerCfg configuration,
List<Message> unacceptableReasons)
{
HTTPConnectionHandlerCfg config = (HTTPConnectionHandlerCfg) configuration;
if ((currentConfig == null) || (!this.enabled && config.isEnabled()))
{
// Attempt to bind to the listen port on all configured addresses to
// verify whether the connection handler will be able to start.
Message errorMessage =
checkAnyListenAddressInUse(config.getListenAddress(), config
.getListenPort(), config.isAllowTCPReuseAddress(), config.dn());
if (errorMessage != null)
{
unacceptableReasons.add(errorMessage);
return false;
}
}
if (config.isEnabled())
{
// Check that the SSL configuration is valid.
if (config.isUseSSL())
{
try
{
createSSLEngineConfigurator(config);
}
catch (DirectoryException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
unacceptableReasons.add(e.getMessageObject());
return false;
}
}
}
return true;
}
/**
* Checks whether any listen address is in use for the given port. The check
* is performed by binding to each address and port.
*
* @param listenAddresses
* the listen {@link InetAddress} to test
* @param listenPort
* the listen port to test
* @param allowReuseAddress
* whether addresses can be reused
* @param configEntryDN
* the configuration entry DN
* @return an error message if at least one of the address is already in use,
* null otherwise.
*/
private Message checkAnyListenAddressInUse(
Collection<InetAddress> listenAddresses, int listenPort,
boolean allowReuseAddress, DN configEntryDN)
{
for (InetAddress a : listenAddresses)
{
try
{
if (isAddressInUse(a, listenPort, allowReuseAddress))
{
throw new IOException(ERR_CONNHANDLER_ADDRESS_INUSE.get().toString());
}
}
catch (IOException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
return ERR_CONNHANDLER_CANNOT_BIND.get("HTTP", String
.valueOf(configEntryDN), a.getHostAddress(), listenPort,
getExceptionMessage(e));
}
}
return null;
}
/** {@inheritDoc} */
@Override
public boolean isConfigurationChangeAcceptable(
HTTPConnectionHandlerCfg configuration, List<Message> unacceptableReasons)
{
return isConfigurationAcceptable(configuration, unacceptableReasons);
}
/**
* Indicates whether this connection handler should maintain usage statistics.
*
* @return <CODE>true</CODE> if this connection handler should maintain usage
* statistics, or <CODE>false</CODE> if not.
*/
public boolean keepStats()
{
return currentConfig.isKeepStats();
}
/** {@inheritDoc} */
@Override
public void processServerShutdown(Message reason)
{
shutdownRequested = true;
}
private boolean isListening()
{
return httpServer != null;
}
/** {@inheritDoc} */
@Override
public void start()
{
// The Directory Server start process should only return
// when the connection handlers port are fully opened
// and working. The start method therefore needs to wait for
// the created thread to
synchronized (waitListen)
{
super.start();
try
{
waitListen.wait();
}
catch (InterruptedException e)
{
// If something interrupted the start its probably better
// to return ASAP.
}
}
}
/**
* Unregisters a client connection to stop tracking it.
*
* @param clientConnection
* the client connection to unregister
*/
void removeClientConnection(ClientConnection clientConnection)
{
clientConnections.remove(clientConnection);
}
/** {@inheritDoc} */
@Override
public void run()
{
setName(handlerName);
boolean lastIterationFailed = false;
while (!shutdownRequested)
{
// If this connection handler is not enabled, then just sleep
// for a bit and check again.
if (!this.enabled)
{
if (isListening())
{
stopHttpServer();
}
StaticUtils.sleep(1000);
continue;
}
if (isListening())
{
// If already listening, then sleep for a bit and check again.
StaticUtils.sleep(1000);
continue;
}
try
{
// At this point, the connection Handler either started
// correctly or failed to start but the start process
// should be notified and resume its work in any cases.
synchronized (waitListen)
{
waitListen.notify();
}
// If we have gotten here, then we are about to start listening
// for the first time since startup or since we were previously
// disabled. Start the embedded HTTP server
startHttpServer();
lastIterationFailed = false;
}
catch (Exception e)
{
// clean up the messed up HTTP server
cleanUpHttpServer();
// error + alert about the horked config
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
logError(ERR_CONNHANDLER_CANNOT_ACCEPT_CONNECTION.get(friendlyName,
String.valueOf(currentConfig.dn()), getExceptionMessage(e)));
if (lastIterationFailed)
{
// The last time through the accept loop we also
// encountered a failure. Rather than enter a potential
// infinite loop of failures, disable this acceptor and
// log an error.
Message message =
ERR_CONNHANDLER_CONSECUTIVE_ACCEPT_FAILURES.get(friendlyName,
String.valueOf(currentConfig.dn()),
stackTraceToSingleLineString(e));
logError(message);
DirectoryServer.sendAlertNotification(this,
ALERT_TYPE_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES, message);
this.enabled = false;
}
else
{
lastIterationFailed = true;
}
}
}
// Initiate shutdown
stopHttpServer();
}
private void startHttpServer() throws Exception
{
// silence Grizzly's own logging
Logger.getLogger("org.glassfish.grizzly").setLevel(Level.OFF);
if (HTTPAccessLogger.getHTTPAccessLogPublishers().isEmpty())
{
logError(WARN_CONFIG_LOGGER_NO_ACTIVE_HTTP_ACCESS_LOGGERS.get());
}
this.httpServer = createHttpServer();
// register servlet as default servlet and also able to serve REST requests
createAndRegisterServlet("OpenDJ Rest2LDAP servlet", "", "/*");
TRACER.debugInfo("Starting HTTP server...");
this.httpServer.start();
TRACER.debugInfo("HTTP server started");
logError(NOTE_CONNHANDLER_STARTED_LISTENING.get(handlerName));
}
private HttpServer createHttpServer()
{
final HttpServer server = new HttpServer();
final int requestSize = (int) currentConfig.getMaxRequestSize();
final ServerConfiguration serverConfig = server.getServerConfiguration();
serverConfig.setMaxBufferedPostSize(requestSize);
serverConfig.setMaxFormPostSize(requestSize);
serverConfig.setDefaultQueryEncoding(Charsets.UTF8_CHARSET);
if (keepStats())
{
setHttpStatsProbe(server);
}
// configure the network listener
final NetworkListener listener =
new NetworkListener("Rest2LDAP", NetworkListener.DEFAULT_NETWORK_HOST,
initConfig.getListenPort());
server.addListener(listener);
// configure the network transport
final TCPNIOTransport transport = listener.getTransport();
transport.setReuseAddress(currentConfig.isAllowTCPReuseAddress());
transport.setKeepAlive(currentConfig.isUseTCPKeepAlive());
transport.setTcpNoDelay(currentConfig.isUseTCPNoDelay());
transport.setWriteTimeout(currentConfig.getMaxBlockedWriteTimeLimit(),
TimeUnit.MILLISECONDS);
final int bufferSize = (int) currentConfig.getBufferSize();
transport.setReadBufferSize(bufferSize);
transport.setWriteBufferSize(bufferSize);
transport.setIOStrategy(SameThreadIOStrategy.getInstance());
final int numRequestHandlers =
getNumRequestHandlers(currentConfig.getNumRequestHandlers(),
friendlyName);
transport.setSelectorRunnersCount(numRequestHandlers);
transport.setServerConnectionBackLog(currentConfig.getAcceptBacklog());
// configure SSL
if (sslEngineConfigurator != null)
{
listener.setSecure(true);
listener.setSSLEngineConfig(sslEngineConfigurator);
}
return server;
}
private void setHttpStatsProbe(HttpServer server)
{
this.httpProbe = new HTTPStatsProbe(this.statTracker);
getHttpConfig(server).addProbes(this.httpProbe);
}
private MonitoringConfig<HttpProbe> getHttpConfig(HttpServer server)
{
final HttpServerMonitoringConfig monitoringCfg =
server.getServerConfiguration().getMonitoringConfig();
return monitoringCfg.getHttpConfig();
}
private void createAndRegisterServlet(final String servletName,
final String... urlPatterns) throws Exception
{
// parse and use JSON config
File jsonConfigFile = getFileForPath(this.currentConfig.getConfigFile());
final JsonValue configuration =
parseJsonConfiguration(jsonConfigFile).recordKeyAccesses();
final HTTPAuthenticationConfig authenticationConfig =
getAuthenticationConfig(configuration);
final ConnectionFactory connFactory = getConnectionFactory(configuration);
configuration.verifyAllKeysAccessed();
Filter filter =
new CollectClientConnectionsFilter(this, authenticationConfig);
final HttpServlet servlet = new HttpServlet(connFactory,
// Used for hooking our HTTPClientConnection in Rest2LDAP
Rest2LDAPContextFactory.getHttpServletContextFactory());
// Create and deploy the Web app context
final WebappContext ctx = new WebappContext(servletName);
ctx.addFilter("collectClientConnections", filter).addMappingForUrlPatterns(
EnumSet.of(DispatcherType.REQUEST), true, urlPatterns);
ctx.addServlet(servletName, servlet).addMapping(urlPatterns);
ctx.deploy(this.httpServer);
}
private HTTPAuthenticationConfig getAuthenticationConfig(
final JsonValue configuration)
{
final HTTPAuthenticationConfig result = new HTTPAuthenticationConfig();
final JsonValue val = configuration.get("authenticationFilter");
result.setBasicAuthenticationSupported(asBool(val,
"supportHTTPBasicAuthentication"));
result.setCustomHeadersAuthenticationSupported(asBool(val,
"supportAltAuthentication"));
result.setCustomHeaderUsername(val.get("altAuthenticationUsernameHeader")
.asString());
result.setCustomHeaderPassword(val.get("altAuthenticationPasswordHeader")
.asString());
final String searchBaseDN = asString(val, "searchBaseDN");
result.setSearchBaseDN(org.forgerock.opendj.ldap.DN.valueOf(searchBaseDN));
result.setSearchScope(SearchScope.valueOf(asString(val, "searchScope")));
result.setSearchFilterTemplate(asString(val, "searchFilterTemplate"));
return result;
}
private String asString(JsonValue value, String key)
{
return value.get(key).required().asString();
}
private boolean asBool(JsonValue value, String key)
{
return value.get(key).defaultTo(false).asBoolean();
}
private ConnectionFactory getConnectionFactory(final JsonValue configuration)
{
final Router router = new Router();
final JsonValue mappings =
configuration.get("servlet").get("mappings").required();
for (final String mappingUrl : mappings.keys())
{
final JsonValue mapping = mappings.get(mappingUrl);
final CollectionResourceProvider provider =
Rest2LDAP.builder().authorizationPolicy(AuthorizationPolicy.REUSE)
.configureMapping(mapping).build();
router.addRoute(mappingUrl, provider);
}
return Resources.newInternalConnectionFactory(router);
}
private JsonValue parseJsonConfiguration(File configFile) throws IOException,
JsonParseException, JsonMappingException, ServletException
{
// Parse the config file.
final Object content = JSON_MAPPER.readValue(configFile, Object.class);
if (!(content instanceof Map))
{
throw new ServletException("Servlet configuration file '" + configFile
+ "' does not contain a valid JSON configuration");
}
// TODO JNR should we restrict the possible configurations in this file?
// Should we remove any config that does not make any sense to the
// HTTP Connection Handler?
return new JsonValue(content);
}
private void stopHttpServer()
{
if (this.httpServer != null)
{
TRACER.debugInfo("Stopping HTTP server...");
this.httpServer.shutdownNow();
cleanUpHttpServer();
TRACER.debugInfo("HTTP server stopped");
logError(NOTE_CONNHANDLER_STOPPED_LISTENING.get(handlerName));
}
}
private void cleanUpHttpServer()
{
this.httpServer = null;
this.httpProbe = null;
}
/** {@inheritDoc} */
@Override
public void toString(StringBuilder buffer)
{
buffer.append(handlerName);
}
private SSLEngineConfigurator createSSLEngineConfigurator(
HTTPConnectionHandlerCfg config) throws DirectoryException
{
if (!config.isUseSSL())
{
return null;
}
try
{
SSLContext sslContext = createSSLContext(config);
SSLEngineConfigurator configurator =
new SSLEngineConfigurator(sslContext);
configurator.setClientMode(false);
// configure with defaults from the JVM
final SSLEngine defaults = sslContext.createSSLEngine();
configurator.setEnabledProtocols(defaults.getEnabledProtocols());
configurator.setEnabledCipherSuites(defaults.getEnabledCipherSuites());
final Set<String> protocols = config.getSSLProtocol();
if (!protocols.isEmpty())
{
String[] array = protocols.toArray(new String[protocols.size()]);
configurator.setEnabledProtocols(array);
}
final Set<String> ciphers = config.getSSLCipherSuite();
if (!ciphers.isEmpty())
{
String[] array = ciphers.toArray(new String[ciphers.size()]);
configurator.setEnabledCipherSuites(array);
}
switch (config.getSSLClientAuthPolicy())
{
case DISABLED:
configurator.setNeedClientAuth(false);
configurator.setWantClientAuth(false);
break;
case REQUIRED:
configurator.setNeedClientAuth(true);
configurator.setWantClientAuth(true);
break;
case OPTIONAL:
default:
configurator.setNeedClientAuth(false);
configurator.setWantClientAuth(true);
break;
}
return configurator;
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
ResultCode resCode = DirectoryServer.getServerErrorResultCode();
Message message =
ERR_CONNHANDLER_SSL_CANNOT_INITIALIZE.get(getExceptionMessage(e));
throw new DirectoryException(resCode, message, e);
}
}
private SSLContext createSSLContext(HTTPConnectionHandlerCfg config)
throws Exception
{
if (!config.isUseSSL())
{
return null;
}
DN keyMgrDN = config.getKeyManagerProviderDN();
KeyManagerProvider<?> keyManagerProvider =
DirectoryServer.getKeyManagerProvider(keyMgrDN);
if (keyManagerProvider == null)
{
keyManagerProvider = new NullKeyManagerProvider();
}
String alias = config.getSSLCertNickname();
KeyManager[] keyManagers;
if (alias == null)
{
keyManagers = keyManagerProvider.getKeyManagers();
}
else
{
keyManagers =
SelectableCertificateKeyManager.wrap(keyManagerProvider
.getKeyManagers(), alias);
}
DN trustMgrDN = config.getTrustManagerProviderDN();
TrustManagerProvider<?> trustManagerProvider =
DirectoryServer.getTrustManagerProvider(trustMgrDN);
if (trustManagerProvider == null)
{
trustManagerProvider = new NullTrustManagerProvider();
}
SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_INSTANCE_NAME);
sslContext.init(keyManagers, trustManagerProvider.getTrustManagers(), null);
return sslContext;
}
}