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