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