001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2013-2015 ForgeRock AS
025 */
026package org.opends.server.protocols.http;
027
028import static org.opends.messages.ConfigMessages.*;
029import static org.opends.messages.ProtocolMessages.*;
030import static org.opends.server.util.ServerConstants.*;
031import static org.opends.server.util.StaticUtils.*;
032
033import java.io.File;
034import java.io.IOException;
035import java.net.InetAddress;
036import java.util.*;
037import java.util.concurrent.ConcurrentHashMap;
038import java.util.concurrent.TimeUnit;
039import java.util.logging.Level;
040import java.util.logging.Logger;
041
042import javax.net.ssl.KeyManager;
043import javax.net.ssl.SSLContext;
044import javax.net.ssl.SSLEngine;
045import javax.servlet.DispatcherType;
046import javax.servlet.Filter;
047import javax.servlet.ServletException;
048
049import org.codehaus.jackson.JsonParseException;
050import org.codehaus.jackson.JsonParser;
051import org.codehaus.jackson.map.JsonMappingException;
052import org.codehaus.jackson.map.ObjectMapper;
053import org.forgerock.i18n.LocalizableMessage;
054import org.forgerock.i18n.slf4j.LocalizedLogger;
055import org.forgerock.json.fluent.JsonValue;
056import org.forgerock.json.resource.CollectionResourceProvider;
057import org.forgerock.json.resource.ConnectionFactory;
058import org.forgerock.json.resource.Resources;
059import org.forgerock.json.resource.Router;
060import org.forgerock.json.resource.servlet.HttpServlet;
061import org.forgerock.opendj.config.server.ConfigChangeResult;
062import org.forgerock.opendj.config.server.ConfigException;
063import org.forgerock.opendj.ldap.ResultCode;
064import org.forgerock.opendj.ldap.SearchScope;
065import org.forgerock.opendj.rest2ldap.AuthorizationPolicy;
066import org.forgerock.opendj.rest2ldap.Rest2LDAP;
067import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory;
068import org.glassfish.grizzly.http.HttpProbe;
069import org.glassfish.grizzly.http.server.HttpServer;
070import org.glassfish.grizzly.http.server.NetworkListener;
071import org.glassfish.grizzly.http.server.ServerConfiguration;
072import org.glassfish.grizzly.monitoring.MonitoringConfig;
073import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
074import org.glassfish.grizzly.servlet.WebappContext;
075import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
076import org.glassfish.grizzly.strategies.SameThreadIOStrategy;
077import org.glassfish.grizzly.utils.Charsets;
078import org.opends.server.admin.server.ConfigurationChangeListener;
079import org.opends.server.admin.std.server.ConnectionHandlerCfg;
080import org.opends.server.admin.std.server.HTTPConnectionHandlerCfg;
081import org.opends.server.api.*;
082import org.opends.server.core.DirectoryServer;
083import org.opends.server.extensions.NullKeyManagerProvider;
084import org.opends.server.extensions.NullTrustManagerProvider;
085import org.opends.server.loggers.HTTPAccessLogger;
086import org.opends.server.monitors.ClientConnectionMonitorProvider;
087import org.opends.server.types.*;
088import org.opends.server.util.SelectableCertificateKeyManager;
089import org.opends.server.util.StaticUtils;
090
091/**
092 * This class defines a connection handler that will be used for communicating
093 * with clients over HTTP. The connection handler is responsible for
094 * starting/stopping the embedded web server.
095 */
096public class HTTPConnectionHandler extends ConnectionHandler<HTTPConnectionHandlerCfg>
097                                   implements ConfigurationChangeListener<HTTPConnectionHandlerCfg>,
098                                              ServerShutdownListener,
099                                              AlertGenerator
100{
101  /** The tracer object for the debug logger. */
102  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
103
104  /** Default friendly name for this connection handler. */
105  private static final String DEFAULT_FRIENDLY_NAME = "HTTP Connection Handler";
106
107  /** SSL instance name used in context creation. */
108  private static final String SSL_CONTEXT_INSTANCE_NAME = "TLS";
109
110  private static final ObjectMapper JSON_MAPPER = new ObjectMapper().configure(JsonParser.Feature.ALLOW_COMMENTS, true);
111
112  /** The initialization configuration. */
113  private HTTPConnectionHandlerCfg initConfig;
114
115  /** The current configuration. */
116  private HTTPConnectionHandlerCfg currentConfig;
117
118  /** Indicates whether the Directory Server is in the process of shutting down. */
119  private volatile boolean shutdownRequested;
120
121  /** Indicates whether this connection handler is enabled. */
122  private boolean enabled;
123
124  /** The set of listeners for this connection handler. */
125  private List<HostPort> listeners = new LinkedList<>();
126
127  /** The HTTP server embedded in OpenDJ. */
128  private HttpServer httpServer;
129
130  /** The HTTP probe that collects stats. */
131  private HTTPStatsProbe httpProbe;
132
133  /**
134   * Holds the current client connections. Using {@link ConcurrentHashMap} to
135   * ensure no concurrent reads/writes can happen and adds/removes are fast. We
136   * only use the keys, so it does not matter what value is put there.
137   */
138  private Map<ClientConnection, ClientConnection> clientConnections = new ConcurrentHashMap<>();
139
140  /** The set of statistics collected for this connection handler. */
141  private HTTPStatistics statTracker;
142
143  /** The client connection monitor provider associated with this connection handler. */
144  private ClientConnectionMonitorProvider connMonitor;
145
146  /** The unique name assigned to this connection handler. */
147  private String handlerName;
148
149  /** The protocol used by this connection handler. */
150  private String protocol;
151
152  /**
153   * The condition variable that will be used by the start method to wait for
154   * the socket port to be opened and ready to process requests before returning.
155   */
156  private final Object waitListen = new Object();
157
158  /** The friendly name of this connection handler. */
159  private String friendlyName;
160
161  /** The SSL engine configurator is used for obtaining default SSL parameters. */
162  private SSLEngineConfigurator sslEngineConfigurator;
163
164  /** Default constructor. It is invoked by reflection to create this {@link ConnectionHandler}. */
165  public HTTPConnectionHandler()
166  {
167    super(DEFAULT_FRIENDLY_NAME);
168  }
169
170  /**
171   * Returns whether unauthenticated HTTP requests are allowed. The server
172   * checks whether unauthenticated requests are allowed server-wide first then
173   * for the HTTP Connection Handler second.
174   *
175   * @return true if unauthenticated requests are allowed, false otherwise.
176   */
177  public boolean acceptUnauthenticatedRequests()
178  {
179    // The global setting overrides the more specific setting here.
180    return !DirectoryServer.rejectUnauthenticatedRequests() && !this.currentConfig.isAuthenticationRequired();
181  }
182
183  /**
184   * Registers a client connection to track it.
185   *
186   * @param clientConnection
187   *          the client connection to register
188   */
189  void addClientConnection(ClientConnection clientConnection)
190  {
191    clientConnections.put(clientConnection, clientConnection);
192  }
193
194  @Override
195  public ConfigChangeResult applyConfigurationChange(HTTPConnectionHandlerCfg config)
196  {
197    final ConfigChangeResult ccr = new ConfigChangeResult();
198
199    if (anyChangeRequiresRestart(config))
200    {
201      ccr.setAdminActionRequired(true);
202      ccr.addMessage(ERR_CONNHANDLER_CONFIG_CHANGES_REQUIRE_RESTART.get("HTTP"));
203    }
204
205    // Reconfigure SSL if needed.
206    try
207    {
208      configureSSL(config);
209    }
210    catch (DirectoryException e)
211    {
212      logger.traceException(e);
213      ccr.setResultCode(e.getResultCode());
214      ccr.addMessage(e.getMessageObject());
215      return ccr;
216    }
217
218    if (config.isEnabled() && this.currentConfig.isEnabled() && isListening())
219    {
220      // Server was running and will still be running if the "enabled" was flipped,
221      // leave it to the stop / start server to handle it.
222      if (!this.currentConfig.isKeepStats() && config.isKeepStats())
223      {
224        // It must now keep stats while it was not previously.
225        setHttpStatsProbe(this.httpServer);
226      }
227      else if (this.currentConfig.isKeepStats() && !config.isKeepStats() && this.httpProbe != null)
228      {
229        // It must NOT keep stats anymore.
230        getHttpConfig(this.httpServer).removeProbes(this.httpProbe);
231        this.httpProbe = null;
232      }
233    }
234
235    this.initConfig = config;
236    this.currentConfig = config;
237    this.enabled = this.currentConfig.isEnabled();
238
239    return ccr;
240  }
241
242  private boolean anyChangeRequiresRestart(HTTPConnectionHandlerCfg newCfg)
243  {
244    return !equals(newCfg.getListenPort(), initConfig.getListenPort())
245        || !Objects.equals(newCfg.getListenAddress(), initConfig.getListenAddress())
246        || !equals(newCfg.getMaxRequestSize(), currentConfig.getMaxRequestSize())
247        || !equals(newCfg.isAllowTCPReuseAddress(), currentConfig.isAllowTCPReuseAddress())
248        || !equals(newCfg.isUseTCPKeepAlive(), currentConfig.isUseTCPKeepAlive())
249        || !equals(newCfg.isUseTCPNoDelay(), currentConfig.isUseTCPNoDelay())
250        || !equals(newCfg.getMaxBlockedWriteTimeLimit(), currentConfig.getMaxBlockedWriteTimeLimit())
251        || !equals(newCfg.getBufferSize(), currentConfig.getBufferSize())
252        || !equals(newCfg.getAcceptBacklog(), currentConfig.getAcceptBacklog())
253        || !equals(newCfg.isUseSSL(), currentConfig.isUseSSL())
254        || !Objects.equals(newCfg.getKeyManagerProviderDN(), currentConfig.getKeyManagerProviderDN())
255        || !Objects.equals(newCfg.getSSLCertNickname(), currentConfig.getSSLCertNickname())
256        || !Objects.equals(newCfg.getTrustManagerProviderDN(), currentConfig.getTrustManagerProviderDN())
257        || !Objects.equals(newCfg.getSSLProtocol(), currentConfig.getSSLProtocol())
258        || !Objects.equals(newCfg.getSSLCipherSuite(), currentConfig.getSSLCipherSuite())
259        || !Objects.equals(newCfg.getSSLClientAuthPolicy(), currentConfig.getSSLClientAuthPolicy());
260  }
261
262  private boolean equals(long l1, long l2)
263  {
264    return l1 == l2;
265  }
266
267  private boolean equals(boolean b1, boolean b2)
268  {
269    return b1 == b2;
270  }
271
272  private void configureSSL(HTTPConnectionHandlerCfg config)
273      throws DirectoryException
274  {
275    protocol = config.isUseSSL() ? "HTTPS" : "HTTP";
276    if (config.isUseSSL())
277    {
278      sslEngineConfigurator = createSSLEngineConfigurator(config);
279    }
280    else
281    {
282      sslEngineConfigurator = null;
283    }
284  }
285
286  @Override
287  public void finalizeConnectionHandler(LocalizableMessage finalizeReason)
288  {
289    shutdownRequested = true;
290    // Unregister this as a change listener.
291    currentConfig.removeHTTPChangeListener(this);
292
293    if (connMonitor != null)
294    {
295      DirectoryServer.deregisterMonitorProvider(connMonitor);
296    }
297
298    if (statTracker != null)
299    {
300      DirectoryServer.deregisterMonitorProvider(statTracker);
301    }
302  }
303
304  @Override
305  public Map<String, String> getAlerts()
306  {
307    Map<String, String> alerts = new LinkedHashMap<>();
308
309    alerts.put(ALERT_TYPE_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES,
310               ALERT_DESCRIPTION_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES);
311
312    return alerts;
313  }
314
315  @Override
316  public String getClassName()
317  {
318    return HTTPConnectionHandler.class.getName();
319  }
320
321  @Override
322  public Collection<ClientConnection> getClientConnections()
323  {
324    return clientConnections.keySet();
325  }
326
327  @Override
328  public DN getComponentEntryDN()
329  {
330    return currentConfig.dn();
331  }
332
333  @Override
334  public String getConnectionHandlerName()
335  {
336    return handlerName;
337  }
338
339  /**
340   * Returns the current config of this connection handler.
341   *
342   * @return the current config of this connection handler
343   */
344  HTTPConnectionHandlerCfg getCurrentConfig()
345  {
346    return this.currentConfig;
347  }
348
349  @Override
350  public Collection<String> getEnabledSSLCipherSuites()
351  {
352    final SSLEngineConfigurator configurator = sslEngineConfigurator;
353    if (configurator != null)
354    {
355      return Arrays.asList(configurator.getEnabledCipherSuites());
356    }
357    return super.getEnabledSSLCipherSuites();
358  }
359
360  @Override
361  public Collection<String> getEnabledSSLProtocols()
362  {
363    final SSLEngineConfigurator configurator = sslEngineConfigurator;
364    if (configurator != null)
365    {
366      return Arrays.asList(configurator.getEnabledProtocols());
367    }
368    return super.getEnabledSSLProtocols();
369  }
370
371  @Override
372  public Collection<HostPort> getListeners()
373  {
374    return listeners;
375  }
376
377  /**
378   * Returns the listen port for this connection handler.
379   *
380   * @return the listen port for this connection handler.
381   */
382  int getListenPort()
383  {
384    return this.initConfig.getListenPort();
385  }
386
387  @Override
388  public String getProtocol()
389  {
390    return protocol;
391  }
392
393  /**
394   * Returns the SSL engine configured for this connection handler if SSL is
395   * enabled, null otherwise.
396   *
397   * @return the SSL engine if SSL is enabled, null otherwise
398   */
399  SSLEngine getSSLEngine()
400  {
401    return sslEngineConfigurator.createSSLEngine();
402  }
403
404  @Override
405  public String getShutdownListenerName()
406  {
407    return handlerName;
408  }
409
410  /**
411   * Retrieves the set of statistics maintained by this connection handler.
412   *
413   * @return The set of statistics maintained by this connection handler.
414   */
415  public HTTPStatistics getStatTracker()
416  {
417    return statTracker;
418  }
419
420  @Override
421  public void initializeConnectionHandler(HTTPConnectionHandlerCfg config)
422      throws ConfigException, InitializationException
423  {
424    this.enabled = config.isEnabled();
425
426    if (friendlyName == null)
427    {
428      friendlyName = config.dn().rdn().getAttributeValue(0).toString();
429    }
430
431    int listenPort = config.getListenPort();
432    for (InetAddress a : config.getListenAddress())
433    {
434      listeners.add(new HostPort(a.getHostAddress(), listenPort));
435    }
436
437    handlerName = getHandlerName(config);
438
439    // Configure SSL if needed.
440    try
441    {
442      // This call may disable the connector if wrong SSL settings
443      configureSSL(config);
444    }
445    catch (DirectoryException e)
446    {
447      logger.traceException(e);
448      throw new InitializationException(e.getMessageObject());
449    }
450
451    // Create and register monitors.
452    statTracker = new HTTPStatistics(handlerName + " Statistics");
453    DirectoryServer.registerMonitorProvider(statTracker);
454
455    connMonitor = new ClientConnectionMonitorProvider(this);
456    DirectoryServer.registerMonitorProvider(connMonitor);
457
458    // Register this as a change listener.
459    config.addHTTPChangeListener(this);
460
461    this.initConfig = config;
462    this.currentConfig = config;
463  }
464
465  private String getHandlerName(HTTPConnectionHandlerCfg config)
466  {
467    StringBuilder nameBuffer = new StringBuilder();
468    nameBuffer.append(friendlyName);
469    for (InetAddress a : config.getListenAddress())
470    {
471      nameBuffer.append(" ");
472      nameBuffer.append(a.getHostAddress());
473    }
474    nameBuffer.append(" port ");
475    nameBuffer.append(config.getListenPort());
476    return nameBuffer.toString();
477  }
478
479  @Override
480  public boolean isConfigurationAcceptable(
481      ConnectionHandlerCfg configuration, List<LocalizableMessage> unacceptableReasons)
482  {
483    HTTPConnectionHandlerCfg config = (HTTPConnectionHandlerCfg) configuration;
484
485    if (currentConfig == null || (!this.enabled && config.isEnabled()))
486    {
487      // Attempt to bind to the listen port on all configured addresses to
488      // verify whether the connection handler will be able to start.
489      LocalizableMessage errorMessage = checkAnyListenAddressInUse(
490          config.getListenAddress(), config.getListenPort(), config.isAllowTCPReuseAddress(), config.dn());
491      if (errorMessage != null)
492      {
493        unacceptableReasons.add(errorMessage);
494        return false;
495      }
496    }
497
498    if (config.isEnabled() && config.isUseSSL())
499    {
500      try
501      {
502        createSSLEngineConfigurator(config);
503      }
504      catch (DirectoryException e)
505      {
506        logger.traceException(e);
507        unacceptableReasons.add(e.getMessageObject());
508        return false;
509      }
510    }
511
512    return true;
513  }
514
515  /**
516   * Checks whether any listen address is in use for the given port. The check
517   * is performed by binding to each address and port.
518   *
519   * @param listenAddresses
520   *          the listen {@link InetAddress} to test
521   * @param listenPort
522   *          the listen port to test
523   * @param allowReuseAddress
524   *          whether addresses can be reused
525   * @param configEntryDN
526   *          the configuration entry DN
527   * @return an error message if at least one of the address is already in use,
528   *         null otherwise.
529   */
530  private LocalizableMessage checkAnyListenAddressInUse(
531      Collection<InetAddress> listenAddresses, int listenPort, boolean allowReuseAddress, DN configEntryDN)
532  {
533    for (InetAddress a : listenAddresses)
534    {
535      try
536      {
537        if (isAddressInUse(a, listenPort, allowReuseAddress))
538        {
539          throw new IOException(ERR_CONNHANDLER_ADDRESS_INUSE.get().toString());
540        }
541      }
542      catch (IOException e)
543      {
544        logger.traceException(e);
545        return ERR_CONNHANDLER_CANNOT_BIND.get(
546            "HTTP", configEntryDN, a.getHostAddress(), listenPort, getExceptionMessage(e));
547      }
548    }
549    return null;
550  }
551
552  @Override
553  public boolean isConfigurationChangeAcceptable(
554      HTTPConnectionHandlerCfg configuration, List<LocalizableMessage> unacceptableReasons)
555  {
556    return isConfigurationAcceptable(configuration, unacceptableReasons);
557  }
558
559  /**
560   * Indicates whether this connection handler should maintain usage statistics.
561   *
562   * @return <CODE>true</CODE> if this connection handler should maintain usage
563   *         statistics, or <CODE>false</CODE> if not.
564   */
565  public boolean keepStats()
566  {
567    return currentConfig.isKeepStats();
568  }
569
570  @Override
571  public void processServerShutdown(LocalizableMessage reason)
572  {
573    shutdownRequested = true;
574  }
575
576  private boolean isListening()
577  {
578    return httpServer != null;
579  }
580
581  @Override
582  public void start()
583  {
584    // The Directory Server start process should only return when the connection handlers port
585    // are fully opened and working.
586    // The start method therefore needs to wait for the created thread too.
587    synchronized (waitListen)
588    {
589      super.start();
590
591      try
592      {
593        waitListen.wait();
594      }
595      catch (InterruptedException e)
596      {
597        // If something interrupted the start its probably better to return ASAP
598      }
599    }
600  }
601
602  /**
603   * Unregisters a client connection to stop tracking it.
604   *
605   * @param clientConnection
606   *          the client connection to unregister
607   */
608  void removeClientConnection(ClientConnection clientConnection)
609  {
610    clientConnections.remove(clientConnection);
611  }
612
613  @Override
614  public void run()
615  {
616    setName(handlerName);
617
618    boolean lastIterationFailed = false;
619    boolean starting = true;
620
621    while (!shutdownRequested)
622    {
623      // If this connection handler is not enabled, then just sleep for a bit and check again.
624      if (!this.enabled)
625      {
626        if (isListening())
627        {
628          stopHttpServer();
629        }
630
631        if (starting)
632        {
633          // This may happen if there was an initialisation error which led to disable the connector.
634          // The main thread is waiting for the connector to listen on its port, which will not occur yet,
635          // so notify here to allow the server startup to complete.
636          synchronized (waitListen)
637          {
638            starting = false;
639            waitListen.notify();
640          }
641        }
642
643        StaticUtils.sleep(1000);
644        continue;
645      }
646
647      if (isListening())
648      {
649        // If already listening, then sleep for a bit and check again.
650        StaticUtils.sleep(1000);
651        continue;
652      }
653
654      try
655      {
656        // At this point, the connection Handler either started correctly or failed
657        // to start but the start process should be notified and resume its work in any cases.
658        synchronized (waitListen)
659        {
660          waitListen.notify();
661        }
662
663        // If we have gotten here, then we are about to start listening
664        // for the first time since startup or since we were previously disabled.
665        // Start the embedded HTTP server
666        startHttpServer();
667        lastIterationFailed = false;
668      }
669      catch (Exception e)
670      {
671        // Clean up the messed up HTTP server
672        cleanUpHttpServer();
673
674        // Error + alert about the horked config
675        logger.traceException(e);
676        logger.error(
677            ERR_CONNHANDLER_CANNOT_ACCEPT_CONNECTION, friendlyName, currentConfig.dn(), getExceptionMessage(e));
678
679        if (lastIterationFailed)
680        {
681          // The last time through the accept loop we also encountered a failure.
682          // Rather than enter a potential infinite loop of failures,
683          // disable this acceptor and log an error.
684          LocalizableMessage message = ERR_CONNHANDLER_CONSECUTIVE_ACCEPT_FAILURES.get(
685              friendlyName, currentConfig.dn(), stackTraceToSingleLineString(e));
686          logger.error(message);
687
688          DirectoryServer.sendAlertNotification(this, ALERT_TYPE_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES, message);
689          this.enabled = false;
690        }
691        else
692        {
693          lastIterationFailed = true;
694        }
695      }
696    }
697
698    // Initiate shutdown
699    stopHttpServer();
700  }
701
702  private void startHttpServer() throws Exception
703  {
704    // Silence Grizzly's own logging
705    Logger.getLogger("org.glassfish.grizzly").setLevel(Level.OFF);
706
707    if (HTTPAccessLogger.getHTTPAccessLogPublishers().isEmpty())
708    {
709      logger.warn(WARN_CONFIG_LOGGER_NO_ACTIVE_HTTP_ACCESS_LOGGERS);
710    }
711
712    this.httpServer = createHttpServer();
713
714    // Register servlet as default servlet and also able to serve REST requests
715    createAndRegisterServlet("OpenDJ Rest2LDAP servlet", "", "/*");
716
717    logger.trace("Starting HTTP server...");
718    this.httpServer.start();
719    logger.trace("HTTP server started");
720    logger.info(NOTE_CONNHANDLER_STARTED_LISTENING, handlerName);
721  }
722
723  private HttpServer createHttpServer()
724  {
725    final HttpServer server = new HttpServer();
726
727    final int requestSize = (int) currentConfig.getMaxRequestSize();
728    final ServerConfiguration serverConfig = server.getServerConfiguration();
729    serverConfig.setMaxBufferedPostSize(requestSize);
730    serverConfig.setMaxFormPostSize(requestSize);
731    serverConfig.setDefaultQueryEncoding(Charsets.UTF8_CHARSET);
732
733    if (keepStats())
734    {
735      setHttpStatsProbe(server);
736    }
737
738    // Configure the network listener
739    final NetworkListener listener = new NetworkListener(
740        "Rest2LDAP", NetworkListener.DEFAULT_NETWORK_HOST, initConfig.getListenPort());
741    server.addListener(listener);
742
743    // Configure the network transport
744    final TCPNIOTransport transport = listener.getTransport();
745    transport.setReuseAddress(currentConfig.isAllowTCPReuseAddress());
746    transport.setKeepAlive(currentConfig.isUseTCPKeepAlive());
747    transport.setTcpNoDelay(currentConfig.isUseTCPNoDelay());
748    transport.setWriteTimeout(currentConfig.getMaxBlockedWriteTimeLimit(), TimeUnit.MILLISECONDS);
749
750    final int bufferSize = (int) currentConfig.getBufferSize();
751    transport.setReadBufferSize(bufferSize);
752    transport.setWriteBufferSize(bufferSize);
753    transport.setIOStrategy(SameThreadIOStrategy.getInstance());
754
755    final int numRequestHandlers = getNumRequestHandlers(currentConfig.getNumRequestHandlers(), friendlyName);
756    transport.setSelectorRunnersCount(numRequestHandlers);
757    transport.setServerConnectionBackLog(currentConfig.getAcceptBacklog());
758
759    // Configure SSL
760    if (sslEngineConfigurator != null)
761    {
762      listener.setSecure(true);
763      listener.setSSLEngineConfig(sslEngineConfigurator);
764    }
765
766    return server;
767  }
768
769  private void setHttpStatsProbe(HttpServer server)
770  {
771    this.httpProbe = new HTTPStatsProbe(this.statTracker);
772    getHttpConfig(server).addProbes(this.httpProbe);
773  }
774
775  private MonitoringConfig<HttpProbe> getHttpConfig(HttpServer server)
776  {
777    return server.getServerConfiguration().getMonitoringConfig().getHttpConfig();
778  }
779
780  private void createAndRegisterServlet(final String servletName, final String... urlPatterns) throws Exception
781  {
782    // Parse and use JSON config
783    File jsonConfigFile = getFileForPath(this.currentConfig.getConfigFile());
784    final JsonValue configuration = parseJsonConfiguration(jsonConfigFile).recordKeyAccesses();
785    final HTTPAuthenticationConfig authenticationConfig = getAuthenticationConfig(configuration);
786    final ConnectionFactory connFactory = getConnectionFactory(configuration);
787    configuration.verifyAllKeysAccessed();
788
789    Filter filter = new CollectClientConnectionsFilter(this, authenticationConfig);
790    // Used for hooking our HTTPClientConnection in Rest2LDAP
791    final HttpServlet servlet = new HttpServlet(connFactory, Rest2LDAPContextFactory.getHttpServletContextFactory());
792
793    // Create and deploy the Web app context
794    final WebappContext ctx = new WebappContext(servletName);
795    ctx.addFilter("collectClientConnections", filter)
796       .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, urlPatterns);
797    ctx.addServlet(servletName, servlet).addMapping(urlPatterns);
798    ctx.deploy(this.httpServer);
799  }
800
801  private HTTPAuthenticationConfig getAuthenticationConfig(final JsonValue configuration)
802  {
803    final HTTPAuthenticationConfig result = new HTTPAuthenticationConfig();
804
805    final JsonValue val = configuration.get("authenticationFilter");
806    result.setBasicAuthenticationSupported(asBool(val, "supportHTTPBasicAuthentication"));
807    result.setCustomHeadersAuthenticationSupported(asBool(val, "supportAltAuthentication"));
808    result.setCustomHeaderUsername(val.get("altAuthenticationUsernameHeader").asString());
809    result.setCustomHeaderPassword(val.get("altAuthenticationPasswordHeader").asString());
810
811    final String searchBaseDN = asString(val, "searchBaseDN");
812    result.setSearchBaseDN(org.forgerock.opendj.ldap.DN.valueOf(searchBaseDN));
813    result.setSearchScope(SearchScope.valueOf(asString(val, "searchScope")));
814    result.setSearchFilterTemplate(asString(val, "searchFilterTemplate"));
815
816    return result;
817  }
818
819  private String asString(JsonValue value, String key)
820  {
821    return value.get(key).required().asString();
822  }
823
824  private boolean asBool(JsonValue value, String key)
825  {
826    return value.get(key).defaultTo(false).asBoolean();
827  }
828
829  private ConnectionFactory getConnectionFactory(final JsonValue configuration)
830  {
831    final Router router = new Router();
832    final JsonValue mappings = configuration.get("servlet").get("mappings").required();
833    for (final String mappingUrl : mappings.keys())
834    {
835      final JsonValue mapping = mappings.get(mappingUrl);
836      final CollectionResourceProvider provider = Rest2LDAP.builder()
837                                                           .authorizationPolicy(AuthorizationPolicy.REUSE)
838                                                           .configureMapping(mapping).build();
839      router.addRoute(mappingUrl, provider);
840    }
841    return Resources.newInternalConnectionFactory(router);
842  }
843
844  private JsonValue parseJsonConfiguration(File configFile)
845      throws IOException, JsonParseException, JsonMappingException, ServletException
846  {
847    // Parse the config file.
848    final Object content = JSON_MAPPER.readValue(configFile, Object.class);
849    if (!(content instanceof Map))
850    {
851      throw new ServletException(
852          "Servlet configuration file '" + configFile + "' does not contain a valid JSON configuration");
853    }
854    return new JsonValue(content);
855  }
856
857  private void stopHttpServer()
858  {
859    if (this.httpServer != null)
860    {
861      logger.trace("Stopping HTTP server...");
862      this.httpServer.shutdownNow();
863      cleanUpHttpServer();
864      logger.trace("HTTP server stopped");
865      logger.info(NOTE_CONNHANDLER_STOPPED_LISTENING, handlerName);
866    }
867  }
868
869  private void cleanUpHttpServer()
870  {
871    this.httpServer = null;
872    this.httpProbe = null;
873  }
874
875  @Override
876  public void toString(StringBuilder buffer)
877  {
878    buffer.append(handlerName);
879  }
880
881  private SSLEngineConfigurator createSSLEngineConfigurator(HTTPConnectionHandlerCfg config) throws DirectoryException
882  {
883    if (!config.isUseSSL())
884    {
885      return null;
886    }
887
888    try
889    {
890      SSLContext sslContext = createSSLContext(config);
891      SSLEngineConfigurator configurator = new SSLEngineConfigurator(sslContext);
892      configurator.setClientMode(false);
893
894      // configure with defaults from the JVM
895      final SSLEngine defaults = sslContext.createSSLEngine();
896      configurator.setEnabledProtocols(defaults.getEnabledProtocols());
897      configurator.setEnabledCipherSuites(defaults.getEnabledCipherSuites());
898
899      final Set<String> protocols = config.getSSLProtocol();
900      if (!protocols.isEmpty())
901      {
902        configurator.setEnabledProtocols(protocols.toArray(new String[protocols.size()]));
903      }
904
905      final Set<String> ciphers = config.getSSLCipherSuite();
906      if (!ciphers.isEmpty())
907      {
908        configurator.setEnabledCipherSuites(ciphers.toArray(new String[ciphers.size()]));
909      }
910
911      switch (config.getSSLClientAuthPolicy())
912      {
913      case DISABLED:
914        configurator.setNeedClientAuth(false);
915        configurator.setWantClientAuth(false);
916        break;
917      case REQUIRED:
918        configurator.setNeedClientAuth(true);
919        configurator.setWantClientAuth(true);
920        break;
921      case OPTIONAL:
922      default:
923        configurator.setNeedClientAuth(false);
924        configurator.setWantClientAuth(true);
925        break;
926      }
927
928      return configurator;
929    }
930    catch (Exception e)
931    {
932      logger.traceException(e);
933      ResultCode resCode = DirectoryServer.getServerErrorResultCode();
934      throw new DirectoryException(resCode, ERR_CONNHANDLER_SSL_CANNOT_INITIALIZE.get(getExceptionMessage(e)), e);
935    }
936  }
937
938  private SSLContext createSSLContext(HTTPConnectionHandlerCfg config) throws Exception
939  {
940    if (!config.isUseSSL())
941    {
942      return null;
943    }
944
945    DN keyMgrDN = config.getKeyManagerProviderDN();
946    KeyManagerProvider<?> keyManagerProvider = DirectoryServer.getKeyManagerProvider(keyMgrDN);
947    if (keyManagerProvider == null) {
948      logger.error(ERR_NULL_KEY_PROVIDER_MANAGER, keyMgrDN, friendlyName);
949      logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
950      keyManagerProvider = new NullKeyManagerProvider();
951      enabled = false;
952    }
953    else if (! keyManagerProvider.containsAtLeastOneKey())
954    {
955      logger.error(ERR_INVALID_KEYSTORE, friendlyName);
956      logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
957      enabled = false;
958    }
959
960    String alias = config.getSSLCertNickname();
961    KeyManager[] keyManagers;
962    if (alias == null)
963    {
964      keyManagers = keyManagerProvider.getKeyManagers();
965    }
966    else
967    {
968      if (! keyManagerProvider.containsKeyWithAlias(alias)) {
969        logger.error(ERR_KEYSTORE_DOES_NOT_CONTAIN_ALIAS, alias, friendlyName);
970        logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
971        enabled = false;
972      }
973      keyManagers = SelectableCertificateKeyManager.wrap(keyManagerProvider.getKeyManagers(), alias);
974    }
975
976    DN trustMgrDN = config.getTrustManagerProviderDN();
977    TrustManagerProvider<?> trustManagerProvider = DirectoryServer.getTrustManagerProvider(trustMgrDN);
978    if (trustManagerProvider == null)
979    {
980      trustManagerProvider = new NullTrustManagerProvider();
981    }
982
983    SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_INSTANCE_NAME);
984    sslContext.init(keyManagers, trustManagerProvider.getTrustManagers(), null);
985    return sslContext;
986  }
987}