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