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 static org.hamcrest.Matchers.greaterThan; 021import static org.junit.jupiter.api.Assertions.assertEquals; 022import static org.junit.jupiter.api.Assertions.assertFalse; 023import static org.junit.jupiter.api.Assertions.assertNotNull; 024import static org.junit.jupiter.api.Assertions.assertNull; 025import static org.junit.jupiter.api.Assertions.assertTrue; 026 027import java.io.BufferedReader; 028import java.io.IOException; 029import java.io.InputStream; 030import java.io.InputStreamReader; 031import java.io.PrintWriter; 032import java.net.HttpURLConnection; 033import java.net.URI; 034import java.net.URL; 035import java.nio.CharBuffer; 036import java.nio.charset.StandardCharsets; 037import java.util.Arrays; 038import java.util.Collections; 039import java.util.Enumeration; 040import java.util.HashMap; 041import java.util.List; 042import java.util.Map; 043import java.util.SortedSet; 044import java.util.TreeSet; 045import java.util.concurrent.CountDownLatch; 046import java.util.concurrent.Executor; 047import java.util.concurrent.Executors; 048import javax.servlet.Filter; 049import javax.servlet.FilterChain; 050import javax.servlet.FilterConfig; 051import javax.servlet.ServletContext; 052import javax.servlet.ServletException; 053import javax.servlet.ServletRequest; 054import javax.servlet.ServletResponse; 055import javax.servlet.http.HttpServlet; 056import javax.servlet.http.HttpServletRequest; 057import javax.servlet.http.HttpServletRequestWrapper; 058import javax.servlet.http.HttpServletResponse; 059import org.apache.hadoop.conf.Configuration; 060import org.apache.hadoop.fs.CommonConfigurationKeys; 061import org.apache.hadoop.hbase.http.HttpServer.QuotingInputFilter.RequestQuoter; 062import org.apache.hadoop.hbase.http.resource.JerseyResource; 063import org.apache.hadoop.hbase.testclassification.MiscTests; 064import org.apache.hadoop.hbase.testclassification.SmallTests; 065import org.apache.hadoop.net.NetUtils; 066import org.apache.hadoop.security.Groups; 067import org.apache.hadoop.security.ShellBasedUnixGroupsMapping; 068import org.apache.hadoop.security.UserGroupInformation; 069import org.apache.hadoop.security.authorize.AccessControlList; 070import org.apache.http.HttpEntity; 071import org.apache.http.HttpHeaders; 072import org.apache.http.client.methods.CloseableHttpResponse; 073import org.apache.http.client.methods.HttpGet; 074import org.apache.http.impl.client.CloseableHttpClient; 075import org.apache.http.impl.client.HttpClients; 076import org.hamcrest.MatcherAssert; 077import org.junit.jupiter.api.AfterAll; 078import org.junit.jupiter.api.BeforeAll; 079import org.junit.jupiter.api.Disabled; 080import org.junit.jupiter.api.Tag; 081import org.junit.jupiter.api.Test; 082import org.mockito.Mockito; 083import org.slf4j.Logger; 084import org.slf4j.LoggerFactory; 085 086import org.apache.hbase.thirdparty.org.eclipse.jetty.server.ServerConnector; 087import org.apache.hbase.thirdparty.org.eclipse.jetty.util.ajax.JSON; 088 089@Tag(MiscTests.TAG) 090@Tag(SmallTests.TAG) 091public class TestHttpServer extends HttpServerFunctionalTest { 092 093 private static final Logger LOG = LoggerFactory.getLogger(TestHttpServer.class); 094 private static HttpServer server; 095 private static URL baseUrl; 096 // jetty 9.4.x needs this many threads to start, even in the small. 097 static final int MAX_THREADS = 16; 098 099 @SuppressWarnings("serial") 100 public static class EchoMapServlet extends HttpServlet { 101 @Override 102 public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { 103 PrintWriter out = response.getWriter(); 104 Map<String, String[]> params = request.getParameterMap(); 105 SortedSet<String> keys = new TreeSet<>(params.keySet()); 106 for (String key : keys) { 107 out.print(key); 108 out.print(':'); 109 String[] values = params.get(key); 110 if (values.length > 0) { 111 out.print(values[0]); 112 for (int i = 1; i < values.length; ++i) { 113 out.print(','); 114 out.print(values[i]); 115 } 116 } 117 out.print('\n'); 118 } 119 out.close(); 120 } 121 } 122 123 @SuppressWarnings("serial") 124 public static class EchoServlet extends HttpServlet { 125 @Override 126 public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { 127 PrintWriter out = response.getWriter(); 128 SortedSet<String> sortedKeys = new TreeSet<>(); 129 Enumeration<String> keys = request.getParameterNames(); 130 while (keys.hasMoreElements()) { 131 sortedKeys.add(keys.nextElement()); 132 } 133 for (String key : sortedKeys) { 134 out.print(key); 135 out.print(':'); 136 out.print(request.getParameter(key)); 137 out.print('\n'); 138 } 139 out.close(); 140 } 141 } 142 143 @SuppressWarnings("serial") 144 public static class LongHeaderServlet extends HttpServlet { 145 @Override 146 public void doGet(HttpServletRequest request, HttpServletResponse response) { 147 assertEquals(63 * 1024, request.getHeader("longheader").length()); 148 response.setStatus(HttpServletResponse.SC_OK); 149 } 150 } 151 152 @SuppressWarnings("serial") 153 public static class HtmlContentServlet extends HttpServlet { 154 @Override 155 public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { 156 response.setContentType("text/html"); 157 PrintWriter out = response.getWriter(); 158 out.print("hello world"); 159 out.close(); 160 } 161 } 162 163 @BeforeAll 164 public static void setup() throws Exception { 165 Configuration conf = new Configuration(); 166 conf.setInt(HttpServer.HTTP_MAX_THREADS, MAX_THREADS); 167 server = createTestServer(conf); 168 server.addUnprivilegedServlet("echo", "/echo", EchoServlet.class); 169 server.addUnprivilegedServlet("echomap", "/echomap", EchoMapServlet.class); 170 server.addUnprivilegedServlet("htmlcontent", "/htmlcontent", HtmlContentServlet.class); 171 server.addUnprivilegedServlet("longheader", "/longheader", LongHeaderServlet.class); 172 server.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*"); 173 server.start(); 174 baseUrl = getServerURL(server); 175 LOG.info("HTTP server started: " + baseUrl); 176 } 177 178 @AfterAll 179 public static void cleanup() throws Exception { 180 server.stop(); 181 } 182 183 /** 184 * Test the maximum number of threads cannot be exceeded. 185 */ 186 @Test 187 public void testMaxThreads() throws Exception { 188 int clientThreads = MAX_THREADS * 10; 189 Executor executor = Executors.newFixedThreadPool(clientThreads); 190 // Run many clients to make server reach its maximum number of threads 191 final CountDownLatch ready = new CountDownLatch(clientThreads); 192 final CountDownLatch start = new CountDownLatch(1); 193 for (int i = 0; i < clientThreads; i++) { 194 executor.execute(() -> { 195 ready.countDown(); 196 try { 197 start.await(); 198 assertEquals("a:b\nc:d\n", readOutput(new URL(baseUrl, "/echo?a=b&c=d"))); 199 int serverThreads = server.webServer.getThreadPool().getThreads(); 200 assertTrue(serverThreads <= MAX_THREADS, 201 "More threads are started than expected, Server Threads count: " + serverThreads); 202 LOG.info("Number of threads = " + serverThreads 203 + " which is less or equal than the max = " + MAX_THREADS); 204 } catch (Exception e) { 205 // do nothing 206 } 207 }); 208 } 209 // Start the client threads when they are all ready 210 ready.await(); 211 start.countDown(); 212 } 213 214 @Test 215 public void testEcho() throws Exception { 216 assertEquals("a:b\nc:d\n", readOutput(new URL(baseUrl, "/echo?a=b&c=d"))); 217 assertEquals("a:b\nc<:d\ne:>\n", readOutput(new URL(baseUrl, "/echo?a=b&c<=d&e=>"))); 218 } 219 220 /** Test the echo map servlet that uses getParameterMap. */ 221 @Test 222 public void testEchoMap() throws Exception { 223 assertEquals("a:b\nc:d\n", readOutput(new URL(baseUrl, "/echomap?a=b&c=d"))); 224 assertEquals("a:b,>\nc<:d\n", readOutput(new URL(baseUrl, "/echomap?a=b&c<=d&a=>"))); 225 } 226 227 /** 228 * Test that verifies headers can be up to 64K long. The test adds a 63K header leaving 1K for 229 * other headers. This is because the header buffer setting is for ALL headers, names and values 230 * included. 231 */ 232 @Test 233 public void testLongHeader() throws Exception { 234 URL url = new URL(baseUrl, "/longheader"); 235 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 236 StringBuilder sb = new StringBuilder(); 237 for (int i = 0; i < 63 * 1024; i++) { 238 sb.append("a"); 239 } 240 conn.setRequestProperty("longheader", sb.toString()); 241 assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); 242 } 243 244 @Test 245 public void testContentTypes() throws Exception { 246 // Static CSS files should have text/css 247 URL cssUrl = new URL(baseUrl, "/static/test.css"); 248 HttpURLConnection conn = (HttpURLConnection) cssUrl.openConnection(); 249 conn.connect(); 250 assertEquals(200, conn.getResponseCode()); 251 assertEquals("text/css", conn.getContentType()); 252 253 // Servlets should have text/plain with proper encoding by default 254 URL servletUrl = new URL(baseUrl, "/echo?a=b"); 255 conn = (HttpURLConnection) servletUrl.openConnection(); 256 conn.connect(); 257 assertEquals(200, conn.getResponseCode()); 258 assertEquals("text/plain;charset=utf-8", conn.getContentType()); 259 260 // We should ignore parameters for mime types - ie a parameter 261 // ending in .css should not change mime type 262 servletUrl = new URL(baseUrl, "/echo?a=b.css"); 263 conn = (HttpURLConnection) servletUrl.openConnection(); 264 conn.connect(); 265 assertEquals(200, conn.getResponseCode()); 266 assertEquals("text/plain;charset=utf-8", conn.getContentType()); 267 268 // Servlets that specify text/html should get that content type 269 servletUrl = new URL(baseUrl, "/htmlcontent"); 270 conn = (HttpURLConnection) servletUrl.openConnection(); 271 conn.connect(); 272 assertEquals(200, conn.getResponseCode()); 273 assertEquals("text/html;charset=utf-8", conn.getContentType()); 274 275 // JSPs should default to text/html with utf8 276 // JSPs do not work from unit tests 277 // servletUrl = new URL(baseUrl, "/testjsp.jsp"); 278 // conn = (HttpURLConnection)servletUrl.openConnection(); 279 // conn.connect(); 280 // assertEquals(200, conn.getResponseCode()); 281 // assertEquals("text/html; charset=utf-8", conn.getContentType()); 282 } 283 284 @Test 285 public void testNegotiatesEncodingGzip() throws IOException { 286 final InputStream stream = ClassLoader.getSystemResourceAsStream("webapps/static/test.css"); 287 assertNotNull(stream); 288 final String sourceContent = readFully(stream); 289 290 try (final CloseableHttpClient client = HttpClients.createMinimal()) { 291 final HttpGet request = new HttpGet(new URL(baseUrl, "/static/test.css").toString()); 292 293 request.setHeader(HttpHeaders.ACCEPT_ENCODING, null); 294 final long unencodedContentLength; 295 try (final CloseableHttpResponse response = client.execute(request)) { 296 final HttpEntity entity = response.getEntity(); 297 assertNotNull(entity); 298 assertNull(entity.getContentEncoding()); 299 unencodedContentLength = entity.getContentLength(); 300 MatcherAssert.assertThat(unencodedContentLength, greaterThan(0L)); 301 final String unencodedEntityBody = readFully(entity.getContent()); 302 assertEquals(sourceContent, unencodedEntityBody); 303 } 304 305 request.setHeader(HttpHeaders.ACCEPT_ENCODING, "gzip"); 306 final long encodedContentLength; 307 try (final CloseableHttpResponse response = client.execute(request)) { 308 final HttpEntity entity = response.getEntity(); 309 assertNotNull(entity); 310 assertNotNull(entity.getContentEncoding()); 311 assertEquals("gzip", entity.getContentEncoding().getValue()); 312 encodedContentLength = entity.getContentLength(); 313 MatcherAssert.assertThat(encodedContentLength, greaterThan(0L)); 314 final String encodedEntityBody = readFully(entity.getContent()); 315 // the encoding/decoding process, as implemented in this specific combination of dependency 316 // versions, does not perfectly preserve trailing whitespace. thus, `trim()`. 317 assertEquals(sourceContent.trim(), encodedEntityBody.trim()); 318 } 319 MatcherAssert.assertThat(unencodedContentLength, greaterThan(encodedContentLength)); 320 } 321 } 322 323 private static String readFully(final InputStream input) throws IOException { 324 // TODO: when the time comes, delete me and replace with a JDK11 IO helper API. 325 try (final BufferedReader reader = 326 new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { 327 final StringBuilder sb = new StringBuilder(); 328 final CharBuffer buffer = CharBuffer.allocate(1024 * 2); 329 while (reader.read(buffer) > 0) { 330 sb.append(buffer); 331 buffer.clear(); 332 } 333 return sb.toString(); 334 } finally { 335 input.close(); 336 } 337 } 338 339 /** 340 * Dummy filter that mimics as an authentication filter. Obtains user identity from the request 341 * parameter user.name. Wraps around the request so that request.getRemoteUser() returns the user 342 * identity. 343 */ 344 public static class DummyServletFilter implements Filter { 345 @Override 346 public void destroy() { 347 } 348 349 @Override 350 public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) 351 throws IOException, ServletException { 352 final String userName = request.getParameter("user.name"); 353 ServletRequest requestModified = new HttpServletRequestWrapper((HttpServletRequest) request) { 354 @Override 355 public String getRemoteUser() { 356 return userName; 357 } 358 }; 359 filterChain.doFilter(requestModified, response); 360 } 361 362 @Override 363 public void init(FilterConfig arg0) { 364 } 365 } 366 367 /** 368 * FilterInitializer that initialized the DummyFilter. 369 */ 370 public static class DummyFilterInitializer extends FilterInitializer { 371 public DummyFilterInitializer() { 372 } 373 374 @Override 375 public void initFilter(FilterContainer container, Configuration conf) { 376 container.addFilter("DummyFilter", DummyServletFilter.class.getName(), null); 377 } 378 } 379 380 /** 381 * Access a URL and get the corresponding return Http status code. The URL will be accessed as the 382 * passed user, by sending user.name request parameter. 383 * @param urlstring The url to access 384 * @param userName The user to perform access as 385 * @return The HTTP response code 386 * @throws IOException if there is a problem communicating with the server 387 */ 388 private static int getHttpStatusCode(String urlstring, String userName) throws IOException { 389 URL url = new URL(urlstring + "?user.name=" + userName); 390 System.out.println("Accessing " + url + " as user " + userName); 391 HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 392 connection.connect(); 393 return connection.getResponseCode(); 394 } 395 396 /** 397 * Custom user->group mapping service. 398 */ 399 public static class MyGroupsProvider extends ShellBasedUnixGroupsMapping { 400 static Map<String, List<String>> mapping = new HashMap<>(); 401 402 static void clearMapping() { 403 mapping.clear(); 404 } 405 406 @Override 407 public List<String> getGroups(String user) { 408 return mapping.get(user); 409 } 410 } 411 412 /** 413 * Verify the access for /logs, /stacks, /conf, /logLevel and /metrics servlets, when 414 * authentication filters are set, but authorization is not enabled. 415 */ 416 @Test 417 @Disabled 418 public void testDisabledAuthorizationOfDefaultServlets() throws Exception { 419 Configuration conf = new Configuration(); 420 421 // Authorization is disabled by default 422 conf.set(HttpServer.FILTER_INITIALIZERS_PROPERTY, DummyFilterInitializer.class.getName()); 423 conf.set(CommonConfigurationKeys.HADOOP_SECURITY_GROUP_MAPPING, 424 MyGroupsProvider.class.getName()); 425 Groups.getUserToGroupsMappingService(conf); 426 MyGroupsProvider.clearMapping(); 427 MyGroupsProvider.mapping.put("userA", Collections.singletonList("groupA")); 428 MyGroupsProvider.mapping.put("userB", Collections.singletonList("groupB")); 429 430 HttpServer myServer = new HttpServer.Builder().setName("test") 431 .addEndpoint(new URI("http://localhost:0")).setFindPort(true).build(); 432 myServer.setAttribute(HttpServer.CONF_CONTEXT_ATTRIBUTE, conf); 433 myServer.start(); 434 String serverURL = 435 "http://" + NetUtils.getHostPortString(myServer.getConnectorAddress(0)) + "/"; 436 for (String servlet : new String[] { "conf", "logs", "stacks", "logLevel", "metrics" }) { 437 for (String user : new String[] { "userA", "userB" }) { 438 assertEquals(HttpURLConnection.HTTP_OK, getHttpStatusCode(serverURL + servlet, user)); 439 } 440 } 441 myServer.stop(); 442 } 443 444 /** 445 * Verify the administrator access for /logs, /stacks, /conf, /logLevel and /metrics servlets. 446 */ 447 @Test 448 @Disabled 449 public void testAuthorizationOfDefaultServlets() throws Exception { 450 Configuration conf = new Configuration(); 451 conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true); 452 conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_INSTRUMENTATION_REQUIRES_ADMIN, true); 453 conf.set(HttpServer.FILTER_INITIALIZERS_PROPERTY, DummyFilterInitializer.class.getName()); 454 455 conf.set(CommonConfigurationKeys.HADOOP_SECURITY_GROUP_MAPPING, 456 MyGroupsProvider.class.getName()); 457 Groups.getUserToGroupsMappingService(conf); 458 MyGroupsProvider.clearMapping(); 459 MyGroupsProvider.mapping.put("userA", Collections.singletonList("groupA")); 460 MyGroupsProvider.mapping.put("userB", Collections.singletonList("groupB")); 461 MyGroupsProvider.mapping.put("userC", Collections.singletonList("groupC")); 462 MyGroupsProvider.mapping.put("userD", Collections.singletonList("groupD")); 463 MyGroupsProvider.mapping.put("userE", Collections.singletonList("groupE")); 464 465 HttpServer myServer = new HttpServer.Builder().setName("test") 466 .addEndpoint(new URI("http://localhost:0")).setFindPort(true).setConf(conf) 467 .setACL(new AccessControlList("userA,userB groupC,groupD")).build(); 468 myServer.setAttribute(HttpServer.CONF_CONTEXT_ATTRIBUTE, conf); 469 myServer.start(); 470 471 String serverURL = 472 "http://" + NetUtils.getHostPortString(myServer.getConnectorAddress(0)) + "/"; 473 for (String servlet : new String[] { "conf", "logs", "stacks", "logLevel", "metrics" }) { 474 for (String user : new String[] { "userA", "userB", "userC", "userD" }) { 475 assertEquals(HttpURLConnection.HTTP_OK, getHttpStatusCode(serverURL + servlet, user)); 476 } 477 assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, 478 getHttpStatusCode(serverURL + servlet, "userE")); 479 } 480 myServer.stop(); 481 } 482 483 @Test 484 public void testRequestQuoterWithNull() { 485 HttpServletRequest request = Mockito.mock(HttpServletRequest.class); 486 Mockito.doReturn(null).when(request).getParameterValues("dummy"); 487 RequestQuoter requestQuoter = new RequestQuoter(request); 488 String[] parameterValues = requestQuoter.getParameterValues("dummy"); 489 assertNull(parameterValues, 490 "It should return null " + "when there are no values for the parameter"); 491 } 492 493 @Test 494 public void testRequestQuoterWithNotNull() { 495 HttpServletRequest request = Mockito.mock(HttpServletRequest.class); 496 String[] values = new String[] { "abc", "def" }; 497 Mockito.doReturn(values).when(request).getParameterValues("dummy"); 498 RequestQuoter requestQuoter = new RequestQuoter(request); 499 String[] parameterValues = requestQuoter.getParameterValues("dummy"); 500 assertTrue(Arrays.equals(values, parameterValues), "It should return Parameter Values"); 501 } 502 503 @SuppressWarnings("unchecked") 504 private static Map<String, Object> parse(String jsonString) { 505 return (Map<String, Object>) new JSON().fromJSON(jsonString); 506 } 507 508 @Test 509 public void testJersey() throws Exception { 510 LOG.info("BEGIN testJersey()"); 511 final String js = readOutput(new URL(baseUrl, "/jersey/foo?op=bar")); 512 final Map<String, Object> m = parse(js); 513 LOG.info("m=" + m); 514 assertEquals("foo", m.get(JerseyResource.PATH)); 515 assertEquals("bar", m.get(JerseyResource.OP)); 516 LOG.info("END testJersey()"); 517 } 518 519 @Test 520 public void testHasAdministratorAccess() throws Exception { 521 Configuration conf = new Configuration(); 522 conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, false); 523 ServletContext context = Mockito.mock(ServletContext.class); 524 Mockito.when(context.getAttribute(HttpServer.CONF_CONTEXT_ATTRIBUTE)).thenReturn(conf); 525 Mockito.when(context.getAttribute(HttpServer.ADMINS_ACL)).thenReturn(null); 526 HttpServletRequest request = Mockito.mock(HttpServletRequest.class); 527 Mockito.when(request.getRemoteUser()).thenReturn(null); 528 HttpServletResponse response = Mockito.mock(HttpServletResponse.class); 529 530 // authorization OFF 531 assertTrue(HttpServer.hasAdministratorAccess(context, request, response)); 532 533 // authorization ON & user NULL 534 response = Mockito.mock(HttpServletResponse.class); 535 conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true); 536 assertFalse(HttpServer.hasAdministratorAccess(context, request, response)); 537 Mockito.verify(response).sendError(Mockito.eq(HttpServletResponse.SC_UNAUTHORIZED), 538 Mockito.anyString()); 539 540 // authorization ON & user NOT NULL & ACLs NULL 541 response = Mockito.mock(HttpServletResponse.class); 542 Mockito.when(request.getRemoteUser()).thenReturn("foo"); 543 assertTrue(HttpServer.hasAdministratorAccess(context, request, response)); 544 545 // authorization ON & user NOT NULL & ACLs NOT NULL & user not in ACLs 546 response = Mockito.mock(HttpServletResponse.class); 547 AccessControlList acls = Mockito.mock(AccessControlList.class); 548 Mockito.when(acls.isUserAllowed(Mockito.<UserGroupInformation> any())).thenReturn(false); 549 Mockito.when(context.getAttribute(HttpServer.ADMINS_ACL)).thenReturn(acls); 550 assertFalse(HttpServer.hasAdministratorAccess(context, request, response)); 551 Mockito.verify(response).sendError(Mockito.eq(HttpServletResponse.SC_FORBIDDEN), 552 Mockito.anyString()); 553 554 // authorization ON & user NOT NULL & ACLs NOT NULL & user in in ACLs 555 response = Mockito.mock(HttpServletResponse.class); 556 Mockito.when(acls.isUserAllowed(Mockito.<UserGroupInformation> any())).thenReturn(true); 557 Mockito.when(context.getAttribute(HttpServer.ADMINS_ACL)).thenReturn(acls); 558 assertTrue(HttpServer.hasAdministratorAccess(context, request, response)); 559 560 } 561 562 @Test 563 public void testRequiresAuthorizationAccess() throws Exception { 564 Configuration conf = new Configuration(); 565 ServletContext context = Mockito.mock(ServletContext.class); 566 Mockito.when(context.getAttribute(HttpServer.CONF_CONTEXT_ATTRIBUTE)).thenReturn(conf); 567 HttpServletRequest request = Mockito.mock(HttpServletRequest.class); 568 HttpServletResponse response = Mockito.mock(HttpServletResponse.class); 569 570 // requires admin access to instrumentation, FALSE by default 571 assertTrue(HttpServer.isInstrumentationAccessAllowed(context, request, response)); 572 573 // requires admin access to instrumentation, TRUE 574 conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_INSTRUMENTATION_REQUIRES_ADMIN, true); 575 conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true); 576 AccessControlList acls = Mockito.mock(AccessControlList.class); 577 Mockito.when(acls.isUserAllowed(Mockito.<UserGroupInformation> any())).thenReturn(false); 578 Mockito.when(context.getAttribute(HttpServer.ADMINS_ACL)).thenReturn(acls); 579 assertFalse(HttpServer.isInstrumentationAccessAllowed(context, request, response)); 580 } 581 582 @Test 583 public void testBindAddress() throws Exception { 584 checkBindAddress("localhost", 0, false).stop(); 585 // hang onto this one for a bit more testing 586 HttpServer myServer = checkBindAddress("localhost", 0, false); 587 HttpServer myServer2 = null; 588 try { 589 int port = myServer.getConnectorAddress(0).getPort(); 590 // it's already in use, true = expect a higher port 591 myServer2 = checkBindAddress("localhost", port, true); 592 // try to reuse the port 593 port = myServer2.getConnectorAddress(0).getPort(); 594 myServer2.stop(); 595 assertNull(myServer2.getConnectorAddress(0)); // not bound 596 myServer2.openListeners(); 597 assertEquals(port, myServer2.getConnectorAddress(0).getPort()); // expect same port 598 } finally { 599 myServer.stop(); 600 if (myServer2 != null) { 601 myServer2.stop(); 602 } 603 } 604 } 605 606 private HttpServer checkBindAddress(String host, int port, boolean findPort) throws Exception { 607 HttpServer server = createServer(host, port); 608 try { 609 // not bound, ephemeral should return requested port (0 for ephemeral) 610 ServerConnector listener = server.getServerConnectors().get(0); 611 612 assertEquals(port, listener.getPort()); 613 // We are doing this as otherwise testBindAddress fails, not sure how we were even starting 614 // server in jetty 9 without this call 615 server.start(); 616 // verify hostname is what was given 617 server.openListeners(); 618 assertEquals(host, server.getConnectorAddress(0).getHostName()); 619 620 int boundPort = server.getConnectorAddress(0).getPort(); 621 if (port == 0) { 622 assertTrue(boundPort != 0); // ephemeral should now return bound port 623 } else if (findPort) { 624 assertTrue(boundPort > port); 625 // allow a little wiggle room to prevent random test failures if 626 // some consecutive ports are already in use 627 assertTrue(boundPort - port < 8); 628 } 629 } catch (Exception e) { 630 server.stop(); 631 throw e; 632 } 633 return server; 634 } 635 636 @Test 637 public void testXFrameHeaderSameOrigin() throws Exception { 638 Configuration conf = new Configuration(); 639 conf.set("hbase.http.filter.xframeoptions.mode", "SAMEORIGIN"); 640 641 HttpServer myServer = new HttpServer.Builder().setName("test") 642 .addEndpoint(new URI("http://localhost:0")).setFindPort(true).setConf(conf).build(); 643 myServer.setAttribute(HttpServer.CONF_CONTEXT_ATTRIBUTE, conf); 644 myServer.addUnprivilegedServlet("echo", "/echo", EchoServlet.class); 645 myServer.start(); 646 647 String serverURL = "http://" + NetUtils.getHostPortString(myServer.getConnectorAddress(0)); 648 URL url = new URL(new URL(serverURL), "/echo?a=b&c=d"); 649 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 650 assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); 651 assertEquals("SAMEORIGIN", conn.getHeaderField("X-Frame-Options")); 652 myServer.stop(); 653 } 654 655 @Test 656 public void testNoCacheHeader() throws Exception { 657 URL url = new URL(baseUrl, "/echo?a=b&c=d"); 658 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 659 assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); 660 assertEquals("no-cache", conn.getHeaderField("Cache-Control")); 661 assertEquals("no-cache", conn.getHeaderField("Pragma")); 662 assertNotNull(conn.getHeaderField("Expires")); 663 assertNotNull(conn.getHeaderField("Date")); 664 assertEquals(conn.getHeaderField("Expires"), conn.getHeaderField("Date")); 665 assertEquals("DENY", conn.getHeaderField("X-Frame-Options")); 666 } 667 668 @Test 669 public void testHttpMethods() throws Exception { 670 // HTTP TRACE method should be disabled for security 671 // See https://www.owasp.org/index.php/Cross_Site_Tracing 672 URL url = new URL(baseUrl, "/echo?a=b"); 673 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 674 conn.setRequestMethod("TRACE"); 675 conn.connect(); 676 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode()); 677 } 678}