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