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