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