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