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