001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2013-2017 ForgeRock AS.
015 */
016package org.opends.server.protocols.http;
017
018import static org.opends.messages.ConfigMessages.*;
019import static org.opends.messages.ProtocolMessages.*;
020import static org.opends.server.util.ServerConstants.*;
021import static org.opends.server.util.StaticUtils.*;
022import static org.forgerock.http.grizzly.GrizzlySupport.newGrizzlyHttpHandler;
023
024import java.io.IOException;
025import java.net.InetAddress;
026import java.util.Arrays;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.Iterator;
030import java.util.LinkedHashMap;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.Map;
034import java.util.Objects;
035import java.util.Set;
036import java.util.SortedSet;
037import java.util.TreeSet;
038import java.util.concurrent.ConcurrentHashMap;
039import java.util.concurrent.TimeUnit;
040
041import javax.net.ssl.KeyManager;
042import javax.net.ssl.SSLContext;
043import javax.net.ssl.SSLEngine;
044
045import org.forgerock.http.Filter;
046import org.forgerock.http.Handler;
047import org.forgerock.http.HttpApplication;
048import org.forgerock.http.HttpApplicationException;
049import org.forgerock.http.handler.Handlers;
050import org.forgerock.http.io.Buffer;
051import org.forgerock.http.protocol.Request;
052import org.forgerock.http.protocol.Response;
053import org.forgerock.http.protocol.Status;
054import org.forgerock.i18n.LocalizableMessage;
055import org.forgerock.i18n.slf4j.LocalizedLogger;
056import org.forgerock.opendj.config.server.ConfigChangeResult;
057import org.forgerock.opendj.config.server.ConfigException;
058import org.forgerock.opendj.config.server.ConfigurationChangeListener;
059import org.forgerock.opendj.ldap.DN;
060import org.forgerock.opendj.ldap.ResultCode;
061import org.forgerock.opendj.rest2ldap.ErrorLoggerFilter;
062import org.forgerock.opendj.server.config.server.ConnectionHandlerCfg;
063import org.forgerock.opendj.server.config.server.HTTPConnectionHandlerCfg;
064import org.forgerock.services.context.Context;
065import org.forgerock.util.promise.NeverThrowsException;
066import org.forgerock.util.promise.Promise;
067import org.forgerock.util.promise.PromiseImpl;
068import org.forgerock.util.time.TimeService;
069import org.glassfish.grizzly.http.HttpProbe;
070import org.glassfish.grizzly.http.server.ErrorPageGenerator;
071import org.glassfish.grizzly.http.server.HttpServer;
072import org.glassfish.grizzly.http.server.NetworkListener;
073import org.glassfish.grizzly.http.server.ServerConfiguration;
074import org.glassfish.grizzly.http.util.HttpStatus;
075import org.glassfish.grizzly.http.util.HttpUtils;
076import org.glassfish.grizzly.monitoring.MonitoringConfig;
077import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
078import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
079import org.glassfish.grizzly.strategies.SameThreadIOStrategy;
080import org.glassfish.grizzly.utils.Charsets;
081import org.opends.server.api.AlertGenerator;
082import org.opends.server.api.ClientConnection;
083import org.opends.server.api.ConnectionHandler;
084import org.opends.server.api.KeyManagerProvider;
085import org.opends.server.api.ServerShutdownListener;
086import org.opends.server.api.TrustManagerProvider;
087import org.opends.server.core.DirectoryServer;
088import org.opends.server.core.ServerContext;
089import org.opends.server.extensions.NullKeyManagerProvider;
090import org.opends.server.extensions.NullTrustManagerProvider;
091import org.opends.server.loggers.HTTPAccessLogger;
092import org.opends.server.monitors.ClientConnectionMonitorProvider;
093import org.opends.server.protocols.internal.InternalClientConnection;
094import org.opends.server.types.AbstractOperation;
095import org.opends.server.types.DirectoryException;
096import org.opends.server.types.HostPort;
097import org.opends.server.types.InitializationException;
098import org.opends.server.types.OperationType;
099import org.opends.server.util.DynamicConstants;
100import org.opends.server.util.SelectableCertificateKeyManager;
101import org.opends.server.util.StaticUtils;
102
103/**
104 * This class defines a connection handler that will be used for communicating
105 * with clients over HTTP. The connection handler is responsible for
106 * starting/stopping the embedded web server.
107 */
108public class HTTPConnectionHandler extends ConnectionHandler<HTTPConnectionHandlerCfg>
109                                   implements ConfigurationChangeListener<HTTPConnectionHandlerCfg>,
110                                              ServerShutdownListener,
111                                              AlertGenerator
112{
113  /** The tracer object for the debug logger. */
114  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
115
116  /** Default friendly name for this connection handler. */
117  private static final String DEFAULT_FRIENDLY_NAME = "HTTP Connection Handler";
118
119  /** SSL instance name used in context creation. */
120  private static final String SSL_CONTEXT_INSTANCE_NAME = "TLS";
121
122  /** The initialization configuration. */
123  private HTTPConnectionHandlerCfg initConfig;
124
125  /** The current configuration. */
126  private HTTPConnectionHandlerCfg currentConfig;
127
128  /** Indicates whether the Directory Server is in the process of shutting down. */
129  private volatile boolean shutdownRequested;
130
131  /** Indicates whether this connection handler is enabled. */
132  private boolean enabled;
133
134  /** The set of listeners for this connection handler. */
135  private final List<HostPort> listeners = new LinkedList<>();
136
137  /** The HTTP server embedded in OpenDJ. */
138  private HttpServer httpServer;
139
140  /** The HTTP probe that collects stats. */
141  private HTTPStatsProbe httpProbe;
142
143  /**
144   * Holds the current client connections. Using {@link ConcurrentHashMap} to
145   * ensure no concurrent reads/writes can happen and adds/removes are fast. We
146   * only use the keys, so it does not matter what value is put there.
147   */
148  private final Map<ClientConnection, ClientConnection> clientConnections = new ConcurrentHashMap<>();
149
150  /** The set of statistics collected for this connection handler. */
151  private HTTPStatistics statTracker;
152
153  /** The client connection monitor provider associated with this connection handler. */
154  private ClientConnectionMonitorProvider connMonitor;
155
156  /** The unique name assigned to this connection handler. */
157  private String handlerName;
158
159  /** The protocol used by this connection handler. */
160  private String protocol;
161
162  /**
163   * The condition variable that will be used by the start method to wait for
164   * the socket port to be opened and ready to process requests before returning.
165   */
166  private final Object waitListen = new Object();
167
168  /** The friendly name of this connection handler. */
169  private String friendlyName;
170
171  /** The SSL engine configurator is used for obtaining default SSL parameters. */
172  private SSLEngineConfigurator sslEngineConfigurator;
173
174  private ServerContext serverContext;
175
176  /** Default constructor. It is invoked by reflection to create this {@link ConnectionHandler}. */
177  public HTTPConnectionHandler()
178  {
179    super(DEFAULT_FRIENDLY_NAME);
180  }
181
182  /**
183   * Registers a client connection to track it.
184   *
185   * @param clientConnection
186   *          the client connection to register
187   */
188  void addClientConnection(ClientConnection clientConnection)
189  {
190    clientConnections.put(clientConnection, clientConnection);
191  }
192
193  @Override
194  public ConfigChangeResult applyConfigurationChange(HTTPConnectionHandlerCfg config)
195  {
196    final ConfigChangeResult ccr = new ConfigChangeResult();
197
198    if (anyChangeRequiresRestart(config))
199    {
200      ccr.setAdminActionRequired(true);
201      ccr.addMessage(ERR_CONNHANDLER_CONFIG_CHANGES_REQUIRE_RESTART.get("HTTP"));
202    }
203
204    // Reconfigure SSL if needed.
205    try
206    {
207      configureSSL(config);
208    }
209    catch (DirectoryException e)
210    {
211      logger.traceException(e);
212      ccr.setResultCode(e.getResultCode());
213      ccr.addMessage(e.getMessageObject());
214      return ccr;
215    }
216
217    if (config.isEnabled() && this.currentConfig.isEnabled() && isListening())
218    {
219      // Server was running and will still be running if the "enabled" was flipped,
220      // leave it to the stop / start server to handle it.
221      if (!this.currentConfig.isKeepStats() && config.isKeepStats())
222      {
223        // It must now keep stats while it was not previously.
224        setHttpStatsProbe(this.httpServer);
225      }
226      else if (this.currentConfig.isKeepStats() && !config.isKeepStats() && this.httpProbe != null)
227      {
228        // It must NOT keep stats anymore.
229        getHttpConfig(this.httpServer).removeProbes(this.httpProbe);
230        this.httpProbe = null;
231      }
232    }
233
234    this.initConfig = config;
235    this.currentConfig = config;
236    this.enabled = this.currentConfig.isEnabled();
237
238    return ccr;
239  }
240
241  private boolean anyChangeRequiresRestart(HTTPConnectionHandlerCfg newCfg)
242  {
243    return !equals(newCfg.getListenPort(), initConfig.getListenPort())
244        || !Objects.equals(newCfg.getListenAddress(), initConfig.getListenAddress())
245        || !equals(newCfg.getMaxRequestSize(), currentConfig.getMaxRequestSize())
246        || !equals(newCfg.isAllowTCPReuseAddress(), currentConfig.isAllowTCPReuseAddress())
247        || !equals(newCfg.isUseTCPKeepAlive(), currentConfig.isUseTCPKeepAlive())
248        || !equals(newCfg.isUseTCPNoDelay(), currentConfig.isUseTCPNoDelay())
249        || !equals(newCfg.getMaxBlockedWriteTimeLimit(), currentConfig.getMaxBlockedWriteTimeLimit())
250        || !equals(newCfg.getBufferSize(), currentConfig.getBufferSize())
251        || !equals(newCfg.getAcceptBacklog(), currentConfig.getAcceptBacklog())
252        || !equals(newCfg.isUseSSL(), currentConfig.isUseSSL())
253        || !Objects.equals(newCfg.getKeyManagerProviderDN(), currentConfig.getKeyManagerProviderDN())
254        || !Objects.equals(newCfg.getSSLCertNickname(), currentConfig.getSSLCertNickname())
255        || !Objects.equals(newCfg.getTrustManagerProviderDN(), currentConfig.getTrustManagerProviderDN())
256        || !Objects.equals(newCfg.getSSLProtocol(), currentConfig.getSSLProtocol())
257        || !Objects.equals(newCfg.getSSLCipherSuite(), currentConfig.getSSLCipherSuite())
258        || !Objects.equals(newCfg.getSSLClientAuthPolicy(), currentConfig.getSSLClientAuthPolicy());
259  }
260
261  private boolean equals(long l1, long l2)
262  {
263    return l1 == l2;
264  }
265
266  private boolean equals(boolean b1, boolean b2)
267  {
268    return b1 == b2;
269  }
270
271  private void configureSSL(HTTPConnectionHandlerCfg config)
272      throws DirectoryException
273  {
274    protocol = config.isUseSSL() ? "HTTPS" : "HTTP";
275    if (config.isUseSSL())
276    {
277      sslEngineConfigurator = createSSLEngineConfigurator(config);
278    }
279    else
280    {
281      sslEngineConfigurator = null;
282    }
283  }
284
285  @Override
286  public void finalizeConnectionHandler(LocalizableMessage finalizeReason)
287  {
288    shutdownRequested = true;
289    // Unregister this as a change listener.
290    currentConfig.removeHTTPChangeListener(this);
291
292    if (connMonitor != null)
293    {
294      DirectoryServer.deregisterMonitorProvider(connMonitor);
295    }
296
297    if (statTracker != null)
298    {
299      DirectoryServer.deregisterMonitorProvider(statTracker);
300    }
301  }
302
303  @Override
304  public Map<String, String> getAlerts()
305  {
306    Map<String, String> alerts = new LinkedHashMap<>();
307
308    alerts.put(ALERT_TYPE_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES,
309               ALERT_DESCRIPTION_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES);
310
311    return alerts;
312  }
313
314  @Override
315  public String getClassName()
316  {
317    return HTTPConnectionHandler.class.getName();
318  }
319
320  @Override
321  public Collection<ClientConnection> getClientConnections()
322  {
323    return clientConnections.keySet();
324  }
325
326  @Override
327  public DN getComponentEntryDN()
328  {
329    return currentConfig.dn();
330  }
331
332  @Override
333  public String getConnectionHandlerName()
334  {
335    return handlerName;
336  }
337
338  /**
339   * Returns the current config of this connection handler.
340   *
341   * @return the current config of this connection handler
342   */
343  HTTPConnectionHandlerCfg getCurrentConfig()
344  {
345    return this.currentConfig;
346  }
347
348  @Override
349  public Collection<String> getEnabledSSLCipherSuites()
350  {
351    final SSLEngineConfigurator configurator = sslEngineConfigurator;
352    if (configurator != null)
353    {
354      return Arrays.asList(configurator.getEnabledCipherSuites());
355    }
356    return super.getEnabledSSLCipherSuites();
357  }
358
359  @Override
360  public Collection<String> getEnabledSSLProtocols()
361  {
362    final SSLEngineConfigurator configurator = sslEngineConfigurator;
363    if (configurator != null)
364    {
365      return Arrays.asList(configurator.getEnabledProtocols());
366    }
367    return super.getEnabledSSLProtocols();
368  }
369
370  @Override
371  public Collection<HostPort> getListeners()
372  {
373    return listeners;
374  }
375
376  /**
377   * Returns the listen port for this connection handler.
378   *
379   * @return the listen port for this connection handler.
380   */
381  int getListenPort()
382  {
383    return this.initConfig.getListenPort();
384  }
385
386  @Override
387  public String getProtocol()
388  {
389    return protocol;
390  }
391
392  @Override
393  public String getShutdownListenerName()
394  {
395    return handlerName;
396  }
397
398  /**
399   * Retrieves the set of statistics maintained by this connection handler.
400   *
401   * @return The set of statistics maintained by this connection handler.
402   */
403  public HTTPStatistics getStatTracker()
404  {
405    return statTracker;
406  }
407
408  @Override
409  public void initializeConnectionHandler(ServerContext serverContext, HTTPConnectionHandlerCfg config)
410      throws ConfigException, InitializationException
411  {
412    this.serverContext = serverContext;
413    this.enabled = config.isEnabled();
414
415    if (friendlyName == null)
416    {
417      friendlyName = config.dn().rdn().getFirstAVA().getAttributeValue().toString();
418    }
419
420    int listenPort = config.getListenPort();
421    for (InetAddress a : config.getListenAddress())
422    {
423      listeners.add(new HostPort(a.getHostAddress(), listenPort));
424    }
425
426    handlerName = getHandlerName(config);
427
428    // Configure SSL if needed.
429    try
430    {
431      // This call may disable the connector if wrong SSL settings
432      configureSSL(config);
433    }
434    catch (DirectoryException e)
435    {
436      logger.traceException(e);
437      throw new InitializationException(e.getMessageObject());
438    }
439
440    // Create and register monitors.
441    statTracker = new HTTPStatistics(handlerName + " Statistics");
442    DirectoryServer.registerMonitorProvider(statTracker);
443
444    connMonitor = new ClientConnectionMonitorProvider(this);
445    DirectoryServer.registerMonitorProvider(connMonitor);
446
447    // Register this as a change listener.
448    config.addHTTPChangeListener(this);
449
450    this.initConfig = config;
451    this.currentConfig = config;
452  }
453
454  private String getHandlerName(HTTPConnectionHandlerCfg config)
455  {
456    StringBuilder nameBuffer = new StringBuilder();
457    nameBuffer.append(friendlyName);
458    for (InetAddress a : config.getListenAddress())
459    {
460      nameBuffer.append(" ");
461      nameBuffer.append(a.getHostAddress());
462    }
463    nameBuffer.append(" port ");
464    nameBuffer.append(config.getListenPort());
465    return nameBuffer.toString();
466  }
467
468  @Override
469  public boolean isConfigurationAcceptable(
470      ConnectionHandlerCfg configuration, List<LocalizableMessage> unacceptableReasons)
471  {
472    HTTPConnectionHandlerCfg config = (HTTPConnectionHandlerCfg) configuration;
473
474    if (currentConfig == null || (!this.enabled && config.isEnabled()))
475    {
476      // Attempt to bind to the listen port on all configured addresses to
477      // verify whether the connection handler will be able to start.
478      LocalizableMessage errorMessage = checkAnyListenAddressInUse(
479          config.getListenAddress(), config.getListenPort(), config.isAllowTCPReuseAddress(), config.dn());
480      if (errorMessage != null)
481      {
482        unacceptableReasons.add(errorMessage);
483        return false;
484      }
485    }
486
487    if (config.isEnabled() && config.isUseSSL())
488    {
489      try
490      {
491        createSSLEngineConfigurator(config);
492      }
493      catch (DirectoryException e)
494      {
495        logger.traceException(e);
496        unacceptableReasons.add(e.getMessageObject());
497        return false;
498      }
499    }
500
501    return true;
502  }
503
504  /**
505   * Checks whether any listen address is in use for the given port. The check
506   * is performed by binding to each address and port.
507   *
508   * @param listenAddresses
509   *          the listen {@link InetAddress} to test
510   * @param listenPort
511   *          the listen port to test
512   * @param allowReuseAddress
513   *          whether addresses can be reused
514   * @param configEntryDN
515   *          the configuration entry DN
516   * @return an error message if at least one of the address is already in use,
517   *         null otherwise.
518   */
519  private LocalizableMessage checkAnyListenAddressInUse(
520      Collection<InetAddress> listenAddresses, int listenPort, boolean allowReuseAddress, DN configEntryDN)
521  {
522    for (InetAddress a : listenAddresses)
523    {
524      try
525      {
526        if (isAddressInUse(a, listenPort, allowReuseAddress))
527        {
528          throw new IOException(ERR_CONNHANDLER_ADDRESS_INUSE.get().toString());
529        }
530      }
531      catch (IOException e)
532      {
533        logger.traceException(e);
534        return ERR_CONNHANDLER_CANNOT_BIND.get(
535            "HTTP", configEntryDN, a.getHostAddress(), listenPort, getExceptionMessage(e));
536      }
537    }
538    return null;
539  }
540
541  @Override
542  public boolean isConfigurationChangeAcceptable(
543      HTTPConnectionHandlerCfg configuration, List<LocalizableMessage> unacceptableReasons)
544  {
545    return isConfigurationAcceptable(configuration, unacceptableReasons);
546  }
547
548  /**
549   * Indicates whether this connection handler should maintain usage statistics.
550   *
551   * @return <CODE>true</CODE> if this connection handler should maintain usage
552   *         statistics, or <CODE>false</CODE> if not.
553   */
554  public boolean keepStats()
555  {
556    return currentConfig.isKeepStats();
557  }
558
559  @Override
560  public void processServerShutdown(LocalizableMessage reason)
561  {
562    shutdownRequested = true;
563  }
564
565  private boolean isListening()
566  {
567    return httpServer != null;
568  }
569
570  @Override
571  public void start()
572  {
573    // The Directory Server start process should only return when the connection handlers port
574    // are fully opened and working.
575    // The start method therefore needs to wait for the created thread too.
576    synchronized (waitListen)
577    {
578      super.start();
579
580      try
581      {
582        waitListen.wait();
583      }
584      catch (InterruptedException e)
585      {
586        // If something interrupted the start its probably better to return ASAP
587      }
588    }
589  }
590
591  /**
592   * Unregisters a client connection to stop tracking it.
593   *
594   * @param clientConnection
595   *          the client connection to unregister
596   */
597  void removeClientConnection(ClientConnection clientConnection)
598  {
599    clientConnections.remove(clientConnection);
600  }
601
602  @Override
603  public void run()
604  {
605    setName(handlerName);
606
607    boolean lastIterationFailed = false;
608    boolean starting = true;
609
610    while (!shutdownRequested)
611    {
612      // If this connection handler is not enabled, then just sleep for a bit and check again.
613      if (!this.enabled)
614      {
615        if (isListening())
616        {
617          stopHttpServer();
618        }
619
620        if (starting)
621        {
622          // This may happen if there was an initialisation error which led to disable the connector.
623          // The main thread is waiting for the connector to listen on its port, which will not occur yet,
624          // so notify here to allow the server startup to complete.
625          synchronized (waitListen)
626          {
627            starting = false;
628            waitListen.notify();
629          }
630        }
631
632        StaticUtils.sleep(1000);
633        continue;
634      }
635
636      if (isListening())
637      {
638        // If already listening, then sleep for a bit and check again.
639        StaticUtils.sleep(1000);
640        continue;
641      }
642
643      try
644      {
645        // At this point, the connection Handler either started correctly or failed
646        // to start but the start process should be notified and resume its work in any cases.
647        synchronized (waitListen)
648        {
649          waitListen.notify();
650        }
651
652        // If we have gotten here, then we are about to start listening
653        // for the first time since startup or since we were previously disabled.
654        // Start the embedded HTTP server
655        startHttpServer();
656        lastIterationFailed = false;
657      }
658      catch (Exception e)
659      {
660        // Clean up the messed up HTTP server
661        cleanUpHttpServer();
662
663        // Error + alert about the horked config
664        logger.traceException(e);
665        logger.error(
666            ERR_CONNHANDLER_CANNOT_ACCEPT_CONNECTION, friendlyName, currentConfig.dn(), getExceptionMessage(e));
667
668        if (lastIterationFailed)
669        {
670          // The last time through the accept loop we also encountered a failure.
671          // Rather than enter a potential infinite loop of failures,
672          // disable this acceptor and log an error.
673          LocalizableMessage message = ERR_CONNHANDLER_CONSECUTIVE_ACCEPT_FAILURES.get(
674              friendlyName, currentConfig.dn(), stackTraceToSingleLineString(e));
675          logger.error(message);
676
677          DirectoryServer.sendAlertNotification(this, ALERT_TYPE_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES, message);
678          this.enabled = false;
679        }
680        else
681        {
682          lastIterationFailed = true;
683        }
684      }
685    }
686
687    // Initiate shutdown
688    stopHttpServer();
689  }
690
691  private void startHttpServer() throws Exception
692  {
693    if (HTTPAccessLogger.getHTTPAccessLogPublishers().isEmpty())
694    {
695      logger.warn(WARN_CONFIG_LOGGER_NO_ACTIVE_HTTP_ACCESS_LOGGERS);
696    }
697
698    this.httpServer = createHttpServer();
699    this.httpServer.getServerConfiguration().addHttpHandler(newGrizzlyHttpHandler(new RootHttpApplication()));
700    logger.trace("Starting HTTP server...");
701    this.httpServer.start();
702    logger.trace("HTTP server started");
703    logger.info(NOTE_CONNHANDLER_STARTED_LISTENING, handlerName);
704  }
705
706  private HttpServer createHttpServer()
707  {
708    final HttpServer server = new HttpServer();
709
710    final int requestSize = (int) currentConfig.getMaxRequestSize();
711    final ServerConfiguration serverConfig = server.getServerConfiguration();
712    serverConfig.setDefaultErrorPageGenerator(new CustomErrorPageGenerator());
713    serverConfig.setMaxBufferedPostSize(requestSize);
714    serverConfig.setMaxFormPostSize(requestSize);
715    serverConfig.setDefaultQueryEncoding(Charsets.UTF8_CHARSET);
716    // FIXME: Workaround for CHF-141 to prevent session leak: since sessions are not needed,
717    // expire these as soon as possible.
718    serverConfig.setSessionTimeoutSeconds(1);
719
720    if (keepStats())
721    {
722      setHttpStatsProbe(server);
723    }
724
725    // Configure the network listener
726    final NetworkListener listener = new NetworkListener(
727        "OpenDJ-HTTP", NetworkListener.DEFAULT_NETWORK_HOST, initConfig.getListenPort());
728    server.addListener(listener);
729
730    // Configure the network transport
731    final TCPNIOTransport transport = listener.getTransport();
732    transport.setReuseAddress(currentConfig.isAllowTCPReuseAddress());
733    transport.setKeepAlive(currentConfig.isUseTCPKeepAlive());
734    transport.setTcpNoDelay(currentConfig.isUseTCPNoDelay());
735    transport.setWriteTimeout(currentConfig.getMaxBlockedWriteTimeLimit(), TimeUnit.MILLISECONDS);
736
737    final int bufferSize = (int) currentConfig.getBufferSize();
738    transport.setReadBufferSize(bufferSize);
739    transport.setWriteBufferSize(bufferSize);
740    transport.setIOStrategy(SameThreadIOStrategy.getInstance());
741
742    final int numRequestHandlers = getNumRequestHandlers(currentConfig.getNumRequestHandlers(), friendlyName);
743    transport.setSelectorRunnersCount(numRequestHandlers);
744    transport.setServerConnectionBackLog(currentConfig.getAcceptBacklog());
745
746    // Configure SSL
747    if (sslEngineConfigurator != null)
748    {
749      listener.setSecure(true);
750      listener.setSSLEngineConfig(sslEngineConfigurator);
751    }
752
753    return server;
754  }
755
756  private void setHttpStatsProbe(HttpServer server)
757  {
758    this.httpProbe = new HTTPStatsProbe(this.statTracker);
759    getHttpConfig(server).addProbes(this.httpProbe);
760  }
761
762  private MonitoringConfig<HttpProbe> getHttpConfig(HttpServer server)
763  {
764    return server.getServerConfiguration().getMonitoringConfig().getHttpConfig();
765  }
766
767  private void stopHttpServer()
768  {
769    if (this.httpServer != null)
770    {
771      logger.trace("Stopping HTTP server...");
772      this.httpServer.shutdownNow();
773      cleanUpHttpServer();
774      logger.trace("HTTP server stopped");
775      logger.info(NOTE_CONNHANDLER_STOPPED_LISTENING, handlerName);
776    }
777  }
778
779  private void cleanUpHttpServer()
780  {
781    this.httpServer = null;
782    this.httpProbe = null;
783  }
784
785  @Override
786  public void toString(StringBuilder buffer)
787  {
788    buffer.append(handlerName);
789  }
790
791  private SSLEngineConfigurator createSSLEngineConfigurator(HTTPConnectionHandlerCfg config) throws DirectoryException
792  {
793    if (!config.isUseSSL())
794    {
795      return null;
796    }
797
798    try
799    {
800      SSLContext sslContext = createSSLContext(config);
801      SSLEngineConfigurator configurator = new SSLEngineConfigurator(sslContext);
802      configurator.setClientMode(false);
803
804      // configure with defaults from the JVM
805      final SSLEngine defaults = sslContext.createSSLEngine();
806      configurator.setEnabledProtocols(defaults.getEnabledProtocols());
807      configurator.setEnabledCipherSuites(defaults.getEnabledCipherSuites());
808
809      final Set<String> protocols = config.getSSLProtocol();
810      if (!protocols.isEmpty())
811      {
812        configurator.setEnabledProtocols(protocols.toArray(new String[protocols.size()]));
813      }
814
815      final Set<String> ciphers = config.getSSLCipherSuite();
816      if (!ciphers.isEmpty())
817      {
818        configurator.setEnabledCipherSuites(ciphers.toArray(new String[ciphers.size()]));
819      }
820
821      switch (config.getSSLClientAuthPolicy())
822      {
823      case DISABLED:
824        configurator.setNeedClientAuth(false);
825        configurator.setWantClientAuth(false);
826        break;
827      case REQUIRED:
828        configurator.setNeedClientAuth(true);
829        configurator.setWantClientAuth(true);
830        break;
831      case OPTIONAL:
832      default:
833        configurator.setNeedClientAuth(false);
834        configurator.setWantClientAuth(true);
835        break;
836      }
837
838      return configurator;
839    }
840    catch (Exception e)
841    {
842      logger.traceException(e);
843      ResultCode resCode = DirectoryServer.getServerErrorResultCode();
844      throw new DirectoryException(resCode, ERR_CONNHANDLER_SSL_CANNOT_INITIALIZE.get(getExceptionMessage(e)), e);
845    }
846  }
847
848  private SSLContext createSSLContext(HTTPConnectionHandlerCfg config) throws Exception
849  {
850    if (!config.isUseSSL())
851    {
852      return null;
853    }
854
855    DN keyMgrDN = config.getKeyManagerProviderDN();
856    KeyManagerProvider<?> keyManagerProvider = DirectoryServer.getKeyManagerProvider(keyMgrDN);
857    if (keyManagerProvider == null)
858    {
859      logger.error(ERR_NULL_KEY_PROVIDER_MANAGER, keyMgrDN, friendlyName);
860      logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
861      keyManagerProvider = new NullKeyManagerProvider();
862      enabled = false;
863    }
864    else if (!keyManagerProvider.containsAtLeastOneKey())
865    {
866      logger.error(ERR_INVALID_KEYSTORE, friendlyName);
867      logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
868      enabled = false;
869    }
870
871    final SortedSet<String> aliases = new TreeSet<>(config.getSSLCertNickname());
872    final KeyManager[] keyManagers;
873    if (aliases.isEmpty())
874    {
875      keyManagers = keyManagerProvider.getKeyManagers();
876    }
877    else
878    {
879      final Iterator<String> it = aliases.iterator();
880      while (it.hasNext())
881      {
882        if (!keyManagerProvider.containsKeyWithAlias(it.next()))
883        {
884          logger.error(ERR_KEYSTORE_DOES_NOT_CONTAIN_ALIAS, aliases, friendlyName);
885          it.remove();
886        }
887      }
888      if (aliases.isEmpty())
889      {
890        logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
891        enabled = false;
892      }
893      keyManagers = SelectableCertificateKeyManager.wrap(keyManagerProvider.getKeyManagers(), aliases);
894    }
895
896    DN trustMgrDN = config.getTrustManagerProviderDN();
897    TrustManagerProvider<?> trustManagerProvider = DirectoryServer.getTrustManagerProvider(trustMgrDN);
898    if (trustManagerProvider == null)
899    {
900      trustManagerProvider = new NullTrustManagerProvider();
901    }
902
903    SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_INSTANCE_NAME);
904    sslContext.init(keyManagers, trustManagerProvider.getTrustManagers(), null);
905    return sslContext;
906  }
907
908  /**
909   * This is the root {@link HttpApplication} handling all the requests from the
910   * {@link HTTPConnectionHandler}. If accepted, requests are audited and then
911   * forwarded to the global {@link ServerContext#getHTTPRouter()}.
912   */
913  private final class RootHttpApplication implements HttpApplication
914  {
915    @Override
916    public Handler start() throws HttpApplicationException
917    {
918      return Handlers.chainOf(
919          serverContext.getHTTPRouter(),
920          new HttpAccessLogFilter(serverContext),
921          new ErrorLoggerFilter(),
922          new ExecuteInWorkerThreadFilter(),
923          new AllowDenyFilter(currentConfig.getDeniedClient(), currentConfig.getAllowedClient()),
924          new CommonAuditTransactionIdFilter(serverContext),
925          new CommonAuditHttpAccessCheckEnabledFilter(serverContext,
926              new CommonAuditHttpAccessAuditFilter(
927                  DynamicConstants.PRODUCT_NAME,
928                  serverContext.getCommonAudit().getAuditServiceForHttpAccessLog(),
929                  TimeService.SYSTEM)),
930          new LDAPContextInjectionFilter(serverContext, HTTPConnectionHandler.this));
931    }
932
933    @Override
934    public void stop()
935    {
936      // Nothing to do
937    }
938
939    @Override
940    public org.forgerock.util.Factory<Buffer> getBufferFactory()
941    {
942      return null;
943    }
944  }
945
946  /**
947   * This allows us to customize the HTML error pages produced from Grizzly.
948   */
949  private final class CustomErrorPageGenerator implements ErrorPageGenerator
950  {
951    @Override
952    public String generate(final org.glassfish.grizzly.http.server.Request request, final int status,
953                           final String reasonPhrase, final String description, final Throwable exception)
954    {
955      if (status == 404) {
956        return getErrorPage(HttpStatus.NOT_FOUND_404.getReasonPhrase(),
957                                       "Resource identified by path '" +
958                                               HttpUtils.filter(request.getRequestURI()) +
959                                               "', does not exist.",
960                                       request.getServerName());
961      }
962
963      return getExceptionErrorPage(reasonPhrase, description,
964                                              request.getServerName(),
965                                              exception);
966    }
967
968    private final static String CSS =
969            "div.header {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#003300;"
970            + "font-size:22px;-moz-border-radius-topleft: 10px;border-top-left-radius: 10px;"
971            + "-moz-border-radius-topright: 10px;border-top-right-radius: 10px;padding-left: 5px}"
972            + "div.body {font-family:Tahoma,Arial,sans-serif;color:black;background-color:#FFFFCC;"
973            + "font-size:16px;padding-top:10px;padding-bottom:10px;padding-left:10px}"
974            + "div.footer {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#666633;"
975            + "font-size:14px;-moz-border-radius-bottomleft: 10px;border-bottom-left-radius: 10px;"
976            + "-moz-border-radius-bottomright: 10px;border-bottom-right-radius: 10px;padding-left: 5px}"
977            + "BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;}"
978            + "B {font-family:Tahoma,Arial,sans-serif;color:black;}"
979            + "A {color : black;}"
980            + "HR {color : #999966;}";
981
982    private String getErrorPage(String headerMessage,
983                                      String message, String serverName)
984    {
985      return prepareBody(headerMessage, message, serverName);
986    }
987
988
989    private String getExceptionErrorPage(String headerMessage,
990                                               String message, String serverName, Throwable t)
991    {
992      return prepareExceptionBody(headerMessage, message, serverName, t);
993    }
994
995    /**
996     * Prepare the HTTP body containing the error messages.
997     */
998    private String prepareBody(String headerMessage, String message,
999                                      String serverName)
1000    {
1001      final StringBuilder sb = new StringBuilder();
1002
1003      sb.append("<html><head><title>");
1004      sb.append(serverName);
1005      sb.append("</title>");
1006      sb.append("<style><!--");
1007      sb.append(CSS);
1008      sb.append("--></style> ");
1009      sb.append("</head><body>");
1010      sb.append("<div class=\"header\">");
1011      sb.append(headerMessage);
1012      sb.append("</div>");
1013      sb.append("<div class=\"body\">");
1014      sb.append((message != null) ? HttpUtils.filter(message) : "<HR size=\"1\" noshade>");
1015      sb.append("</div>");
1016      sb.append("<div class=\"footer\">").append(serverName).append("</div>");
1017      sb.append("</body></html>");
1018      return sb.toString();
1019    }
1020
1021    @SuppressWarnings({"ThrowableResultOfMethodCallIgnored"})
1022    private String prepareExceptionBody(String headerMessage,
1023                                               String message, String serverName, Throwable t)
1024    {
1025      if (t == null) {
1026        return prepareBody(headerMessage, message, serverName);
1027      }
1028
1029      final String exMessage = t.getMessage() != null ?
1030                               t.getMessage() : t.toString();
1031
1032      StringBuilder sb = new StringBuilder();
1033      sb.append("<html><head><title>");
1034      sb.append(serverName);
1035      sb.append("</title>");
1036      sb.append("<style><!--");
1037      sb.append(CSS);
1038      sb.append("--></style> ");
1039      sb.append("</head><body>");
1040      sb.append("<div class=\"header\">");
1041      sb.append(headerMessage);
1042      sb.append("</div>");
1043      sb.append("<div class=\"body\">");
1044      sb.append("<b>").append(HttpUtils.filter(exMessage)).append("</b>");
1045      sb.append("<pre>");
1046      sb.append(HttpUtils.filter(stackTraceToString(t)));
1047      sb.append("</pre>");
1048
1049      sb.append("Please see the log for more detail.");
1050      sb.append("</div>");
1051      sb.append("<div class=\"footer\">").append(serverName).append("</div>");
1052      sb.append("</body></html>");
1053      return sb.toString();
1054    }
1055  }
1056
1057  /** Moves the processing of the request in this Directory Server's worker thread. */
1058  private static final class ExecuteInWorkerThreadFilter implements Filter
1059  {
1060    @Override
1061    public Promise<Response, NeverThrowsException> filter(final Context context, final Request request,
1062        final Handler next)
1063    {
1064      final PromiseImpl<Response, NeverThrowsException> promise = PromiseImpl.create();
1065      try
1066      {
1067        DirectoryServer.getWorkQueue().submitOperation(new AsyncOperation<>(
1068            InternalClientConnection.getRootConnection(),
1069            new Runnable()
1070            {
1071              @Override
1072              public void run()
1073              {
1074                // Trap and forward runtime exceptions.
1075                next.handle(context, request).thenOnResult(promise).thenOnRuntimeException(promise);
1076              }
1077            }));
1078      }
1079      catch (Exception e)
1080      {
1081        promise.handleResult(new Response(Status.INTERNAL_SERVER_ERROR).setCause(e));
1082      }
1083      return promise;
1084    }
1085
1086    /** This operation is hack to be able to execute a {@link Runnable} in a Directory Server's worker thread. */
1087    private static final class AsyncOperation<V> extends AbstractOperation
1088    {
1089      private final Runnable runnable;
1090
1091      AsyncOperation(InternalClientConnection icc, Runnable runnable)
1092      {
1093        super(icc, icc.nextOperationID(), icc.nextMessageID(),
1094            Collections.<org.opends.server.types.Control> emptyList());
1095        this.setInternalOperation(true);
1096        this.runnable = runnable;
1097      }
1098
1099      @Override
1100      public void run()
1101      {
1102        runnable.run();
1103      }
1104
1105      @Override
1106      public OperationType getOperationType()
1107      {
1108        return null;
1109      }
1110
1111      @Override
1112      public List<org.opends.server.types.Control> getResponseControls()
1113      {
1114        return Collections.emptyList();
1115      }
1116
1117      @Override
1118      public void addResponseControl(org.opends.server.types.Control control)
1119      {
1120      }
1121
1122      @Override
1123      public void removeResponseControl(org.opends.server.types.Control control)
1124      {
1125      }
1126
1127      @Override
1128      public DN getProxiedAuthorizationDN()
1129      {
1130        return null;
1131      }
1132
1133      @Override
1134      public void setProxiedAuthorizationDN(DN proxiedAuthorizationDN)
1135      {
1136      }
1137
1138      @Override
1139      public void toString(StringBuilder buffer)
1140      {
1141        buffer.append(AsyncOperation.class.getSimpleName());
1142      }
1143    }
1144  }
1145}