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