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/<name>. 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/<name>. 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}