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