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