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