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