001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.http; 019 020import java.io.FileNotFoundException; 021import java.io.IOException; 022import java.io.InterruptedIOException; 023import java.io.PrintStream; 024import java.net.BindException; 025import java.net.InetSocketAddress; 026import java.net.URI; 027import java.net.URISyntaxException; 028import java.net.URL; 029import java.nio.file.Files; 030import java.nio.file.Path; 031import java.nio.file.Paths; 032import java.util.ArrayList; 033import java.util.Collections; 034import java.util.Enumeration; 035import java.util.HashMap; 036import java.util.List; 037import java.util.Map; 038import java.util.stream.Collectors; 039import javax.servlet.Filter; 040import javax.servlet.FilterChain; 041import javax.servlet.FilterConfig; 042import javax.servlet.Servlet; 043import javax.servlet.ServletContext; 044import javax.servlet.ServletException; 045import javax.servlet.ServletRequest; 046import javax.servlet.ServletResponse; 047import javax.servlet.http.HttpServlet; 048import javax.servlet.http.HttpServletRequest; 049import javax.servlet.http.HttpServletRequestWrapper; 050import javax.servlet.http.HttpServletResponse; 051import org.apache.hadoop.HadoopIllegalArgumentException; 052import org.apache.hadoop.conf.Configuration; 053import org.apache.hadoop.fs.CommonConfigurationKeys; 054import org.apache.hadoop.hbase.HBaseInterfaceAudience; 055import org.apache.hadoop.hbase.http.conf.ConfServlet; 056import org.apache.hadoop.hbase.http.jmx.JMXJsonServlet; 057import org.apache.hadoop.hbase.http.log.LogLevel; 058import org.apache.hadoop.hbase.util.ReflectionUtils; 059import org.apache.hadoop.hbase.util.Threads; 060import org.apache.hadoop.security.SecurityUtil; 061import org.apache.hadoop.security.UserGroupInformation; 062import org.apache.hadoop.security.authentication.server.AuthenticationFilter; 063import org.apache.hadoop.security.authorize.AccessControlList; 064import org.apache.hadoop.security.authorize.ProxyUsers; 065import org.apache.hadoop.util.Shell; 066import org.apache.hadoop.util.StringUtils; 067import org.apache.yetus.audience.InterfaceAudience; 068import org.apache.yetus.audience.InterfaceStability; 069import org.slf4j.Logger; 070import org.slf4j.LoggerFactory; 071 072import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; 073import org.apache.hbase.thirdparty.com.google.common.collect.Lists; 074import org.apache.hbase.thirdparty.org.eclipse.jetty.http.HttpVersion; 075import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Handler; 076import org.apache.hbase.thirdparty.org.eclipse.jetty.server.HttpConfiguration; 077import org.apache.hbase.thirdparty.org.eclipse.jetty.server.HttpConnectionFactory; 078import org.apache.hbase.thirdparty.org.eclipse.jetty.server.RequestLog; 079import org.apache.hbase.thirdparty.org.eclipse.jetty.server.SecureRequestCustomizer; 080import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Server; 081import org.apache.hbase.thirdparty.org.eclipse.jetty.server.ServerConnector; 082import org.apache.hbase.thirdparty.org.eclipse.jetty.server.SslConnectionFactory; 083import org.apache.hbase.thirdparty.org.eclipse.jetty.server.handler.ContextHandlerCollection; 084import org.apache.hbase.thirdparty.org.eclipse.jetty.server.handler.HandlerCollection; 085import org.apache.hbase.thirdparty.org.eclipse.jetty.server.handler.RequestLogHandler; 086import org.apache.hbase.thirdparty.org.eclipse.jetty.server.handler.gzip.GzipHandler; 087import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.DefaultServlet; 088import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.FilterHolder; 089import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.FilterMapping; 090import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.ServletContextHandler; 091import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.ServletHolder; 092import org.apache.hbase.thirdparty.org.eclipse.jetty.util.MultiException; 093import org.apache.hbase.thirdparty.org.eclipse.jetty.util.ssl.SslContextFactory; 094import org.apache.hbase.thirdparty.org.eclipse.jetty.util.thread.QueuedThreadPool; 095import org.apache.hbase.thirdparty.org.eclipse.jetty.webapp.WebAppContext; 096import org.apache.hbase.thirdparty.org.glassfish.jersey.server.ResourceConfig; 097import org.apache.hbase.thirdparty.org.glassfish.jersey.servlet.ServletContainer; 098 099/** 100 * Create a Jetty embedded server to answer http requests. The primary goal is to serve up status 101 * information for the server. There are three contexts: "/logs/" -> points to the log directory 102 * "/static/" -> points to common static files (src/webapps/static) "/" -> the jsp server code 103 * from (src/webapps/<name>) 104 */ 105@InterfaceAudience.Private 106@InterfaceStability.Evolving 107public class HttpServer implements FilterContainer { 108 private static final Logger LOG = LoggerFactory.getLogger(HttpServer.class); 109 private static final String EMPTY_STRING = ""; 110 111 // Jetty's max header size is Character.MAX_VALUE - 1, See ArrayTernaryTrie for more details 112 // And in newer jetty version, they add a check when creating a server so we must follow this 113 // limitation otherwise the UTs will fail 114 private static final int DEFAULT_MAX_HEADER_SIZE = Character.MAX_VALUE - 1; 115 116 static final String FILTER_INITIALIZERS_PROPERTY = "hbase.http.filter.initializers"; 117 static final String HTTP_MAX_THREADS = "hbase.http.max.threads"; 118 119 public static final String HTTP_UI_AUTHENTICATION = "hbase.security.authentication.ui"; 120 static final String HTTP_AUTHENTICATION_PREFIX = "hbase.security.authentication."; 121 static final String HTTP_SPNEGO_AUTHENTICATION_PREFIX = HTTP_AUTHENTICATION_PREFIX + "spnego."; 122 static final String HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_SUFFIX = "kerberos.principal"; 123 public static final String HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY = 124 HTTP_SPNEGO_AUTHENTICATION_PREFIX + HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_SUFFIX; 125 static final String HTTP_SPNEGO_AUTHENTICATION_KEYTAB_SUFFIX = "kerberos.keytab"; 126 public static final String HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY = 127 HTTP_SPNEGO_AUTHENTICATION_PREFIX + HTTP_SPNEGO_AUTHENTICATION_KEYTAB_SUFFIX; 128 static final String HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_SUFFIX = "kerberos.name.rules"; 129 public static final String HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_KEY = 130 HTTP_SPNEGO_AUTHENTICATION_PREFIX + HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_SUFFIX; 131 static final String HTTP_SPNEGO_AUTHENTICATION_PROXYUSER_ENABLE_SUFFIX = 132 "kerberos.proxyuser.enable"; 133 public static final String HTTP_SPNEGO_AUTHENTICATION_PROXYUSER_ENABLE_KEY = 134 HTTP_SPNEGO_AUTHENTICATION_PREFIX + HTTP_SPNEGO_AUTHENTICATION_PROXYUSER_ENABLE_SUFFIX; 135 public static final boolean HTTP_SPNEGO_AUTHENTICATION_PROXYUSER_ENABLE_DEFAULT = false; 136 static final String HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_SUFFIX = "signature.secret.file"; 137 public static final String HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_KEY = 138 HTTP_AUTHENTICATION_PREFIX + HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_SUFFIX; 139 public static final String HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY = 140 HTTP_SPNEGO_AUTHENTICATION_PREFIX + "admin.users"; 141 public static final String HTTP_SPNEGO_AUTHENTICATION_ADMIN_GROUPS_KEY = 142 HTTP_SPNEGO_AUTHENTICATION_PREFIX + "admin.groups"; 143 public static final String HTTP_PRIVILEGED_CONF_KEY = 144 "hbase.security.authentication.ui.config.protected"; 145 public static final boolean HTTP_PRIVILEGED_CONF_DEFAULT = false; 146 147 // The ServletContext attribute where the daemon Configuration 148 // gets stored. 149 public static final String CONF_CONTEXT_ATTRIBUTE = "hbase.conf"; 150 public static final String ADMINS_ACL = "admins.acl"; 151 public static final String BIND_ADDRESS = "bind.address"; 152 public static final String SPNEGO_FILTER = "SpnegoFilter"; 153 public static final String SPNEGO_PROXYUSER_FILTER = "SpnegoProxyUserFilter"; 154 public static final String NO_CACHE_FILTER = "NoCacheFilter"; 155 public static final String APP_DIR = "webapps"; 156 157 private final AccessControlList adminsAcl; 158 159 protected final Server webServer; 160 protected String appDir; 161 protected String logDir; 162 163 private static final class ListenerInfo { 164 /** 165 * Boolean flag to determine whether the HTTP server should clean up the listener in stop(). 166 */ 167 private final boolean isManaged; 168 private final ServerConnector listener; 169 170 private ListenerInfo(boolean isManaged, ServerConnector listener) { 171 this.isManaged = isManaged; 172 this.listener = listener; 173 } 174 } 175 176 private final List<ListenerInfo> listeners = Lists.newArrayList(); 177 178 public List<ServerConnector> getServerConnectors() { 179 return listeners.stream().map(info -> info.listener).collect(Collectors.toList()); 180 } 181 182 protected final WebAppContext webAppContext; 183 protected final boolean findPort; 184 protected final Map<ServletContextHandler, Boolean> defaultContexts = new HashMap<>(); 185 protected final List<String> filterNames = new ArrayList<>(); 186 protected final boolean authenticationEnabled; 187 static final String STATE_DESCRIPTION_ALIVE = " - alive"; 188 static final String STATE_DESCRIPTION_NOT_LIVE = " - not live"; 189 190 /** 191 * Class to construct instances of HTTP server with specific options. 192 */ 193 public static class Builder { 194 private ArrayList<URI> endpoints = Lists.newArrayList(); 195 private Configuration conf; 196 private String[] pathSpecs; 197 private AccessControlList adminsAcl; 198 private boolean securityEnabled = false; 199 private String usernameConfKey; 200 private String keytabConfKey; 201 private boolean needsClientAuth; 202 private String excludeCiphers; 203 204 private String hostName; 205 private String appDir = APP_DIR; 206 private String logDir; 207 private boolean findPort; 208 209 private String trustStore; 210 private String trustStorePassword; 211 private String trustStoreType; 212 213 private String keyStore; 214 private String keyStorePassword; 215 private String keyStoreType; 216 217 // The -keypass option in keytool 218 private String keyPassword; 219 220 private String kerberosNameRulesKey; 221 private String signatureSecretFileKey; 222 223 /** 224 * @see #setAppDir(String) 225 * @deprecated Since 0.99.0. Use builder pattern via {@link #setAppDir(String)} instead. 226 */ 227 @Deprecated 228 private String name; 229 /** 230 * @see #addEndpoint(URI) 231 * @deprecated Since 0.99.0. Use builder pattern via {@link #addEndpoint(URI)} instead. 232 */ 233 @Deprecated 234 private String bindAddress; 235 /** 236 * @see #addEndpoint(URI) 237 * @deprecated Since 0.99.0. Use builder pattern via {@link #addEndpoint(URI)} instead. 238 */ 239 @Deprecated 240 private int port = -1; 241 242 /** 243 * Add an endpoint that the HTTP server should listen to. n * the endpoint of that the HTTP 244 * server should listen to. The scheme specifies the protocol (i.e. HTTP / HTTPS), the host 245 * specifies the binding address, and the port specifies the listening port. Unspecified or zero 246 * port means that the server can listen to any port. 247 */ 248 public Builder addEndpoint(URI endpoint) { 249 endpoints.add(endpoint); 250 return this; 251 } 252 253 /** 254 * Set the hostname of the http server. The host name is used to resolve the _HOST field in 255 * Kerberos principals. The hostname of the first listener will be used if the name is 256 * unspecified. 257 */ 258 public Builder hostName(String hostName) { 259 this.hostName = hostName; 260 return this; 261 } 262 263 public Builder trustStore(String location, String password, String type) { 264 this.trustStore = location; 265 this.trustStorePassword = password; 266 this.trustStoreType = type; 267 return this; 268 } 269 270 public Builder keyStore(String location, String password, String type) { 271 this.keyStore = location; 272 this.keyStorePassword = password; 273 this.keyStoreType = type; 274 return this; 275 } 276 277 public Builder keyPassword(String password) { 278 this.keyPassword = password; 279 return this; 280 } 281 282 /** 283 * Specify whether the server should authorize the client in SSL connections. 284 */ 285 public Builder needsClientAuth(boolean value) { 286 this.needsClientAuth = value; 287 return this; 288 } 289 290 /** 291 * @see #setAppDir(String) 292 * @deprecated Since 0.99.0. Use {@link #setAppDir(String)} instead. 293 */ 294 @Deprecated 295 public Builder setName(String name) { 296 this.name = name; 297 return this; 298 } 299 300 /** 301 * @see #addEndpoint(URI) 302 * @deprecated Since 0.99.0. Use {@link #addEndpoint(URI)} instead. 303 */ 304 @Deprecated 305 public Builder setBindAddress(String bindAddress) { 306 this.bindAddress = bindAddress; 307 return this; 308 } 309 310 /** 311 * @see #addEndpoint(URI) 312 * @deprecated Since 0.99.0. Use {@link #addEndpoint(URI)} instead. 313 */ 314 @Deprecated 315 public Builder setPort(int port) { 316 this.port = port; 317 return this; 318 } 319 320 public Builder setFindPort(boolean findPort) { 321 this.findPort = findPort; 322 return this; 323 } 324 325 public Builder setConf(Configuration conf) { 326 this.conf = conf; 327 return this; 328 } 329 330 public Builder setPathSpec(String[] pathSpec) { 331 this.pathSpecs = pathSpec; 332 return this; 333 } 334 335 public Builder setACL(AccessControlList acl) { 336 this.adminsAcl = acl; 337 return this; 338 } 339 340 public Builder setSecurityEnabled(boolean securityEnabled) { 341 this.securityEnabled = securityEnabled; 342 return this; 343 } 344 345 public Builder setUsernameConfKey(String usernameConfKey) { 346 this.usernameConfKey = usernameConfKey; 347 return this; 348 } 349 350 public Builder setKeytabConfKey(String keytabConfKey) { 351 this.keytabConfKey = keytabConfKey; 352 return this; 353 } 354 355 public Builder setKerberosNameRulesKey(String kerberosNameRulesKey) { 356 this.kerberosNameRulesKey = kerberosNameRulesKey; 357 return this; 358 } 359 360 public Builder setSignatureSecretFileKey(String signatureSecretFileKey) { 361 this.signatureSecretFileKey = signatureSecretFileKey; 362 return this; 363 } 364 365 public Builder setAppDir(String appDir) { 366 this.appDir = appDir; 367 return this; 368 } 369 370 public Builder setLogDir(String logDir) { 371 this.logDir = logDir; 372 return this; 373 } 374 375 public void excludeCiphers(String excludeCiphers) { 376 this.excludeCiphers = excludeCiphers; 377 } 378 379 public HttpServer build() throws IOException { 380 381 // Do we still need to assert this non null name if it is deprecated? 382 if (this.name == null) { 383 throw new HadoopIllegalArgumentException("name is not set"); 384 } 385 386 // Make the behavior compatible with deprecated interfaces 387 if (bindAddress != null && port != -1) { 388 try { 389 endpoints.add(0, new URI("http", "", bindAddress, port, "", "", "")); 390 } catch (URISyntaxException e) { 391 throw new HadoopIllegalArgumentException("Invalid endpoint: " + e); 392 } 393 } 394 395 if (endpoints.isEmpty()) { 396 throw new HadoopIllegalArgumentException("No endpoints specified"); 397 } 398 399 if (hostName == null) { 400 hostName = endpoints.get(0).getHost(); 401 } 402 403 if (this.conf == null) { 404 conf = new Configuration(); 405 } 406 407 HttpServer server = new HttpServer(this); 408 409 for (URI ep : endpoints) { 410 ServerConnector listener = null; 411 String scheme = ep.getScheme(); 412 HttpConfiguration httpConfig = new HttpConfiguration(); 413 httpConfig.setSecureScheme("https"); 414 httpConfig.setHeaderCacheSize(DEFAULT_MAX_HEADER_SIZE); 415 httpConfig.setResponseHeaderSize(DEFAULT_MAX_HEADER_SIZE); 416 httpConfig.setRequestHeaderSize(DEFAULT_MAX_HEADER_SIZE); 417 httpConfig.setSendServerVersion(false); 418 419 if ("http".equals(scheme)) { 420 listener = new ServerConnector(server.webServer, new HttpConnectionFactory(httpConfig)); 421 } else if ("https".equals(scheme)) { 422 HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); 423 httpsConfig.addCustomizer(new SecureRequestCustomizer()); 424 SslContextFactory.Server sslCtxFactory = new SslContextFactory.Server(); 425 sslCtxFactory.setNeedClientAuth(needsClientAuth); 426 sslCtxFactory.setKeyManagerPassword(keyPassword); 427 428 if (keyStore != null) { 429 sslCtxFactory.setKeyStorePath(keyStore); 430 sslCtxFactory.setKeyStoreType(keyStoreType); 431 sslCtxFactory.setKeyStorePassword(keyStorePassword); 432 } 433 434 if (trustStore != null) { 435 sslCtxFactory.setTrustStorePath(trustStore); 436 sslCtxFactory.setTrustStoreType(trustStoreType); 437 sslCtxFactory.setTrustStorePassword(trustStorePassword); 438 } 439 440 if (excludeCiphers != null && !excludeCiphers.trim().isEmpty()) { 441 sslCtxFactory.setExcludeCipherSuites(StringUtils.getTrimmedStrings(excludeCiphers)); 442 LOG.debug("Excluded SSL Cipher List:" + excludeCiphers); 443 } 444 445 listener = new ServerConnector(server.webServer, 446 new SslConnectionFactory(sslCtxFactory, HttpVersion.HTTP_1_1.toString()), 447 new HttpConnectionFactory(httpsConfig)); 448 } else { 449 throw new HadoopIllegalArgumentException("unknown scheme for endpoint:" + ep); 450 } 451 452 // default settings for connector 453 listener.setAcceptQueueSize(128); 454 if (Shell.WINDOWS) { 455 // result of setting the SO_REUSEADDR flag is different on Windows 456 // http://msdn.microsoft.com/en-us/library/ms740621(v=vs.85).aspx 457 // without this 2 NN's can start on the same machine and listen on 458 // the same port with indeterminate routing of incoming requests to them 459 listener.setReuseAddress(false); 460 } 461 462 listener.setHost(ep.getHost()); 463 listener.setPort(ep.getPort() == -1 ? 0 : ep.getPort()); 464 server.addManagedListener(listener); 465 } 466 467 server.loadListeners(); 468 return server; 469 470 } 471 472 } 473 474 /** 475 * @see #HttpServer(String, String, int, boolean, Configuration) 476 * @deprecated Since 0.99.0 477 */ 478 @Deprecated 479 public HttpServer(String name, String bindAddress, int port, boolean findPort) 480 throws IOException { 481 this(name, bindAddress, port, findPort, new Configuration()); 482 } 483 484 /** 485 * Create a status server on the given port. Allows you to specify the path specifications that 486 * this server will be serving so that they will be added to the filters properly. 487 * @param name The name of the server 488 * @param bindAddress The address for this server 489 * @param port The port to use on the server 490 * @param findPort whether the server should start at the given port and increment by 1 until 491 * it finds a free port. 492 * @param conf Configuration 493 * @param pathSpecs Path specifications that this httpserver will be serving. These will be 494 * added to any filters. 495 * @deprecated Since 0.99.0 496 */ 497 @Deprecated 498 public HttpServer(String name, String bindAddress, int port, boolean findPort, Configuration conf, 499 String[] pathSpecs) throws IOException { 500 this(name, bindAddress, port, findPort, conf, null, pathSpecs); 501 } 502 503 /** 504 * Create a status server on the given port. The jsp scripts are taken from 505 * src/webapps/<name>. 506 * @param name The name of the server 507 * @param port The port to use on the server 508 * @param findPort whether the server should start at the given port and increment by 1 until it 509 * finds a free port. 510 * @param conf Configuration 511 * @deprecated Since 0.99.0 512 */ 513 @Deprecated 514 public HttpServer(String name, String bindAddress, int port, boolean findPort, Configuration conf) 515 throws IOException { 516 this(name, bindAddress, port, findPort, conf, null, null); 517 } 518 519 /** 520 * Creates a status server on the given port. The JSP scripts are taken from 521 * src/webapp<name>. 522 * @param name the name of the server 523 * @param bindAddress the address for this server 524 * @param port the port to use on the server 525 * @param findPort whether the server should start at the given port and increment by 1 until 526 * it finds a free port 527 * @param conf the configuration to use 528 * @param adminsAcl {@link AccessControlList} of the admins 529 * @throws IOException when creating the server fails 530 * @deprecated Since 0.99.0 531 */ 532 @Deprecated 533 public HttpServer(String name, String bindAddress, int port, boolean findPort, Configuration conf, 534 AccessControlList adminsAcl) throws IOException { 535 this(name, bindAddress, port, findPort, conf, adminsAcl, null); 536 } 537 538 /** 539 * Create a status server on the given port. The jsp scripts are taken from 540 * src/webapps/<name>. 541 * @param name The name of the server 542 * @param bindAddress The address for this server 543 * @param port The port to use on the server 544 * @param findPort whether the server should start at the given port and increment by 1 until 545 * it finds a free port. 546 * @param conf Configuration 547 * @param adminsAcl {@link AccessControlList} of the admins 548 * @param pathSpecs Path specifications that this httpserver will be serving. These will be 549 * added to any filters. 550 * @deprecated Since 0.99.0 551 */ 552 @Deprecated 553 public HttpServer(String name, String bindAddress, int port, boolean findPort, Configuration conf, 554 AccessControlList adminsAcl, String[] pathSpecs) throws IOException { 555 this(new Builder().setName(name).addEndpoint(URI.create("http://" + bindAddress + ":" + port)) 556 .setFindPort(findPort).setConf(conf).setACL(adminsAcl).setPathSpec(pathSpecs)); 557 } 558 559 private HttpServer(final Builder b) throws IOException { 560 this.appDir = b.appDir; 561 this.logDir = b.logDir; 562 final String appDir = getWebAppsPath(b.name); 563 564 int maxThreads = b.conf.getInt(HTTP_MAX_THREADS, 16); 565 // If HTTP_MAX_THREADS is less than or equal to 0, QueueThreadPool() will use the 566 // default value (currently 200). 567 QueuedThreadPool threadPool = 568 maxThreads <= 0 ? new QueuedThreadPool() : new QueuedThreadPool(maxThreads); 569 threadPool.setDaemon(true); 570 this.webServer = new Server(threadPool); 571 572 this.adminsAcl = b.adminsAcl; 573 this.webAppContext = createWebAppContext(b.name, b.conf, adminsAcl, appDir); 574 this.findPort = b.findPort; 575 this.authenticationEnabled = b.securityEnabled; 576 initializeWebServer(b.name, b.hostName, b.conf, b.pathSpecs, b); 577 this.webServer.setHandler(buildGzipHandler(this.webServer.getHandler())); 578 } 579 580 private void initializeWebServer(String name, String hostName, Configuration conf, 581 String[] pathSpecs, HttpServer.Builder b) throws FileNotFoundException, IOException { 582 583 Preconditions.checkNotNull(webAppContext); 584 585 HandlerCollection handlerCollection = new HandlerCollection(); 586 587 ContextHandlerCollection contexts = new ContextHandlerCollection(); 588 RequestLog requestLog = HttpRequestLog.getRequestLog(name); 589 590 if (requestLog != null) { 591 RequestLogHandler requestLogHandler = new RequestLogHandler(); 592 requestLogHandler.setRequestLog(requestLog); 593 handlerCollection.addHandler(requestLogHandler); 594 } 595 596 final String appDir = getWebAppsPath(name); 597 598 handlerCollection.addHandler(contexts); 599 handlerCollection.addHandler(webAppContext); 600 601 webServer.setHandler(handlerCollection); 602 603 webAppContext.setAttribute(ADMINS_ACL, adminsAcl); 604 605 // Default apps need to be set first, so that all filters are applied to them. 606 // Because they're added to defaultContexts, we need them there before we start 607 // adding filters 608 addDefaultApps(contexts, appDir, conf); 609 610 addGlobalFilter("safety", QuotingInputFilter.class.getName(), null); 611 612 addGlobalFilter("clickjackingprevention", ClickjackingPreventionFilter.class.getName(), 613 ClickjackingPreventionFilter.getDefaultParameters(conf)); 614 615 HttpConfig httpConfig = new HttpConfig(conf); 616 617 addGlobalFilter("securityheaders", SecurityHeadersFilter.class.getName(), 618 SecurityHeadersFilter.getDefaultParameters(conf, httpConfig.isSecure())); 619 620 // But security needs to be enabled prior to adding the other servlets 621 if (authenticationEnabled) { 622 initSpnego(conf, hostName, b.usernameConfKey, b.keytabConfKey, b.kerberosNameRulesKey, 623 b.signatureSecretFileKey); 624 } 625 626 final FilterInitializer[] initializers = getFilterInitializers(conf); 627 if (initializers != null) { 628 conf = new Configuration(conf); 629 conf.set(BIND_ADDRESS, hostName); 630 for (FilterInitializer c : initializers) { 631 c.initFilter(this, conf); 632 } 633 } 634 635 addDefaultServlets(contexts, conf); 636 637 if (pathSpecs != null) { 638 for (String path : pathSpecs) { 639 LOG.info("adding path spec: " + path); 640 addFilterPathMapping(path, webAppContext); 641 } 642 } 643 } 644 645 private void addManagedListener(ServerConnector connector) { 646 listeners.add(new ListenerInfo(true, connector)); 647 } 648 649 private static WebAppContext createWebAppContext(String name, Configuration conf, 650 AccessControlList adminsAcl, final String appDir) { 651 WebAppContext ctx = new WebAppContext(); 652 ctx.setDisplayName(name); 653 ctx.setContextPath("/"); 654 ctx.setWar(appDir + "/" + name); 655 ctx.getServletContext().setAttribute(CONF_CONTEXT_ATTRIBUTE, conf); 656 // for org.apache.hadoop.metrics.MetricsServlet 657 ctx.getServletContext().setAttribute(org.apache.hadoop.http.HttpServer2.CONF_CONTEXT_ATTRIBUTE, 658 conf); 659 ctx.getServletContext().setAttribute(ADMINS_ACL, adminsAcl); 660 addNoCacheFilter(ctx); 661 return ctx; 662 } 663 664 /** 665 * Construct and configure an instance of {@link GzipHandler}. With complex 666 * multi-{@link WebAppContext} configurations, it's easiest to apply this handler directly to the 667 * instance of {@link Server} near the end of its configuration, something like 668 * 669 * <pre> 670 * Server server = new Server(); 671 * // ... 672 * server.setHandler(buildGzipHandler(server.getHandler())); 673 * server.start(); 674 * </pre> 675 */ 676 public static GzipHandler buildGzipHandler(final Handler wrapped) { 677 final GzipHandler gzipHandler = new GzipHandler(); 678 gzipHandler.setHandler(wrapped); 679 return gzipHandler; 680 } 681 682 private static void addNoCacheFilter(WebAppContext ctxt) { 683 defineFilter(ctxt, NO_CACHE_FILTER, NoCacheFilter.class.getName(), 684 Collections.<String, String> emptyMap(), new String[] { "/*" }); 685 } 686 687 /** Get an array of FilterConfiguration specified in the conf */ 688 private static FilterInitializer[] getFilterInitializers(Configuration conf) { 689 if (conf == null) { 690 return null; 691 } 692 693 Class<?>[] classes = conf.getClasses(FILTER_INITIALIZERS_PROPERTY); 694 if (classes == null) { 695 return null; 696 } 697 698 FilterInitializer[] initializers = new FilterInitializer[classes.length]; 699 for (int i = 0; i < classes.length; i++) { 700 initializers[i] = (FilterInitializer) ReflectionUtils.newInstance(classes[i]); 701 } 702 return initializers; 703 } 704 705 /** 706 * Add default apps. 707 * @param appDir The application directory 708 */ 709 protected void addDefaultApps(ContextHandlerCollection parent, final String appDir, 710 Configuration conf) { 711 // set up the context for "/logs/" if "hadoop.log.dir" property is defined. 712 String logDir = this.logDir; 713 if (logDir == null) { 714 logDir = System.getProperty("hadoop.log.dir"); 715 } 716 if (logDir != null) { 717 ServletContextHandler logContext = new ServletContextHandler(parent, "/logs"); 718 logContext.addServlet(AdminAuthorizedServlet.class, "/*"); 719 logContext.setResourceBase(logDir); 720 721 if ( 722 conf.getBoolean(ServerConfigurationKeys.HBASE_JETTY_LOGS_SERVE_ALIASES, 723 ServerConfigurationKeys.DEFAULT_HBASE_JETTY_LOGS_SERVE_ALIASES) 724 ) { 725 Map<String, String> params = logContext.getInitParams(); 726 params.put("org.mortbay.jetty.servlet.Default.aliases", "true"); 727 } 728 logContext.setDisplayName("logs"); 729 setContextAttributes(logContext, conf); 730 defaultContexts.put(logContext, true); 731 } 732 // set up the context for "/static/*" 733 ServletContextHandler staticContext = new ServletContextHandler(parent, "/static"); 734 staticContext.setResourceBase(appDir + "/static"); 735 staticContext.addServlet(DefaultServlet.class, "/*"); 736 staticContext.setDisplayName("static"); 737 setContextAttributes(staticContext, conf); 738 defaultContexts.put(staticContext, true); 739 } 740 741 private void setContextAttributes(ServletContextHandler context, Configuration conf) { 742 context.getServletContext().setAttribute(CONF_CONTEXT_ATTRIBUTE, conf); 743 context.getServletContext().setAttribute(ADMINS_ACL, adminsAcl); 744 } 745 746 /** 747 * Add default servlets. 748 */ 749 protected void addDefaultServlets(ContextHandlerCollection contexts, Configuration conf) 750 throws IOException { 751 // set up default servlets 752 addPrivilegedServlet("stacks", "/stacks", StackServlet.class); 753 addPrivilegedServlet("logLevel", "/logLevel", LogLevel.Servlet.class); 754 // Hadoop3 has moved completely to metrics2, and dropped support for Metrics v1's 755 // MetricsServlet (see HADOOP-12504). We'll using reflection to load if against hadoop2. 756 // Remove when we drop support for hbase on hadoop2.x. 757 try { 758 Class<?> clz = Class.forName("org.apache.hadoop.metrics.MetricsServlet"); 759 addPrivilegedServlet("metrics", "/metrics", clz.asSubclass(HttpServlet.class)); 760 } catch (Exception e) { 761 // do nothing 762 } 763 addPrivilegedServlet("jmx", "/jmx", JMXJsonServlet.class); 764 // While we don't expect users to have sensitive information in their configuration, they 765 // might. Give them an option to not expose the service configuration to all users. 766 if (conf.getBoolean(HTTP_PRIVILEGED_CONF_KEY, HTTP_PRIVILEGED_CONF_DEFAULT)) { 767 addPrivilegedServlet("conf", "/conf", ConfServlet.class); 768 } else { 769 addUnprivilegedServlet("conf", "/conf", ConfServlet.class); 770 } 771 final String asyncProfilerHome = ProfileServlet.getAsyncProfilerHome(); 772 if (asyncProfilerHome != null && !asyncProfilerHome.trim().isEmpty()) { 773 addPrivilegedServlet("prof", "/prof", ProfileServlet.class); 774 Path tmpDir = Paths.get(ProfileServlet.OUTPUT_DIR); 775 if (Files.notExists(tmpDir)) { 776 Files.createDirectories(tmpDir); 777 } 778 ServletContextHandler genCtx = new ServletContextHandler(contexts, "/prof-output-hbase"); 779 genCtx.addServlet(ProfileOutputServlet.class, "/*"); 780 genCtx.setResourceBase(tmpDir.toAbsolutePath().toString()); 781 genCtx.setDisplayName("prof-output-hbase"); 782 } else { 783 addUnprivilegedServlet("prof", "/prof", ProfileServlet.DisabledServlet.class); 784 LOG.info("ASYNC_PROFILER_HOME environment variable and async.profiler.home system property " 785 + "not specified. Disabling /prof endpoint."); 786 } 787 } 788 789 /** 790 * Set a value in the webapp context. These values are available to the jsp pages as 791 * "application.getAttribute(name)". 792 * @param name The name of the attribute 793 * @param value The value of the attribute 794 */ 795 public void setAttribute(String name, Object value) { 796 webAppContext.setAttribute(name, value); 797 } 798 799 /** 800 * Add a Jersey resource package. 801 * @param packageName The Java package name containing the Jersey resource. 802 * @param pathSpec The path spec for the servlet 803 */ 804 public void addJerseyResourcePackage(final String packageName, final String pathSpec) { 805 LOG.info("addJerseyResourcePackage: packageName=" + packageName + ", pathSpec=" + pathSpec); 806 807 ResourceConfig application = new ResourceConfig().packages(packageName); 808 final ServletHolder sh = new ServletHolder(new ServletContainer(application)); 809 webAppContext.addServlet(sh, pathSpec); 810 } 811 812 /** 813 * Adds a servlet in the server that any user can access. This method differs from 814 * {@link #addPrivilegedServlet(String, String, Class)} in that any authenticated user can 815 * interact with the servlet added by this method. 816 * @param name The name of the servlet (can be passed as null) 817 * @param pathSpec The path spec for the servlet 818 * @param clazz The servlet class 819 */ 820 public void addUnprivilegedServlet(String name, String pathSpec, 821 Class<? extends HttpServlet> clazz) { 822 addServletWithAuth(name, pathSpec, clazz, false); 823 } 824 825 /** 826 * Adds a servlet in the server that any user can access. This method differs from 827 * {@link #addPrivilegedServlet(String, ServletHolder)} in that any authenticated user can 828 * interact with the servlet added by this method. 829 * @param pathSpec The path spec for the servlet 830 * @param holder The servlet holder 831 */ 832 public void addUnprivilegedServlet(String pathSpec, ServletHolder holder) { 833 addServletWithAuth(pathSpec, holder, false); 834 } 835 836 /** 837 * Adds a servlet in the server that only administrators can access. This method differs from 838 * {@link #addUnprivilegedServlet(String, String, Class)} in that only those authenticated user 839 * who are identified as administrators can interact with the servlet added by this method. 840 */ 841 public void addPrivilegedServlet(String name, String pathSpec, 842 Class<? extends HttpServlet> clazz) { 843 addServletWithAuth(name, pathSpec, clazz, true); 844 } 845 846 /** 847 * Adds a servlet in the server that only administrators can access. This method differs from 848 * {@link #addUnprivilegedServlet(String, ServletHolder)} in that only those authenticated user 849 * who are identified as administrators can interact with the servlet added by this method. 850 */ 851 public void addPrivilegedServlet(String pathSpec, ServletHolder holder) { 852 addServletWithAuth(pathSpec, holder, true); 853 } 854 855 /** 856 * Internal method to add a servlet to the HTTP server. Developers should not call this method 857 * directly, but invoke it via {@link #addUnprivilegedServlet(String, String, Class)} or 858 * {@link #addPrivilegedServlet(String, String, Class)}. 859 */ 860 void addServletWithAuth(String name, String pathSpec, Class<? extends HttpServlet> clazz, 861 boolean requireAuthz) { 862 addInternalServlet(name, pathSpec, clazz, requireAuthz); 863 addFilterPathMapping(pathSpec, webAppContext); 864 } 865 866 /** 867 * Internal method to add a servlet to the HTTP server. Developers should not call this method 868 * directly, but invoke it via {@link #addUnprivilegedServlet(String, ServletHolder)} or 869 * {@link #addPrivilegedServlet(String, ServletHolder)}. 870 */ 871 void addServletWithAuth(String pathSpec, ServletHolder holder, boolean requireAuthz) { 872 addInternalServlet(pathSpec, holder, requireAuthz); 873 addFilterPathMapping(pathSpec, webAppContext); 874 } 875 876 /** 877 * Add an internal servlet in the server, specifying whether or not to protect with Kerberos 878 * authentication. Note: This method is to be used for adding servlets that facilitate internal 879 * communication and not for user facing functionality. For servlets added using this method, 880 * filters (except internal Kerberos filters) are not enabled. 881 * @param name The name of the {@link Servlet} (can be passed as null) 882 * @param pathSpec The path spec for the {@link Servlet} 883 * @param clazz The {@link Servlet} class 884 * @param requireAuthz Require Kerberos authenticate to access servlet 885 */ 886 void addInternalServlet(String name, String pathSpec, Class<? extends HttpServlet> clazz, 887 boolean requireAuthz) { 888 ServletHolder holder = new ServletHolder(clazz); 889 if (name != null) { 890 holder.setName(name); 891 } 892 addInternalServlet(pathSpec, holder, requireAuthz); 893 } 894 895 /** 896 * Add an internal servlet in the server, specifying whether or not to protect with Kerberos 897 * authentication. Note: This method is to be used for adding servlets that facilitate internal 898 * communication and not for user facing functionality. For servlets added using this method, 899 * filters (except internal Kerberos filters) are not enabled. 900 * @param pathSpec The path spec for the {@link Servlet} 901 * @param holder The object providing the {@link Servlet} instance 902 * @param requireAuthz Require Kerberos authenticate to access servlet 903 */ 904 void addInternalServlet(String pathSpec, ServletHolder holder, boolean requireAuthz) { 905 if (authenticationEnabled && requireAuthz) { 906 FilterHolder filter = new FilterHolder(AdminAuthorizedFilter.class); 907 filter.setName(AdminAuthorizedFilter.class.getSimpleName()); 908 FilterMapping fmap = new FilterMapping(); 909 fmap.setPathSpec(pathSpec); 910 fmap.setDispatches(FilterMapping.ALL); 911 fmap.setFilterName(AdminAuthorizedFilter.class.getSimpleName()); 912 webAppContext.getServletHandler().addFilter(filter, fmap); 913 } 914 webAppContext.getSessionHandler().getSessionCookieConfig().setHttpOnly(true); 915 webAppContext.getSessionHandler().getSessionCookieConfig().setSecure(true); 916 webAppContext.addServlet(holder, pathSpec); 917 } 918 919 @Override 920 public void addFilter(String name, String classname, Map<String, String> parameters) { 921 final String[] USER_FACING_URLS = { "*.html", "*.jsp" }; 922 defineFilter(webAppContext, name, classname, parameters, USER_FACING_URLS); 923 LOG.info("Added filter " + name + " (class=" + classname + ") to context " 924 + webAppContext.getDisplayName()); 925 final String[] ALL_URLS = { "/*" }; 926 for (Map.Entry<ServletContextHandler, Boolean> e : defaultContexts.entrySet()) { 927 if (e.getValue()) { 928 ServletContextHandler handler = e.getKey(); 929 defineFilter(handler, name, classname, parameters, ALL_URLS); 930 LOG.info("Added filter " + name + " (class=" + classname + ") to context " 931 + handler.getDisplayName()); 932 } 933 } 934 filterNames.add(name); 935 } 936 937 @Override 938 public void addGlobalFilter(String name, String classname, Map<String, String> parameters) { 939 final String[] ALL_URLS = { "/*" }; 940 defineFilter(webAppContext, name, classname, parameters, ALL_URLS); 941 for (ServletContextHandler ctx : defaultContexts.keySet()) { 942 defineFilter(ctx, name, classname, parameters, ALL_URLS); 943 } 944 LOG.info("Added global filter '" + name + "' (class=" + classname + ")"); 945 } 946 947 /** 948 * Define a filter for a context and set up default url mappings. 949 */ 950 public static void defineFilter(ServletContextHandler handler, String name, String classname, 951 Map<String, String> parameters, String[] urls) { 952 FilterHolder holder = new FilterHolder(); 953 holder.setName(name); 954 holder.setClassName(classname); 955 if (parameters != null) { 956 holder.setInitParameters(parameters); 957 } 958 FilterMapping fmap = new FilterMapping(); 959 fmap.setPathSpecs(urls); 960 fmap.setDispatches(FilterMapping.ALL); 961 fmap.setFilterName(name); 962 handler.getServletHandler().addFilter(holder, fmap); 963 } 964 965 /** 966 * Add the path spec to the filter path mapping. 967 * @param pathSpec The path spec 968 * @param webAppCtx The WebApplicationContext to add to 969 */ 970 protected void addFilterPathMapping(String pathSpec, WebAppContext webAppCtx) { 971 for (String name : filterNames) { 972 FilterMapping fmap = new FilterMapping(); 973 fmap.setPathSpec(pathSpec); 974 fmap.setFilterName(name); 975 fmap.setDispatches(FilterMapping.ALL); 976 webAppCtx.getServletHandler().addFilterMapping(fmap); 977 } 978 } 979 980 /** 981 * Get the value in the webapp context. 982 * @param name The name of the attribute 983 * @return The value of the attribute 984 */ 985 public Object getAttribute(String name) { 986 return webAppContext.getAttribute(name); 987 } 988 989 public WebAppContext getWebAppContext() { 990 return this.webAppContext; 991 } 992 993 public String getWebAppsPath(String appName) throws FileNotFoundException { 994 return getWebAppsPath(this.appDir, appName); 995 } 996 997 /** 998 * Get the pathname to the webapps files. 999 * @param appName eg "secondary" or "datanode" 1000 * @return the pathname as a URL 1001 * @throws FileNotFoundException if 'webapps' directory cannot be found on CLASSPATH. 1002 */ 1003 protected String getWebAppsPath(String webapps, String appName) throws FileNotFoundException { 1004 URL url = getClass().getClassLoader().getResource(webapps + "/" + appName); 1005 1006 if (url == null) { 1007 throw new FileNotFoundException(webapps + "/" + appName + " not found in CLASSPATH"); 1008 } 1009 1010 String urlString = url.toString(); 1011 return urlString.substring(0, urlString.lastIndexOf('/')); 1012 } 1013 1014 /** 1015 * Get the port that the server is on 1016 * @return the port 1017 * @deprecated Since 0.99.0 1018 */ 1019 @Deprecated 1020 public int getPort() { 1021 return ((ServerConnector) webServer.getConnectors()[0]).getLocalPort(); 1022 } 1023 1024 /** 1025 * Get the address that corresponds to a particular connector. 1026 * @return the corresponding address for the connector, or null if there's no such connector or 1027 * the connector is not bounded. 1028 */ 1029 public InetSocketAddress getConnectorAddress(int index) { 1030 Preconditions.checkArgument(index >= 0); 1031 1032 if (index > webServer.getConnectors().length) { 1033 return null; 1034 } 1035 1036 ServerConnector c = (ServerConnector) webServer.getConnectors()[index]; 1037 if (c.getLocalPort() == -1 || c.getLocalPort() == -2) { 1038 // -1 if the connector has not been opened 1039 // -2 if it has been closed 1040 return null; 1041 } 1042 1043 return new InetSocketAddress(c.getHost(), c.getLocalPort()); 1044 } 1045 1046 /** 1047 * Set the min, max number of worker threads (simultaneous connections). 1048 */ 1049 public void setThreads(int min, int max) { 1050 QueuedThreadPool pool = (QueuedThreadPool) webServer.getThreadPool(); 1051 pool.setMinThreads(min); 1052 pool.setMaxThreads(max); 1053 } 1054 1055 private void initSpnego(Configuration conf, String hostName, String usernameConfKey, 1056 String keytabConfKey, String kerberosNameRuleKey, String signatureSecretKeyFileKey) 1057 throws IOException { 1058 Map<String, String> params = new HashMap<>(); 1059 String principalInConf = getOrEmptyString(conf, usernameConfKey); 1060 if (!principalInConf.isEmpty()) { 1061 params.put(HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_SUFFIX, 1062 SecurityUtil.getServerPrincipal(principalInConf, hostName)); 1063 } 1064 String httpKeytab = getOrEmptyString(conf, keytabConfKey); 1065 if (!httpKeytab.isEmpty()) { 1066 params.put(HTTP_SPNEGO_AUTHENTICATION_KEYTAB_SUFFIX, httpKeytab); 1067 } 1068 String kerberosNameRule = getOrEmptyString(conf, kerberosNameRuleKey); 1069 if (!kerberosNameRule.isEmpty()) { 1070 params.put(HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_SUFFIX, kerberosNameRule); 1071 } 1072 String signatureSecretKeyFile = getOrEmptyString(conf, signatureSecretKeyFileKey); 1073 if (!signatureSecretKeyFile.isEmpty()) { 1074 params.put(HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_SUFFIX, signatureSecretKeyFile); 1075 } 1076 params.put(AuthenticationFilter.AUTH_TYPE, "kerberos"); 1077 1078 // Verify that the required options were provided 1079 if ( 1080 isMissing(params.get(HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_SUFFIX)) 1081 || isMissing(params.get(HTTP_SPNEGO_AUTHENTICATION_KEYTAB_SUFFIX)) 1082 ) { 1083 throw new IllegalArgumentException( 1084 usernameConfKey + " and " + keytabConfKey + " are both required in the configuration " 1085 + "to enable SPNEGO/Kerberos authentication for the Web UI"); 1086 } 1087 1088 if ( 1089 conf.getBoolean(HTTP_SPNEGO_AUTHENTICATION_PROXYUSER_ENABLE_KEY, 1090 HTTP_SPNEGO_AUTHENTICATION_PROXYUSER_ENABLE_DEFAULT) 1091 ) { 1092 // Copy/rename standard hadoop proxyuser settings to filter 1093 for (Map.Entry<String, String> proxyEntry : conf 1094 .getPropsWithPrefix(ProxyUsers.CONF_HADOOP_PROXYUSER).entrySet()) { 1095 params.put(ProxyUserAuthenticationFilter.PROXYUSER_PREFIX + proxyEntry.getKey(), 1096 proxyEntry.getValue()); 1097 } 1098 addGlobalFilter(SPNEGO_PROXYUSER_FILTER, ProxyUserAuthenticationFilter.class.getName(), 1099 params); 1100 } else { 1101 addGlobalFilter(SPNEGO_FILTER, AuthenticationFilter.class.getName(), params); 1102 } 1103 } 1104 1105 /** 1106 * Returns true if the argument is non-null and not whitespace 1107 */ 1108 private boolean isMissing(String value) { 1109 if (null == value) { 1110 return true; 1111 } 1112 return value.trim().isEmpty(); 1113 } 1114 1115 /** 1116 * Extracts the value for the given key from the configuration of returns a string of zero length. 1117 */ 1118 private String getOrEmptyString(Configuration conf, String key) { 1119 if (null == key) { 1120 return EMPTY_STRING; 1121 } 1122 final String value = conf.get(key.trim()); 1123 return null == value ? EMPTY_STRING : value; 1124 } 1125 1126 /** 1127 * Start the server. Does not wait for the server to start. 1128 */ 1129 public void start() throws IOException { 1130 try { 1131 try { 1132 openListeners(); 1133 webServer.start(); 1134 } catch (IOException ex) { 1135 LOG.info("HttpServer.start() threw a non Bind IOException", ex); 1136 throw ex; 1137 } catch (MultiException ex) { 1138 LOG.info("HttpServer.start() threw a MultiException", ex); 1139 throw ex; 1140 } 1141 // Make sure there is no handler failures. 1142 Handler[] handlers = webServer.getHandlers(); 1143 for (int i = 0; i < handlers.length; i++) { 1144 if (handlers[i].isFailed()) { 1145 throw new IOException("Problem in starting http server. Server handlers failed"); 1146 } 1147 } 1148 // Make sure there are no errors initializing the context. 1149 Throwable unavailableException = webAppContext.getUnavailableException(); 1150 if (unavailableException != null) { 1151 // Have to stop the webserver, or else its non-daemon threads 1152 // will hang forever. 1153 webServer.stop(); 1154 throw new IOException("Unable to initialize WebAppContext", unavailableException); 1155 } 1156 } catch (IOException e) { 1157 throw e; 1158 } catch (InterruptedException e) { 1159 throw (IOException) new InterruptedIOException("Interrupted while starting HTTP server") 1160 .initCause(e); 1161 } catch (Exception e) { 1162 throw new IOException("Problem starting http server", e); 1163 } 1164 } 1165 1166 private void loadListeners() { 1167 for (ListenerInfo li : listeners) { 1168 webServer.addConnector(li.listener); 1169 } 1170 } 1171 1172 /** 1173 * Open the main listener for the server 1174 * @throws Exception if the listener cannot be opened or the appropriate port is already in use 1175 */ 1176 void openListeners() throws Exception { 1177 for (ListenerInfo li : listeners) { 1178 ServerConnector listener = li.listener; 1179 if (!li.isManaged || (li.listener.getLocalPort() != -1 && li.listener.getLocalPort() != -2)) { 1180 // This listener is either started externally, or has not been opened, or has been closed 1181 continue; 1182 } 1183 int port = listener.getPort(); 1184 while (true) { 1185 // jetty has a bug where you can't reopen a listener that previously 1186 // failed to open w/o issuing a close first, even if the port is changed 1187 try { 1188 listener.close(); 1189 listener.open(); 1190 LOG.info("Jetty bound to port " + listener.getLocalPort()); 1191 break; 1192 } catch (IOException ex) { 1193 if (!(ex instanceof BindException) && !(ex.getCause() instanceof BindException)) { 1194 throw ex; 1195 } 1196 if (port == 0 || !findPort) { 1197 BindException be = 1198 new BindException("Port in use: " + listener.getHost() + ":" + listener.getPort()); 1199 be.initCause(ex); 1200 throw be; 1201 } 1202 } 1203 // try the next port number 1204 listener.setPort(++port); 1205 Thread.sleep(100); 1206 } 1207 } 1208 } 1209 1210 /** 1211 * stop the server 1212 */ 1213 public void stop() throws Exception { 1214 MultiException exception = null; 1215 for (ListenerInfo li : listeners) { 1216 if (!li.isManaged) { 1217 continue; 1218 } 1219 1220 try { 1221 li.listener.close(); 1222 } catch (Exception e) { 1223 LOG.error("Error while stopping listener for webapp" + webAppContext.getDisplayName(), e); 1224 exception = addMultiException(exception, e); 1225 } 1226 } 1227 1228 try { 1229 // clear & stop webAppContext attributes to avoid memory leaks. 1230 webAppContext.clearAttributes(); 1231 webAppContext.stop(); 1232 } catch (Exception e) { 1233 LOG.error("Error while stopping web app context for webapp " + webAppContext.getDisplayName(), 1234 e); 1235 exception = addMultiException(exception, e); 1236 } 1237 1238 try { 1239 webServer.stop(); 1240 } catch (Exception e) { 1241 LOG.error("Error while stopping web server for webapp " + webAppContext.getDisplayName(), e); 1242 exception = addMultiException(exception, e); 1243 } 1244 1245 if (exception != null) { 1246 exception.ifExceptionThrow(); 1247 } 1248 1249 } 1250 1251 private MultiException addMultiException(MultiException exception, Exception e) { 1252 if (exception == null) { 1253 exception = new MultiException(); 1254 } 1255 exception.add(e); 1256 return exception; 1257 } 1258 1259 public void join() throws InterruptedException { 1260 webServer.join(); 1261 } 1262 1263 /** 1264 * Test for the availability of the web server 1265 * @return true if the web server is started, false otherwise 1266 */ 1267 public boolean isAlive() { 1268 return webServer != null && webServer.isStarted(); 1269 } 1270 1271 /** 1272 * Return the host and port of the HttpServer, if live 1273 * @return the classname and any HTTP URL 1274 */ 1275 @Override 1276 public String toString() { 1277 if (listeners.isEmpty()) { 1278 return "Inactive HttpServer"; 1279 } else { 1280 StringBuilder sb = new StringBuilder("HttpServer (") 1281 .append(isAlive() ? STATE_DESCRIPTION_ALIVE : STATE_DESCRIPTION_NOT_LIVE) 1282 .append("), listening at:"); 1283 for (ListenerInfo li : listeners) { 1284 ServerConnector l = li.listener; 1285 sb.append(l.getHost()).append(":").append(l.getPort()).append("/,"); 1286 } 1287 return sb.toString(); 1288 } 1289 } 1290 1291 /** 1292 * Checks the user has privileges to access to instrumentation servlets. 1293 * <p> 1294 * If <code>hadoop.security.instrumentation.requires.admin</code> is set to FALSE (default value) 1295 * it always returns TRUE. 1296 * </p> 1297 * <p> 1298 * If <code>hadoop.security.instrumentation.requires.admin</code> is set to TRUE it will check 1299 * that if the current user is in the admin ACLS. If the user is in the admin ACLs it returns 1300 * TRUE, otherwise it returns FALSE. 1301 * </p> 1302 * @param servletContext the servlet context. 1303 * @param request the servlet request. 1304 * @param response the servlet response. 1305 * @return TRUE/FALSE based on the logic decribed above. 1306 */ 1307 public static boolean isInstrumentationAccessAllowed(ServletContext servletContext, 1308 HttpServletRequest request, HttpServletResponse response) throws IOException { 1309 Configuration conf = (Configuration) servletContext.getAttribute(CONF_CONTEXT_ATTRIBUTE); 1310 1311 boolean access = true; 1312 boolean adminAccess = conf 1313 .getBoolean(CommonConfigurationKeys.HADOOP_SECURITY_INSTRUMENTATION_REQUIRES_ADMIN, false); 1314 if (adminAccess) { 1315 access = hasAdministratorAccess(servletContext, request, response); 1316 } 1317 return access; 1318 } 1319 1320 /** 1321 * Does the user sending the HttpServletRequest has the administrator ACLs? If it isn't the case, 1322 * response will be modified to send an error to the user. 1323 * @param servletContext the {@link ServletContext} to use 1324 * @param request the {@link HttpServletRequest} to check 1325 * @param response used to send the error response if user does not have admin access. 1326 * @return true if admin-authorized, false otherwise 1327 * @throws IOException if an unauthenticated or unauthorized user tries to access the page 1328 */ 1329 public static boolean hasAdministratorAccess(ServletContext servletContext, 1330 HttpServletRequest request, HttpServletResponse response) throws IOException { 1331 Configuration conf = (Configuration) servletContext.getAttribute(CONF_CONTEXT_ATTRIBUTE); 1332 AccessControlList acl = (AccessControlList) servletContext.getAttribute(ADMINS_ACL); 1333 1334 return hasAdministratorAccess(conf, acl, request, response); 1335 } 1336 1337 public static boolean hasAdministratorAccess(Configuration conf, AccessControlList acl, 1338 HttpServletRequest request, HttpServletResponse response) throws IOException { 1339 // If there is no authorization, anybody has administrator access. 1340 if (!conf.getBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, false)) { 1341 return true; 1342 } 1343 1344 String remoteUser = request.getRemoteUser(); 1345 if (remoteUser == null) { 1346 response.sendError(HttpServletResponse.SC_UNAUTHORIZED, 1347 "Unauthenticated users are not " + "authorized to access this page."); 1348 return false; 1349 } 1350 1351 if (acl != null && !userHasAdministratorAccess(acl, remoteUser)) { 1352 response.sendError(HttpServletResponse.SC_FORBIDDEN, 1353 "User " + remoteUser + " is unauthorized to access this page."); 1354 return false; 1355 } 1356 1357 return true; 1358 } 1359 1360 /** 1361 * Get the admin ACLs from the given ServletContext and check if the given user is in the ACL. 1362 * @param servletContext the context containing the admin ACL. 1363 * @param remoteUser the remote user to check for. 1364 * @return true if the user is present in the ACL, false if no ACL is set or the user is not 1365 * present 1366 */ 1367 public static boolean userHasAdministratorAccess(ServletContext servletContext, 1368 String remoteUser) { 1369 AccessControlList adminsAcl = (AccessControlList) servletContext.getAttribute(ADMINS_ACL); 1370 return userHasAdministratorAccess(adminsAcl, remoteUser); 1371 } 1372 1373 public static boolean userHasAdministratorAccess(AccessControlList acl, String remoteUser) { 1374 UserGroupInformation remoteUserUGI = UserGroupInformation.createRemoteUser(remoteUser); 1375 return acl != null && acl.isUserAllowed(remoteUserUGI); 1376 } 1377 1378 /** 1379 * A very simple servlet to serve up a text representation of the current stack traces. It both 1380 * returns the stacks to the caller and logs them. Currently the stack traces are done 1381 * sequentially rather than exactly the same data. 1382 */ 1383 public static class StackServlet extends HttpServlet { 1384 private static final long serialVersionUID = -6284183679759467039L; 1385 1386 @Override 1387 public void doGet(HttpServletRequest request, HttpServletResponse response) 1388 throws ServletException, IOException { 1389 if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), request, response)) { 1390 return; 1391 } 1392 response.setContentType("text/plain; charset=UTF-8"); 1393 try (PrintStream out = new PrintStream(response.getOutputStream(), false, "UTF-8")) { 1394 Threads.printThreadInfo(out, ""); 1395 out.flush(); 1396 } 1397 ReflectionUtils.logThreadInfo(LOG, "jsp requested", 1); 1398 } 1399 } 1400 1401 /** 1402 * A Servlet input filter that quotes all HTML active characters in the parameter names and 1403 * values. The goal is to quote the characters to make all of the servlets resistant to cross-site 1404 * scripting attacks. 1405 */ 1406 @InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG) 1407 public static class QuotingInputFilter implements Filter { 1408 private FilterConfig config; 1409 1410 public static class RequestQuoter extends HttpServletRequestWrapper { 1411 private final HttpServletRequest rawRequest; 1412 1413 public RequestQuoter(HttpServletRequest rawRequest) { 1414 super(rawRequest); 1415 this.rawRequest = rawRequest; 1416 } 1417 1418 /** 1419 * Return the set of parameter names, quoting each name. 1420 */ 1421 @Override 1422 public Enumeration<String> getParameterNames() { 1423 return new Enumeration<String>() { 1424 private Enumeration<String> rawIterator = rawRequest.getParameterNames(); 1425 1426 @Override 1427 public boolean hasMoreElements() { 1428 return rawIterator.hasMoreElements(); 1429 } 1430 1431 @Override 1432 public String nextElement() { 1433 return HtmlQuoting.quoteHtmlChars(rawIterator.nextElement()); 1434 } 1435 }; 1436 } 1437 1438 /** 1439 * Unquote the name and quote the value. 1440 */ 1441 @Override 1442 public String getParameter(String name) { 1443 return HtmlQuoting 1444 .quoteHtmlChars(rawRequest.getParameter(HtmlQuoting.unquoteHtmlChars(name))); 1445 } 1446 1447 @Override 1448 public String[] getParameterValues(String name) { 1449 String unquoteName = HtmlQuoting.unquoteHtmlChars(name); 1450 String[] unquoteValue = rawRequest.getParameterValues(unquoteName); 1451 if (unquoteValue == null) { 1452 return null; 1453 } 1454 String[] result = new String[unquoteValue.length]; 1455 for (int i = 0; i < result.length; ++i) { 1456 result[i] = HtmlQuoting.quoteHtmlChars(unquoteValue[i]); 1457 } 1458 return result; 1459 } 1460 1461 @Override 1462 public Map<String, String[]> getParameterMap() { 1463 Map<String, String[]> result = new HashMap<>(); 1464 Map<String, String[]> raw = rawRequest.getParameterMap(); 1465 for (Map.Entry<String, String[]> item : raw.entrySet()) { 1466 String[] rawValue = item.getValue(); 1467 String[] cookedValue = new String[rawValue.length]; 1468 for (int i = 0; i < rawValue.length; ++i) { 1469 cookedValue[i] = HtmlQuoting.quoteHtmlChars(rawValue[i]); 1470 } 1471 result.put(HtmlQuoting.quoteHtmlChars(item.getKey()), cookedValue); 1472 } 1473 return result; 1474 } 1475 1476 /** 1477 * Quote the url so that users specifying the HOST HTTP header can't inject attacks. 1478 */ 1479 @Override 1480 public StringBuffer getRequestURL() { 1481 String url = rawRequest.getRequestURL().toString(); 1482 return new StringBuffer(HtmlQuoting.quoteHtmlChars(url)); 1483 } 1484 1485 /** 1486 * Quote the server name so that users specifying the HOST HTTP header can't inject attacks. 1487 */ 1488 @Override 1489 public String getServerName() { 1490 return HtmlQuoting.quoteHtmlChars(rawRequest.getServerName()); 1491 } 1492 } 1493 1494 @Override 1495 public void init(FilterConfig config) throws ServletException { 1496 this.config = config; 1497 } 1498 1499 @Override 1500 public void destroy() { 1501 } 1502 1503 @Override 1504 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 1505 throws IOException, ServletException { 1506 HttpServletRequestWrapper quoted = new RequestQuoter((HttpServletRequest) request); 1507 HttpServletResponse httpResponse = (HttpServletResponse) response; 1508 1509 String mime = inferMimeType(request); 1510 if (mime == null) { 1511 httpResponse.setContentType("text/plain; charset=utf-8"); 1512 } else if (mime.startsWith("text/html")) { 1513 // HTML with unspecified encoding, we want to 1514 // force HTML with utf-8 encoding 1515 // This is to avoid the following security issue: 1516 // http://openmya.hacker.jp/hasegawa/security/utf7cs.html 1517 httpResponse.setContentType("text/html; charset=utf-8"); 1518 } else if (mime.startsWith("application/xml")) { 1519 httpResponse.setContentType("text/xml; charset=utf-8"); 1520 } 1521 chain.doFilter(quoted, httpResponse); 1522 } 1523 1524 /** 1525 * Infer the mime type for the response based on the extension of the request URI. Returns null 1526 * if unknown. 1527 */ 1528 private String inferMimeType(ServletRequest request) { 1529 String path = ((HttpServletRequest) request).getRequestURI(); 1530 ServletContext context = config.getServletContext(); 1531 return context.getMimeType(path); 1532 } 1533 } 1534}