diff --git a/examples/embedded/src/main/resources/jetty-logging.properties b/examples/embedded/src/main/resources/jetty-logging.properties index 810f9896c7e..c0a226179d4 100644 --- a/examples/embedded/src/main/resources/jetty-logging.properties +++ b/examples/embedded/src/main/resources/jetty-logging.properties @@ -9,3 +9,4 @@ #org.eclipse.jetty.server.LEVEL=DEBUG #org.eclipse.jetty.servlets.LEVEL=DEBUG #org.eclipse.jetty.alpn.LEVEL=DEBUG +#org.eclipse.jetty.jmx.LEVEL=DEBUG diff --git a/jetty-client/pom.xml b/jetty-client/pom.xml index 5a6c364209f..59f586c2261 100644 --- a/jetty-client/pom.xml +++ b/jetty-client/pom.xml @@ -105,6 +105,7 @@ ${project.version} true + org.eclipse.jetty jetty-server @@ -117,6 +118,17 @@ ${project.version} test + + org.apache.kerby + kerb-simplekdc + 1.1.1 + test + + + org.slf4j + slf4j-simple + test + org.eclipse.jetty.toolchain jetty-test-helper diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java index b0441b334cb..11328247ad1 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java @@ -49,7 +49,7 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler private final int maxContentLength; private final ResponseNotifier notifier; - private static final Pattern CHALLENGE_PATTERN = Pattern.compile("(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)|(?:(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s+)?(?:(?[a-zA-Z0-9\\-._~+\\/]+=*)|(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s*=\\s*(?:(?.*)))"); + private static final Pattern CHALLENGE_PATTERN = Pattern.compile("(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)|(?:(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s+)?(?:(?[a-zA-Z0-9\\-._~+/]+=*)|(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s*=\\s*(?:(?.*)))"); protected AuthenticationProtocolHandler(HttpClient client, int maxContentLength) { @@ -122,7 +122,6 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler return headerInfos; } - private class AuthenticationListener extends BufferingResponseListener { private AuthenticationListener() @@ -225,13 +224,12 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler copyIfAbsent(request, newRequest, HttpHeader.AUTHORIZATION); copyIfAbsent(request, newRequest, HttpHeader.PROXY_AUTHORIZATION); - newRequest.onResponseSuccess(r -> client.getAuthenticationStore().addAuthenticationResult(authnResult)); - + AfterAuthenticationListener listener = new AfterAuthenticationListener(authnResult); Connection connection = (Connection)request.getAttributes().get(Connection.class.getName()); if (connection != null) - connection.send(newRequest, null); + connection.send(newRequest, listener); else - newRequest.send(null); + newRequest.send(listener); } catch (Throwable x) { @@ -298,4 +296,20 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler return result; } } + + private class AfterAuthenticationListener extends Response.Listener.Adapter + { + private final Authentication.Result authenticationResult; + + private AfterAuthenticationListener(Authentication.Result authenticationResult) + { + this.authenticationResult = authenticationResult; + } + + @Override + public void onSuccess(Response response) + { + client.getAuthenticationStore().addAuthenticationResult(authenticationResult); + } + } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/SPNEGOAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/SPNEGOAuthentication.java new file mode 100644 index 00000000000..601f4abfd24 --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/SPNEGOAuthentication.java @@ -0,0 +1,378 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.util; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.AuthenticationStore; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.Attributes; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +/** + *

Implementation of the SPNEGO (or "Negotiate") authentication defined in RFC 4559.

+ *

A {@link #getUserName() user} is logged in via JAAS (either via userName/password or + * via userName/keyTab) once only.

+ *

For every request that needs authentication, a {@link GSSContext} is initiated and + * later established after reading the response from the server.

+ *

Applications should create objects of this class and add them to the + * {@link AuthenticationStore} retrieved from the {@link HttpClient} + * via {@link HttpClient#getAuthenticationStore()}.

+ */ +public class SPNEGOAuthentication extends AbstractAuthentication +{ + private static final Logger LOG = Log.getLogger(SPNEGOAuthentication.class); + private static final String NEGOTIATE = HttpHeader.NEGOTIATE.asString(); + + private final GSSManager gssManager = GSSManager.getInstance(); + private String userName; + private String userPassword; + private Path userKeyTabPath; + private String serviceName; + private boolean useTicketCache; + private Path ticketCachePath; + private boolean renewTGT; + + public SPNEGOAuthentication(URI uri) + { + super(uri, ANY_REALM); + } + + @Override + public String getType() + { + return NEGOTIATE; + } + + /** + * @return the user name of the user to login + */ + public String getUserName() + { + return userName; + } + + /** + * @param userName user name of the user to login + */ + public void setUserName(String userName) + { + this.userName = userName; + } + + /** + * @return the password of the user to login + */ + public String getUserPassword() + { + return userPassword; + } + + /** + * @param userPassword the password of the user to login + * @see #setUserKeyTabPath(Path) + */ + public void setUserPassword(String userPassword) + { + this.userPassword = userPassword; + } + + /** + * @return the path of the keyTab file with the user credentials + */ + public Path getUserKeyTabPath() + { + return userKeyTabPath; + } + + /** + * @param userKeyTabPath the path of the keyTab file with the user credentials + * @see #setUserPassword(String) + */ + public void setUserKeyTabPath(Path userKeyTabPath) + { + this.userKeyTabPath = userKeyTabPath; + } + + /** + * @return the name of the service to use + */ + public String getServiceName() + { + return serviceName; + } + + /** + * @param serviceName the name of the service to use + */ + public void setServiceName(String serviceName) + { + this.serviceName = serviceName; + } + + /** + * @return whether to use the ticket cache during login + */ + public boolean isUseTicketCache() + { + return useTicketCache; + } + + /** + * @param useTicketCache whether to use the ticket cache during login + * @see #setTicketCachePath(Path) + */ + public void setUseTicketCache(boolean useTicketCache) + { + this.useTicketCache = useTicketCache; + } + + /** + * @return the path of the ticket cache file + */ + public Path getTicketCachePath() + { + return ticketCachePath; + } + + /** + * @param ticketCachePath the path of the ticket cache file + * @see #setUseTicketCache(boolean) + */ + public void setTicketCachePath(Path ticketCachePath) + { + this.ticketCachePath = ticketCachePath; + } + + /** + * @return whether to renew the ticket granting ticket + */ + public boolean isRenewTGT() + { + return renewTGT; + } + + /** + * @param renewTGT whether to renew the ticket granting ticket + */ + public void setRenewTGT(boolean renewTGT) + { + this.renewTGT = renewTGT; + } + + @Override + public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context) + { + SPNEGOContext spnegoContext = (SPNEGOContext)context.getAttribute(SPNEGOContext.ATTRIBUTE); + if (LOG.isDebugEnabled()) + LOG.debug("Authenticate with context {}", spnegoContext); + if (spnegoContext == null) + { + spnegoContext = login(); + context.setAttribute(SPNEGOContext.ATTRIBUTE, spnegoContext); + } + + String b64Input = headerInfo.getBase64(); + byte[] input = b64Input == null ? new byte[0] : Base64.getDecoder().decode(b64Input); + byte[] output = Subject.doAs(spnegoContext.subject, initGSSContext(spnegoContext, request.getHost(), input)); + String b64Output = output == null ? null : new String(Base64.getEncoder().encode(output)); + + // The result cannot be used for subsequent requests, + // so it always has a null URI to avoid being cached. + return new SPNEGOResult(null, b64Output); + } + + private SPNEGOContext login() + { + try + { + // First login via JAAS using the Kerberos AS_REQ call, with a client user. + // This will populate the Subject with the client user principal and the TGT. + String user = getUserName(); + if (LOG.isDebugEnabled()) + LOG.debug("Logging in user {}", user); + CallbackHandler callbackHandler = new PasswordCallbackHandler(); + LoginContext loginContext = new LoginContext("", null, callbackHandler, new SPNEGOConfiguration()); + loginContext.login(); + Subject subject = loginContext.getSubject(); + + SPNEGOContext spnegoContext = new SPNEGOContext(); + spnegoContext.subject = subject; + if (LOG.isDebugEnabled()) + LOG.debug("Initialized {}", spnegoContext); + return spnegoContext; + } + catch (LoginException x) + { + throw new RuntimeException(x); + } + } + + private PrivilegedAction initGSSContext(SPNEGOContext spnegoContext, String host, byte[] bytes) + { + return () -> + { + try + { + // The call to initSecContext with the service name will + // trigger the Kerberos TGS_REQ call, asking for the SGT, + // which will be added to the Subject credentials because + // initSecContext() is called from within Subject.doAs(). + GSSContext gssContext = spnegoContext.gssContext; + if (gssContext == null) + { + String principal = getServiceName() + "@" + host; + GSSName serviceName = gssManager.createName(principal, GSSName.NT_HOSTBASED_SERVICE); + Oid spnegoOid = new Oid("1.3.6.1.5.5.2"); + gssContext = gssManager.createContext(serviceName, spnegoOid, null, GSSContext.INDEFINITE_LIFETIME); + spnegoContext.gssContext = gssContext; + gssContext.requestMutualAuth(true); + } + byte[] result = gssContext.initSecContext(bytes, 0, bytes.length); + if (LOG.isDebugEnabled()) + LOG.debug("{} {}", gssContext.isEstablished() ? "Initialized" : "Initializing", gssContext); + return result; + } + catch (GSSException x) + { + throw new RuntimeException(x); + } + }; + } + + public static class SPNEGOResult implements Result + { + private final URI uri; + private final HttpHeader header; + private final String value; + + public SPNEGOResult(URI uri, String token) + { + this(uri, HttpHeader.AUTHORIZATION, token); + } + + public SPNEGOResult(URI uri, HttpHeader header, String token) + { + this.uri = uri; + this.header = header; + this.value = NEGOTIATE + (token == null ? "" : " " + token); + } + + @Override + public URI getURI() + { + return uri; + } + + @Override + public void apply(Request request) + { + request.header(header, value); + } + } + + private static class SPNEGOContext + { + private static final String ATTRIBUTE = SPNEGOContext.class.getName(); + + private Subject subject; + private GSSContext gssContext; + + @Override + public String toString() + { + return String.format("%s@%x[context=%s]", getClass().getSimpleName(), hashCode(), gssContext); + } + } + + private class PasswordCallbackHandler implements CallbackHandler + { + @Override + public void handle(Callback[] callbacks) throws IOException + { + PasswordCallback callback = Arrays.stream(callbacks) + .filter(PasswordCallback.class::isInstance) + .map(PasswordCallback.class::cast) + .findAny() + .filter(c -> c.getPrompt().contains(getUserName())) + .orElseThrow(IOException::new); + callback.setPassword(getUserPassword().toCharArray()); + } + } + + private class SPNEGOConfiguration extends Configuration + { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) + { + Map options = new HashMap<>(); + if (LOG.isDebugEnabled()) + options.put("debug", "true"); + options.put("refreshKrb5Config", "true"); + options.put("principal", getUserName()); + options.put("isInitiator", "true"); + Path keyTabPath = getUserKeyTabPath(); + if (keyTabPath != null) + { + options.put("doNotPrompt", "true"); + options.put("useKeyTab", "true"); + options.put("keyTab", keyTabPath.toAbsolutePath().toString()); + options.put("storeKey", "true"); + } + boolean useTicketCache = isUseTicketCache(); + if (useTicketCache) + { + options.put("useTicketCache", "true"); + Path ticketCachePath = getTicketCachePath(); + if (ticketCachePath != null) + options.put("ticketCache", ticketCachePath.toAbsolutePath().toString()); + options.put("renewTGT", String.valueOf(isRenewTGT())); + } + + String moduleClass = "com.sun.security.auth.module.Krb5LoginModule"; + AppConfigurationEntry config = new AppConfigurationEntry(moduleClass, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options); + return new AppConfigurationEntry[]{config}; + } + } +} diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/EmptyServerHandler.java b/jetty-client/src/test/java/org/eclipse/jetty/client/EmptyServerHandler.java index be39d16ee17..dca7a45c53b 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/EmptyServerHandler.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/EmptyServerHandler.java @@ -26,7 +26,6 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.util.log.Log; public class EmptyServerHandler extends AbstractHandler.ErrorDispatchHandler { @@ -39,6 +38,5 @@ public class EmptyServerHandler extends AbstractHandler.ErrorDispatchHandler protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - Log.getRootLogger().info("EMPTY service {}",target); } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java index a738af91c95..b30c0a39765 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java @@ -18,11 +18,6 @@ package org.eclipse.jetty.client.ssl; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.BufferedReader; import java.io.File; import java.io.InputStream; @@ -54,9 +49,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnJre; import org.junit.jupiter.api.condition.JRE; -/* This whole test is very specific to how TLS < 1.3 works. - * Starting in Java 11, TLS/1.3 is now enabled by default. - */ +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +// This whole test is very specific to how TLS < 1.3 works. +// Starting in Java 11, TLS/1.3 is now enabled by default. @EnabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10}) public class SslBytesClientTest extends SslBytesTest { diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java index 92c3ff6ef8c..1fc70235043 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java @@ -18,12 +18,6 @@ package org.eclipse.jetty.client.ssl; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static org.junit.jupiter.api.condition.OS.LINUX; -import static org.junit.jupiter.api.condition.OS.WINDOWS; - import java.io.BufferedReader; import java.io.EOFException; import java.io.File; @@ -72,20 +66,31 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.util.JavaVersion; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.hamcrest.Matchers; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledOnJre; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.JRE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.condition.OS.LINUX; +import static org.junit.jupiter.api.condition.OS.WINDOWS; + +// This whole test is very specific to how TLS < 1.3 works. +@EnabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10}) public class SslBytesServerTest extends SslBytesTest { private final AtomicInteger sslFills = new AtomicInteger(); @@ -101,8 +106,6 @@ public class SslBytesServerTest extends SslBytesTest private SimpleProxy proxy; private Runnable idleHook; - // This whole test is very specific to how TLS < 1.3 works. - @DisabledOnJre( JRE.JAVA_11 ) @BeforeEach public void init() throws Exception { diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/util/SPNEGOAuthenticationTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/util/SPNEGOAuthenticationTest.java new file mode 100644 index 00000000000..8fa771ad4cd --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/util/SPNEGOAuthenticationTest.java @@ -0,0 +1,301 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.util; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; +import org.eclipse.jetty.client.AbstractHttpClientServerTest; +import org.eclipse.jetty.client.EmptyServerHandler; +import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.AuthenticationStore; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.security.ConfigurableSpnegoLoginService; +import org.eclipse.jetty.security.ConstraintMapping; +import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.security.HashLoginService; +import org.eclipse.jetty.security.authentication.AuthorizationService; +import org.eclipse.jetty.security.authentication.ConfigurableSpnegoAuthenticator; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.session.DefaultSessionIdManager; +import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.security.Constraint; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.condition.DisabledOnJre; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +// Apparently only JDK 11 is able to run these tests. +// See for example: https://bugs.openjdk.java.net/browse/JDK-8202439 +// where apparently the compiler gets the AES CPU instructions wrong. +@DisabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10}) +public class SPNEGOAuthenticationTest extends AbstractHttpClientServerTest +{ + private static final Logger LOG = Log.getLogger(SPNEGOAuthenticationTest.class); + + static + { + if (LOG.isDebugEnabled()) + { + System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug"); + System.setProperty("sun.security.jgss.debug", "true"); + System.setProperty("sun.security.krb5.debug", "true"); + System.setProperty("sun.security.spnego.debug", "true"); + } + } + + private Path testDirPath = MavenTestingUtils.getTargetTestingPath(SPNEGOAuthenticationTest.class.getSimpleName()); + private String clientName = "spnego_client"; + private String clientPassword = "spnego_client_pwd"; + private String serviceName = "srvc"; + private String serviceHost = "localhost"; + private String realm = "jetty.org"; + private Path realmPropsPath = MavenTestingUtils.getTestResourcePath("realm.properties"); + private Path serviceKeyTabPath = testDirPath.resolve("service.keytab"); + private Path clientKeyTabPath = testDirPath.resolve("client.keytab"); + private SimpleKdcServer kdc; + private ConfigurableSpnegoAuthenticator authenticator; + + @BeforeEach + public void prepare() throws Exception + { + IO.delete(testDirPath.toFile()); + Files.createDirectories(testDirPath); + System.setProperty("java.security.krb5.conf", testDirPath.toAbsolutePath().toString()); + + kdc = new SimpleKdcServer(); + kdc.setAllowUdp(false); + kdc.setAllowTcp(true); + kdc.setKdcRealm(realm); + kdc.setWorkDir(testDirPath.toFile()); + kdc.init(); + + kdc.createAndExportPrincipals(serviceKeyTabPath.toFile(), serviceName + "/" + serviceHost); + kdc.createPrincipal(clientName + "@" + realm, clientPassword); + kdc.exportPrincipal(clientName, clientKeyTabPath.toFile()); + kdc.start(); + + if (LOG.isDebugEnabled()) + { + LOG.debug("KDC started on port {}", kdc.getKdcTcpPort()); + String krb5 = Files.readAllLines(testDirPath.resolve("krb5.conf")).stream() + .filter(line -> !line.startsWith("#")) + .collect(Collectors.joining(System.lineSeparator())); + LOG.debug("krb5.conf{}{}", System.lineSeparator(), krb5); + } + } + + @AfterEach + public void dispose() throws Exception + { + if (kdc != null) + kdc.stop(); + } + + private void startSPNEGO(Scenario scenario, Handler handler) throws Exception + { + server = new Server(); + server.setSessionIdManager(new DefaultSessionIdManager(server)); + HashLoginService authorizationService = new HashLoginService(realm, realmPropsPath.toString()); + ConfigurableSpnegoLoginService loginService = new ConfigurableSpnegoLoginService(realm, AuthorizationService.from(authorizationService, "")); + loginService.addBean(authorizationService); + loginService.setKeyTabPath(serviceKeyTabPath); + loginService.setServiceName(serviceName); + loginService.setHostName(serviceHost); + server.addBean(loginService); + + ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); + Constraint constraint = new Constraint(); + constraint.setAuthenticate(true); + constraint.setRoles(new String[]{"**"}); //allow any authenticated user + ConstraintMapping mapping = new ConstraintMapping(); + mapping.setPathSpec("/secure"); + mapping.setConstraint(constraint); + securityHandler.addConstraintMapping(mapping); + authenticator = new ConfigurableSpnegoAuthenticator(); + securityHandler.setAuthenticator(authenticator); + securityHandler.setLoginService(loginService); + securityHandler.setHandler(handler); + + SessionHandler sessionHandler = new SessionHandler(); + sessionHandler.setHandler(securityHandler); + start(scenario, sessionHandler); + } + + @ParameterizedTest + @ArgumentsSource(ScenarioProvider.class) + public void testPasswordSPNEGOAuthentication(Scenario scenario) throws Exception + { + testSPNEGOAuthentication(scenario, false); + } + + @ParameterizedTest + @ArgumentsSource(ScenarioProvider.class) + public void testKeyTabSPNEGOAuthentication(Scenario scenario) throws Exception + { + testSPNEGOAuthentication(scenario, true); + } + + private void testSPNEGOAuthentication(Scenario scenario, boolean useKeyTab) throws Exception + { + startSPNEGO(scenario, new EmptyServerHandler()); + authenticator.setAuthenticationDuration(Duration.ZERO); + + URI uri = URI.create(scenario.getScheme() + "://localhost:" + connector.getLocalPort()); + + // Request without Authentication causes a 401 + Request request = client.newRequest(uri).path("/secure"); + ContentResponse response = request.timeout(15, TimeUnit.SECONDS).send(); + assertNotNull(response); + assertEquals(401, response.getStatus()); + + // Add authentication. + SPNEGOAuthentication authentication = new SPNEGOAuthentication(uri); + authentication.setUserName(clientName + "@" + realm); + if (useKeyTab) + authentication.setUserKeyTabPath(clientKeyTabPath); + else + authentication.setUserPassword(clientPassword); + authentication.setServiceName(serviceName); + AuthenticationStore authenticationStore = client.getAuthenticationStore(); + authenticationStore.addAuthentication(authentication); + + // Request with authentication causes a 401 (no previous successful authentication) + 200 + request = client.newRequest(uri).path("/secure"); + response = request.timeout(15, TimeUnit.SECONDS).send(); + assertNotNull(response); + assertEquals(200, response.getStatus()); + // Authentication results for SPNEGO cannot be cached. + Authentication.Result authnResult = authenticationStore.findAuthenticationResult(uri); + assertNull(authnResult); + + AtomicInteger requests = new AtomicInteger(); + client.getRequestListeners().add(new Request.Listener.Adapter() + { + @Override + public void onSuccess(Request request) + { + requests.incrementAndGet(); + } + }); + + // The server has infinite authentication duration, so + // subsequent requests will be preemptively authorized. + request = client.newRequest(uri).path("/secure"); + response = request.timeout(15, TimeUnit.SECONDS).send(); + assertNotNull(response); + assertEquals(200, response.getStatus()); + assertEquals(1, requests.get()); + } + + @ParameterizedTest + @ArgumentsSource(ScenarioProvider.class) + public void testAuthenticationExpiration(Scenario scenario) throws Exception + { + startSPNEGO(scenario, new EmptyServerHandler() + { + @Override + protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException + { + IO.readBytes(request.getInputStream()); + } + }); + long timeout = 1000; + authenticator.setAuthenticationDuration(Duration.ofMillis(timeout)); + + URI uri = URI.create(scenario.getScheme() + "://localhost:" + connector.getLocalPort()); + + // Add authentication. + SPNEGOAuthentication authentication = new SPNEGOAuthentication(uri); + authentication.setUserName(clientName + "@" + realm); + authentication.setUserPassword(clientPassword); + authentication.setServiceName(serviceName); + AuthenticationStore authenticationStore = client.getAuthenticationStore(); + authenticationStore.addAuthentication(authentication); + + AtomicInteger requests = new AtomicInteger(); + client.getRequestListeners().add(new Request.Listener.Adapter() + { + @Override + public void onSuccess(Request request) + { + requests.incrementAndGet(); + } + }); + + Request request = client.newRequest(uri).path("/secure"); + Response response = request.timeout(15, TimeUnit.SECONDS).send(); + assertEquals(200, response.getStatus()); + // Expect 401 + 200. + assertEquals(2, requests.get()); + + requests.set(0); + request = client.newRequest(uri).path("/secure"); + response = request.timeout(15, TimeUnit.SECONDS).send(); + assertEquals(200, response.getStatus()); + // Authentication not expired on server, expect 200 only. + assertEquals(1, requests.get()); + + // Let authentication expire. + Thread.sleep(2 * timeout); + + requests.set(0); + request = client.newRequest(uri).path("/secure"); + response = request.timeout(15, TimeUnit.SECONDS).send(); + assertEquals(200, response.getStatus()); + // Authentication expired, expect 401 + 200. + assertEquals(2, requests.get()); + + // Let authentication expire again. + Thread.sleep(2 * timeout); + + requests.set(0); + ByteArrayInputStream input = new ByteArrayInputStream("hello_world".getBytes(StandardCharsets.UTF_8)); + request = client.newRequest(uri).method("POST").path("/secure").content(new InputStreamContentProvider(input)); + response = request.timeout(15, TimeUnit.SECONDS).send(); + assertEquals(200, response.getStatus()); + // Authentication expired, but POSTs are allowed. + assertEquals(1, requests.get()); + } +} diff --git a/jetty-client/src/test/resources/jetty-logging.properties b/jetty-client/src/test/resources/jetty-logging.properties index 50f3165273b..f74a4da98d1 100644 --- a/jetty-client/src/test/resources/jetty-logging.properties +++ b/jetty-client/src/test/resources/jetty-logging.properties @@ -1,5 +1,5 @@ -class=org.eclipse.jetty.util.log.StdErrLog -#org.eclipse.jetty.LEVEL=INFO +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +#org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.client.LEVEL=DEBUG #org.eclipse.jetty.io.ChannelEndPoint.LEVEL=DEBUG #org.eclipse.jetty.io.ssl.LEVEL=DEBUG diff --git a/jetty-client/src/test/resources/realm.properties b/jetty-client/src/test/resources/realm.properties index 54ace472cb6..27e300ad53c 100644 --- a/jetty-client/src/test/resources/realm.properties +++ b/jetty-client/src/test/resources/realm.properties @@ -1,3 +1,4 @@ # Format is :, basic:basic digest:digest +spnego_client:,admin diff --git a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java index 950251b1e49..17ef201b4e7 100644 --- a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java +++ b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java @@ -19,18 +19,27 @@ package org.eclipse.jetty.jmx; import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import javax.management.InstanceNotFoundException; +import javax.management.MBeanInfo; import javax.management.MBeanRegistrationException; import javax.management.MBeanServer; import javax.management.ObjectName; +import javax.management.modelmbean.ModelMBean; +import org.eclipse.jetty.util.Loader; +import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.component.Container; import org.eclipse.jetty.util.component.ContainerLifeCycle; @@ -45,15 +54,190 @@ import org.eclipse.jetty.util.log.Logger; @ManagedObject("The component that registers beans as MBeans") public class MBeanContainer implements Container.InheritedListener, Dumpable, Destroyable { - private final static Logger LOG = Log.getLogger(MBeanContainer.class.getName()); - private final static ConcurrentMap __unique = new ConcurrentHashMap<>(); + private static final Logger LOG = Log.getLogger(MBeanContainer.class.getName()); + private static final ConcurrentMap __unique = new ConcurrentHashMap<>(); private static final Container ROOT = new ContainerLifeCycle(); private final MBeanServer _mbeanServer; + private final boolean _useCacheForOtherClassLoaders; + private final ConcurrentMap _metaData = new ConcurrentHashMap<>(); private final ConcurrentMap _beans = new ConcurrentHashMap<>(); private final ConcurrentMap _mbeans = new ConcurrentHashMap<>(); private String _domain = null; + /** + * Constructs MBeanContainer + * + * @param server instance of MBeanServer for use by container + */ + public MBeanContainer(MBeanServer server) + { + this(server, true); + } + + /** + * Constructs MBeanContainer + * + * @param server instance of MBeanServer for use by container + * @param cacheOtherClassLoaders If true, MBeans from other classloaders (eg WebAppClassLoader) will be cached. + * The cache is never flushed, so this should be false if some classloaders do not live forever. + */ + public MBeanContainer(MBeanServer server, boolean cacheOtherClassLoaders) + { + _mbeanServer = server; + _useCacheForOtherClassLoaders = cacheOtherClassLoaders; + } + + /** + * Retrieve instance of MBeanServer used by container + * + * @return instance of MBeanServer + */ + public MBeanServer getMBeanServer() + { + return _mbeanServer; + } + + @ManagedAttribute(value = "Whether to use the cache for MBeans loaded by other ClassLoaders", readonly = true) + public boolean isUseCacheForOtherClassLoaders() + { + return _useCacheForOtherClassLoaders; + } + + /** + * Set domain to be used to add MBeans + * + * @param domain domain name + */ + public void setDomain(String domain) + { + _domain = domain; + } + + /** + * Retrieve domain name used to add MBeans + * + * @return domain name + */ + @ManagedAttribute("The default ObjectName domain") + public String getDomain() + { + return _domain; + } + + /** + *

Creates an ObjectMBean for the given object.

+ *

Attempts to create an ObjectMBean for the object by searching the package + * and class name space. For example an object of the type:

+ *
+     * class com.acme.MyClass extends com.acme.util.BaseClass implements com.acme.Iface
+     * 
+ *

then this method would look for the following classes:

+ *
    + *
  • com.acme.jmx.MyClassMBean
  • + *
  • com.acme.util.jmx.BaseClassMBean
  • + *
  • org.eclipse.jetty.jmx.ObjectMBean
  • + *
+ * + * @param o The object + * @return A new instance of an MBean for the object or null. + */ + public Object mbeanFor(Object o) + { + return mbeanFor(this, o); + } + + static Object mbeanFor(MBeanContainer container, Object o) + { + if (o == null) + return null; + Object mbean = findMetaData(container, o.getClass()).newInstance(o); + if (mbean instanceof ObjectMBean) + ((ObjectMBean)mbean).setMBeanContainer(container); + if (LOG.isDebugEnabled()) + { + LOG.debug("MBean for {} is {}", o, mbean); + if (mbean instanceof ObjectMBean) + { + MBeanInfo info = ((ObjectMBean)mbean).getMBeanInfo(); + for (Object a : info.getAttributes()) + LOG.debug(" {}", a); + for (Object a : info.getOperations()) + LOG.debug(" {}", a); + } + } + return mbean; + } + + static MetaData findMetaData(MBeanContainer container, Class klass) + { + if (klass == null) + return null; + MetaData metaData = getMetaData(container, klass); + if (metaData != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("Found cached {}", metaData); + return metaData; + } + return newMetaData(container, klass); + } + + private static MetaData getMetaData(MBeanContainer container, Class klass) + { + return container == null ? null : container._metaData.get(klass); + } + + private static MetaData newMetaData(MBeanContainer container, Class klass) + { + if (klass == null) + return null; + if (klass == Object.class) + return new MetaData(klass, null, null, Collections.emptyList()); + + List interfaces = Arrays.stream(klass.getInterfaces()) + .map(intf -> findMetaData(container, intf)) + .collect(Collectors.toList()); + MetaData metaData = new MetaData(klass, findConstructor(klass), findMetaData(container, klass.getSuperclass()), interfaces); + + if (container != null) + { + if (container.isUseCacheForOtherClassLoaders() || klass.getClassLoader() == container.getClass().getClassLoader()) + { + MetaData existing = container._metaData.putIfAbsent(klass, metaData); + if (existing != null) + metaData = existing; + if (LOG.isDebugEnabled()) + LOG.debug("Cached {}", metaData); + } + } + + return metaData; + } + + private static Constructor findConstructor(Class klass) + { + String pName = klass.getPackage().getName(); + String cName = klass.getName().substring(pName.length() + 1); + String mName = pName + ".jmx." + cName + "MBean"; + try + { + Class mbeanClass = Loader.loadClass(mName); + Constructor constructor = ModelMBean.class.isAssignableFrom(mbeanClass) + ? mbeanClass.getConstructor() + : mbeanClass.getConstructor(Object.class); + if (LOG.isDebugEnabled()) + LOG.debug("Found MBean wrapper: {} for {}", mName, klass.getName()); + return constructor; + } + catch (Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("MBean wrapper not found: {} for {}", mName, klass.getName()); + return null; + } + } + /** * Lookup an object name by instance * @@ -81,47 +265,6 @@ public class MBeanContainer implements Container.InheritedListener, Dumpable, De return null; } - /** - * Constructs MBeanContainer - * - * @param server instance of MBeanServer for use by container - */ - public MBeanContainer(MBeanServer server) - { - _mbeanServer = server; - } - - /** - * Retrieve instance of MBeanServer used by container - * - * @return instance of MBeanServer - */ - public MBeanServer getMBeanServer() - { - return _mbeanServer; - } - - /** - * Set domain to be used to add MBeans - * - * @param domain domain name - */ - public void setDomain(String domain) - { - _domain = domain; - } - - /** - * Retrieve domain name used to add MBeans - * - * @return domain name - */ - public String getDomain() - { - return _domain; - } - - @Override public void beanAdded(Container parent, Object obj) { @@ -154,14 +297,13 @@ public class MBeanContainer implements Container.InheritedListener, Dumpable, De try { // Create an MBean for the object. - Object mbean = ObjectMBean.mbeanFor(obj); + Object mbean = mbeanFor(obj); if (mbean == null) return; ObjectName objectName = null; if (mbean instanceof ObjectMBean) { - ((ObjectMBean)mbean).setMBeanContainer(this); objectName = ((ObjectMBean)mbean).getObjectName(); } @@ -256,7 +398,7 @@ public class MBeanContainer implements Container.InheritedListener, Dumpable, De @Override public void dump(Appendable out, String indent) throws IOException { - ContainerLifeCycle.dumpObject(out,this); + ContainerLifeCycle.dumpObject(out, this); ContainerLifeCycle.dump(out, indent, _mbeans.entrySet()); } @@ -269,6 +411,7 @@ public class MBeanContainer implements Container.InheritedListener, Dumpable, De @Override public void destroy() { + _metaData.clear(); _mbeans.values().stream() .filter(Objects::nonNull) .forEach(this::unregister); diff --git a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MetaData.java b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MetaData.java new file mode 100644 index 00000000000..c290c727770 --- /dev/null +++ b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MetaData.java @@ -0,0 +1,566 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.jmx; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.management.Attribute; +import javax.management.AttributeNotFoundException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanConstructorInfo; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanNotificationInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanParameterInfo; +import javax.management.ObjectName; +import javax.management.ReflectionException; +import javax.management.modelmbean.ModelMBean; + +import org.eclipse.jetty.util.annotation.ManagedAttribute; +import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.annotation.ManagedOperation; +import org.eclipse.jetty.util.annotation.Name; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +class MetaData +{ + private static final Logger LOG = Log.getLogger(MetaData.class); + private static final MBeanAttributeInfo[] NO_ATTRIBUTES = new MBeanAttributeInfo[0]; + private static final MBeanConstructorInfo[] NO_CONSTRUCTORS = new MBeanConstructorInfo[0]; + private static final MBeanOperationInfo[] NO_OPERATIONS = new MBeanOperationInfo[0]; + private static final MBeanNotificationInfo[] NO_NOTIFICATIONS = new MBeanNotificationInfo[0]; + + private final Map _attributes = new HashMap<>(); + private final Map _operations = new HashMap<>(); + private final Class _klass; + private final MetaData _parent; + private final List _interfaces; + private final Constructor _constructor; + private final MBeanInfo _info; + + MetaData(Class klass, Constructor constructor, MetaData parent, List interfaces) + { + _klass = klass; + _parent = parent; + _interfaces = interfaces; + _constructor = constructor; + if (_constructor != null) + parseMethods(klass, _constructor.getDeclaringClass()); + else + parseMethods(klass); + _info = buildMBeanInfo(klass); + } + + Object newInstance(Object bean) + { + Object mbean; + if (_constructor != null) + mbean = newInstance(_constructor, bean); + else if (_parent != null) + mbean = _parent.newInstance(bean); + else + mbean = new ObjectMBean(bean); + return mbean; + } + + MBeanInfo getMBeanInfo() + { + return _info; + } + + Object getAttribute(String name, ObjectMBean mbean) throws AttributeNotFoundException, ReflectionException, MBeanException + { + AttributeInfo info = findAttribute(name); + if (info == null) + throw new AttributeNotFoundException(name); + return info.getAttribute(mbean); + } + + void setAttribute(Attribute attribute, ObjectMBean mbean) throws AttributeNotFoundException, ReflectionException, MBeanException + { + if (attribute == null) + return; + String name = attribute.getName(); + AttributeInfo info = findAttribute(name); + if (info == null) + throw new AttributeNotFoundException(name); + info.setAttribute(attribute.getValue(), mbean); + } + + private AttributeInfo findAttribute(String name) + { + if (name == null) + return null; + + AttributeInfo result = null; + for (MetaData intf : _interfaces) + { + AttributeInfo r = intf.findAttribute(name); + if (r != null) + result = r; + } + + if (_parent != null) + { + AttributeInfo r = _parent.findAttribute(name); + if (r != null) + result = r; + } + + AttributeInfo r = _attributes.get(name); + if (r != null) + result = r; + + return result; + } + + Object invoke(String name, String[] params, Object[] args, ObjectMBean mbean) throws ReflectionException, MBeanException + { + String signature = signature(name, params); + OperationInfo info = findOperation(signature); + if (info == null) + throw new ReflectionException(new NoSuchMethodException(signature)); + return info.invoke(args, mbean); + } + + private OperationInfo findOperation(String signature) + { + OperationInfo result = null; + for (MetaData intf : _interfaces) + { + OperationInfo r = intf.findOperation(signature); + if (r != null) + result = r; + } + + if (_parent != null) + { + OperationInfo r = _parent.findOperation(signature); + if (r != null) + result = r; + } + + OperationInfo r = _operations.get(signature); + if (r != null) + result = r; + + return result; + } + + private static Object newInstance(Constructor constructor, Object bean) + { + try + { + Object mbean = constructor.getParameterCount() == 0 ? constructor.newInstance() : constructor.newInstance(bean); + if (mbean instanceof ModelMBean) + ((ModelMBean)mbean).setManagedResource(bean, "objectReference"); + return mbean; + } + catch (Throwable x) + { + return null; + } + } + + private void parseMethods(Class... classes) + { + for (Class klass : classes) + { + // Only work on the public method of the class, not of the hierarchy. + for (Method method : klass.getDeclaredMethods()) + { + if (!Modifier.isPublic(method.getModifiers())) + continue; + ManagedAttribute attribute = method.getAnnotation(ManagedAttribute.class); + if (attribute != null) + { + AttributeInfo info = new AttributeInfo(attribute, method); + if (LOG.isDebugEnabled()) + LOG.debug("Found attribute {} for {}: {}", info._name, klass.getName(), info); + _attributes.put(info._name, info); + } + ManagedOperation operation = method.getAnnotation(ManagedOperation.class); + if (operation != null) + { + OperationInfo info = new OperationInfo(operation, method); + if (LOG.isDebugEnabled()) + LOG.debug("Found operation {} for {}: {}", info._name, klass.getName(), info); + _operations.put(info._name, info); + } + } + } + } + + static String toAttributeName(String methodName) + { + String attributeName = methodName; + if (methodName.startsWith("get") || methodName.startsWith("set")) + attributeName = attributeName.substring(3); + else if (methodName.startsWith("is")) + attributeName = attributeName.substring(2); + return attributeName.substring(0, 1).toLowerCase(Locale.ENGLISH) + attributeName.substring(1); + } + + private static boolean isManagedObject(Class klass) + { + if (klass.isArray()) + klass = klass.getComponentType(); + if (klass.isPrimitive()) + return false; + while (klass != null) + { + if (klass.isAnnotationPresent(ManagedObject.class)) + return true; + klass = klass.getSuperclass(); + } + return false; + } + + private static String signature(String name, String[] params) + { + return String.format("%s(%s)", name, String.join(",", params)); + } + + private static String signature(Method method) + { + String signature = Arrays.stream(method.getParameterTypes()) + .map(Class::getName) + .collect(Collectors.joining(",")); + return String.format("%s(%s)", method.getName(), signature); + } + + private MBeanInfo buildMBeanInfo(Class klass) + { + ManagedObject managedObject = klass.getAnnotation(ManagedObject.class); + String description = managedObject == null ? "" : managedObject.value(); + + Map attributeInfos = new HashMap<>(); + collectMBeanAttributeInfos(attributeInfos); + + Map operationInfos = new HashMap<>(); + collectMBeanOperationInfos(operationInfos); + + MBeanInfo mbeanInfo = _parent == null ? null : _parent.getMBeanInfo(); + MBeanAttributeInfo[] attributes = attributeInfos.values().toArray(NO_ATTRIBUTES); + MBeanConstructorInfo[] constructors = mbeanInfo == null ? NO_CONSTRUCTORS : mbeanInfo.getConstructors(); + MBeanOperationInfo[] operations = operationInfos.values().toArray(NO_OPERATIONS); + MBeanNotificationInfo[] notifications = mbeanInfo == null ? NO_NOTIFICATIONS : mbeanInfo.getNotifications(); + return new MBeanInfo(klass.getName(), description, attributes, constructors, operations, notifications); + } + + private void collectMBeanAttributeInfos(Map attributeInfos) + { + // Start with interfaces, overwrite with superClass, then overwrite with local attributes. + for (MetaData intf : _interfaces) + intf.collectMBeanAttributeInfos(attributeInfos); + if (_parent != null) + { + MBeanAttributeInfo[] parentAttributes = _parent.getMBeanInfo().getAttributes(); + for (MBeanAttributeInfo parentAttribute : parentAttributes) + attributeInfos.put(parentAttribute.getName(), parentAttribute); + } + for (Map.Entry entry : _attributes.entrySet()) + attributeInfos.put(entry.getKey(), entry.getValue()._info); + } + + private void collectMBeanOperationInfos(Map operationInfos) + { + // Start with interfaces, overwrite with superClass, then overwrite with local operations. + for (MetaData intf : _interfaces) + intf.collectMBeanOperationInfos(operationInfos); + if (_parent != null) + { + MBeanOperationInfo[] parentOperations = _parent.getMBeanInfo().getOperations(); + for (MBeanOperationInfo parentOperation : parentOperations) + { + String signature = signature(parentOperation.getName(), Arrays.stream(parentOperation.getSignature()).map(MBeanParameterInfo::getType).toArray(String[]::new)); + operationInfos.put(signature, parentOperation); + } + } + for (Map.Entry entry : _operations.entrySet()) + operationInfos.put(entry.getKey(), entry.getValue()._info); + } + + private static MBeanException toMBeanException(InvocationTargetException x) + { + Throwable cause = x.getCause(); + if (cause instanceof Exception) + return new MBeanException((Exception)cause); + else + return new MBeanException(x); + } + + @Override + public String toString() + { + return String.format("%s@%x[%s, attrs=%s, opers=%s]", getClass().getSimpleName(), hashCode(), + _klass.getName(), _attributes.keySet(), _operations.keySet()); + } + + private static class AttributeInfo + { + private final String _name; + private final Method _getter; + private final Method _setter; + private final boolean _proxied; + private final boolean _convert; + private final MBeanAttributeInfo _info; + + private AttributeInfo(ManagedAttribute attribute, Method getter) + { + String name = attribute.name(); + if ("".equals(name)) + name = toAttributeName(getter.getName()); + _name = name; + + _getter = getter; + + boolean readOnly = attribute.readonly(); + _setter = readOnly ? null : findSetter(attribute, getter, name); + + _proxied = attribute.proxied(); + + Class returnType = getter.getReturnType(); + _convert = isManagedObject(returnType); + String signature = _convert ? + returnType.isArray() ? ObjectName[].class.getName() : ObjectName.class.getName() : + returnType.getName(); + + String description = attribute.value(); + _info = new MBeanAttributeInfo(name, signature, description, true, + _setter != null, getter.getName().startsWith("is")); + } + + Object getAttribute(ObjectMBean mbean) throws ReflectionException, MBeanException + { + try + { + Object target = mbean.getManagedObject(); + if (_proxied || _getter.getDeclaringClass().isInstance(mbean)) + target = mbean; + Object result = _getter.invoke(target); + if (result == null) + return null; + if (!_convert) + return result; + if (!_getter.getReturnType().isArray()) + return mbean.findObjectName(result); + int length = Array.getLength(result); + ObjectName[] names = new ObjectName[length]; + for (int i = 0; i < length; ++i) + names[i] = mbean.findObjectName(Array.get(result, i)); + return names; + } + catch (InvocationTargetException x) + { + throw toMBeanException(x); + } + catch (Exception x) + { + throw new ReflectionException(x); + } + } + + void setAttribute(Object value, ObjectMBean mbean) throws ReflectionException, MBeanException + { + if (LOG.isDebugEnabled()) + LOG.debug("setAttribute {}.{}={} {}", mbean, _info.getName(), value, _info); + try + { + if (_setter == null) + return; + Object target = mbean.getManagedObject(); + if (_proxied || _setter.getDeclaringClass().isInstance(mbean)) + target = mbean; + if (!_convert || value == null) + { + _setter.invoke(target, value); + return; + } + if (!_getter.getReturnType().isArray()) + { + value = mbean.findBean((ObjectName)value); + _setter.invoke(target, value); + return; + } + ObjectName[] names = (ObjectName[])value; + Object result = new Object[names.length]; + for (int i = 0; i < names.length; ++i) + Array.set(result, i, mbean.findBean(names[i])); + _setter.invoke(target, result); + } + catch (InvocationTargetException x) + { + throw toMBeanException(x); + } + catch (Exception x) + { + throw new ReflectionException(x); + } + } + + private Method findSetter(ManagedAttribute attribute, Method getter, String name) + { + String setterName = attribute.setter(); + if ("".equals(setterName)) + setterName = "set" + name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1); + + Method setter = null; + Class klass = getter.getDeclaringClass(); + for (Method method : klass.getMethods()) + { + if (method.getName().equals(setterName) && method.getParameterCount() == 1) + { + if (setter != null) + { + LOG.info("Multiple setters for mbean attribute {} in {}", name, klass); + continue; + } + if (!getter.getReturnType().equals(method.getParameterTypes()[0])) + { + LOG.info("Getter/setter type mismatch for mbean attribute {} in {}", name, klass); + continue; + } + setter = method; + } + } + + return setter; + } + + @Override + public String toString() + { + return String.format("%s@%x[%s,proxied=%b,convert=%b,info=%s]", getClass().getSimpleName(), hashCode(), + _name, _proxied, _convert, _info); + } + } + + private static class OperationInfo + { + private final String _name; + private final Method _method; + private final boolean _proxied; + private final boolean _convert; + private final MBeanOperationInfo _info; + + private OperationInfo(ManagedOperation operation, Method method) + { + _name = signature(method); + + _method = method; + + _proxied = operation.proxied(); + + Class returnType = method.getReturnType(); + _convert = isManagedObject(returnType); + String returnSignature = _convert ? + returnType.isArray() ? ObjectName[].class.getName() : ObjectName.class.getName() : + returnType.getName(); + + String impactName = operation.impact(); + int impact = MBeanOperationInfo.UNKNOWN; + if ("ACTION".equals(impactName)) + impact = MBeanOperationInfo.ACTION; + else if ("INFO".equals(impactName)) + impact = MBeanOperationInfo.INFO; + else if ("ACTION_INFO".equals(impactName)) + impact = MBeanOperationInfo.ACTION_INFO; + + String description = operation.value(); + MBeanParameterInfo[] parameterInfos = parameters(method.getParameterTypes(), method.getParameterAnnotations()); + _info = new MBeanOperationInfo(method.getName(), description, parameterInfos, returnSignature, impact); + } + + public Object invoke(Object[] args, ObjectMBean mbean) throws ReflectionException, MBeanException + { + if (LOG.isDebugEnabled()) + LOG.debug("invoke {}.{}({}) {}", mbean, _info.getName(), Arrays.asList(args), _info); + try + { + Object target = mbean.getManagedObject(); + if (_proxied || _method.getDeclaringClass().isInstance(mbean)) + target = mbean; + Object result = _method.invoke(target, args); + if (result == null) + return null; + if (!_convert) + return result; + if (!_method.getReturnType().isArray()) + return mbean.findObjectName(result); + int length = Array.getLength(result); + ObjectName[] names = new ObjectName[length]; + for (int i = 0; i < length; ++i) + names[i] = mbean.findObjectName(Array.get(result, i)); + return names; + } + catch (InvocationTargetException x) + { + throw toMBeanException(x); + } + catch (Exception x) + { + throw new ReflectionException(x); + } + } + + private static MBeanParameterInfo[] parameters(Class[] parameterTypes, Annotation[][] parametersAnnotations) + { + MBeanParameterInfo[] result = new MBeanParameterInfo[parameterTypes.length]; + for (int i = 0; i < parametersAnnotations.length; i++) + { + MBeanParameterInfo info = null; + String typeName = parameterTypes[i].getName(); + Annotation[] parameterAnnotations = parametersAnnotations[i]; + for (Annotation parameterAnnotation : parameterAnnotations) + { + if (parameterAnnotation instanceof Name) + { + Name name = (Name)parameterAnnotation; + info = result[i] = new MBeanParameterInfo(name.value(), typeName, name.description()); + break; + } + } + if (info == null) + result[i] = new MBeanParameterInfo("p" + i, typeName, ""); + } + return result; + } + + @Override + public String toString() + { + return String.format("%s@%x[%s,proxied=%b,convert=%b]", getClass().getSimpleName(), hashCode(), + _name, _proxied, _convert); + } + } +} diff --git a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ObjectMBean.java b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ObjectMBean.java index e7010719122..5782ffe660e 100644 --- a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ObjectMBean.java +++ b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ObjectMBean.java @@ -18,42 +18,15 @@ package org.eclipse.jetty.jmx; -import java.lang.annotation.Annotation; -import java.lang.reflect.Array; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - import javax.management.Attribute; import javax.management.AttributeList; import javax.management.AttributeNotFoundException; import javax.management.DynamicMBean; -import javax.management.MBeanAttributeInfo; -import javax.management.MBeanConstructorInfo; import javax.management.MBeanException; import javax.management.MBeanInfo; -import javax.management.MBeanNotificationInfo; -import javax.management.MBeanOperationInfo; -import javax.management.MBeanParameterInfo; import javax.management.ObjectName; import javax.management.ReflectionException; -import javax.management.modelmbean.ModelMBean; -import org.eclipse.jetty.util.Loader; -import org.eclipse.jetty.util.TypeUtil; -import org.eclipse.jetty.util.annotation.ManagedAttribute; -import org.eclipse.jetty.util.annotation.ManagedObject; -import org.eclipse.jetty.util.annotation.ManagedOperation; -import org.eclipse.jetty.util.annotation.Name; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -72,119 +45,11 @@ import org.eclipse.jetty.util.log.Logger; public class ObjectMBean implements DynamicMBean { private static final Logger LOG = Log.getLogger(ObjectMBean.class); - private static final Class[] OBJ_ARG = new Class[]{Object.class}; - private static final String OBJECT_NAME_CLASS = ObjectName.class.getName(); - private static final String OBJECT_NAME_ARRAY_CLASS = ObjectName[].class.getName(); - protected Object _managed; - private MBeanInfo _info; - private Map _getters = new HashMap<>(); - private Map _setters = new HashMap<>(); - private Map _methods = new HashMap<>(); - // set of attributes mined from influence hierarchy - private Set _attributes = new HashSet<>(); - // set of attributes that are automatically converted to ObjectName - // as they represent other managed beans which can be linked to - private Set _convert = new HashSet<>(); - private ClassLoader _loader; + protected final Object _managed; + private MetaData _metaData; private MBeanContainer _mbeanContainer; - /** - *

Creates an ObjectMBean for the given object.

- *

Attempts to create an ObjectMBean for the object by searching the package - * and class name space. For example an object of the type:

- *
-     * class com.acme.MyClass extends com.acme.util.BaseClass implements com.acme.Iface
-     * 
- *

then this method would look for the following classes:

- *
    - *
  • com.acme.jmx.MyClassMBean
  • - *
  • com.acme.util.jmx.BaseClassMBean
  • - *
  • org.eclipse.jetty.jmx.ObjectMBean
  • - *
- * - * @param o The object - * @return A new instance of an MBean for the object or null. - */ - public static Object mbeanFor(Object o) - { - try - { - Class oClass = o.getClass(); - while (oClass != null) - { - String pName = oClass.getPackage().getName(); - String cName = oClass.getName().substring(pName.length() + 1); - String mName = pName + ".jmx." + cName + "MBean"; - - try - { - Class mClass; - try - { - // Look for an MBean class from the same loader that loaded the original class - mClass = (Object.class.equals(oClass)) ? oClass = ObjectMBean.class : Loader.loadClass(oClass, mName); - } - catch (ClassNotFoundException e) - { - // Not found, so if not the same as the thread context loader, try that. - if (Thread.currentThread().getContextClassLoader() == oClass.getClassLoader()) - throw e; - LOG.ignore(e); - mClass = Loader.loadClass(oClass, mName); - } - - if (LOG.isDebugEnabled()) - LOG.debug("ObjectMBean: mbeanFor {} mClass={}", o, mClass); - - Object mbean = null; - try - { - Constructor constructor = mClass.getConstructor(OBJ_ARG); - mbean = constructor.newInstance(o); - } - catch (Exception e) - { - LOG.ignore(e); - if (ModelMBean.class.isAssignableFrom(mClass)) - { - mbean = mClass.getDeclaredConstructor().newInstance(); - ((ModelMBean)mbean).setManagedResource(o, "objectReference"); - } - } - - if (LOG.isDebugEnabled()) - LOG.debug("mbeanFor {} is {}", o, mbean); - - return mbean; - } - catch (ClassNotFoundException e) - { - // The code below was modified to fix bugs 332200 and JETTY-1416 - // The issue was caused by additional information added to the - // message after the class name when running in Apache Felix, - // as well as before the class name when running in JBoss. - if (e.getMessage().contains(mName)) - LOG.ignore(e); - else - LOG.warn(e); - } - catch (Throwable e) - { - LOG.warn(e); - } - - oClass = oClass.getSuperclass(); - } - } - catch (Exception e) - { - LOG.ignore(e); - } - - return null; - } - /** * Creates a new ObjectMBean wrapping the given {@code managedObject}. * @@ -193,7 +58,6 @@ public class ObjectMBean implements DynamicMBean public ObjectMBean(Object managedObject) { _managed = managedObject; - _loader = Thread.currentThread().getContextClassLoader(); } /** @@ -257,177 +121,34 @@ public class ObjectMBean implements DynamicMBean return this._mbeanContainer; } - @Override - public MBeanInfo getMBeanInfo() + /** + * @param o the object to wrap as MBean + * @return a new instance of an MBean for the object or null if the MBean cannot be created + * @deprecated Use {@link MBeanContainer#mbeanFor(Object)} instead + */ + @Deprecated + public static Object mbeanFor(Object o) { - try - { - if (_info == null) - { - String desc = null; - List attributes = new ArrayList<>(); - List operations = new ArrayList<>(); - - // Find list of classes that can influence the mbean - Class o_class = _managed.getClass(); - List> influences = new ArrayList<>(); - influences.add(this.getClass()); // always add MBean itself - influences = findInfluences(influences, _managed.getClass()); - - if (LOG.isDebugEnabled()) - LOG.debug("Influence Count: {}", influences.size()); - - // Process Type Annotations - ManagedObject primary = o_class.getAnnotation(ManagedObject.class); - - if (primary != null) - { - desc = primary.value(); - } - else - { - if (LOG.isDebugEnabled()) - LOG.debug("No @ManagedObject declared on {}", _managed.getClass()); - } - - // For each influence - for (Class oClass : influences) - { - ManagedObject typeAnnotation = oClass.getAnnotation(ManagedObject.class); - - if (LOG.isDebugEnabled()) - LOG.debug("Influenced by: " + oClass.getCanonicalName()); - - if (typeAnnotation == null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Annotations not found for: {}", oClass.getCanonicalName()); - continue; - } - - // Process Method Annotations - - for (Method method : oClass.getDeclaredMethods()) - { - ManagedAttribute methodAttributeAnnotation = method.getAnnotation(ManagedAttribute.class); - - if (methodAttributeAnnotation != null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Attribute Annotation found for: {}", method.getName()); - MBeanAttributeInfo mai = defineAttribute(method, methodAttributeAnnotation); - if (mai != null) - attributes.add(mai); - } - - ManagedOperation methodOperationAnnotation = method.getAnnotation(ManagedOperation.class); - - if (methodOperationAnnotation != null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Method Annotation found for: {}", method.getName()); - MBeanOperationInfo oi = defineOperation(method, methodOperationAnnotation); - if (oi != null) - operations.add(oi); - } - } - } - - _info = new MBeanInfo(o_class.getName(), - desc, - attributes.toArray(new MBeanAttributeInfo[attributes.size()]), - new MBeanConstructorInfo[0], - operations.toArray(new MBeanOperationInfo[operations.size()]), - new MBeanNotificationInfo[0]); - } - } - catch (RuntimeException e) - { - LOG.warn(e); - throw e; - } - return _info; + return MBeanContainer.mbeanFor(null, o); } @Override - public Object getAttribute(String name) throws AttributeNotFoundException, ReflectionException + public MBeanInfo getMBeanInfo() { - Method getter = _getters.get(name); - if (getter == null) - throw new AttributeNotFoundException(name); + return metaData().getMBeanInfo(); + } + @Override + public Object getAttribute(String name) throws AttributeNotFoundException, ReflectionException, MBeanException + { + ClassLoader prevLoader = Thread.currentThread().getContextClassLoader(); try { - Object o = _managed; - if (getter.getDeclaringClass().isInstance(this)) - o = this; // mbean method - - // get the attribute - Object r = getter.invoke(o, (java.lang.Object[])null); - - // convert to ObjectName if the type has the @ManagedObject annotation - if (r != null) - { - if (r.getClass().isArray()) - { - if (r.getClass().getComponentType().isAnnotationPresent(ManagedObject.class)) - { - ObjectName[] on = new ObjectName[Array.getLength(r)]; - for (int i = 0; i < on.length; i++) - on[i] = _mbeanContainer.findMBean(Array.get(r, i)); - r = on; - } - } - else if (r instanceof Collection) - { - @SuppressWarnings("unchecked") - Collection c = (Collection)r; - if (!c.isEmpty() && c.iterator().next().getClass().isAnnotationPresent(ManagedObject.class)) - { - // check the first thing out - ObjectName[] on = new ObjectName[c.size()]; - int i = 0; - for (Object obj : c) - { - on[i++] = _mbeanContainer.findMBean(obj); - } - r = on; - } - } - else - { - Class clazz = r.getClass(); - while (clazz != null) - { - if (clazz.isAnnotationPresent(ManagedObject.class)) - { - ObjectName mbean = _mbeanContainer.findMBean(r); - - if (mbean != null) - { - return mbean; - } - else - { - return null; - } - } - clazz = clazz.getSuperclass(); - } - } - } - - return r; + return metaData().getAttribute(name, this); } - catch (IllegalAccessException e) + finally { - LOG.warn(Log.EXCEPTION, e); - throw new AttributeNotFoundException(e.toString()); - } - catch (InvocationTargetException e) - { - LOG.warn(Log.EXCEPTION, e); - throw new ReflectionException(new Exception(e.getCause())); + Thread.currentThread().setContextClassLoader(prevLoader); } } @@ -441,408 +162,77 @@ public class ObjectMBean implements DynamicMBean { results.add(new Attribute(name, getAttribute(name))); } - catch (Exception e) + catch (Throwable x) { - LOG.warn(Log.EXCEPTION, e); + if (LOG.isDebugEnabled()) + LOG.debug(x); } } return results; } @Override - public void setAttribute(Attribute attr) throws AttributeNotFoundException, ReflectionException + public void setAttribute(Attribute attribute) throws AttributeNotFoundException, ReflectionException, MBeanException { - if (attr == null) - return; - - if (LOG.isDebugEnabled()) - LOG.debug("setAttribute " + _managed + ":" + attr.getName() + "=" + attr.getValue()); - Method setter = _setters.get(attr.getName()); - if (setter == null) - throw new AttributeNotFoundException(attr.getName()); - + ClassLoader prevLoader = Thread.currentThread().getContextClassLoader(); try { - Object o = _managed; - if (setter.getDeclaringClass().isInstance(this)) - o = this; - - // get the value - Object value = attr.getValue(); - - // convert from ObjectName if need be - if (value != null && _convert.contains(attr.getName())) - { - if (value.getClass().isArray()) - { - Class t = setter.getParameterTypes()[0].getComponentType(); - Object na = Array.newInstance(t, Array.getLength(value)); - for (int i = Array.getLength(value); i-- > 0; ) - Array.set(na, i, _mbeanContainer.findBean((ObjectName)Array.get(value, i))); - value = na; - } - else - value = _mbeanContainer.findBean((ObjectName)value); - } - - // do the setting - setter.invoke(o, value); - } - catch (IllegalAccessException e) - { - LOG.warn(Log.EXCEPTION, e); - throw new AttributeNotFoundException(e.toString()); - } - catch (InvocationTargetException e) - { - LOG.warn(Log.EXCEPTION, e); - throw new ReflectionException(new Exception(e.getCause())); - } - } - - @Override - public AttributeList setAttributes(AttributeList attrs) - { - if (LOG.isDebugEnabled()) - LOG.debug("setAttributes"); - - AttributeList results = new AttributeList(attrs.size()); - for (Object element : attrs) - { - try - { - Attribute attr = (Attribute)element; - setAttribute(attr); - results.add(new Attribute(attr.getName(), getAttribute(attr.getName()))); - } - catch (Exception e) - { - LOG.warn(Log.EXCEPTION, e); - } - } - return results; - } - - @Override - public Object invoke(String name, Object[] params, String[] signature) throws MBeanException, ReflectionException - { - if (LOG.isDebugEnabled()) - LOG.debug("ObjectMBean:invoke " + name); - - StringBuilder builder = new StringBuilder(name); - builder.append("("); - if (signature != null) - for (int i = 0; i < signature.length; i++) - builder.append(i > 0 ? "," : "").append(signature[i]); - builder.append(")"); - String methodKey = builder.toString(); - - ClassLoader old_loader = Thread.currentThread().getContextClassLoader(); - try - { - Thread.currentThread().setContextClassLoader(_loader); - Method method = _methods.get(methodKey); - if (method == null) - throw new NoSuchMethodException(methodKey); - - Object o = _managed; - - if (method.getDeclaringClass().isInstance(this)) - o = this; - - return method.invoke(o, params); - } - catch (NoSuchMethodException e) - { - LOG.warn(Log.EXCEPTION, e); - throw new ReflectionException(e); - } - catch (IllegalAccessException e) - { - LOG.warn(Log.EXCEPTION, e); - throw new MBeanException(e); - } - catch (InvocationTargetException e) - { - LOG.warn(Log.EXCEPTION, e); - throw new ReflectionException(new Exception(e.getCause())); + metaData().setAttribute(attribute, this); } finally { - Thread.currentThread().setContextClassLoader(old_loader); + Thread.currentThread().setContextClassLoader(prevLoader); } } - private static List> findInfluences(List> influences, Class aClass) + @Override + public AttributeList setAttributes(AttributeList attributes) { - if (aClass != null) + AttributeList results = new AttributeList(attributes.size()); + for (Attribute attribute : attributes.asList()) { - if (!influences.contains(aClass)) + try { - // This class is a new influence - influences.add(aClass); + setAttribute(attribute); + results.add(new Attribute(attribute.getName(), getAttribute(attribute.getName()))); + } + catch (Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug(x); } - - // So are the super classes - influences = findInfluences(influences, aClass.getSuperclass()); - - // So are the interfaces - Class[] ifs = aClass.getInterfaces(); - for (int i = 0; ifs != null && i < ifs.length; i++) - influences = findInfluences(influences, ifs[i]); } - return influences; + return results; } - /** - *

Defines an attribute for the managed object using the annotation attributes.

- * - * @param method the method on the managed objec - * @param attributeAnnotation the annotation with the attribute metadata - * @return an MBeanAttributeInfo with the attribute metadata - */ - private MBeanAttributeInfo defineAttribute(Method method, ManagedAttribute attributeAnnotation) + @Override + public Object invoke(String name, Object[] params, String[] signature) throws ReflectionException, MBeanException { - // determine the name of the managed attribute - String name = attributeAnnotation.name(); - - if ("".equals(name)) - name = toVariableName(method.getName()); - - if (_attributes.contains(name)) - return null; // we have an attribute named this already - - String description = attributeAnnotation.value(); - boolean readonly = attributeAnnotation.readonly(); - boolean onMBean = attributeAnnotation.proxied(); - - // determine if we should convert - Class return_type = method.getReturnType(); - - // get the component type - Class component_type = return_type; - while (component_type.isArray()) - component_type = component_type.getComponentType(); - - // Test to see if the returnType or any of its super classes are managed objects - boolean convert = isAnnotationPresent(component_type, ManagedObject.class); - - String uName = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1); - Class oClass = onMBean ? this.getClass() : _managed.getClass(); - - if (LOG.isDebugEnabled()) - LOG.debug("defineAttribute {} {}:{}:{}:{}", name, onMBean, readonly, oClass, description); - - Method setter = null; - - // dig out a setter if one exists - if (!readonly) - { - String declaredSetter = attributeAnnotation.setter(); - - if (LOG.isDebugEnabled()) - LOG.debug("DeclaredSetter: {}", declaredSetter); - - for (Method method1 : oClass.getMethods()) - { - if (!Modifier.isPublic(method1.getModifiers())) - continue; - - if (!"".equals(declaredSetter)) - { - // look for a declared setter - if (method1.getName().equals(declaredSetter) && method1.getParameterCount() == 1) - { - if (setter != null) - { - LOG.warn("Multiple setters for mbean attr {} in {}", name, oClass); - continue; - } - setter = method1; - if (!component_type.equals(method1.getParameterTypes()[0])) - { - LOG.warn("Type conflict for mbean attr {} in {}", name, oClass); - continue; - } - if (LOG.isDebugEnabled()) - LOG.debug("Declared Setter: " + declaredSetter); - } - } - - // look for a setter - if (method1.getName().equals("set" + uName) && method1.getParameterCount() == 1) - { - if (setter != null) - { - LOG.warn("Multiple setters for mbean attr {} in {}", name, oClass); - continue; - } - setter = method1; - if (!return_type.equals(method1.getParameterTypes()[0])) - LOG.warn("Type conflict for mbean attr {} in {}", name, oClass); - } - } - } - - if (convert) - { - if (component_type.isPrimitive() && !component_type.isArray()) - { - LOG.warn("Cannot convert mbean primitive {}", name); - return null; - } - if (LOG.isDebugEnabled()) - LOG.debug("passed convert checks {} for type {}", name, component_type); - } - + ClassLoader prevLoader = Thread.currentThread().getContextClassLoader(); try { - // Remember the methods - _getters.put(name, method); - _setters.put(name, setter); - - MBeanAttributeInfo info; - if (convert) - { - _convert.add(name); - info = new MBeanAttributeInfo(name, - component_type.isArray() ? OBJECT_NAME_ARRAY_CLASS : OBJECT_NAME_CLASS, - description, - true, - setter != null, - method.getName().startsWith("is")); - } - else - { - info = new MBeanAttributeInfo(name, description, method, setter); - } - - _attributes.add(name); - - return info; + return metaData().invoke(name, signature, params, this); } - catch (Exception e) + finally { - LOG.warn(e); - throw new IllegalArgumentException(e.toString()); + Thread.currentThread().setContextClassLoader(prevLoader); } } - /** - *

Defines an operation for the managed object using the annotation attributes.

- * - * @param method the method on the managed object - * @param methodAnnotation the annotation with the operation metadata - * @return an MBeanOperationInfo with the operation metadata - */ - private MBeanOperationInfo defineOperation(Method method, ManagedOperation methodAnnotation) + ObjectName findObjectName(Object bean) { - String description = methodAnnotation.value(); - boolean onMBean = methodAnnotation.proxied(); - - // determine if we should convert - Class returnType = method.getReturnType(); - - if (returnType.isArray()) - { - if (LOG.isDebugEnabled()) - LOG.debug("returnType is array, get component type"); - returnType = returnType.getComponentType(); - } - - boolean convert = false; - if (returnType.isAnnotationPresent(ManagedObject.class)) - convert = true; - - String impactName = methodAnnotation.impact(); - - if (LOG.isDebugEnabled()) - LOG.debug("defineOperation {} {}:{}:{}", method.getName(), onMBean, impactName, description); - - try - { - // Resolve the impact - int impact = MBeanOperationInfo.UNKNOWN; - if ("UNKNOWN".equals(impactName)) - impact = MBeanOperationInfo.UNKNOWN; - else if ("ACTION".equals(impactName)) - impact = MBeanOperationInfo.ACTION; - else if ("INFO".equals(impactName)) - impact = MBeanOperationInfo.INFO; - else if ("ACTION_INFO".equals(impactName)) - impact = MBeanOperationInfo.ACTION_INFO; - else - LOG.warn("Unknown impact '" + impactName + "' for " + method); - - Annotation[][] allParameterAnnotations = method.getParameterAnnotations(); - Class[] methodTypes = method.getParameterTypes(); - MBeanParameterInfo[] pInfo = new MBeanParameterInfo[allParameterAnnotations.length]; - - for (int i = 0; i < allParameterAnnotations.length; ++i) - { - Annotation[] parameterAnnotations = allParameterAnnotations[i]; - for (Annotation anno : parameterAnnotations) - { - if (anno instanceof Name) - { - Name nameAnnotation = (Name)anno; - pInfo[i] = new MBeanParameterInfo(nameAnnotation.value(), methodTypes[i].getName(), nameAnnotation.description()); - } - } - } - - StringBuilder builder = new StringBuilder(method.getName()); - builder.append("("); - for (int i = 0; i < methodTypes.length; ++i) - { - builder.append(methodTypes[i].getName()); - if (i != methodTypes.length - 1) - builder.append(","); - } - builder.append(")"); - String signature = builder.toString(); - - Class returnClass = method.getReturnType(); - - if (LOG.isDebugEnabled()) - LOG.debug("Method Cache: " + signature); - - if (_methods.containsKey(signature)) - return null; // we have an operation for this already - - _methods.put(signature, method); - if (convert) - _convert.add(signature); - - return new MBeanOperationInfo(method.getName(), description, pInfo, returnClass.isPrimitive() ? TypeUtil.toName(returnClass) : (returnClass.getName()), impact); - } - catch (Exception e) - { - LOG.warn("Operation '" + method + "'", e); - throw new IllegalArgumentException(e.toString()); - } + return _mbeanContainer.findMBean(bean); } - protected String toVariableName(String methodName) + Object findBean(ObjectName objectName) { - String variableName = methodName; - if (methodName.startsWith("get") || methodName.startsWith("set")) - variableName = variableName.substring(3); - else if (methodName.startsWith("is")) - variableName = variableName.substring(2); - return variableName.substring(0, 1).toLowerCase(Locale.ENGLISH) + variableName.substring(1); + return _mbeanContainer.findBean(objectName); } - protected boolean isAnnotationPresent(Class clazz, Class annotation) + MetaData metaData() { - Class test = clazz; - while (test != null) - { - if (test.isAnnotationPresent(annotation)) - return true; - else - test = test.getSuperclass(); - } - return false; + if (_metaData == null) + _metaData = MBeanContainer.findMetaData(_mbeanContainer, _managed.getClass()); + return _metaData; } } diff --git a/jetty-jmx/src/test/java/com/acme/DerivedExtended.java b/jetty-jmx/src/test/java/com/acme/DerivedExtended.java index b25280e2154..daa3f34aea5 100644 --- a/jetty-jmx/src/test/java/com/acme/DerivedExtended.java +++ b/jetty-jmx/src/test/java/com/acme/DerivedExtended.java @@ -25,7 +25,6 @@ import org.eclipse.jetty.util.annotation.ManagedOperation; @ManagedObject(value = "Test the mbean extended stuff") public class DerivedExtended extends Derived { - private String doodle4 = "doodle4"; @ManagedAttribute(value = "The doodle4 name of something", name = "doodle4", setter = "setDoodle4") diff --git a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/MBeanContainerTest.java b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/MBeanContainerTest.java index ded64808426..630c95374a8 100644 --- a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/MBeanContainerTest.java +++ b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/MBeanContainerTest.java @@ -18,24 +18,23 @@ package org.eclipse.jetty.jmx; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.acme.Managed; - import java.lang.management.ManagementFactory; import javax.management.MBeanServer; import javax.management.ObjectName; +import com.acme.Managed; import org.eclipse.jetty.util.component.Container; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class MBeanContainerTest { private MBeanContainer mbeanContainer; @@ -54,27 +53,21 @@ public class MBeanContainerTest @Test public void testMakeName() { - // given beanName = "mngd:bean"; - // when beanName = mbeanContainer.makeName(beanName); - // then assertEquals("mngd_bean", beanName, "Bean name should be mngd_bean"); } @Test public void testFindBean() { - // given managed = getManaged(); - // when objectName = mbeanContainer.findMBean(managed); assertNotNull(objectName); - // then assertEquals(managed, mbeanContainer.findBean(objectName), "Bean must be added"); assertNull(mbeanContainer.findBean(null), "It must return null as there is no bean with the name null"); } @@ -104,40 +97,31 @@ public class MBeanContainerTest @Test public void testDomain() { - // given String domain = "Test"; - // when mbeanContainer.setDomain(domain); - // then assertEquals(domain, mbeanContainer.getDomain(), "Domain name must be Test"); } @Test - public void testBeanAdded() throws Exception + public void testBeanAdded() { - // given setBeanAdded(); - // when objectName = mbeanContainer.findMBean(managed); - // then assertTrue(mbeanServer.isRegistered(objectName), "Bean must have been registered"); } @Test - public void testBeanAddedNullCheck() throws Exception + public void testBeanAddedNullCheck() { - // given setBeanAdded(); Integer mbeanCount = mbeanServer.getMBeanCount(); - // when mbeanContainer.beanAdded(null, null); - // then assertEquals(mbeanCount, mbeanServer.getMBeanCount(), "MBean count must not change after beanAdded(null, null) call"); } @@ -150,15 +134,12 @@ public class MBeanContainerTest } @Test - public void testBeanRemoved() throws Exception + public void testBeanRemoved() { - // given setUpBeanRemoved(); - // when mbeanContainer.beanRemoved(null, managed); - // then assertNull(mbeanContainer.findMBean(managed), "Bean shouldn't be registered with container as we removed the bean"); } @@ -200,30 +181,24 @@ public class MBeanContainerTest } @Test - public void testDestroy() throws Exception + public void testDestroy() { - // given setUpDestroy(); - // when objectName = mbeanContainer.findMBean(managed); mbeanContainer.destroy(); - // then assertFalse(mbeanContainer.getMBeanServer().isRegistered(objectName), "Unregistered bean - managed"); } @Test public void testDestroyInstanceNotFoundException() throws Exception { - // given setUpDestroy(); - // when objectName = mbeanContainer.findMBean(managed); mbeanContainer.getMBeanServer().unregisterMBean(objectName); - // then assertFalse(mbeanContainer.getMBeanServer().isRegistered(objectName), "Unregistered bean - managed"); // this flow covers InstanceNotFoundException. Actual code just eating // the exception. i.e Actual code just printing the stacktrace, whenever diff --git a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ObjectMBeanTest.java b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ObjectMBeanTest.java index 0c0b9f95223..00b6b81b952 100644 --- a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ObjectMBeanTest.java +++ b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ObjectMBeanTest.java @@ -18,12 +18,6 @@ package org.eclipse.jetty.jmx; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.acme.Derived; - import java.lang.management.ManagementFactory; import javax.management.Attribute; @@ -31,198 +25,127 @@ import javax.management.MBeanInfo; import javax.management.MBeanOperationInfo; import javax.management.MBeanParameterInfo; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.QueuedThreadPool; +import com.acme.Derived; +import com.acme.Managed; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class ObjectMBeanTest { - private static final Logger LOG = Log.getLogger(ObjectMBeanTest.class); - - private static MBeanContainer container; + private MBeanContainer container; @BeforeEach - public void before() throws Exception + public void before() { container = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); } @AfterEach - public void after() throws Exception + public void after() { container.destroy(); container = null; } - /* - * this test uses the com.acme.Derived test classes - */ + @Test + public void testMetaDataCaching() + { + Derived derived = new Derived(); + ObjectMBean derivedMBean = (ObjectMBean)container.mbeanFor(derived); + ObjectMBean derivedMBean2 = (ObjectMBean)container.mbeanFor(derived); + assertNotSame(derivedMBean, derivedMBean2); + assertSame(derivedMBean.metaData(), derivedMBean2.metaData()); + } + @Test public void testDerivedAttributes() throws Exception { Derived derived = new Derived(); - ObjectMBean mbean = (ObjectMBean)ObjectMBean.mbeanFor(derived); + Managed managed = derived.getManagedInstance(); + ObjectMBean derivedMBean = (ObjectMBean)container.mbeanFor(derived); + ObjectMBean managedMBean = (ObjectMBean)container.mbeanFor(managed); - ObjectMBean managed = (ObjectMBean)ObjectMBean.mbeanFor(derived.getManagedInstance()); - mbean.setMBeanContainer(container); - managed.setMBeanContainer(container); + container.beanAdded(null, derived); + container.beanAdded(null, managed); - container.beanAdded(null,derived); - container.beanAdded(null,derived.getManagedInstance()); + MBeanInfo derivedInfo = derivedMBean.getMBeanInfo(); + assertNotNull(derivedInfo); + MBeanInfo managedInfo = managedMBean.getMBeanInfo(); + assertNotNull(managedInfo); - MBeanInfo toss = managed.getMBeanInfo(); + assertEquals("com.acme.Derived", derivedInfo.getClassName(), "name does not match"); + assertEquals("Test the mbean stuff", derivedInfo.getDescription(), "description does not match"); + assertEquals(6, derivedInfo.getAttributes().length, "attribute count does not match"); + assertEquals("Full Name", derivedMBean.getAttribute("fname"), "attribute values does not match"); - assertNotNull(mbean.getMBeanInfo()); - - MBeanInfo info = mbean.getMBeanInfo(); - - assertEquals("com.acme.Derived", info.getClassName(), "name does not match"); - assertEquals("Test the mbean stuff", info.getDescription(), "description does not match"); - - // for ( MBeanAttributeInfo i : info.getAttributes()) - // { - // LOG.debug(i.toString()); - // } - - /* - * 2 attributes from lifecycle and 2 from Derived and 1 from MBean - */ - assertEquals(6, info.getAttributes().length, "attribute count does not match"); - - assertEquals("Full Name", mbean.getAttribute("fname"), "attribute values does not match"); - - mbean.setAttribute(new Attribute("fname","Fuller Name")); - - assertEquals("Fuller Name", mbean.getAttribute("fname"), "set attribute value does not match"); - - assertEquals("goop", mbean.getAttribute("goop"), "proxy attribute values do not match"); - - // Thread.sleep(100000); + derivedMBean.setAttribute(new Attribute("fname", "Fuller Name")); + assertEquals("Fuller Name", derivedMBean.getAttribute("fname"), "set attribute value does not match"); + assertEquals("goop", derivedMBean.getAttribute("goop"), "proxy attribute values do not match"); } @Test public void testDerivedOperations() throws Exception { Derived derived = new Derived(); - ObjectMBean mbean = (ObjectMBean)ObjectMBean.mbeanFor(derived); + ObjectMBean mbean = (ObjectMBean)container.mbeanFor(derived); - mbean.setMBeanContainer(container); - - container.beanAdded(null,derived); + container.beanAdded(null, derived); MBeanInfo info = mbean.getMBeanInfo(); - assertEquals(5, info.getOperations().length, "operation count does not match"); - MBeanOperationInfo[] opinfos = info.getOperations(); + MBeanOperationInfo[] operationInfos = info.getOperations(); boolean publish = false; boolean doodle = false; boolean good = false; - for (int i = 0; i < opinfos.length; ++i) + for (MBeanOperationInfo operationInfo : operationInfos) { - MBeanOperationInfo opinfo = opinfos[i]; - - if ("publish".equals(opinfo.getName())) + if ("publish".equals(operationInfo.getName())) { publish = true; - assertEquals("publish something", opinfo.getDescription(), "description doesn't match"); + assertEquals("publish something", operationInfo.getDescription(), "description doesn't match"); } - if ("doodle".equals(opinfo.getName())) + if ("doodle".equals(operationInfo.getName())) { doodle = true; - assertEquals("Doodle something", opinfo.getDescription(), "description doesn't match"); - - MBeanParameterInfo[] pinfos = opinfo.getSignature(); - - assertEquals("A description of the argument", pinfos[0].getDescription(), "parameter description doesn't match"); - assertEquals("doodle", pinfos[0].getName(), "parameter name doesn't match"); + assertEquals("Doodle something", operationInfo.getDescription(), "description doesn't match"); + MBeanParameterInfo[] parameterInfos = operationInfo.getSignature(); + assertEquals("A description of the argument", parameterInfos[0].getDescription(), "parameter description doesn't match"); + assertEquals("doodle", parameterInfos[0].getName(), "parameter name doesn't match"); } - // This is a proxied operation on the JMX wrapper - if ("good".equals(opinfo.getName())) + // This is a proxied operation on the MBean wrapper. + if ("good".equals(operationInfo.getName())) { good = true; - - assertEquals("test of proxy operations", opinfo.getDescription(), "description does not match"); - assertEquals("not bad",mbean.invoke("good",new Object[] {}, new String[] {}), "execution contexts wrong"); + assertEquals("test of proxy operations", operationInfo.getDescription(), "description does not match"); + assertEquals("not bad", mbean.invoke("good", new Object[]{}, new String[]{}), "execution contexts wrong"); } } assertTrue(publish, "publish operation was not not found"); assertTrue(doodle, "doodle operation was not not found"); assertTrue(good, "good operation was not not found"); - } @Test - public void testDerivedObjectAttributes() throws Exception + public void testMethodNameMining() { - Derived derived = new Derived(); - ObjectMBean mbean = (ObjectMBean)ObjectMBean.mbeanFor(derived); - - ObjectMBean managed = (ObjectMBean)ObjectMBean.mbeanFor(derived.getManagedInstance()); - mbean.setMBeanContainer(container); - managed.setMBeanContainer(container); - - assertNotNull(mbean.getMBeanInfo()); - - container.beanAdded(null,derived); - container.beanAdded(null,derived.getManagedInstance()); - container.beanAdded(null,mbean); - container.beanAdded(null,managed); - - // Managed managedInstance = (Managed)mbean.getAttribute("managedInstance"); - // assertNotNull(managedInstance); - // assertEquals("foo", managedInstance.getManaged(), "managed instance returning nonsense"); - + assertEquals("fullName", MetaData.toAttributeName("getFullName")); + assertEquals("fullName", MetaData.toAttributeName("getfullName")); + assertEquals("fullName", MetaData.toAttributeName("isFullName")); + assertEquals("fullName", MetaData.toAttributeName("isfullName")); + assertEquals("fullName", MetaData.toAttributeName("setFullName")); + assertEquals("fullName", MetaData.toAttributeName("setfullName")); + assertEquals("fullName", MetaData.toAttributeName("FullName")); + assertEquals("fullName", MetaData.toAttributeName("fullName")); } - - @Test - @Disabled("ignore, used in testing jconsole atm") - public void testThreadPool() throws Exception - { - - Derived derived = new Derived(); - ObjectMBean mbean = (ObjectMBean)ObjectMBean.mbeanFor(derived); - - ObjectMBean managed = (ObjectMBean)ObjectMBean.mbeanFor(derived.getManagedInstance()); - mbean.setMBeanContainer(container); - managed.setMBeanContainer(container); - - QueuedThreadPool qtp = new QueuedThreadPool(); - - ObjectMBean bqtp = (ObjectMBean)ObjectMBean.mbeanFor(qtp); - - bqtp.getMBeanInfo(); - - container.beanAdded(null,derived); - container.beanAdded(null,derived.getManagedInstance()); - container.beanAdded(null,mbean); - container.beanAdded(null,managed); - container.beanAdded(null,qtp); - - Thread.sleep(10000000); - - } - - @Test - public void testMethodNameMining() throws Exception - { - ObjectMBean mbean = new ObjectMBean(new Derived()); - - assertEquals("fullName",mbean.toVariableName("getFullName")); - assertEquals("fullName",mbean.toVariableName("getfullName")); - assertEquals("fullName",mbean.toVariableName("isFullName")); - assertEquals("fullName",mbean.toVariableName("isfullName")); - assertEquals("fullName",mbean.toVariableName("setFullName")); - assertEquals("fullName",mbean.toVariableName("setfullName")); - assertEquals("fullName",mbean.toVariableName("FullName")); - assertEquals("fullName",mbean.toVariableName("fullName")); - } - } diff --git a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ObjectMBeanUtilTest.java b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ObjectMBeanUtilTest.java index 16c3c313a5d..0f1321bbbfe 100644 --- a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ObjectMBeanUtilTest.java +++ b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ObjectMBeanUtilTest.java @@ -18,10 +18,6 @@ package org.eclipse.jetty.jmx; -import com.acme.Derived; -import com.acme.DerivedExtended; -import com.acme.DerivedManaged; - import java.lang.management.ManagementFactory; import java.util.ArrayList; import java.util.Arrays; @@ -33,68 +29,33 @@ import javax.management.MBeanException; import javax.management.MBeanInfo; import javax.management.ReflectionException; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.log.StdErrLog; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import com.acme.Derived; +import com.acme.DerivedExtended; +import com.acme.DerivedManaged; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; public class ObjectMBeanUtilTest { - private ObjectMBean objectMBean; - private DerivedExtended derivedExtended; - private MBeanContainer container; - private MBeanInfo objectMBeanInfo; - - private Object mBean; - - private String value; - private Attribute attribute; - - private AttributeList attributes; - private ObjectMBean mBeanDerivedManaged; - - private Derived[] derivedes; - - private ArrayList aliasNames; - private DerivedManaged derivedManaged; - private static final int EMPTY = 0; - - @BeforeAll - public static void beforeClass() - { - Logger ombLog = Log.getLogger(ObjectMBean.class); - if (ombLog instanceof StdErrLog && !ombLog.isDebugEnabled()) - ((StdErrLog)ombLog).setHideStacks(true); - } - - @AfterAll - public static void afterClass() - { - Logger ombLog = Log.getLogger(ObjectMBean.class); - if (ombLog instanceof StdErrLog) - ((StdErrLog)ombLog).setHideStacks(false); - } - @BeforeEach public void setUp() { - derivedExtended = new DerivedExtended(); - objectMBean = new ObjectMBean(derivedExtended); container = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); - objectMBean.setMBeanContainer(container); + derivedExtended = new DerivedExtended(); + objectMBean = (ObjectMBean)container.mbeanFor(derivedExtended); objectMBeanInfo = objectMBean.getMBeanInfo(); } @@ -112,131 +73,93 @@ public class ObjectMBeanUtilTest @Test public void testMbeanForNullCheck() { - // when - mBean = ObjectMBean.mbeanFor(null); - - // then + Object mBean = container.mbeanFor(null); assertNull(mBean, "As we are passing null value the output should be null"); } @Test - public void testGetAttributeReflectionException() throws Exception + public void testGetAttributeMBeanException() throws Exception { - // given - setUpGetAttribute("doodle4","charu"); + Attribute attribute = new Attribute("doodle4", "charu"); + objectMBean.setAttribute(attribute); - // when - ReflectionException e = assertThrows(ReflectionException.class, ()-> { - objectMBean.getAttribute("doodle4"); - }); + MBeanException e = assertThrows(MBeanException.class, () -> objectMBean.getAttribute("doodle4")); - // then assertNotNull(e, "An InvocationTargetException must have occurred by now as doodle4() internally throwing exception"); } - private void setUpGetAttribute(String property, String value) throws Exception - { - Attribute attribute = new Attribute(property,value); - objectMBean.setAttribute(attribute); - } - @Test - public void testGetAttributeAttributeNotFoundException() throws Exception + public void testGetAttributeAttributeNotFoundException() { - // when - AttributeNotFoundException e = assertThrows(AttributeNotFoundException.class, ()->{ - objectMBean.getAttribute("ffname"); - }); + AttributeNotFoundException e = assertThrows(AttributeNotFoundException.class, () -> objectMBean.getAttribute("ffname")); - // then - assertNotNull(e, "An AttributeNotFoundException must have occurred by now as there is no " + "attribute with the name ffname in bean"); + assertNotNull(e, "An AttributeNotFoundException must have occurred by now as there is no attribute with the name ffname in bean"); } @Test public void testSetAttributeWithCorrectAttrName() throws Exception { - // given - setUpGetAttribute("fname","charu"); + Attribute attribute = new Attribute("fname", "charu"); + objectMBean.setAttribute(attribute); - // when - value = (String)objectMBean.getAttribute("fname"); + String value = (String)objectMBean.getAttribute("fname"); - // then - assertEquals("charu", value, "Attribute(fname) value must be equl to charu"); + assertEquals("charu", value, "Attribute(fname) value must be equal to charu"); } @Test public void testSetAttributeNullCheck() throws Exception { - // given objectMBean.setAttribute(null); - // when - AttributeNotFoundException e = assertThrows(AttributeNotFoundException.class, ()->{ - objectMBean.getAttribute(null); - }); + AttributeNotFoundException e = assertThrows(AttributeNotFoundException.class, () -> objectMBean.getAttribute(null)); - // then - assertNotNull(e,"An AttributeNotFoundException must have occurred by now as there is no attribute with the name null"); + assertNotNull(e, "An AttributeNotFoundException must have occurred by now as there is no attribute with the name null"); } @Test - public void testSetAttributeAttributeWithWrongAttrName() throws Exception + public void testSetAttributeAttributeWithWrongAttrName() { - // given - attribute = new Attribute("fnameee","charu"); + attribute = new Attribute("fnameee", "charu"); - // when - AttributeNotFoundException e = assertThrows(AttributeNotFoundException.class, ()->{ - objectMBean.setAttribute(attribute); - }); + AttributeNotFoundException e = assertThrows(AttributeNotFoundException.class, () -> objectMBean.setAttribute(attribute)); - // then assertNotNull(e, "An AttributeNotFoundException must have occurred by now as there is no attribute " + "with the name ffname in bean"); } @Test - public void testSetAttributesWithCorrectValues() throws Exception + public void testSetAttributesWithCorrectValues() { - // given - attributes = getAttributes("fname","vijay"); - attributes = objectMBean.setAttributes(attributes); + AttributeList attributes = getAttributes("fname", "vijay"); + objectMBean.setAttributes(attributes); - // when - attributes = objectMBean.getAttributes(new String[] - { "fname" }); + attributes = objectMBean.getAttributes(new String[]{"fname"}); - // then + assertEquals(1, attributes.size()); assertEquals("vijay", ((Attribute)(attributes.get(0))).getValue(), "Fname value must be equal to vijay"); } @Test - public void testSetAttributesForArrayTypeAttribue() throws Exception + public void testSetAttributesForArrayTypeAttribute() throws Exception { - // given - derivedes = getArrayTypeAttribute(); + Derived[] deriveds = getArrayTypeAttribute(); - // when - derivedManaged.setAddresses(derivedes); + derivedManaged.setAddresses(deriveds); mBeanDerivedManaged.getMBeanInfo(); - // then assertNotNull(mBeanDerivedManaged.getAttribute("addresses"), "Address object shouldn't be null"); } @Test public void testSetAttributesForCollectionTypeAttribue() throws Exception { - // given - aliasNames = getCollectionTypeAttribute(); + ArrayList aliasNames = new ArrayList<>(Arrays.asList(getArrayTypeAttribute())); - // when derivedManaged.setAliasNames(aliasNames); mBeanDerivedManaged.getMBeanInfo(); - // then assertNotNull(mBeanDerivedManaged.getAttribute("aliasNames"), "Address object shouldn't be null"); - assertNull(mBeanDerivedManaged.getAttribute("derived"), "Derived object shouldn't registerd with container so its value will be null"); + assertNull(mBeanDerivedManaged.getAttribute("derived"), "Derived object shouldn't registered with container so its value will be null"); } private Derived[] getArrayTypeAttribute() @@ -246,119 +169,74 @@ public class ObjectMBeanUtilTest MBeanContainer mBeanDerivedManagedContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); mBeanDerivedManaged.setMBeanContainer(mBeanDerivedManagedContainer); Derived derived0 = new Derived(); - mBeanDerivedManagedContainer.beanAdded(null,derived0); - Derived[] derivedes = new Derived[3]; + mBeanDerivedManagedContainer.beanAdded(null, derived0); + Derived[] deriveds = new Derived[3]; for (int i = 0; i < 3; i++) - { - derivedes[i] = new Derived(); - } - derivedManaged.setAddresses(derivedes); + deriveds[i] = new Derived(); + derivedManaged.setAddresses(deriveds); mBeanDerivedManaged.getMBeanInfo(); - ArrayList aliasNames = new ArrayList(Arrays.asList(derivedes)); + ArrayList aliasNames = new ArrayList<>(Arrays.asList(deriveds)); derivedManaged.setAliasNames(aliasNames); - return derivedes; - } - - private ArrayList getCollectionTypeAttribute() - { - ArrayList aliasNames = new ArrayList(Arrays.asList(getArrayTypeAttribute())); - return aliasNames; + return deriveds; } @Test public void testSetAttributesException() { - // given - attributes = getAttributes("fnameee","charu"); + AttributeList attributes = getAttributes("fnameee", "charu"); - // when attributes = objectMBean.setAttributes(attributes); - // then // Original code eating the exception and returning zero size list - assertEquals(EMPTY,attributes.size(),"As there is no attribute with the name fnameee, this should return empty"); + assertEquals(0, attributes.size(), "As there is no attribute with the name fnameee, this should return empty"); } private AttributeList getAttributes(String name, String value) { - Attribute attribute = new Attribute(name,value); + Attribute attribute = new Attribute(name, value); AttributeList attributes = new AttributeList(); attributes.add(attribute); return attributes; } @Test - public void testInvokeMBeanException() throws Exception + public void testInvokeMBeanException() { - // given - setMBeanInfoForInvoke(); + ReflectionException e = assertThrows(ReflectionException.class, () -> objectMBean.invoke("doodle2", new Object[0], new String[0])); - // when - MBeanException e = assertThrows(MBeanException.class, ()->{ - objectMBean.invoke("doodle2",new Object[] {},new String[] {}); - }); - - // then - assertNotNull(e, "An MBeanException must have occurred by now as doodle2() in Derived bean throwing exception"); + assertNotNull(e, "An ReflectionException must have occurred by now as doodle2() in Derived bean is private"); } @Test - public void testInvokeReflectionException() throws Exception + public void testInvokeReflectionException() { - // given - setMBeanInfoForInvoke(); + MBeanException e = assertThrows(MBeanException.class, () -> objectMBean.invoke("doodle1", new Object[0], new String[0])); - // when - ReflectionException e = assertThrows(ReflectionException.class, ()->{ - objectMBean.invoke("doodle1",new Object[] {},new String[] {}); - }); - - // then - assertNotNull(e, "ReflectionException is null"); + assertNotNull(e, "MBeanException is null"); } @Test public void testInvoke() throws Exception { - // given - setMBeanInfoForInvoke(); + String value = (String)objectMBean.invoke("good", new Object[0], new String[0]); - // when - value = (String)objectMBean.invoke("good",new Object[] {},new String[] {}); - - // then assertEquals("not bad", value, "Method(good) invocation on objectMBean must return not bad"); } @Test - public void testInvokeNoSuchMethodException() throws Exception + public void testInvokeNoSuchMethodException() { - // given - setMBeanInfoForInvoke(); + // DerivedMBean contains a managed method with the name good, + // we must call this method without any arguments. + ReflectionException e = assertThrows(ReflectionException.class, () -> + objectMBean.invoke("good", new Object[0], new String[]{"int aone"})); - // when - // DerivedMBean contains a managed method with the name good,we must - // call this method without any arguments - ReflectionException e = assertThrows(ReflectionException.class, ()->{ - objectMBean.invoke("good",new Object[] {},new String[] - { "int aone" }); - }); - - // then assertNotNull(e, "An ReflectionException must have occurred by now as we cannot call a methow with wrong signature"); - - } - - private void setMBeanInfoForInvoke() - { - objectMBean = (ObjectMBean)ObjectMBean.mbeanFor(derivedExtended); - container.beanAdded(null,derivedExtended); - objectMBean.getMBeanInfo(); } @Test - public void testToVariableName() + public void testToAttributeName() { - assertEquals("fullName",objectMBean.toVariableName("isfullName")); + assertEquals("fullName", MetaData.toAttributeName("isfullName")); } } diff --git a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/PojoTest.java b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/PojoTest.java index 3ea782d22ff..2d1aad0bae0 100644 --- a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/PojoTest.java +++ b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/PojoTest.java @@ -20,27 +20,25 @@ package org.eclipse.jetty.jmx; import java.util.Arrays; import java.util.List; -import org.eclipse.jetty.util.log.jmx.LogMBean; -import org.junit.jupiter.api.Test; + import com.openpojo.reflection.impl.PojoClassFactory; import com.openpojo.validation.Validator; import com.openpojo.validation.ValidatorBuilder; import com.openpojo.validation.test.impl.GetterTester; import com.openpojo.validation.test.impl.SetterTester; +import org.eclipse.jetty.util.log.jmx.LogMBean; +import org.junit.jupiter.api.Test; /* * This class tests all the getters and setters for a given list of classes. */ public class PojoTest { - - private Validator validator; - @Test public void testOpenPojo() { - validator = ValidatorBuilder.create().with(new SetterTester()).with(new GetterTester()).build(); - List classes = Arrays.asList(MBeanContainer.class,ObjectMBean.class,LogMBean.class); + Validator validator = ValidatorBuilder.create().with(new SetterTester()).with(new GetterTester()).build(); + List classes = Arrays.asList(MBeanContainer.class, ObjectMBean.class, LogMBean.class); for (Class clazz : classes) { validator.validate(PojoClassFactory.getPojoClass(clazz)); diff --git a/jetty-jmx/src/test/resources/jetty-logging.properties b/jetty-jmx/src/test/resources/jetty-logging.properties index f0392ad78c1..ad4b63c91cc 100644 --- a/jetty-jmx/src/test/resources/jetty-logging.properties +++ b/jetty-jmx/src/test/resources/jetty-logging.properties @@ -1,2 +1,2 @@ org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog -org.eclipse.jetty.jmx.LEVEL=INFO +#org.eclipse.jetty.jmx.LEVEL=DEBUG diff --git a/jetty-osgi/test-jetty-osgi/pom.xml b/jetty-osgi/test-jetty-osgi/pom.xml index 7892f0d6f43..94dcbb85e84 100644 --- a/jetty-osgi/test-jetty-osgi/pom.xml +++ b/jetty-osgi/test-jetty-osgi/pom.xml @@ -14,7 +14,7 @@ ${project.groupId}.boot.test.osgi http://download.eclipse.org/jetty/orbit/ target/distribution - 4.11.0 + 4.12.0 2.5.2 1.0 true @@ -45,15 +45,6 @@ pax-exam-junit4 ${exam.version} test - - org.ops4j.pax.exam @@ -410,13 +401,11 @@ org.ow2.asm asm - ${asm.version} test org.ow2.asm asm-commons - ${asm.version} test @@ -443,6 +432,14 @@ ${settings.localRepository} + + + + org.apache.maven.surefire + surefire-junit47 + ${maven.surefire.version} + + org.apache.servicemix.tooling diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java index 076945ed9db..715ca1e4967 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java @@ -18,16 +18,6 @@ package org.eclipse.jetty.proxy; -import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; @@ -73,7 +63,6 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.DuplexConnectionPool; import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.HttpContentResponse; import org.eclipse.jetty.client.HttpProxy; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; @@ -96,7 +85,6 @@ import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.hamcrest.Matchers; @@ -105,7 +93,15 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; + +import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class ProxyServletTest { @@ -1400,8 +1396,8 @@ public class ProxyServletTest // Wait more than the idle timeout to break the connection. Thread.sleep(2 * idleTimeout); - assertTrue(serverLatch.await(555, TimeUnit.SECONDS)); - assertTrue(clientLatch.await(555, TimeUnit.SECONDS)); + assertTrue(serverLatch.await(5, TimeUnit.SECONDS)); + assertTrue(clientLatch.await(5, TimeUnit.SECONDS)); } @ParameterizedTest diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java new file mode 100644 index 00000000000..fe256d8e749 --- /dev/null +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java @@ -0,0 +1,331 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.security; + +import java.io.Serializable; +import java.net.InetAddress; +import java.nio.file.Path; +import java.security.PrivilegedAction; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.security.authentication.AuthorizationService; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +/** + *

A configurable (as opposed to using system properties) SPNEGO LoginService.

+ *

At startup, this LoginService will login via JAAS the service principal, composed + * of the {@link #getServiceName() service name} and the {@link #getHostName() host name}, + * for example {@code HTTP/wonder.com}, using a {@code keyTab} file as the service principal + * credentials.

+ *

Upon receiving a HTTP request, the server tries to authenticate the client + * calling {@link #login(String, Object, ServletRequest)} where the GSS APIs are used to + * verify client tokens and (perhaps after a few round-trips) a {@code GSSContext} is + * established.

+ */ +public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implements LoginService +{ + private static final Logger LOG = Log.getLogger(ConfigurableSpnegoLoginService.class); + + private final GSSManager _gssManager = GSSManager.getInstance(); + private final String _realm; + private final AuthorizationService _authorizationService; + private IdentityService _identityService = new DefaultIdentityService(); + private String _serviceName; + private Path _keyTabPath; + private String _hostName; + private SpnegoContext _context; + + public ConfigurableSpnegoLoginService(String realm, AuthorizationService authorizationService) + { + _realm = realm; + _authorizationService = authorizationService; + } + + /** + * @return the realm name + */ + @Override + public String getName() + { + return _realm; + } + + /** + * @return the path of the keyTab file containing service credentials + */ + public Path getKeyTabPath() + { + return _keyTabPath; + } + + /** + * @param keyTabFile the path of the keyTab file containing service credentials + */ + public void setKeyTabPath(Path keyTabFile) + { + _keyTabPath = keyTabFile; + } + + /** + * @return the service name, typically "HTTP" + * @see #getHostName() + */ + public String getServiceName() + { + return _serviceName; + } + + /** + * @param serviceName the service name + * @see #setHostName(String) + */ + public void setServiceName(String serviceName) + { + _serviceName = serviceName; + } + + /** + * @return the host name of the service + * @see #setServiceName(String) + */ + public String getHostName() + { + return _hostName; + } + + /** + * @param hostName the host name of the service + */ + public void setHostName(String hostName) + { + _hostName = hostName; + } + + @Override + protected void doStart() throws Exception + { + if (_hostName == null) + _hostName = InetAddress.getLocalHost().getCanonicalHostName(); + if (LOG.isDebugEnabled()) + LOG.debug("Retrieving credentials for service {}/{}", getServiceName(), getHostName()); + LoginContext loginContext = new LoginContext("", null, null, new SpnegoConfiguration()); + loginContext.login(); + Subject subject = loginContext.getSubject(); + _context = Subject.doAs(subject, newSpnegoContext(subject)); + super.doStart(); + } + + private PrivilegedAction newSpnegoContext(Subject subject) + { + return () -> + { + try + { + GSSName serviceName = _gssManager.createName(getServiceName() + "@" + getHostName(), GSSName.NT_HOSTBASED_SERVICE); + Oid kerberosOid = new Oid("1.2.840.113554.1.2.2"); + Oid spnegoOid = new Oid("1.3.6.1.5.5.2"); + Oid[] mechanisms = new Oid[]{kerberosOid, spnegoOid}; + GSSCredential serviceCredential = _gssManager.createCredential(serviceName, GSSCredential.DEFAULT_LIFETIME, mechanisms, GSSCredential.ACCEPT_ONLY); + SpnegoContext context = new SpnegoContext(); + context._subject = subject; + context._serviceCredential = serviceCredential; + return context; + } + catch (GSSException x) + { + throw new RuntimeException(x); + } + }; + } + + @Override + public UserIdentity login(String username, Object credentials, ServletRequest req) + { + Subject subject = _context._subject; + HttpServletRequest request = (HttpServletRequest)req; + HttpSession httpSession = request.getSession(false); + GSSContext gssContext = null; + if (httpSession != null) + { + GSSContextHolder holder = (GSSContextHolder)httpSession.getAttribute(GSSContextHolder.ATTRIBUTE); + gssContext = holder == null ? null : holder.gssContext; + } + if (gssContext == null) + gssContext = Subject.doAs(subject, newGSSContext()); + + byte[] input = Base64.getDecoder().decode((String)credentials); + byte[] output = Subject.doAs(_context._subject, acceptGSSContext(gssContext, input)); + String token = Base64.getEncoder().encodeToString(output); + + String userName = toUserName(gssContext); + // Save the token in the principal so it can be sent in the response. + SpnegoUserPrincipal principal = new SpnegoUserPrincipal(userName, token); + if (gssContext.isEstablished()) + { + if (httpSession != null) + httpSession.removeAttribute(GSSContextHolder.ATTRIBUTE); + + UserIdentity roles = _authorizationService.getUserIdentity(request, userName); + return new SpnegoUserIdentity(subject, principal, roles); + } + else + { + // The GSS context is not established yet, save it into the HTTP session. + if (httpSession == null) + httpSession = request.getSession(true); + GSSContextHolder holder = new GSSContextHolder(gssContext); + httpSession.setAttribute(GSSContextHolder.ATTRIBUTE, holder); + + // Return an unestablished UserIdentity. + return new SpnegoUserIdentity(subject, principal, null); + } + } + + private PrivilegedAction newGSSContext() + { + return () -> + { + try + { + return _gssManager.createContext(_context._serviceCredential); + } + catch (GSSException x) + { + throw new RuntimeException(x); + } + }; + } + + private PrivilegedAction acceptGSSContext(GSSContext gssContext, byte[] token) + { + return () -> + { + try + { + return gssContext.acceptSecContext(token, 0, token.length); + } + catch (GSSException x) + { + throw new RuntimeException(x); + } + }; + } + + private String toUserName(GSSContext gssContext) + { + try + { + String name = gssContext.getSrcName().toString(); + int at = name.indexOf('@'); + if (at < 0) + return name; + return name.substring(0, at); + } + catch (GSSException x) + { + throw new RuntimeException(x); + } + } + + @Override + public boolean validate(UserIdentity user) + { + return false; + } + + @Override + public IdentityService getIdentityService() + { + return _identityService; + } + + @Override + public void setIdentityService(IdentityService identityService) + { + _identityService = identityService; + } + + @Override + public void logout(UserIdentity user) + { + } + + private class SpnegoConfiguration extends Configuration + { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) + { + String principal = getServiceName() + "/" + getHostName(); + Map options = new HashMap<>(); + if (LOG.isDebugEnabled()) + options.put("debug", "true"); + options.put("doNotPrompt", "true"); + options.put("refreshKrb5Config", "true"); + options.put("principal", principal); + options.put("useKeyTab", "true"); + Path keyTabPath = getKeyTabPath(); + if (keyTabPath != null) + options.put("keyTab", keyTabPath.toAbsolutePath().toString()); + // This option is required to store the service credentials in + // the Subject, so that it can be later used by acceptSecContext(). + options.put("storeKey", "true"); + options.put("isInitiator", "false"); + String moduleClass = "com.sun.security.auth.module.Krb5LoginModule"; + AppConfigurationEntry config = new AppConfigurationEntry(moduleClass, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options); + return new AppConfigurationEntry[]{config}; + } + } + + private static class SpnegoContext + { + private Subject _subject; + private GSSCredential _serviceCredential; + } + + private static class GSSContextHolder implements Serializable + { + public static final String ATTRIBUTE = GSSContextHolder.class.getName(); + + private transient final GSSContext gssContext; + + private GSSContextHolder(GSSContext gssContext) + { + this.gssContext = gssContext; + } + } +} diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoLoginService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoLoginService.java index 5ea42ddb145..41f30f0a736 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoLoginService.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoLoginService.java @@ -36,6 +36,10 @@ import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; +/** + * @deprecated use {@link ConfigurableSpnegoLoginService} instead + */ +@Deprecated public class SpnegoLoginService extends AbstractLifeCycle implements LoginService { private static final Logger LOG = Log.getLogger(SpnegoLoginService.class); diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserIdentity.java b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserIdentity.java index d787d98c5bf..307f4ba5e41 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserIdentity.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserIdentity.java @@ -19,7 +19,6 @@ package org.eclipse.jetty.security; import java.security.Principal; -import java.util.List; import javax.security.auth.Subject; @@ -27,18 +26,17 @@ import org.eclipse.jetty.server.UserIdentity; public class SpnegoUserIdentity implements UserIdentity { - private Subject _subject; - private Principal _principal; - private List _roles; + private final Subject _subject; + private final Principal _principal; + private final UserIdentity _roleDelegate; - public SpnegoUserIdentity( Subject subject, Principal principal, List roles ) + public SpnegoUserIdentity(Subject subject, Principal principal, UserIdentity roleDelegate) { _subject = subject; _principal = principal; - _roles = roles; + _roleDelegate = roleDelegate; } - @Override public Subject getSubject() { @@ -54,7 +52,11 @@ public class SpnegoUserIdentity implements UserIdentity @Override public boolean isUserInRole(String role, Scope scope) { - return _roles.contains(role); + return _roleDelegate != null && _roleDelegate.isUserInRole(role, scope); } + public boolean isEstablished() + { + return _roleDelegate != null; + } } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserPrincipal.java b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserPrincipal.java index b503cf6d2be..4ad45083953 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserPrincipal.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserPrincipal.java @@ -19,8 +19,7 @@ package org.eclipse.jetty.security; import java.security.Principal; - -import org.eclipse.jetty.util.B64Code; +import java.util.Base64; public class SpnegoUserPrincipal implements Principal { @@ -28,13 +27,13 @@ public class SpnegoUserPrincipal implements Principal private byte[] _token; private String _encodedToken; - public SpnegoUserPrincipal( String name, String encodedToken ) + public SpnegoUserPrincipal(String name, String encodedToken) { _name = name; _encodedToken = encodedToken; } - public SpnegoUserPrincipal( String name, byte[] token ) + public SpnegoUserPrincipal(String name, byte[] token) { _name = name; _token = token; @@ -48,19 +47,15 @@ public class SpnegoUserPrincipal implements Principal public byte[] getToken() { - if ( _token == null ) - { - _token = B64Code.decode(_encodedToken); - } + if (_token == null) + _token = Base64.getDecoder().decode(_encodedToken); return _token; } public String getEncodedToken() { - if ( _encodedToken == null ) - { - _encodedToken = new String(B64Code.encode(_token,true)); - } + if (_encodedToken == null) + _encodedToken = new String(Base64.getEncoder().encode(_token)); return _encodedToken; } } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/AuthorizationService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/AuthorizationService.java new file mode 100644 index 00000000000..87adc2db830 --- /dev/null +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/AuthorizationService.java @@ -0,0 +1,50 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.security.authentication; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.security.LoginService; +import org.eclipse.jetty.server.UserIdentity; + +/** + *

A service to query for user roles.

+ */ +@FunctionalInterface +public interface AuthorizationService +{ + /** + * @param request the current HTTP request + * @param name the user name + * @return a {@link UserIdentity} to query for roles of the given user + */ + UserIdentity getUserIdentity(HttpServletRequest request, String name); + + /** + *

Wraps a {@link LoginService} as an AuthorizationService

+ * + * @param loginService the {@link LoginService} to wrap + * @param credentials + * @return an AuthorizationService that delegates the query for roles to the given {@link LoginService} + */ + public static AuthorizationService from(LoginService loginService, Object credentials) + { + return (request, name) -> loginService.login(name, credentials, request); + } +} diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java new file mode 100644 index 00000000000..6cae1a4745b --- /dev/null +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java @@ -0,0 +1,232 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.security.authentication; + +import java.io.IOException; +import java.io.Serializable; +import java.time.Duration; +import java.time.Instant; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.security.ServerAuthException; +import org.eclipse.jetty.security.SpnegoUserIdentity; +import org.eclipse.jetty.security.SpnegoUserPrincipal; +import org.eclipse.jetty.security.UserAuthentication; +import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.server.Authentication.User; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.security.Constraint; + +/** + *

A LoginAuthenticator that uses SPNEGO and the GSS API to authenticate requests.

+ *

A successful authentication from a client is cached for a configurable + * {@link #getAuthenticationDuration() duration} using the HTTP session; this avoids + * that the client is asked to authenticate for every request.

+ * + * @see org.eclipse.jetty.security.ConfigurableSpnegoLoginService + */ +public class ConfigurableSpnegoAuthenticator extends LoginAuthenticator +{ + private static final Logger LOG = Log.getLogger(ConfigurableSpnegoAuthenticator.class); + + private final String _authMethod; + private Duration _authenticationDuration = Duration.ofNanos(-1); + + public ConfigurableSpnegoAuthenticator() + { + this(Constraint.__SPNEGO_AUTH); + } + + /** + * Allow for a custom authMethod value to be set for instances where SPNEGO may not be appropriate + * + * @param authMethod the auth method + */ + public ConfigurableSpnegoAuthenticator(String authMethod) + { + _authMethod = authMethod; + } + + @Override + public String getAuthMethod() + { + return _authMethod; + } + + /** + * @return the authentication duration + */ + public Duration getAuthenticationDuration() + { + return _authenticationDuration; + } + + /** + *

Sets the duration of the authentication.

+ *

A negative duration means that the authentication is only valid for the current request.

+ *

A zero duration means that the authentication is valid forever.

+ *

A positive value means that the authentication is valid for the specified duration.

+ * + * @param authenticationDuration the authentication duration + */ + public void setAuthenticationDuration(Duration authenticationDuration) + { + _authenticationDuration = authenticationDuration; + } + + @Override + public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException + { + if (!mandatory) + return new DeferredAuthentication(this); + + HttpServletRequest request = (HttpServletRequest)req; + HttpServletResponse response = (HttpServletResponse)res; + + String header = request.getHeader(HttpHeader.AUTHORIZATION.asString()); + String spnegoToken = getSpnegoToken(header); + HttpSession httpSession = request.getSession(false); + + // We have a token from the client, so run the login. + if (header != null && spnegoToken != null) + { + SpnegoUserIdentity identity = (SpnegoUserIdentity)login(null, spnegoToken, request); + if (identity.isEstablished()) + { + if (!DeferredAuthentication.isDeferred(response)) + { + if (LOG.isDebugEnabled()) + LOG.debug("Sending final token"); + // Send to the client the final token so that the + // client can establish the GSS context with the server. + SpnegoUserPrincipal principal = (SpnegoUserPrincipal)identity.getUserPrincipal(); + setSpnegoToken(response, principal.getEncodedToken()); + } + + Duration authnDuration = getAuthenticationDuration(); + if (!authnDuration.isNegative()) + { + if (httpSession == null) + httpSession = request.getSession(true); + httpSession.setAttribute(UserIdentityHolder.ATTRIBUTE, new UserIdentityHolder(identity)); + } + return new UserAuthentication(getAuthMethod(), identity); + } + else + { + if (DeferredAuthentication.isDeferred(response)) + return Authentication.UNAUTHENTICATED; + if (LOG.isDebugEnabled()) + LOG.debug("Sending intermediate challenge"); + SpnegoUserPrincipal principal = (SpnegoUserPrincipal)identity.getUserPrincipal(); + sendChallenge(response, principal.getEncodedToken()); + return Authentication.SEND_CONTINUE; + } + } + // No token from the client; check if the client has logged in + // successfully before and the authentication has not expired. + else if (httpSession != null) + { + UserIdentityHolder holder = (UserIdentityHolder)httpSession.getAttribute(UserIdentityHolder.ATTRIBUTE); + if (holder != null) + { + UserIdentity identity = holder._userIdentity; + if (identity != null) + { + Duration authnDuration = getAuthenticationDuration(); + if (!authnDuration.isNegative()) + { + boolean expired = !authnDuration.isZero() && Instant.now().isAfter(holder._validFrom.plus(authnDuration)); + // Allow non-GET requests even if they're expired, so that + // the client does not need to send the request content again. + if (!expired || !HttpMethod.GET.is(request.getMethod())) + return new UserAuthentication(getAuthMethod(), identity); + } + } + } + } + + if (DeferredAuthentication.isDeferred(response)) + return Authentication.UNAUTHENTICATED; + + if (LOG.isDebugEnabled()) + LOG.debug("Sending initial challenge"); + sendChallenge(response, null); + return Authentication.SEND_CONTINUE; + } + + private void sendChallenge(HttpServletResponse response, String token) throws ServerAuthException + { + try + { + setSpnegoToken(response, token); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + catch (IOException x) + { + throw new ServerAuthException(x); + } + } + + private void setSpnegoToken(HttpServletResponse response, String token) + { + String value = HttpHeader.NEGOTIATE.asString(); + if (token != null) + value += " " + token; + response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), value); + } + + private String getSpnegoToken(String header) + { + if (header == null) + return null; + String scheme = HttpHeader.NEGOTIATE.asString() + " "; + if (header.regionMatches(true, 0, scheme, 0, scheme.length())) + return header.substring(scheme.length()).trim(); + return null; + } + + @Override + public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, User validatedUser) + { + return true; + } + + private static class UserIdentityHolder implements Serializable + { + private static final String ATTRIBUTE = UserIdentityHolder.class.getName(); + + private transient final Instant _validFrom = Instant.now(); + private transient final UserIdentity _userIdentity; + + private UserIdentityHolder(UserIdentity userIdentity) + { + _userIdentity = userIdentity; + } + } +} diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java index 0dfe1b6add5..8142eb7c4d8 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java @@ -35,6 +35,10 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.security.Constraint; +/** + * @deprecated use {@link ConfigurableSpnegoAuthenticator} instead. + */ +@Deprecated public class SpnegoAuthenticator extends LoginAuthenticator { private static final Logger LOG = Log.getLogger(SpnegoAuthenticator.class); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java index 224f523b129..ec7b51c62b4 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java @@ -226,7 +226,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co } @Override - @ManagedAttribute("Idle timeout") + @ManagedAttribute("The connection idle timeout in milliseconds") public long getIdleTimeout() { return _idleTimeout; diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index dd2f1f7bfec..5ca8913035c 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -31,6 +31,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.LocalConnector; @@ -63,11 +64,13 @@ public class ErrorPageTest context.addServlet(FailServlet.class, "/fail/*"); context.addServlet(FailClosedServlet.class, "/fail-closed/*"); context.addServlet(ErrorServlet.class, "/error/*"); + context.addServlet(AppServlet.class, "/app/*"); ErrorPageErrorHandler error = new ErrorPageErrorHandler(); context.setErrorHandler(error); error.addErrorPage(599,"/error/599"); error.addErrorPage(IllegalStateException.class.getCanonicalName(),"/error/TestException"); + error.addErrorPage(BadMessageException.class,"/error/TestException"); error.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE,"/error/GlobalErrorPage"); _server.start(); @@ -157,6 +160,33 @@ public class ErrorPageTest } } + @Test + public void testBadMessage() throws Exception + { + String response = _connector.getResponse("GET /app?baa=%88%A4 HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 400 Unable to parse URI query")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /TestException")); + assertThat(response, Matchers.containsString("ERROR_MESSAGE: Unable to parse URI query")); + assertThat(response, Matchers.containsString("ERROR_CODE: 400")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: org.eclipse.jetty.http.BadMessageException: 400: Unable to parse URI query")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: class org.eclipse.jetty.http.BadMessageException")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AppServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /app")); + assertThat(response, Matchers.containsString("getParameterMap()= {}")); + } + + + public static class AppServlet extends HttpServlet implements Servlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + PrintWriter writer = response.getWriter(); + writer.println(request.getRequestURI()); + writer.println(request.getParameterMap().toString()); + } + } + public static class FailServlet extends HttpServlet implements Servlet { @Override @@ -202,6 +232,7 @@ public class ErrorPageTest writer.println("ERROR_EXCEPTION_TYPE: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION_TYPE)); writer.println("ERROR_SERVLET: " + request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); writer.println("ERROR_REQUEST_URI: " + request.getAttribute(Dispatcher.ERROR_REQUEST_URI)); + writer.println("getParameterMap()= " + request.getParameterMap()); } } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/MultiReleaseJarFileTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/MultiReleaseJarFileTest.java index e22e98065b2..eaffe9edd5b 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/MultiReleaseJarFileTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/MultiReleaseJarFileTest.java @@ -18,10 +18,6 @@ package org.eclipse.jetty.util; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.File; import java.net.URL; import java.net.URLClassLoader; @@ -31,9 +27,13 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.MultiReleaseJarFile.VersionedJarEntry; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.JRE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class MultiReleaseJarFileTest { private File example = MavenTestingUtils.getTestResourceFile("example.jar"); @@ -124,7 +124,7 @@ public class MultiReleaseJarFileTest @Test - @EnabledOnJre({JRE.JAVA_9, JRE.JAVA_10, JRE.JAVA_11}) + @DisabledOnJre(JRE.JAVA_8) public void testClassLoaderJava9() throws Exception { try(URLClassLoader loader = new URLClassLoader(new URL[]{example.toURI().toURL()})) diff --git a/pom.xml b/pom.xml index 115ad628715..7334242c0a2 100644 --- a/pom.xml +++ b/pom.xml @@ -469,13 +469,6 @@ org.apache.maven.plugins maven-failsafe-plugin ${maven.surefire.version} - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - -
org.apache.maven.plugins