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