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