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}