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