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.log;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertNotEquals;
023import static org.junit.jupiter.api.Assertions.assertSame;
024import static org.junit.jupiter.api.Assertions.assertTrue;
025import static org.junit.jupiter.api.Assertions.fail;
026
027import java.io.File;
028import java.io.IOException;
029import java.net.BindException;
030import java.net.SocketException;
031import java.net.URI;
032import java.security.PrivilegedExceptionAction;
033import java.util.Locale;
034import java.util.Properties;
035import java.util.regex.Matcher;
036import java.util.regex.Pattern;
037import javax.net.ssl.SSLException;
038import javax.servlet.http.HttpServletResponse;
039import org.apache.commons.io.FileUtils;
040import org.apache.hadoop.HadoopIllegalArgumentException;
041import org.apache.hadoop.conf.Configuration;
042import org.apache.hadoop.fs.CommonConfigurationKeys;
043import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
044import org.apache.hadoop.fs.FileUtil;
045import org.apache.hadoop.hbase.HBaseCommonTestingUtil;
046import org.apache.hadoop.hbase.http.HttpConfig;
047import org.apache.hadoop.hbase.http.HttpServer;
048import org.apache.hadoop.hbase.http.log.LogLevel.CLI;
049import org.apache.hadoop.hbase.http.ssl.KeyStoreTestUtil;
050import org.apache.hadoop.hbase.logging.Log4jUtils;
051import org.apache.hadoop.hbase.testclassification.MiscTests;
052import org.apache.hadoop.hbase.testclassification.SmallTests;
053import org.apache.hadoop.hdfs.DFSConfigKeys;
054import org.apache.hadoop.minikdc.MiniKdc;
055import org.apache.hadoop.net.NetUtils;
056import org.apache.hadoop.security.UserGroupInformation;
057import org.apache.hadoop.security.authorize.AccessControlList;
058import org.apache.hadoop.security.ssl.SSLFactory;
059import org.apache.hadoop.test.GenericTestUtils;
060import org.apache.hadoop.util.StringUtils;
061import org.junit.jupiter.api.AfterAll;
062import org.junit.jupiter.api.BeforeAll;
063import org.junit.jupiter.api.Tag;
064import org.junit.jupiter.api.Test;
065
066/**
067 * Test LogLevel.
068 */
069@Tag(MiscTests.TAG)
070@Tag(SmallTests.TAG)
071public class TestLogLevel {
072
073  private static String keystoresDir;
074  private static String sslConfDir;
075  private static Configuration serverConf;
076  private static Configuration clientConf;
077  private static Configuration sslConf;
078  private static final String logName = TestLogLevel.class.getName();
079  private static final String protectedPrefix = "protected";
080  private static final String protectedLogName = protectedPrefix + "." + logName;
081  private static final org.apache.logging.log4j.Logger log =
082    org.apache.logging.log4j.LogManager.getLogger(logName);
083  private final static String PRINCIPAL = "loglevel.principal";
084  private final static String KEYTAB = "loglevel.keytab";
085
086  private static MiniKdc kdc;
087
088  private static final String LOCALHOST = "localhost";
089  private static final String clientPrincipal = "client/" + LOCALHOST;
090  private static String HTTP_PRINCIPAL = "HTTP/" + LOCALHOST;
091  private static HBaseCommonTestingUtil HTU;
092  private static File keyTabFile;
093  private static final Pattern EFFECTIVE_LEVEL = Pattern.compile("Effective level:\\s*(\\S+)");
094
095  @FunctionalInterface
096  private interface ThrowingRunnable {
097    void run() throws Exception;
098  }
099
100  @FunctionalInterface
101  private interface ThrowingConsumer {
102    void accept(String url) throws Exception;
103  }
104
105  private enum Protocol {
106    C_HTTP_S_HTTP(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP),
107    C_HTTP_S_HTTPS(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTPS),
108    C_HTTPS_S_HTTP(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTP),
109    C_HTTPS_S_HTTPS(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTPS);
110
111    final String client;
112    final String server;
113
114    Protocol(String client, String server) {
115      this.client = client;
116      this.server = server;
117    }
118  }
119
120  @BeforeAll
121  public static void setUp() throws Exception {
122    serverConf = new Configuration();
123    serverConf.setStrings(LogLevel.READONLY_LOGGERS_CONF_KEY, protectedPrefix);
124    HTU = new HBaseCommonTestingUtil(serverConf);
125
126    File keystoreDir = new File(HTU.getDataTestDir("keystore").toString());
127    keystoreDir.mkdirs();
128    keyTabFile = new File(HTU.getDataTestDir("keytab").toString(), "keytabfile");
129    keyTabFile.getParentFile().mkdirs();
130    clientConf = new Configuration();
131
132    setupSSL(keystoreDir);
133
134    kdc = setupMiniKdc();
135    // Create two principles: a client and an HTTP principal
136    kdc.createPrincipal(keyTabFile, clientPrincipal, HTTP_PRINCIPAL);
137  }
138
139  /**
140   * Sets up {@link MiniKdc} for testing security. Copied from HBaseTestingUtility#setupMiniKdc().
141   */
142  static private MiniKdc setupMiniKdc() throws Exception {
143    Properties conf = MiniKdc.createConf();
144    conf.put(MiniKdc.DEBUG, true);
145    MiniKdc kdc = null;
146    File dir = null;
147    // There is time lag between selecting a port and trying to bind with it. It's possible that
148    // another service captures the port in between which'll result in BindException.
149    boolean bindException;
150    int numTries = 0;
151    do {
152      try {
153        bindException = false;
154        dir = new File(HTU.getDataTestDir("kdc").toUri().getPath());
155        kdc = new MiniKdc(conf, dir);
156        kdc.start();
157      } catch (Exception e) {
158        // Catch Exception, not BindException/KrbException: Kerby wraps the bind failure in a
159        // KrbException whose shape varies by version (see isBindException), so we recognise the
160        // port conflict via that predicate rather than a type. We also avoid importing kerby types
161        // here (see HBASE-29117).
162        FileUtils.deleteDirectory(dir); // clean directory regardless of failure type
163        if (!isBindException(e)) {
164          throw e; // not a port conflict, do not mask the real failure behind a retry
165        }
166        numTries++;
167        if (numTries == 3) {
168          log.error("Failed setting up MiniKDC. Tried " + numTries + " times.");
169          throw e;
170        }
171        log.error("Bind conflict encountered when setting up MiniKdc, retrying (attempt " + numTries
172          + ").");
173        bindException = true;
174      }
175    } while (bindException);
176    return kdc;
177  }
178
179  /**
180   * The Kerby-backed {@link MiniKdc} wraps a failure to bind the KDC port in a
181   * {@code org.apache.kerby...KrbException} rather than surfacing a {@link BindException} directly,
182   * so we inspect the whole cause chain plus the message to recognise a port conflict. Both checks
183   * are load-bearing across Kerby versions: kerby 1.x preserves the original {@link BindException}
184   * as the cause (caught by the cause-chain check) while kerby 2.x drops the cause and only appends
185   * the bind message (caught by the message check). The message match is lower-cased since the
186   * wording is JDK/OS specific.
187   */
188  static boolean isBindException(Throwable t) {
189    for (Throwable cause = t; cause != null; cause = cause.getCause()) {
190      if (cause instanceof BindException) {
191        return true;
192      }
193      String msg = cause.getMessage();
194      if (msg != null && msg.toLowerCase(Locale.ROOT).contains("address already in use")) {
195        return true;
196      }
197    }
198    return false;
199  }
200
201  /**
202   * Deterministic regression test for the retry predicate. A port conflict raised by the
203   * Kerby-backed {@link MiniKdc} must be recognised as a bind failure whether the
204   * {@link BindException} is preserved in the cause chain or only reflected in the message;
205   * unrelated failures must not be. We model the Kerby wrapper with a plain {@link Exception}
206   * rather than constructing a real Kerby exception, so the test does not import kerby types (see
207   * HBASE-29117).
208   */
209  @Test
210  public void testIsBindExceptionRecognizesKerbyWrappedBindFailure() {
211    assertTrue(
212      isBindException(new Exception("Failed to start DefaultKrbServer",
213        new BindException("Address already in use"))),
214      "a bind failure preserved in the cause chain should be recognised");
215    assertTrue(
216      isBindException(new Exception("Failed to start DefaultKrbServer. Address already in use")),
217      "a wrapper whose message reports the bind conflict should be recognised");
218    assertFalse(isBindException(new RuntimeException("boom")),
219      "an unrelated failure must not be treated as a retryable bind conflict");
220  }
221
222  static private void setupSSL(File base) throws Exception {
223    clientConf.set(DFSConfigKeys.DFS_HTTP_POLICY_KEY, HttpConfig.Policy.HTTPS_ONLY.name());
224    clientConf.set(DFSConfigKeys.DFS_NAMENODE_HTTPS_ADDRESS_KEY, "localhost:0");
225    clientConf.set(DFSConfigKeys.DFS_DATANODE_HTTPS_ADDRESS_KEY, "localhost:0");
226
227    keystoresDir = base.getAbsolutePath();
228    sslConfDir = KeyStoreTestUtil.getClasspathDir(TestLogLevel.class);
229    KeyStoreTestUtil.setupSSLConfig(keystoresDir, sslConfDir, serverConf, false);
230
231    sslConf = getSslConfig(serverConf);
232  }
233
234  /**
235   * Get the SSL configuration. This method is copied from KeyStoreTestUtil#getSslConfig() in
236   * Hadoop.
237   * @return {@link Configuration} instance with ssl configs loaded.
238   * @param conf to pull client/server SSL settings filename from
239   */
240  private static Configuration getSslConfig(Configuration conf) {
241    Configuration sslConf = new Configuration(false);
242    String sslServerConfFile = conf.get(SSLFactory.SSL_SERVER_CONF_KEY);
243    String sslClientConfFile = conf.get(SSLFactory.SSL_CLIENT_CONF_KEY);
244    sslConf.addResource(sslServerConfFile);
245    sslConf.addResource(sslClientConfFile);
246    sslConf.set(SSLFactory.SSL_SERVER_CONF_KEY, sslServerConfFile);
247    sslConf.set(SSLFactory.SSL_CLIENT_CONF_KEY, sslClientConfFile);
248    return sslConf;
249  }
250
251  @AfterAll
252  public static void tearDown() {
253    if (kdc != null) {
254      kdc.stop();
255    }
256
257    FileUtil.fullyDelete(new File(HTU.getDataTestDir().toString()));
258  }
259
260  /**
261   * Test client command line options. Does not validate server behavior.
262   * @throws Exception if commands return unexpected results.
263   */
264  @Test
265  public void testCommandOptions() throws Exception {
266    final String className = this.getClass().getName();
267
268    assertFalse(validateCommand(new String[] { "-foo" }));
269    // fail due to insufficient number of arguments
270    assertFalse(validateCommand(new String[] {}));
271    assertFalse(validateCommand(new String[] { "-getlevel" }));
272    assertFalse(validateCommand(new String[] { "-setlevel" }));
273    assertFalse(validateCommand(new String[] { "-getlevel", "foo.bar:8080" }));
274
275    // valid command arguments
276    assertTrue(validateCommand(new String[] { "-getlevel", "foo.bar:8080", className }));
277    assertTrue(validateCommand(new String[] { "-setlevel", "foo.bar:8080", className, "DEBUG" }));
278    assertTrue(validateCommand(new String[] { "-getlevel", "foo.bar:8080", className }));
279    assertTrue(validateCommand(new String[] { "-setlevel", "foo.bar:8080", className, "DEBUG" }));
280
281    // fail due to the extra argument
282    assertFalse(validateCommand(new String[] { "-getlevel", "foo.bar:8080", className, "blah" }));
283    assertFalse(
284      validateCommand(new String[] { "-setlevel", "foo.bar:8080", className, "DEBUG", "blah" }));
285    assertFalse(validateCommand(new String[] { "-getlevel", "foo.bar:8080", className, "-setlevel",
286      "foo.bar:8080", className }));
287  }
288
289  /**
290   * Check to see if a command can be accepted.
291   * @param args a String array of arguments
292   * @return true if the command can be accepted, false if not.
293   */
294  private boolean validateCommand(String[] args) {
295    CLI cli = new CLI(clientConf);
296    try {
297      cli.parseArguments(args);
298    } catch (HadoopIllegalArgumentException e) {
299      return false;
300    } catch (Exception e) {
301      // this is used to verify the command arguments only.
302      // no HadoopIllegalArgumentException = the arguments are good.
303      return true;
304    }
305    return true;
306  }
307
308  /**
309   * Creates and starts a Jetty server binding at an ephemeral port to run LogLevel servlet.
310   * @param protocol "http" or "https"
311   * @param isSpnego true if SPNEGO is enabled
312   * @return a created HttpServer object
313   * @throws Exception if unable to create or start a Jetty server
314   */
315  private HttpServer createServer(String protocol, boolean isSpnego) throws Exception {
316    // Changed to "" as ".." moves it a steps back in path because the path is relative to the
317    // current working directory. throws "java.lang.IllegalArgumentException: Base Resource is not
318    // valid: hbase-http/target/test-classes/static" as it is not able to find the static folder.
319    HttpServer.Builder builder = new HttpServer.Builder().setName("")
320      .addEndpoint(new URI(protocol + "://localhost:0")).setFindPort(true).setConf(serverConf);
321    if (isSpnego) {
322      // Set up server Kerberos credentials.
323      // Since the server may fall back to simple authentication,
324      // use ACL to make sure the connection is Kerberos/SPNEGO authenticated.
325      builder.setSecurityEnabled(true).setUsernameConfKey(PRINCIPAL).setKeytabConfKey(KEYTAB)
326        .setACL(new AccessControlList("client"));
327    }
328
329    // if using HTTPS, configure keystore/truststore properties.
330    if (protocol.equals(LogLevel.PROTOCOL_HTTPS)) {
331      builder = builder.keyPassword(sslConf.get("ssl.server.keystore.keypassword"))
332        .keyStore(sslConf.get("ssl.server.keystore.location"),
333          sslConf.get("ssl.server.keystore.password"),
334          sslConf.get("ssl.server.keystore.type", "jks"))
335        .trustStore(sslConf.get("ssl.server.truststore.location"),
336          sslConf.get("ssl.server.truststore.password"),
337          sslConf.get("ssl.server.truststore.type", "jks"));
338    }
339
340    HttpServer server = builder.build();
341    server.start();
342    return server;
343  }
344
345  private void testGetLogLevel(Protocol protocol, boolean isSpnego, String loggerName,
346    String expectedLevel) throws Exception {
347    withLogLevelServer(protocol, isSpnego, (authority) -> {
348      final String level = getLevel(protocol.client, authority, loggerName);
349      assertEquals(expectedLevel, level, "Log level not equal to expected: ");
350    });
351  }
352
353  private void testSetLogLevel(Protocol protocol, boolean isSpnego, String loggerName,
354    String newLevel) throws Exception {
355    String oldLevel = Log4jUtils.getEffectiveLevel(loggerName);
356    assertNotEquals(newLevel, oldLevel, "New level is same as old level: ");
357
358    try {
359      withLogLevelServer(protocol, isSpnego, (authority) -> {
360        setLevel(protocol.client, authority, loggerName, newLevel);
361      });
362    } finally {
363      // restore log level
364      Log4jUtils.setLogLevel(loggerName, oldLevel);
365    }
366  }
367
368  /**
369   * Starts a LogLevel server and executes a client action against it.
370   * @param protocol protocol configuration for client and server
371   * @param isSpnego whether SPNEGO authentication is enabled
372   * @param consumer client action executed with the server authority (host:port)
373   * @throws Exception if server setup or client execution fails
374   */
375  private void withLogLevelServer(Protocol protocol, final boolean isSpnego,
376    ThrowingConsumer consumer) throws Exception {
377    if (!LogLevel.isValidProtocol(protocol.server)) {
378      throw new Exception("Invalid server protocol " + protocol.server);
379    }
380    if (!LogLevel.isValidProtocol(protocol.client)) {
381      throw new Exception("Invalid client protocol " + protocol.client);
382    }
383
384    // configs needed for SPNEGO at server side
385    if (isSpnego) {
386      serverConf.set(PRINCIPAL, HTTP_PRINCIPAL);
387      serverConf.set(KEYTAB, keyTabFile.getAbsolutePath());
388      serverConf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos");
389      serverConf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true);
390      UserGroupInformation.setConfiguration(serverConf);
391    } else {
392      serverConf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "simple");
393      serverConf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, false);
394      UserGroupInformation.setConfiguration(serverConf);
395    }
396
397    final HttpServer server = createServer(protocol.server, isSpnego);
398    final String authority = NetUtils.getHostPortString(server.getConnectorAddress(0));
399    String keytabFilePath = keyTabFile.getAbsolutePath();
400
401    UserGroupInformation clientUGI =
402      UserGroupInformation.loginUserFromKeytabAndReturnUGI(clientPrincipal, keytabFilePath);
403    try {
404      clientUGI.doAs((PrivilegedExceptionAction<Void>) () -> {
405        consumer.accept(authority);
406        return null;
407      });
408    } finally {
409      clientUGI.logoutUserFromKeytab();
410      server.stop();
411    }
412  }
413
414  /**
415   * Run LogLevel command line to start a client to get log level of this test class.
416   * @param protocol  specify either http or https
417   * @param authority daemon's web UI address
418   * @throws Exception if unable to connect
419   */
420  private String getLevel(String protocol, String authority, String logName) throws Exception {
421    String[] getLevelArgs = { "-getlevel", authority, logName, "-protocol", protocol };
422    CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf);
423    cli.parseArguments(getLevelArgs);
424    final String response = cli.fetchGetLevelResponse();
425    return extractEffectiveLevel(response);
426  }
427
428  /**
429   * Run LogLevel command line to start a client to set log level of this test class to debug.
430   * @param protocol  specify either http or https
431   * @param authority daemon's web UI address
432   * @throws Exception if unable to run or log level does not change as expected
433   */
434  private void setLevel(String protocol, String authority, String logName, String newLevel)
435    throws Exception {
436    String[] setLevelArgs = { "-setlevel", authority, logName, newLevel, "-protocol", protocol };
437    CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf);
438    cli.parseArguments(setLevelArgs);
439    final String response = cli.fetchSetLevelResponse();
440    final String responseLevel = extractEffectiveLevel(response);
441    final String currentLevel = Log4jUtils.getEffectiveLevel(logName);
442    assertEquals(newLevel, currentLevel, "new level not equal to expected: ");
443    assertSame(newLevel, responseLevel, "new level not equal to response level: ");
444  }
445
446  /**
447   * Extract effective log level from server response.
448   * @param response server body response string
449   * @return the effective log level
450   */
451  private String extractEffectiveLevel(String response) {
452    Matcher m = EFFECTIVE_LEVEL.matcher(response);
453    if (m.find()) {
454      return org.apache.logging.log4j.Level.toLevel(m.group(1)).name();
455    }
456
457    fail("Cannot find effective log level from response: " + response);
458    return null;
459  }
460
461  @Test
462  public void testSettingProtectedLogLevel() throws Exception {
463    try {
464      testSetLogLevel(Protocol.C_HTTP_S_HTTP, true, protectedLogName, "DEBUG");
465      fail("Expected IO exception due to protected logger");
466    } catch (IOException e) {
467      assertTrue(e.getMessage().contains("" + HttpServletResponse.SC_PRECONDITION_FAILED));
468      assertTrue(e.getMessage().contains(
469        "Modification of logger " + protectedLogName + " is disallowed in configuration."));
470    }
471  }
472
473  @Test
474  public void testGetDebugLogLevel() throws Exception {
475    Log4jUtils.setLogLevel(logName, "DEBUG");
476    testGetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "DEBUG");
477  }
478
479  @Test
480  public void testGetInfoLogLevel() throws Exception {
481    Log4jUtils.setLogLevel(logName, "INFO");
482    testGetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "INFO");
483  }
484
485  /**
486   * Test setting log level to "Info".
487   * @throws Exception if client can't set log level to INFO.
488   */
489  @Test
490  public void testSetInfoLogLevel() throws Exception {
491    Log4jUtils.setLogLevel(logName, "DEBUG");
492    testSetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "INFO");
493  }
494
495  /**
496   * Test setting log level to "Error".
497   * @throws Exception if client can't set log level to ERROR.
498   */
499  @Test
500  public void testSetErrorLogLevel() throws Exception {
501    Log4jUtils.setLogLevel(logName, "DEBUG");
502    testSetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "ERROR");
503  }
504
505  /**
506   * Server runs HTTP, no SPNEGO.
507   * @throws Exception if http client can't access http server, or http client can access https
508   *                   server.
509   */
510  @Test
511  public void testLogLevelByHttp() throws Exception {
512    Log4jUtils.setLogLevel(logName, "DEBUG");
513    testGetLogLevel(Protocol.C_HTTP_S_HTTP, false, logName, "DEBUG");
514    try {
515      testGetLogLevel(Protocol.C_HTTPS_S_HTTP, false, logName, "DEBUG");
516      fail("An HTTPS Client should not have succeeded in connecting to a HTTP server");
517    } catch (SSLException e) {
518      exceptionShouldContains("Unrecognized SSL message", e);
519    }
520  }
521
522  /**
523   * Server runs HTTP + SPNEGO.
524   * @throws Exception if http client can't access http server, or http client can access https
525   *                   server.
526   */
527  @Test
528  public void testLogLevelByHttpWithSpnego() throws Exception {
529    Log4jUtils.setLogLevel(logName, "DEBUG");
530    testGetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "DEBUG");
531    try {
532      testGetLogLevel(Protocol.C_HTTPS_S_HTTP, true, logName, "DEBUG");
533      fail("An HTTPS Client should not have succeeded in connecting to a HTTP server");
534    } catch (SSLException e) {
535      exceptionShouldContains("Unrecognized SSL message", e);
536    }
537  }
538
539  /**
540   * Server runs HTTPS, no SPNEGO.
541   * @throws Exception if https client can't access https server, or https client can access http
542   *                   server.
543   */
544  @Test
545  public void testLogLevelByHttps() throws Exception {
546    Log4jUtils.setLogLevel(logName, "DEBUG");
547    testGetLogLevel(Protocol.C_HTTPS_S_HTTPS, false, logName, "DEBUG");
548    try {
549      testGetLogLevel(Protocol.C_HTTP_S_HTTPS, false, logName, "DEBUG");
550      fail("An HTTP Client should not have succeeded in connecting to a HTTPS server");
551    } catch (SocketException e) {
552      exceptionShouldContains("Unexpected end of file from server", e);
553    }
554  }
555
556  /**
557   * Server runs HTTPS + SPNEGO.
558   * @throws Exception if https client can't access https server, or https client can access http
559   *                   server.
560   */
561  @Test
562  public void testLogLevelByHttpsWithSpnego() throws Exception {
563    Log4jUtils.setLogLevel(logName, "DEBUG");
564    testGetLogLevel(Protocol.C_HTTPS_S_HTTPS, true, logName, "DEBUG");
565    try {
566      testGetLogLevel(Protocol.C_HTTP_S_HTTPS, true, logName, "DEBUG");
567      fail("An HTTP Client should not have succeeded in connecting to a HTTPS server");
568    } catch (SocketException e) {
569      exceptionShouldContains("Unexpected end of file from server", e);
570    }
571  }
572
573  /**
574   * Test getting log level in readonly mode.
575   * @throws Exception if a client can't get log level.
576   */
577  @Test
578  public void testGetLogLevelAllowedInReadonlyMode() throws Exception {
579    withMasterUIReadonly(() -> {
580      Log4jUtils.setLogLevel(logName, "DEBUG");
581      testGetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "DEBUG");
582    });
583  }
584
585  /**
586   * Test setting log level in readonly mode.
587   * @throws Exception if a client can set log level.
588   */
589  @Test
590  public void testSetLogLevelDisallowedInReadonlyMode() throws Exception {
591    withMasterUIReadonly(() -> {
592      Log4jUtils.setLogLevel(logName, "DEBUG");
593      try {
594        testSetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "INFO");
595        fail("Setting log level should be disallowed in readonly mode.");
596      } catch (IOException e) {
597        exceptionShouldContains("Modification of HBase via the UI is disallowed in configuration.",
598          e);
599      }
600    });
601  }
602
603  private void withMasterUIReadonly(ThrowingRunnable runnable) throws Exception {
604    boolean prev = serverConf.getBoolean(LogLevel.MASTER_UI_READONLY_CONF_KEY, false);
605    serverConf.setBoolean(LogLevel.MASTER_UI_READONLY_CONF_KEY, true);
606    try {
607      runnable.run();
608    } finally {
609      serverConf.setBoolean(LogLevel.MASTER_UI_READONLY_CONF_KEY, prev);
610    }
611  }
612
613  /**
614   * Assert that a throwable or one of its causes should contain the substr in its message. Ideally
615   * we should use {@link GenericTestUtils#assertExceptionContains(String, Throwable)} util method
616   * which asserts t.toString() contains the substr. As the original throwable may have been wrapped
617   * in Hadoop3 because of HADOOP-12897, it's required to check all the wrapped causes. After stop
618   * supporting Hadoop2, this method can be removed and assertion in tests can use t.getCause()
619   * directly, similar to HADOOP-15280.
620   */
621  private static void exceptionShouldContains(String substr, Throwable throwable) {
622    Throwable t = throwable;
623    while (t != null) {
624      String msg = t.toString();
625      if (msg != null && msg.toLowerCase().contains(substr.toLowerCase())) {
626        return;
627      }
628      t = t.getCause();
629    }
630    throw new AssertionError("Expected to find '" + substr + "' but got unexpected exception:"
631      + StringUtils.stringifyException(throwable), throwable);
632  }
633}