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}