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