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