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}