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&lt;:d\ne:&gt;\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,&gt;\nc&lt;: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}