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