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.Assert.assertEquals; 021import static org.junit.Assert.assertFalse; 022import static org.junit.Assert.assertNotEquals; 023import static org.junit.Assert.assertTrue; 024import static org.junit.Assert.fail; 025import java.io.File; 026import java.net.BindException; 027import java.net.SocketException; 028import java.net.URI; 029import java.security.PrivilegedExceptionAction; 030import java.util.Properties; 031import javax.net.ssl.SSLException; 032import org.apache.commons.io.FileUtils; 033import org.apache.hadoop.HadoopIllegalArgumentException; 034import org.apache.hadoop.conf.Configuration; 035import org.apache.hadoop.fs.CommonConfigurationKeys; 036import org.apache.hadoop.fs.CommonConfigurationKeysPublic; 037import org.apache.hadoop.fs.FileUtil; 038import org.apache.hadoop.hbase.HBaseClassTestRule; 039import org.apache.hadoop.hbase.HBaseCommonTestingUtility; 040import org.apache.hadoop.hbase.http.HttpConfig; 041import org.apache.hadoop.hbase.http.HttpServer; 042import org.apache.hadoop.hbase.http.log.LogLevel.CLI; 043import org.apache.hadoop.hbase.http.ssl.KeyStoreTestUtil; 044import org.apache.hadoop.hbase.testclassification.MiscTests; 045import org.apache.hadoop.hbase.testclassification.SmallTests; 046import org.apache.hadoop.hdfs.DFSConfigKeys; 047import org.apache.hadoop.minikdc.MiniKdc; 048import org.apache.hadoop.net.NetUtils; 049import org.apache.hadoop.security.UserGroupInformation; 050import org.apache.hadoop.security.authorize.AccessControlList; 051import org.apache.hadoop.security.ssl.SSLFactory; 052import org.apache.hadoop.test.GenericTestUtils; 053import org.apache.hadoop.util.StringUtils; 054import org.apache.log4j.Level; 055import org.apache.log4j.LogManager; 056import org.apache.log4j.Logger; 057import org.junit.AfterClass; 058import org.junit.BeforeClass; 059import org.junit.ClassRule; 060import org.junit.Test; 061import org.junit.experimental.categories.Category; 062 063/** 064 * Test LogLevel. 065 */ 066@Category({MiscTests.class, SmallTests.class}) 067public class TestLogLevel { 068 @ClassRule 069 public static final HBaseClassTestRule CLASS_RULE = 070 HBaseClassTestRule.forClass(TestLogLevel.class); 071 072 private static String keystoresDir; 073 private static String sslConfDir; 074 private static Configuration serverConf; 075 private static Configuration clientConf; 076 private static Configuration sslConf; 077 private static final String logName = TestLogLevel.class.getName(); 078 private static final Logger log = LogManager.getLogger(logName); 079 private final static String PRINCIPAL = "loglevel.principal"; 080 private final static String KEYTAB = "loglevel.keytab"; 081 082 private static MiniKdc kdc; 083 084 private static final String LOCALHOST = "localhost"; 085 private static final String clientPrincipal = "client/" + LOCALHOST; 086 private static String HTTP_PRINCIPAL = "HTTP/" + LOCALHOST; 087 private static HBaseCommonTestingUtility HTU; 088 private static File keyTabFile; 089 090 @BeforeClass 091 public static void setUp() throws Exception { 092 serverConf = new Configuration(); 093 HTU = new HBaseCommonTestingUtility(serverConf); 094 095 File keystoreDir = new File(HTU.getDataTestDir("keystore").toString()); 096 keystoreDir.mkdirs(); 097 keyTabFile = new File(HTU.getDataTestDir("keytab").toString(), "keytabfile"); 098 keyTabFile.getParentFile().mkdirs(); 099 clientConf = new Configuration(); 100 101 setupSSL(keystoreDir); 102 103 kdc = setupMiniKdc(); 104 // Create two principles: a client and an HTTP principal 105 kdc.createPrincipal(keyTabFile, clientPrincipal, HTTP_PRINCIPAL); 106 } 107 108 /** 109 * Sets up {@link MiniKdc} for testing security. 110 * Copied from HBaseTestingUtility#setupMiniKdc(). 111 */ 112 static private MiniKdc setupMiniKdc() throws Exception { 113 Properties conf = MiniKdc.createConf(); 114 conf.put(MiniKdc.DEBUG, true); 115 MiniKdc kdc = null; 116 File dir = null; 117 // There is time lag between selecting a port and trying to bind with it. It's possible that 118 // another service captures the port in between which'll result in BindException. 119 boolean bindException; 120 int numTries = 0; 121 do { 122 try { 123 bindException = false; 124 dir = new File(HTU.getDataTestDir("kdc").toUri().getPath()); 125 kdc = new MiniKdc(conf, dir); 126 kdc.start(); 127 } catch (BindException e) { 128 FileUtils.deleteDirectory(dir); // clean directory 129 numTries++; 130 if (numTries == 3) { 131 log.error("Failed setting up MiniKDC. Tried " + numTries + " times."); 132 throw e; 133 } 134 log.error("BindException encountered when setting up MiniKdc. Trying again."); 135 bindException = true; 136 } 137 } while (bindException); 138 return kdc; 139 } 140 141 static private void setupSSL(File base) throws Exception { 142 clientConf.set(DFSConfigKeys.DFS_HTTP_POLICY_KEY, HttpConfig.Policy.HTTPS_ONLY.name()); 143 clientConf.set(DFSConfigKeys.DFS_NAMENODE_HTTPS_ADDRESS_KEY, "localhost:0"); 144 clientConf.set(DFSConfigKeys.DFS_DATANODE_HTTPS_ADDRESS_KEY, "localhost:0"); 145 146 keystoresDir = base.getAbsolutePath(); 147 sslConfDir = KeyStoreTestUtil.getClasspathDir(TestLogLevel.class); 148 KeyStoreTestUtil.setupSSLConfig(keystoresDir, sslConfDir, serverConf, false); 149 150 sslConf = getSslConfig(serverConf); 151 } 152 153 /** 154 * Get the SSL configuration. 155 * This method is copied from KeyStoreTestUtil#getSslConfig() in Hadoop. 156 * @return {@link Configuration} instance with ssl configs loaded. 157 * @param conf to pull client/server SSL settings filename from 158 */ 159 private static Configuration getSslConfig(Configuration conf){ 160 Configuration sslConf = new Configuration(false); 161 String sslServerConfFile = conf.get(SSLFactory.SSL_SERVER_CONF_KEY); 162 String sslClientConfFile = conf.get(SSLFactory.SSL_CLIENT_CONF_KEY); 163 sslConf.addResource(sslServerConfFile); 164 sslConf.addResource(sslClientConfFile); 165 sslConf.set(SSLFactory.SSL_SERVER_CONF_KEY, sslServerConfFile); 166 sslConf.set(SSLFactory.SSL_CLIENT_CONF_KEY, sslClientConfFile); 167 return sslConf; 168 } 169 170 @AfterClass 171 public static void tearDown() { 172 if (kdc != null) { 173 kdc.stop(); 174 } 175 176 FileUtil.fullyDelete(new File(HTU.getDataTestDir().toString())); 177 } 178 179 /** 180 * Test client command line options. Does not validate server behavior. 181 * @throws Exception if commands return unexpected results. 182 */ 183 @Test 184 public void testCommandOptions() throws Exception { 185 final String className = this.getClass().getName(); 186 187 assertFalse(validateCommand(new String[] {"-foo" })); 188 // fail due to insufficient number of arguments 189 assertFalse(validateCommand(new String[] {})); 190 assertFalse(validateCommand(new String[] {"-getlevel" })); 191 assertFalse(validateCommand(new String[] {"-setlevel" })); 192 assertFalse(validateCommand(new String[] {"-getlevel", "foo.bar:8080" })); 193 194 // valid command arguments 195 assertTrue(validateCommand( 196 new String[] {"-getlevel", "foo.bar:8080", className })); 197 assertTrue(validateCommand( 198 new String[] {"-setlevel", "foo.bar:8080", className, "DEBUG" })); 199 assertTrue(validateCommand( 200 new String[] {"-getlevel", "foo.bar:8080", className })); 201 assertTrue(validateCommand( 202 new String[] {"-setlevel", "foo.bar:8080", className, "DEBUG" })); 203 204 // fail due to the extra argument 205 assertFalse(validateCommand( 206 new String[] {"-getlevel", "foo.bar:8080", className, "blah" })); 207 assertFalse(validateCommand( 208 new String[] {"-setlevel", "foo.bar:8080", className, "DEBUG", "blah" })); 209 assertFalse(validateCommand( 210 new String[] {"-getlevel", "foo.bar:8080", className, "-setlevel", "foo.bar:8080", 211 className })); 212 } 213 214 /** 215 * Check to see if a command can be accepted. 216 * 217 * @param args a String array of arguments 218 * @return true if the command can be accepted, false if not. 219 */ 220 private boolean validateCommand(String[] args) { 221 CLI cli = new CLI(clientConf); 222 try { 223 cli.parseArguments(args); 224 } catch (HadoopIllegalArgumentException e) { 225 return false; 226 } catch (Exception e) { 227 // this is used to verify the command arguments only. 228 // no HadoopIllegalArgumentException = the arguments are good. 229 return true; 230 } 231 return true; 232 } 233 234 /** 235 * Creates and starts a Jetty server binding at an ephemeral port to run 236 * LogLevel servlet. 237 * @param protocol "http" or "https" 238 * @param isSpnego true if SPNEGO is enabled 239 * @return a created HttpServer object 240 * @throws Exception if unable to create or start a Jetty server 241 */ 242 private HttpServer createServer(String protocol, boolean isSpnego) 243 throws Exception { 244 HttpServer.Builder builder = new HttpServer.Builder() 245 .setName("..") 246 .addEndpoint(new URI(protocol + "://localhost:0")) 247 .setFindPort(true) 248 .setConf(serverConf); 249 if (isSpnego) { 250 // Set up server Kerberos credentials. 251 // Since the server may fall back to simple authentication, 252 // use ACL to make sure the connection is Kerberos/SPNEGO authenticated. 253 builder.setSecurityEnabled(true) 254 .setUsernameConfKey(PRINCIPAL) 255 .setKeytabConfKey(KEYTAB) 256 .setACL(new AccessControlList("client")); 257 } 258 259 // if using HTTPS, configure keystore/truststore properties. 260 if (protocol.equals(LogLevel.PROTOCOL_HTTPS)) { 261 builder = builder. 262 keyPassword(sslConf.get("ssl.server.keystore.keypassword")) 263 .keyStore(sslConf.get("ssl.server.keystore.location"), 264 sslConf.get("ssl.server.keystore.password"), 265 sslConf.get("ssl.server.keystore.type", "jks")) 266 .trustStore(sslConf.get("ssl.server.truststore.location"), 267 sslConf.get("ssl.server.truststore.password"), 268 sslConf.get("ssl.server.truststore.type", "jks")); 269 } 270 271 HttpServer server = builder.build(); 272 server.start(); 273 return server; 274 } 275 276 private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol, 277 final boolean isSpnego) 278 throws Exception { 279 testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego, Level.DEBUG.toString()); 280 } 281 282 /** 283 * Run both client and server using the given protocol. 284 * 285 * @param bindProtocol specify either http or https for server 286 * @param connectProtocol specify either http or https for client 287 * @param isSpnego true if SPNEGO is enabled 288 * @throws Exception if client can't accesss server. 289 */ 290 private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol, 291 final boolean isSpnego, final String newLevel) 292 throws Exception { 293 if (!LogLevel.isValidProtocol(bindProtocol)) { 294 throw new Exception("Invalid server protocol " + bindProtocol); 295 } 296 if (!LogLevel.isValidProtocol(connectProtocol)) { 297 throw new Exception("Invalid client protocol " + connectProtocol); 298 } 299 Level oldLevel = log.getEffectiveLevel(); 300 assertNotEquals("Get default Log Level which shouldn't be ERROR.", 301 Level.ERROR, oldLevel); 302 303 // configs needed for SPNEGO at server side 304 if (isSpnego) { 305 serverConf.set(PRINCIPAL, HTTP_PRINCIPAL); 306 serverConf.set(KEYTAB, keyTabFile.getAbsolutePath()); 307 serverConf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos"); 308 serverConf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true); 309 UserGroupInformation.setConfiguration(serverConf); 310 } else { 311 serverConf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "simple"); 312 serverConf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, false); 313 UserGroupInformation.setConfiguration(serverConf); 314 } 315 316 final HttpServer server = createServer(bindProtocol, isSpnego); 317 // get server port 318 final String authority = NetUtils.getHostPortString(server.getConnectorAddress(0)); 319 320 String keytabFilePath = keyTabFile.getAbsolutePath(); 321 322 UserGroupInformation clientUGI = UserGroupInformation. 323 loginUserFromKeytabAndReturnUGI(clientPrincipal, keytabFilePath); 324 try { 325 clientUGI.doAs((PrivilegedExceptionAction<Void>) () -> { 326 // client command line 327 getLevel(connectProtocol, authority); 328 setLevel(connectProtocol, authority, newLevel); 329 return null; 330 }); 331 } finally { 332 clientUGI.logoutUserFromKeytab(); 333 server.stop(); 334 } 335 336 // restore log level 337 GenericTestUtils.setLogLevel(log, oldLevel); 338 } 339 340 /** 341 * Run LogLevel command line to start a client to get log level of this test 342 * class. 343 * 344 * @param protocol specify either http or https 345 * @param authority daemon's web UI address 346 * @throws Exception if unable to connect 347 */ 348 private void getLevel(String protocol, String authority) throws Exception { 349 String[] getLevelArgs = {"-getlevel", authority, logName, "-protocol", protocol}; 350 CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf); 351 cli.run(getLevelArgs); 352 } 353 354 /** 355 * Run LogLevel command line to start a client to set log level of this test 356 * class to debug. 357 * 358 * @param protocol specify either http or https 359 * @param authority daemon's web UI address 360 * @throws Exception if unable to run or log level does not change as expected 361 */ 362 private void setLevel(String protocol, String authority, String newLevel) 363 throws Exception { 364 String[] setLevelArgs = {"-setlevel", authority, logName, newLevel, "-protocol", protocol}; 365 CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf); 366 cli.run(setLevelArgs); 367 368 assertEquals("new level not equal to expected: ", newLevel.toUpperCase(), 369 log.getEffectiveLevel().toString()); 370 } 371 372 /** 373 * Test setting log level to "Info". 374 * 375 * @throws Exception if client can't set log level to INFO. 376 */ 377 @Test 378 public void testInfoLogLevel() throws Exception { 379 testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, true, "INFO"); 380 } 381 382 /** 383 * Test setting log level to "Error". 384 * 385 * @throws Exception if client can't set log level to ERROR. 386 */ 387 @Test 388 public void testErrorLogLevel() throws Exception { 389 testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, true, "ERROR"); 390 } 391 392 /** 393 * Server runs HTTP, no SPNEGO. 394 * 395 * @throws Exception if http client can't access http server, 396 * or http client can access https server. 397 */ 398 @Test 399 public void testLogLevelByHttp() throws Exception { 400 testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, false); 401 try { 402 testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTPS, 403 false); 404 fail("An HTTPS Client should not have succeeded in connecting to a " + 405 "HTTP server"); 406 } catch (SSLException e) { 407 exceptionShouldContains("Unrecognized SSL message", e); 408 } 409 } 410 411 /** 412 * Server runs HTTP + SPNEGO. 413 * 414 * @throws Exception if http client can't access http server, 415 * or http client can access https server. 416 */ 417 @Test 418 public void testLogLevelByHttpWithSpnego() throws Exception { 419 testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, true); 420 try { 421 testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTPS, 422 true); 423 fail("An HTTPS Client should not have succeeded in connecting to a " + 424 "HTTP server"); 425 } catch (SSLException e) { 426 exceptionShouldContains("Unrecognized SSL message", e); 427 } 428 } 429 430 /** 431 * Server runs HTTPS, no SPNEGO. 432 * 433 * @throws Exception if https client can't access https server, 434 * or https client can access http server. 435 */ 436 @Test 437 public void testLogLevelByHttps() throws Exception { 438 testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTPS, 439 false); 440 try { 441 testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTP, 442 false); 443 fail("An HTTP Client should not have succeeded in connecting to a " + 444 "HTTPS server"); 445 } catch (SocketException e) { 446 exceptionShouldContains("Unexpected end of file from server", e); 447 } 448 } 449 450 /** 451 * Server runs HTTPS + SPNEGO. 452 * 453 * @throws Exception if https client can't access https server, 454 * or https client can access http server. 455 */ 456 @Test 457 public void testLogLevelByHttpsWithSpnego() throws Exception { 458 testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTPS, 459 true); 460 try { 461 testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTP, 462 true); 463 fail("An HTTP Client should not have succeeded in connecting to a " + 464 "HTTPS server"); 465 } catch (SocketException e) { 466 exceptionShouldContains("Unexpected end of file from server", e); 467 } 468 } 469 470 /** 471 * Assert that a throwable or one of its causes should contain the substr in its message. 472 * 473 * Ideally we should use {@link GenericTestUtils#assertExceptionContains(String, Throwable)} util 474 * method which asserts t.toString() contains the substr. As the original throwable may have been 475 * wrapped in Hadoop3 because of HADOOP-12897, it's required to check all the wrapped causes. 476 * After stop supporting Hadoop2, this method can be removed and assertion in tests can use 477 * t.getCause() directly, similar to HADOOP-15280. 478 */ 479 private static void exceptionShouldContains(String substr, Throwable throwable) { 480 Throwable t = throwable; 481 while (t != null) { 482 String msg = t.toString(); 483 if (msg != null && msg.toLowerCase().contains(substr.toLowerCase())) { 484 return; 485 } 486 t = t.getCause(); 487 } 488 throw new AssertionError("Expected to find '" + substr + "' but got unexpected exception:" + 489 StringUtils.stringifyException(throwable), throwable); 490 } 491}