| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.hadoop.log; |
| |
| import java.io.File; |
| import java.net.SocketException; |
| import java.net.URI; |
| import java.util.concurrent.Callable; |
| |
| import org.apache.hadoop.HadoopIllegalArgumentException; |
| import org.apache.hadoop.conf.Configuration; |
| import org.apache.hadoop.fs.CommonConfigurationKeys; |
| import org.apache.hadoop.fs.CommonConfigurationKeysPublic; |
| import org.apache.hadoop.fs.FileUtil; |
| import org.apache.hadoop.http.HttpServer2; |
| import org.apache.hadoop.log.LogLevel.CLI; |
| import org.apache.hadoop.minikdc.KerberosSecurityTestcase; |
| import org.apache.hadoop.net.NetUtils; |
| import org.apache.hadoop.security.AuthenticationFilterInitializer; |
| import org.apache.hadoop.security.UserGroupInformation; |
| import org.apache.hadoop.security.authentication.KerberosTestUtils; |
| import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; |
| import org.apache.hadoop.security.authorize.AccessControlList; |
| import org.apache.hadoop.security.ssl.KeyStoreTestUtil; |
| import org.junit.Assert; |
| |
| import org.apache.hadoop.test.GenericTestUtils; |
| import org.apache.log4j.Level; |
| import org.apache.log4j.Logger; |
| import org.junit.AfterClass; |
| import org.junit.Before; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| import org.slf4j.LoggerFactory; |
| |
| import javax.net.ssl.SSLException; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| /** |
| * Test LogLevel. |
| */ |
| public class TestLogLevel extends KerberosSecurityTestcase { |
| private static final File BASEDIR = GenericTestUtils.getRandomizedTestDir(); |
| private static String keystoresDir; |
| private static String sslConfDir; |
| private static Configuration conf; |
| private static Configuration sslConf; |
| private final String logName = TestLogLevel.class.getName(); |
| private String clientPrincipal; |
| private String serverPrincipal; |
| private final Logger log = Logger.getLogger(logName); |
| private final static String PRINCIPAL = "loglevel.principal"; |
| private final static String KEYTAB = "loglevel.keytab"; |
| private static final String PREFIX = "hadoop.http.authentication."; |
| |
| @BeforeClass |
| public static void setUp() throws Exception { |
| org.slf4j.Logger logger = |
| LoggerFactory.getLogger(KerberosAuthenticator.class); |
| GenericTestUtils.setLogLevel(logger, Level.DEBUG); |
| FileUtil.fullyDelete(BASEDIR); |
| if (!BASEDIR.mkdirs()) { |
| throw new Exception("unable to create the base directory for testing"); |
| } |
| conf = new Configuration(); |
| |
| setupSSL(BASEDIR); |
| } |
| |
| static private void setupSSL(File base) throws Exception { |
| keystoresDir = base.getAbsolutePath(); |
| sslConfDir = KeyStoreTestUtil.getClasspathDir(TestLogLevel.class); |
| KeyStoreTestUtil.setupSSLConfig(keystoresDir, sslConfDir, conf, false); |
| |
| sslConf = KeyStoreTestUtil.getSslConfig(); |
| } |
| |
| @Before |
| public void setupKerberos() throws Exception { |
| File keytabFile = new File(KerberosTestUtils.getKeytabFile()); |
| clientPrincipal = KerberosTestUtils.getClientPrincipal(); |
| serverPrincipal = KerberosTestUtils.getServerPrincipal(); |
| clientPrincipal = clientPrincipal.substring(0, |
| clientPrincipal.lastIndexOf("@")); |
| serverPrincipal = serverPrincipal.substring(0, |
| serverPrincipal.lastIndexOf("@")); |
| getKdc().createPrincipal(keytabFile, clientPrincipal, serverPrincipal); |
| } |
| |
| @AfterClass |
| public static void tearDown() throws Exception { |
| KeyStoreTestUtil.cleanupSSLConfig(keystoresDir, sslConfDir); |
| FileUtil.fullyDelete(BASEDIR); |
| } |
| |
| /** |
| * Test client command line options. Does not validate server behavior. |
| * @throws Exception |
| */ |
| @Test(timeout=120000) |
| public void testCommandOptions() throws Exception { |
| final String className = this.getClass().getName(); |
| |
| assertFalse(validateCommand(new String[] {"-foo" })); |
| // fail due to insufficient number of arguments |
| assertFalse(validateCommand(new String[] {})); |
| assertFalse(validateCommand(new String[] {"-getlevel" })); |
| assertFalse(validateCommand(new String[] {"-setlevel" })); |
| assertFalse(validateCommand(new String[] {"-getlevel", "foo.bar:8080" })); |
| |
| // valid command arguments |
| assertTrue(validateCommand( |
| new String[] {"-getlevel", "foo.bar:8080", className })); |
| assertTrue(validateCommand( |
| new String[] {"-setlevel", "foo.bar:8080", className, "DEBUG" })); |
| assertTrue(validateCommand( |
| new String[] {"-getlevel", "foo.bar:8080", className, "-protocol", |
| "http" })); |
| assertTrue(validateCommand( |
| new String[] {"-getlevel", "foo.bar:8080", className, "-protocol", |
| "https" })); |
| assertTrue(validateCommand( |
| new String[] {"-setlevel", "foo.bar:8080", className, "DEBUG", |
| "-protocol", "http" })); |
| assertTrue(validateCommand( |
| new String[] {"-setlevel", "foo.bar:8080", className, "DEBUG", |
| "-protocol", "https" })); |
| |
| // fail due to the extra argument |
| assertFalse(validateCommand( |
| new String[] {"-getlevel", "foo.bar:8080", className, "-protocol", |
| "https", "blah" })); |
| assertFalse(validateCommand( |
| new String[] {"-setlevel", "foo.bar:8080", className, "DEBUG", |
| "-protocol", "https", "blah" })); |
| assertFalse(validateCommand( |
| new String[] {"-getlevel", "foo.bar:8080", className, "-protocol", |
| "https", "-protocol", "https" })); |
| assertFalse(validateCommand( |
| new String[] {"-getlevel", "foo.bar:8080", className, |
| "-setlevel", "foo.bar:8080", className })); |
| } |
| |
| /** |
| * Check to see if a command can be accepted. |
| * |
| * @param args a String array of arguments |
| * @return true if the command can be accepted, false if not. |
| */ |
| private boolean validateCommand(String[] args) throws Exception { |
| CLI cli = new CLI(sslConf); |
| try { |
| cli.parseArguments(args); |
| } catch (HadoopIllegalArgumentException e) { |
| return false; |
| } catch (Exception e) { |
| // this is used to verify the command arguments only. |
| // no HadoopIllegalArgumentException = the arguments are good. |
| return true; |
| } |
| return true; |
| } |
| |
| /** |
| * Creates and starts a Jetty server binding at an ephemeral port to run |
| * LogLevel servlet. |
| * @param protocol "http" or "https" |
| * @param isSpnego true if SPNEGO is enabled |
| * @return a created HttpServer2 object |
| * @throws Exception if unable to create or start a Jetty server |
| */ |
| private HttpServer2 createServer(String protocol, boolean isSpnego) |
| throws Exception { |
| HttpServer2.Builder builder = new HttpServer2.Builder() |
| .setName("..") |
| .addEndpoint(new URI(protocol + "://localhost:0")) |
| .setFindPort(true) |
| .setConf(conf); |
| if (isSpnego) { |
| // Set up server Kerberos credentials. |
| // Since the server may fall back to simple authentication, |
| // use ACL to make sure the connection is Kerberos/SPNEGO authenticated. |
| builder.setSecurityEnabled(true) |
| .setUsernameConfKey(PRINCIPAL) |
| .setKeytabConfKey(KEYTAB) |
| .setACL(new AccessControlList(clientPrincipal)); |
| } |
| |
| // if using HTTPS, configure keystore/truststore properties. |
| if (protocol.equals(LogLevel.PROTOCOL_HTTPS)) { |
| builder = builder. |
| keyPassword(sslConf.get("ssl.server.keystore.keypassword")) |
| .keyStore(sslConf.get("ssl.server.keystore.location"), |
| sslConf.get("ssl.server.keystore.password"), |
| sslConf.get("ssl.server.keystore.type", "jks")) |
| .trustStore(sslConf.get("ssl.server.truststore.location"), |
| sslConf.get("ssl.server.truststore.password"), |
| sslConf.get("ssl.server.truststore.type", "jks")); |
| } |
| HttpServer2 server = builder.build(); |
| // Enable SPNEGO for LogLevel servlet |
| if (isSpnego) { |
| server.addInternalServlet("logLevel", "/logLevel", LogLevel.Servlet.class, |
| true); |
| } |
| server.start(); |
| return server; |
| } |
| |
| private void testDynamicLogLevel(final String bindProtocol, |
| final String connectProtocol, final boolean isSpnego) |
| throws Exception { |
| testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego, |
| Level.DEBUG.toString()); |
| } |
| |
| /** |
| * Run both client and server using the given protocol. |
| * |
| * @param bindProtocol specify either http or https for server |
| * @param connectProtocol specify either http or https for client |
| * @param isSpnego true if SPNEGO is enabled |
| * @throws Exception |
| */ |
| private void testDynamicLogLevel(final String bindProtocol, |
| final String connectProtocol, final boolean isSpnego, |
| final String newLevel) throws Exception { |
| if (!LogLevel.isValidProtocol(bindProtocol)) { |
| throw new Exception("Invalid server protocol " + bindProtocol); |
| } |
| if (!LogLevel.isValidProtocol(connectProtocol)) { |
| throw new Exception("Invalid client protocol " + connectProtocol); |
| } |
| Level oldLevel = log.getEffectiveLevel(); |
| Assert.assertNotEquals("Get default Log Level which shouldn't be ERROR.", |
| Level.ERROR, oldLevel); |
| |
| // configs needed for SPNEGO at server side |
| if (isSpnego) { |
| conf.set(PRINCIPAL, KerberosTestUtils.getServerPrincipal()); |
| conf.set(KEYTAB, KerberosTestUtils.getKeytabFile()); |
| conf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, |
| "kerberos"); |
| conf.set(PREFIX + "type", "kerberos"); |
| conf.set(PREFIX + "kerberos.keytab", KerberosTestUtils.getKeytabFile()); |
| conf.set(PREFIX + "kerberos.principal", |
| KerberosTestUtils.getServerPrincipal()); |
| conf.set(HttpServer2.FILTER_INITIALIZER_PROPERTY, |
| AuthenticationFilterInitializer.class.getName()); |
| |
| conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, |
| true); |
| UserGroupInformation.setConfiguration(conf); |
| } |
| |
| final HttpServer2 server = createServer(bindProtocol, isSpnego); |
| // get server port |
| final String authority = NetUtils.getHostPortString(server |
| .getConnectorAddress(0)); |
| |
| KerberosTestUtils.doAsClient(new Callable<Void>() { |
| @Override |
| public Void call() throws Exception { |
| // client command line |
| getLevel(connectProtocol, authority); |
| setLevel(connectProtocol, authority, newLevel); |
| return null; |
| } |
| }); |
| server.stop(); |
| // restore log level |
| GenericTestUtils.setLogLevel(log, oldLevel); |
| } |
| |
| /** |
| * Run LogLevel command line to start a client to get log level of this test |
| * class. |
| * |
| * @param protocol specify either http or https |
| * @param authority daemon's web UI address |
| * @throws Exception if unable to connect |
| */ |
| private void getLevel(String protocol, String authority) throws Exception { |
| String[] getLevelArgs = {"-getlevel", authority, logName, "-protocol", |
| protocol}; |
| CLI cli = new CLI(sslConf); |
| cli.run(getLevelArgs); |
| } |
| |
| /** |
| * Run LogLevel command line to start a client to set log level of this test |
| * class to debug. |
| * |
| * @param protocol specify either http or https |
| * @param authority daemon's web UI address |
| * @throws Exception if unable to run or log level does not change as expected |
| */ |
| private void setLevel(String protocol, String authority, String newLevel) |
| throws Exception { |
| String[] setLevelArgs = {"-setlevel", authority, logName, |
| newLevel, "-protocol", protocol}; |
| CLI cli = new CLI(sslConf); |
| cli.run(setLevelArgs); |
| |
| assertEquals("new level not equal to expected: ", newLevel.toUpperCase(), |
| log.getEffectiveLevel().toString()); |
| } |
| |
| /** |
| * Test setting log level to "Info". |
| * |
| * @throws Exception |
| */ |
| @Test(timeout=60000) |
| public void testInfoLogLevel() throws Exception { |
| testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, false, |
| "Info"); |
| } |
| |
| /** |
| * Test setting log level to "Error". |
| * |
| * @throws Exception |
| */ |
| @Test(timeout=60000) |
| public void testErrorLogLevel() throws Exception { |
| testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, false, |
| "Error"); |
| } |
| |
| /** |
| * Server runs HTTP, no SPNEGO. |
| * |
| * @throws Exception |
| */ |
| @Test(timeout=60000) |
| public void testLogLevelByHttp() throws Exception { |
| testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, false); |
| try { |
| testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTPS, |
| false); |
| fail("A HTTPS Client should not have succeeded in connecting to a " + |
| "HTTP server"); |
| } catch (SSLException e) { |
| GenericTestUtils.assertExceptionContains("Error while authenticating " |
| + "with endpoint", e); |
| GenericTestUtils.assertExceptionContains("recognized SSL message", e |
| .getCause()); |
| } |
| } |
| |
| /** |
| * Server runs HTTP + SPNEGO. |
| * |
| * @throws Exception |
| */ |
| @Test(timeout=60000) |
| public void testLogLevelByHttpWithSpnego() throws Exception { |
| testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, true); |
| try { |
| testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTPS, |
| true); |
| fail("A HTTPS Client should not have succeeded in connecting to a " + |
| "HTTP server"); |
| } catch (SSLException e) { |
| GenericTestUtils.assertExceptionContains("Error while authenticating " |
| + "with endpoint", e); |
| GenericTestUtils.assertExceptionContains("recognized SSL message", e |
| .getCause()); |
| } |
| } |
| |
| /** |
| * Server runs HTTPS, no SPNEGO. |
| * |
| * @throws Exception |
| */ |
| @Test(timeout=60000) |
| public void testLogLevelByHttps() throws Exception { |
| testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTPS, |
| false); |
| try { |
| testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTP, |
| false); |
| fail("A HTTP Client should not have succeeded in connecting to a " + |
| "HTTPS server"); |
| } catch (SocketException e) { |
| GenericTestUtils.assertExceptionContains("Error while authenticating " |
| + "with endpoint", e); |
| GenericTestUtils.assertExceptionContains( |
| "Unexpected end of file from server", e.getCause()); |
| } |
| } |
| |
| /** |
| * Server runs HTTPS + SPNEGO. |
| * |
| * @throws Exception |
| */ |
| @Test(timeout=60000) |
| public void testLogLevelByHttpsWithSpnego() throws Exception { |
| testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTPS, |
| true); |
| try { |
| testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTP, |
| true); |
| fail("A HTTP Client should not have succeeded in connecting to a " + |
| "HTTPS server"); |
| } catch (SocketException e) { |
| GenericTestUtils.assertExceptionContains("Error while authenticating " |
| + "with endpoint", e); |
| GenericTestUtils.assertExceptionContains( |
| "Unexpected end of file from server", e.getCause()); |
| } |
| } |
| } |